@musnows/scriverse 0.5.3 → 0.5.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.md +4 -3
- package/dist/ai-protocol.js +216 -0
- package/dist/ai-protocol.js.map +1 -0
- package/dist/ai.js +132 -42
- package/dist/ai.js.map +1 -1
- package/dist/app.js +2 -0
- package/dist/app.js.map +1 -1
- package/dist/database.js +18 -0
- package/dist/database.js.map +1 -1
- package/dist/public/app.js +18 -6
- package/dist/public/display-labels.d.ts +1 -0
- package/dist/public/display-labels.js +7 -0
- package/dist/public/index.html +1 -1
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
- 大纲与伏笔:维护章节目标、冲突、转折和伏笔的埋设、提醒与回收。
|
|
42
42
|
- AI 创作助手:支持 Markdown 和流式输出,可引用章节行、附加角色与设定上下文。
|
|
43
43
|
- AI 任务:结构分析、章节分析、角色抽取、时间线分析、关系分析和一致性检查。
|
|
44
|
-
- 供应商管理:兼容 OpenAI Chat Completions 协议,可配置模型、最大输出 Token、并发数和 RPM。
|
|
44
|
+
- 供应商管理:兼容 OpenAI Chat Completions 与 Anthropic Messages 协议,可配置模型、最大输出 Token、并发数和 RPM。
|
|
45
45
|
- 安全导出:支持 JSON、TXT 和 Markdown,导出内容不包含 AI 密钥。
|
|
46
46
|
|
|
47
47
|
## 技术栈
|
|
@@ -141,7 +141,7 @@ npm start
|
|
|
141
141
|
配置前请先阅读 [AI 供应商兼容性与配置指南](docs/AI-PROVIDER-COMPATIBILITY.md),其中列出了已验证的服务商、基础地址、模型标识符和已知差异。
|
|
142
142
|
|
|
143
143
|
1. 启动项目后,点击顶部“AI 管理”进入平台级配置。
|
|
144
|
-
2. 新建兼容 OpenAI Chat Completions
|
|
144
|
+
2. 新建兼容 OpenAI Chat Completions 或 Anthropic Messages 的供应商,选择协议并填写基础地址、API 密钥、并发数、RPM 与最大输出 Token。
|
|
145
145
|
3. 为模型填写其支持的上下文总量(Token),再添加模型。
|
|
146
146
|
4. 在平台页设置全局系统提示词;它会追加在内置提示词之后。
|
|
147
147
|
5. 打开一本作品,在“更多 → AI 设置”中设置该书的追加系统提示词和任务默认模型;书籍提示词会追加在全局提示词之后。
|
|
@@ -216,7 +216,8 @@ curl http://127.0.0.1:13210/api/health
|
|
|
216
216
|
"data": {
|
|
217
217
|
"status": "ok",
|
|
218
218
|
"version": "0.3.3",
|
|
219
|
-
"protocol": "openai-chat-completions"
|
|
219
|
+
"protocol": "openai-chat-completions",
|
|
220
|
+
"protocols": ["openai-chat-completions", "anthropic-messages"]
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
```
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { normalizeBaseUrl } from "./utils.js";
|
|
2
|
+
export const AI_PROVIDER_PROTOCOLS = ["openai-chat-completions", "anthropic-messages"];
|
|
3
|
+
export function normalizeProviderBaseUrl(value) {
|
|
4
|
+
return normalizeBaseUrl(value).replace(/\/messages$/u, "");
|
|
5
|
+
}
|
|
6
|
+
function appendVersionedResource(baseUrl, resource) {
|
|
7
|
+
const normalized = normalizeProviderBaseUrl(baseUrl);
|
|
8
|
+
return /\/v1$/u.test(normalized) ? `${normalized}/${resource}` : `${normalized}/v1/${resource}`;
|
|
9
|
+
}
|
|
10
|
+
export function providerCompletionEndpoint(baseUrl, protocol) {
|
|
11
|
+
const normalized = normalizeProviderBaseUrl(baseUrl);
|
|
12
|
+
return protocol === "anthropic-messages"
|
|
13
|
+
? appendVersionedResource(normalized, "messages")
|
|
14
|
+
: `${normalized}/chat/completions`;
|
|
15
|
+
}
|
|
16
|
+
export function providerModelEndpoints(baseUrl, protocol) {
|
|
17
|
+
const normalized = normalizeProviderBaseUrl(baseUrl);
|
|
18
|
+
if (protocol === "openai-chat-completions")
|
|
19
|
+
return [`${normalized}/models`];
|
|
20
|
+
const primary = appendVersionedResource(normalized, "models");
|
|
21
|
+
const root = new URL("/v1/models", normalized).toString();
|
|
22
|
+
return primary === root ? [primary] : [primary, root];
|
|
23
|
+
}
|
|
24
|
+
export function providerRequestHeaders(protocol, apiKey, accept) {
|
|
25
|
+
return {
|
|
26
|
+
Authorization: `Bearer ${apiKey}`,
|
|
27
|
+
...(protocol === "anthropic-messages" ? { "x-api-key": apiKey, "anthropic-version": "2023-06-01" } : {}),
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
Accept: accept
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function textContent(value) {
|
|
33
|
+
return typeof value === "string" && value.length > 0 ? [{ type: "text", text: value }] : [];
|
|
34
|
+
}
|
|
35
|
+
function parsedToolInput(value) {
|
|
36
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
37
|
+
return value;
|
|
38
|
+
if (typeof value !== "string")
|
|
39
|
+
return {};
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(value);
|
|
42
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function anthropicAssistantContent(message) {
|
|
49
|
+
if (Array.isArray(message.anthropic_content) && message.anthropic_content.length > 0) {
|
|
50
|
+
return structuredClone(message.anthropic_content);
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
...textContent(message.content),
|
|
54
|
+
...message.tool_calls.map((toolCall) => ({
|
|
55
|
+
type: "tool_use",
|
|
56
|
+
id: toolCall.id,
|
|
57
|
+
name: toolCall.function.name,
|
|
58
|
+
input: parsedToolInput(toolCall.function.arguments)
|
|
59
|
+
}))
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function anthropicToolResult(message) {
|
|
63
|
+
let isError = false;
|
|
64
|
+
try {
|
|
65
|
+
const result = JSON.parse(message.content);
|
|
66
|
+
isError = result.ok === false;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
isError = false;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
type: "tool_result",
|
|
73
|
+
tool_use_id: message.tool_call_id,
|
|
74
|
+
content: message.content,
|
|
75
|
+
...(isError ? { is_error: true } : {})
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function anthropicMessages(messages) {
|
|
79
|
+
const system = messages
|
|
80
|
+
.filter((message) => message.role === "system")
|
|
81
|
+
.map((message) => message.content)
|
|
82
|
+
.filter(Boolean)
|
|
83
|
+
.join("\n\n");
|
|
84
|
+
const output = [];
|
|
85
|
+
const append = (role, content) => {
|
|
86
|
+
if (content.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
const previous = output.at(-1);
|
|
89
|
+
if (previous?.role === role)
|
|
90
|
+
previous.content.push(...content);
|
|
91
|
+
else
|
|
92
|
+
output.push({ role, content });
|
|
93
|
+
};
|
|
94
|
+
for (const message of messages) {
|
|
95
|
+
if (message.role === "system")
|
|
96
|
+
continue;
|
|
97
|
+
if (message.role === "tool") {
|
|
98
|
+
append("user", [anthropicToolResult(message)]);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (message.role === "assistant" && "tool_calls" in message) {
|
|
102
|
+
append("assistant", anthropicAssistantContent(message));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
append(message.role, textContent(message.content));
|
|
106
|
+
}
|
|
107
|
+
return { ...(system ? { system } : {}), messages: output };
|
|
108
|
+
}
|
|
109
|
+
function anthropicTools(tools) {
|
|
110
|
+
return tools.flatMap((tool) => {
|
|
111
|
+
const fn = tool.function && typeof tool.function === "object" && !Array.isArray(tool.function)
|
|
112
|
+
? tool.function
|
|
113
|
+
: null;
|
|
114
|
+
if (!fn || typeof fn.name !== "string")
|
|
115
|
+
return [];
|
|
116
|
+
return [{
|
|
117
|
+
name: fn.name,
|
|
118
|
+
...(typeof fn.description === "string" ? { description: fn.description } : {}),
|
|
119
|
+
input_schema: fn.parameters && typeof fn.parameters === "object" && !Array.isArray(fn.parameters)
|
|
120
|
+
? fn.parameters
|
|
121
|
+
: { type: "object", properties: {} }
|
|
122
|
+
}];
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export function buildCompletionRequestBody(input) {
|
|
126
|
+
const tools = input.toolChoice === "auto" ? input.tools ?? [] : [];
|
|
127
|
+
if (input.protocol === "openai-chat-completions") {
|
|
128
|
+
return {
|
|
129
|
+
model: input.model,
|
|
130
|
+
messages: input.messages,
|
|
131
|
+
...input.parameters,
|
|
132
|
+
...(tools.length > 0 ? { tools, tool_choice: "auto" } : {}),
|
|
133
|
+
...(input.stream ? { stream: true, stream_options: { include_usage: true } } : {})
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const translated = anthropicMessages(input.messages);
|
|
137
|
+
const parameters = Object.fromEntries(Object.entries(input.parameters)
|
|
138
|
+
.filter(([key]) => ["temperature", "top_p", "max_tokens", "thinking"].includes(key)));
|
|
139
|
+
return {
|
|
140
|
+
model: input.model,
|
|
141
|
+
...translated,
|
|
142
|
+
...parameters,
|
|
143
|
+
...(tools.length > 0 ? { tools: anthropicTools(tools), tool_choice: { type: "auto" } } : {}),
|
|
144
|
+
...(input.stream ? { stream: true } : {})
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function replayableAnthropicBlock(value) {
|
|
148
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
149
|
+
return null;
|
|
150
|
+
const block = value;
|
|
151
|
+
if (block.type === "text" && typeof block.text === "string")
|
|
152
|
+
return { type: "text", text: block.text };
|
|
153
|
+
if (block.type === "thinking" && typeof block.thinking === "string" && typeof block.signature === "string") {
|
|
154
|
+
return { type: "thinking", thinking: block.thinking, signature: block.signature };
|
|
155
|
+
}
|
|
156
|
+
if (block.type === "redacted_thinking" && typeof block.data === "string") {
|
|
157
|
+
return { type: "redacted_thinking", data: block.data };
|
|
158
|
+
}
|
|
159
|
+
if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
160
|
+
return { type: "tool_use", id: block.id, name: block.name, input: parsedToolInput(block.input) };
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
function anthropicFinishReason(value) {
|
|
165
|
+
if (value === "max_tokens")
|
|
166
|
+
return "length";
|
|
167
|
+
return typeof value === "string" ? value : null;
|
|
168
|
+
}
|
|
169
|
+
export function parseCompletionPayload(protocol, value) {
|
|
170
|
+
if (protocol === "openai-chat-completions") {
|
|
171
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
172
|
+
}
|
|
173
|
+
const response = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
174
|
+
const content = Array.isArray(response.content) ? response.content : [];
|
|
175
|
+
const replay = content.map(replayableAnthropicBlock).filter((block) => block !== null);
|
|
176
|
+
const text = content.flatMap((value) => {
|
|
177
|
+
const block = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
178
|
+
return block.type === "text" && typeof block.text === "string" ? [block.text] : [];
|
|
179
|
+
}).join("");
|
|
180
|
+
const reasoning = content.flatMap((value) => {
|
|
181
|
+
const block = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
182
|
+
if (block.type === "thinking" && typeof block.thinking === "string")
|
|
183
|
+
return [block.thinking];
|
|
184
|
+
if (block.type === "text" && typeof block.thinking === "string")
|
|
185
|
+
return [block.thinking];
|
|
186
|
+
return [];
|
|
187
|
+
}).join("");
|
|
188
|
+
const toolCalls = content.flatMap((value) => {
|
|
189
|
+
const block = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
190
|
+
if (block.type !== "tool_use" || typeof block.id !== "string" || typeof block.name !== "string")
|
|
191
|
+
return [];
|
|
192
|
+
return [{
|
|
193
|
+
id: block.id,
|
|
194
|
+
type: "function",
|
|
195
|
+
function: {
|
|
196
|
+
name: block.name,
|
|
197
|
+
arguments: parsedToolInput(block.input)
|
|
198
|
+
}
|
|
199
|
+
}];
|
|
200
|
+
});
|
|
201
|
+
return {
|
|
202
|
+
...(response.usage && typeof response.usage === "object" && !Array.isArray(response.usage)
|
|
203
|
+
? { usage: response.usage }
|
|
204
|
+
: {}),
|
|
205
|
+
choices: [{
|
|
206
|
+
finish_reason: anthropicFinishReason(response.stop_reason),
|
|
207
|
+
message: {
|
|
208
|
+
content: text || null,
|
|
209
|
+
reasoning_content: reasoning || null,
|
|
210
|
+
tool_calls: toolCalls,
|
|
211
|
+
anthropic_content: replay
|
|
212
|
+
}
|
|
213
|
+
}]
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=ai-protocol.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-protocol.js","sourceRoot":"","sources":["../src/ai-protocol.ts"],"names":[],"mappings":"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"}
|
package/dist/ai.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildCompletionRequestBody, normalizeProviderBaseUrl, parseCompletionPayload, providerCompletionEndpoint, providerModelEndpoints, providerRequestHeaders } from "./ai-protocol.js";
|
|
1
2
|
import { PLATFORM_AI_WORK_ID } from "./database.js";
|
|
2
3
|
import { AppError, notFound } from "./errors.js";
|
|
3
4
|
import { logger, sanitizeError } from "./logger.js";
|
|
@@ -5,7 +6,7 @@ import { paginated, paginationSql } from "./pagination.js";
|
|
|
5
6
|
import { currentRequestActor } from "./request-context.js";
|
|
6
7
|
import { fetchSafeAiEndpoint } from "./security.js";
|
|
7
8
|
import { RELATIONSHIP_SEARCH_POLICY_VERSION, RelationshipApproximateMatchLimitError, findApproximateNameMatchesChunked, ftsPhrase, normalizeRelationshipSearchText, relationshipCharacterTokenText, relationshipCharacterTokens, relationshipPinyinTokenText, relationshipPinyinTokens } from "./relationship-search.js";
|
|
8
|
-
import { clamp, id, json, maskSecret,
|
|
9
|
+
import { clamp, id, json, maskSecret, now } from "./utils.js";
|
|
9
10
|
import { z } from "zod";
|
|
10
11
|
export function aiErrorForLog(error) {
|
|
11
12
|
const sanitized = sanitizeError(error);
|
|
@@ -33,9 +34,22 @@ function isGeminiProviderOrModel(provider, model) {
|
|
|
33
34
|
function isKimiModelId(modelId) {
|
|
34
35
|
return modelId.toLowerCase().includes("kimi");
|
|
35
36
|
}
|
|
37
|
+
function providerProtocol(provider) {
|
|
38
|
+
return stringValue(provider, "protocol") === "anthropic-messages" ? "anthropic-messages" : "openai-chat-completions";
|
|
39
|
+
}
|
|
40
|
+
function isLongCatProvider(provider) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(stringValue(provider, "base_url")).hostname.toLowerCase() === "api.longcat.chat";
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
36
48
|
function thinkingParameters(provider, model) {
|
|
37
49
|
if (isGeminiProviderOrModel(provider, model))
|
|
38
50
|
return {};
|
|
51
|
+
if (providerProtocol(provider) === "anthropic-messages" && !isLongCatProvider(provider))
|
|
52
|
+
return {};
|
|
39
53
|
return { thinking: { type: boolValue(model, "thinking_enabled") ? "enabled" : "disabled" } };
|
|
40
54
|
}
|
|
41
55
|
const AGENT_TOOL_IDS = ["story_index", "read_chapters", "grep", "search_story_entities", "read_character_sections"];
|
|
@@ -990,10 +1004,12 @@ export class AiManager {
|
|
|
990
1004
|
const providerId = id("provider");
|
|
991
1005
|
const encrypted = this.vault.encrypt(input.apiKey);
|
|
992
1006
|
const timestamp = now();
|
|
993
|
-
|
|
1007
|
+
const protocol = input.protocol ?? "openai-chat-completions";
|
|
1008
|
+
const baseUrl = normalizeProviderBaseUrl(input.baseUrl);
|
|
1009
|
+
this.store.db.run(`INSERT INTO providers (id, work_id, name, base_url, protocol, encrypted_key, key_iv, key_tag, key_hint, status,
|
|
994
1010
|
connection_status, concurrency_limit, rpm_limit, max_tokens, note, created_at, updated_at)
|
|
995
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'unchecked', ?, ?, ?, ?, ?, ?)`, providerId, PLATFORM_AI_WORK_ID, input.name,
|
|
996
|
-
this.store.audit(PLATFORM_AI_WORK_ID, "provider.created", "provider", providerId, { name: input.name, baseUrl
|
|
1011
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'unchecked', ?, ?, ?, ?, ?, ?)`, providerId, PLATFORM_AI_WORK_ID, input.name, baseUrl, protocol, encrypted.encrypted, encrypted.iv, encrypted.tag, maskSecret(input.apiKey), input.status ?? "disabled", input.concurrencyLimit ?? 10, input.rpmLimit ?? 10, input.maxTokens ?? DEFAULT_MAX_TOKENS, input.note ?? "", timestamp, timestamp);
|
|
1012
|
+
this.store.audit(PLATFORM_AI_WORK_ID, "provider.created", "provider", providerId, { name: input.name, baseUrl, protocol });
|
|
997
1013
|
return this.getProvider(providerId);
|
|
998
1014
|
}
|
|
999
1015
|
listProviders() {
|
|
@@ -1022,10 +1038,12 @@ export class AiManager {
|
|
|
1022
1038
|
keyHint = maskSecret(input.apiKey);
|
|
1023
1039
|
connectionStatus = "unchecked";
|
|
1024
1040
|
}
|
|
1025
|
-
if (input.baseUrl &&
|
|
1041
|
+
if (input.baseUrl && normalizeProviderBaseUrl(input.baseUrl) !== stringValue(row, "base_url"))
|
|
1042
|
+
connectionStatus = "unchecked";
|
|
1043
|
+
if (input.protocol && input.protocol !== providerProtocol(row))
|
|
1026
1044
|
connectionStatus = "unchecked";
|
|
1027
|
-
this.store.db.run(`UPDATE providers SET name = ?, base_url = ?, encrypted_key = ?, key_iv = ?, key_tag = ?, key_hint = ?,
|
|
1028
|
-
status = ?, connection_status = ?, concurrency_limit = ?, rpm_limit = ?, max_tokens = ?, note = ?, updated_at = ? WHERE id = ?`, input.name ?? stringValue(row, "name"), input.baseUrl ?
|
|
1045
|
+
this.store.db.run(`UPDATE providers SET name = ?, base_url = ?, protocol = ?, encrypted_key = ?, key_iv = ?, key_tag = ?, key_hint = ?,
|
|
1046
|
+
status = ?, connection_status = ?, concurrency_limit = ?, rpm_limit = ?, max_tokens = ?, note = ?, updated_at = ? WHERE id = ?`, input.name ?? stringValue(row, "name"), input.baseUrl ? normalizeProviderBaseUrl(input.baseUrl) : stringValue(row, "base_url"), input.protocol ?? providerProtocol(row), encryptedKey, keyIv, keyTag, keyHint, input.status ?? stringValue(row, "status"), connectionStatus, input.concurrencyLimit ?? numberValue(row, "concurrency_limit"), input.rpmLimit ?? numberValue(row, "rpm_limit"), input.maxTokens ?? numberValue(row, "max_tokens"), input.note ?? stringValue(row, "note"), now(), providerId);
|
|
1029
1047
|
this.store.audit(PLATFORM_AI_WORK_ID, "provider.updated", "provider", providerId, {
|
|
1030
1048
|
fields: Object.keys(input).filter((key) => key !== "apiKey"),
|
|
1031
1049
|
keyReplaced: Boolean(input.apiKey)
|
|
@@ -1051,26 +1069,40 @@ export class AiManager {
|
|
|
1051
1069
|
async testProvider(providerId) {
|
|
1052
1070
|
const row = this.getProviderRow(providerId);
|
|
1053
1071
|
const apiKey = this.decryptKey(row);
|
|
1072
|
+
const protocol = providerProtocol(row);
|
|
1054
1073
|
const controller = new AbortController();
|
|
1055
1074
|
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
1056
1075
|
const startedAt = process.hrtime.bigint();
|
|
1057
1076
|
logger.info("ai.provider_test.started", { providerId });
|
|
1058
1077
|
try {
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1078
|
+
let payload = null;
|
|
1079
|
+
let lastFailure = "AI 供应商没有返回模型列表";
|
|
1080
|
+
const endpoints = providerModelEndpoints(stringValue(row, "base_url"), protocol);
|
|
1081
|
+
for (let index = 0; index < endpoints.length; index += 1) {
|
|
1082
|
+
const endpoint = endpoints[index];
|
|
1083
|
+
if (!endpoint)
|
|
1084
|
+
continue;
|
|
1085
|
+
const response = await this.outboundFetch(endpoint, {
|
|
1086
|
+
headers: providerRequestHeaders(protocol, apiKey, "application/json"),
|
|
1087
|
+
signal: controller.signal
|
|
1088
|
+
});
|
|
1089
|
+
if (response.ok) {
|
|
1090
|
+
payload = (await response.json());
|
|
1091
|
+
break;
|
|
1092
|
+
}
|
|
1065
1093
|
const message = await response.text();
|
|
1066
|
-
|
|
1094
|
+
lastFailure = `HTTP ${response.status}: ${message.slice(0, 300)}`;
|
|
1095
|
+
if (response.status !== 404 || index === endpoints.length - 1)
|
|
1096
|
+
break;
|
|
1067
1097
|
}
|
|
1068
|
-
|
|
1098
|
+
if (!payload)
|
|
1099
|
+
throw new Error(lastFailure);
|
|
1069
1100
|
const availableModels = Array.isArray(payload.data) ? payload.data.map((item) => item.id).filter(Boolean) : [];
|
|
1070
1101
|
const timestamp = now();
|
|
1071
1102
|
this.store.db.run("UPDATE providers SET connection_status = 'success', last_error = NULL, last_success_at = ?, updated_at = ? WHERE id = ?", timestamp, timestamp, providerId);
|
|
1072
1103
|
logger.info("ai.provider_test.completed", {
|
|
1073
1104
|
providerId,
|
|
1105
|
+
protocol,
|
|
1074
1106
|
ok: true,
|
|
1075
1107
|
availableModelCount: availableModels.length,
|
|
1076
1108
|
durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000
|
|
@@ -1078,10 +1110,11 @@ export class AiManager {
|
|
|
1078
1110
|
return { ok: true, availableModels, provider: this.getProvider(providerId) };
|
|
1079
1111
|
}
|
|
1080
1112
|
catch (error) {
|
|
1081
|
-
const message = error instanceof Error ? error.message : "连接失败";
|
|
1113
|
+
const message = error instanceof Error ? redactProviderSecret(error.message, apiKey) : "连接失败";
|
|
1082
1114
|
this.store.db.run("UPDATE providers SET connection_status = 'failed', last_error = ?, updated_at = ? WHERE id = ?", message, now(), providerId);
|
|
1083
1115
|
logger.warn("ai.provider_test.completed", {
|
|
1084
1116
|
providerId,
|
|
1117
|
+
protocol,
|
|
1085
1118
|
ok: false,
|
|
1086
1119
|
durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000,
|
|
1087
1120
|
error: aiErrorForLog(error)
|
|
@@ -2028,12 +2061,14 @@ export class AiManager {
|
|
|
2028
2061
|
this.store.db.run("UPDATE ai_call_traces SET rounds_json = ?, source_refs_json = ?, updated_at = ? WHERE call_id = ?", JSON.stringify(traceRounds), JSON.stringify(taskTraceSourceRefs(messages, traceRounds)), now(), callId);
|
|
2029
2062
|
};
|
|
2030
2063
|
const callStartedAt = process.hrtime.bigint();
|
|
2064
|
+
const protocol = providerProtocol(provider);
|
|
2031
2065
|
logger.info("ai.call.started", {
|
|
2032
2066
|
callId,
|
|
2033
2067
|
workId: input.workId,
|
|
2034
2068
|
taskType: input.taskType,
|
|
2035
2069
|
providerId: stringValue(provider, "id"),
|
|
2036
2070
|
modelId: stringValue(model, "id"),
|
|
2071
|
+
protocol,
|
|
2037
2072
|
streaming: false,
|
|
2038
2073
|
contextChars: context.length,
|
|
2039
2074
|
instructionChars: input.instruction.length,
|
|
@@ -2043,7 +2078,7 @@ export class AiManager {
|
|
|
2043
2078
|
try {
|
|
2044
2079
|
const apiKey = this.decryptKey(provider);
|
|
2045
2080
|
activeApiKey = apiKey;
|
|
2046
|
-
const endpoint =
|
|
2081
|
+
const endpoint = providerCompletionEndpoint(stringValue(provider, "base_url"), protocol);
|
|
2047
2082
|
const timeoutMs = input.taskType === "book-analysis" || input.taskType === "relationship-analysis" ? 300_000 : 60_000;
|
|
2048
2083
|
const maximumAttempts = Math.round(clamp(input.maxAttempts ?? 3, 1, 5));
|
|
2049
2084
|
let completionRequestCount = 0;
|
|
@@ -2090,13 +2125,15 @@ export class AiManager {
|
|
|
2090
2125
|
try {
|
|
2091
2126
|
const response = await this.outboundFetch(endpoint, {
|
|
2092
2127
|
method: "POST",
|
|
2093
|
-
headers:
|
|
2094
|
-
body: JSON.stringify({
|
|
2128
|
+
headers: providerRequestHeaders(protocol, apiKey, "application/json"),
|
|
2129
|
+
body: JSON.stringify(buildCompletionRequestBody({
|
|
2130
|
+
protocol,
|
|
2095
2131
|
model: stringValue(model, "model_id"),
|
|
2096
2132
|
messages: completionMessages,
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2133
|
+
parameters,
|
|
2134
|
+
tools,
|
|
2135
|
+
toolChoice
|
|
2136
|
+
})),
|
|
2100
2137
|
signal: controller.signal
|
|
2101
2138
|
});
|
|
2102
2139
|
return { ok: response.ok, status: response.status, body: await response.text() };
|
|
@@ -2115,7 +2152,7 @@ export class AiManager {
|
|
|
2115
2152
|
});
|
|
2116
2153
|
if (candidate.ok) {
|
|
2117
2154
|
try {
|
|
2118
|
-
const parsed = redactProviderSecrets(JSON.parse(candidate.body), apiKey);
|
|
2155
|
+
const parsed = parseCompletionPayload(protocol, redactProviderSecrets(JSON.parse(candidate.body), apiKey));
|
|
2119
2156
|
traceAttempt.completedAt = now();
|
|
2120
2157
|
traceAttempt.status = "completed";
|
|
2121
2158
|
traceAttempt.httpStatus = candidate.status;
|
|
@@ -2132,7 +2169,7 @@ export class AiManager {
|
|
|
2132
2169
|
return parsed;
|
|
2133
2170
|
}
|
|
2134
2171
|
catch {
|
|
2135
|
-
throw new Error(
|
|
2172
|
+
throw new Error(`${protocol === "anthropic-messages" ? "Anthropic Messages" : "Chat Completions"} returned invalid JSON: ${candidate.body.slice(0, 500)}`);
|
|
2136
2173
|
}
|
|
2137
2174
|
}
|
|
2138
2175
|
lastFailure = new Error(`HTTP ${candidate.status}: ${candidate.body.slice(0, 500)}`);
|
|
@@ -2211,7 +2248,8 @@ export class AiManager {
|
|
|
2211
2248
|
role: "assistant",
|
|
2212
2249
|
content: choice.message.content ?? null,
|
|
2213
2250
|
reasoning_content: choice.message.reasoning_content ?? null,
|
|
2214
|
-
tool_calls: normalizedToolCalls
|
|
2251
|
+
tool_calls: normalizedToolCalls,
|
|
2252
|
+
...(choice.message.anthropic_content?.length ? { anthropic_content: choice.message.anthropic_content } : {})
|
|
2215
2253
|
});
|
|
2216
2254
|
for (const toolCall of toolCalls) {
|
|
2217
2255
|
const execution = this.executeAgentTool(input.workId, toolCall);
|
|
@@ -2244,7 +2282,7 @@ export class AiManager {
|
|
|
2244
2282
|
const suffix = choice?.finish_reason === "length" || reasoningLength > 0
|
|
2245
2283
|
? `;模型已生成 ${reasoningLength} 个推理字符,请提高 max_tokens 输出预算`
|
|
2246
2284
|
: "";
|
|
2247
|
-
throw new Error(
|
|
2285
|
+
throw new Error(`${protocol === "anthropic-messages" ? "Anthropic Messages" : "Chat Completions"} 响应缺少可用正文,finish_reason=${choice?.finish_reason ?? "unknown"}${suffix}`);
|
|
2248
2286
|
}
|
|
2249
2287
|
this.store.db.run("UPDATE ai_calls SET status = 'completed', output_chars = ?, completed_at = ? WHERE id = ?", content.length, now(), callId);
|
|
2250
2288
|
const outputTokens = resolveOutputTokens(payload.usage, content);
|
|
@@ -2290,19 +2328,23 @@ export class AiManager {
|
|
|
2290
2328
|
this.store.db.run(`INSERT INTO ai_calls (id, work_id, task_type, provider_id, model_id, context_scope_json, parameters_json,
|
|
2291
2329
|
status, input_chars, created_at, created_by_user_id) VALUES (?, ?, ?, ?, ?, ?, ?, 'running', ?, ?, ?)`, callId, input.workId, input.taskType, stringValue(provider, "id"), stringValue(model, "id"), JSON.stringify(input.scope), JSON.stringify(parameters), context.length + input.instruction.length, now(), currentRequestActor()?.userId ?? null);
|
|
2292
2330
|
const callStartedAt = process.hrtime.bigint();
|
|
2331
|
+
const protocol = providerProtocol(provider);
|
|
2293
2332
|
logger.info("ai.call.started", {
|
|
2294
2333
|
callId,
|
|
2295
2334
|
workId: input.workId,
|
|
2296
2335
|
taskType: input.taskType,
|
|
2297
2336
|
providerId: stringValue(provider, "id"),
|
|
2298
2337
|
modelId: stringValue(model, "id"),
|
|
2338
|
+
protocol,
|
|
2299
2339
|
streaming: true,
|
|
2300
2340
|
contextChars: context.length,
|
|
2301
2341
|
instructionChars: input.instruction.length
|
|
2302
2342
|
});
|
|
2343
|
+
let activeApiKey = "";
|
|
2303
2344
|
try {
|
|
2304
2345
|
const apiKey = this.decryptKey(provider);
|
|
2305
|
-
|
|
2346
|
+
activeApiKey = apiKey;
|
|
2347
|
+
const endpoint = providerCompletionEndpoint(stringValue(provider, "base_url"), protocol);
|
|
2306
2348
|
const maximumAttempts = Math.round(clamp(input.maxAttempts ?? 3, 1, 5));
|
|
2307
2349
|
let streamedResult = null;
|
|
2308
2350
|
let lastFailure = null;
|
|
@@ -2324,13 +2366,19 @@ export class AiManager {
|
|
|
2324
2366
|
try {
|
|
2325
2367
|
const response = await this.outboundFetch(endpoint, {
|
|
2326
2368
|
method: "POST",
|
|
2327
|
-
headers:
|
|
2328
|
-
body: JSON.stringify(
|
|
2369
|
+
headers: providerRequestHeaders(protocol, apiKey, "text/event-stream"),
|
|
2370
|
+
body: JSON.stringify(buildCompletionRequestBody({
|
|
2371
|
+
protocol,
|
|
2372
|
+
model: stringValue(model, "model_id"),
|
|
2373
|
+
messages,
|
|
2374
|
+
parameters,
|
|
2375
|
+
stream: true
|
|
2376
|
+
})),
|
|
2329
2377
|
signal: controller.signal
|
|
2330
2378
|
});
|
|
2331
2379
|
if (!response.ok)
|
|
2332
2380
|
return { ok: false, status: response.status, body: await response.text() };
|
|
2333
|
-
const streamed = await this.readCompletionStream(response, (delta) => {
|
|
2381
|
+
const streamed = await this.readCompletionStream(response, protocol, (delta) => {
|
|
2334
2382
|
emitted = true;
|
|
2335
2383
|
onDelta(delta);
|
|
2336
2384
|
}, (delta) => {
|
|
@@ -2395,7 +2443,7 @@ export class AiManager {
|
|
|
2395
2443
|
return { callId, content, outputTokens, ...(cacheHitPercent === undefined ? {} : { cacheHitPercent }), provider: this.mapProvider(provider), model: this.mapModel(model), context, toolCalls: [], processSteps };
|
|
2396
2444
|
}
|
|
2397
2445
|
catch (error) {
|
|
2398
|
-
const message = error instanceof Error ? error.message : "AI 流式调用失败";
|
|
2446
|
+
const message = error instanceof Error ? redactProviderSecret(error.message, activeApiKey) : "AI 流式调用失败";
|
|
2399
2447
|
this.store.db.run("UPDATE ai_calls SET status = 'failed', failure = ?, completed_at = ? WHERE id = ?", message, now(), callId);
|
|
2400
2448
|
logger.error("ai.call.failed", {
|
|
2401
2449
|
callId,
|
|
@@ -2408,9 +2456,10 @@ export class AiManager {
|
|
|
2408
2456
|
throw new AppError(502, "AI_CALL_FAILED", "AI 调用失败", { callId, failure: message });
|
|
2409
2457
|
}
|
|
2410
2458
|
}
|
|
2411
|
-
async readCompletionStream(response, onDelta, onThinkingDelta) {
|
|
2459
|
+
async readCompletionStream(response, protocol, onDelta, onThinkingDelta) {
|
|
2460
|
+
const protocolLabel = protocol === "anthropic-messages" ? "Anthropic Messages" : "Chat Completions";
|
|
2412
2461
|
if (!response.body)
|
|
2413
|
-
throw new Error(
|
|
2462
|
+
throw new Error(`${protocolLabel} 流式响应缺少正文`);
|
|
2414
2463
|
const reader = response.body.getReader();
|
|
2415
2464
|
const decoder = new TextDecoder();
|
|
2416
2465
|
let buffer = "";
|
|
@@ -2427,19 +2476,59 @@ export class AiManager {
|
|
|
2427
2476
|
if (!data || data === "[DONE]")
|
|
2428
2477
|
return;
|
|
2429
2478
|
const payload = JSON.parse(data);
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
if (
|
|
2479
|
+
const error = payload.error && typeof payload.error === "object" && !Array.isArray(payload.error)
|
|
2480
|
+
? payload.error
|
|
2481
|
+
: null;
|
|
2482
|
+
if (error)
|
|
2483
|
+
throw new Error(typeof error.message === "string" ? error.message : "上游流式响应返回错误");
|
|
2484
|
+
if (protocol === "anthropic-messages") {
|
|
2485
|
+
const eventUsage = payload.usage && typeof payload.usage === "object" && !Array.isArray(payload.usage)
|
|
2486
|
+
? payload.usage
|
|
2487
|
+
: null;
|
|
2488
|
+
const message = payload.message && typeof payload.message === "object" && !Array.isArray(payload.message)
|
|
2489
|
+
? payload.message
|
|
2490
|
+
: null;
|
|
2491
|
+
const messageUsage = message?.usage && typeof message.usage === "object" && !Array.isArray(message.usage)
|
|
2492
|
+
? message.usage
|
|
2493
|
+
: null;
|
|
2494
|
+
if (eventUsage || messageUsage) {
|
|
2495
|
+
usage = { ...(usage && typeof usage === "object" ? usage : {}), ...(messageUsage ?? {}), ...(eventUsage ?? {}) };
|
|
2496
|
+
}
|
|
2497
|
+
const eventDelta = payload.delta && typeof payload.delta === "object" && !Array.isArray(payload.delta)
|
|
2498
|
+
? payload.delta
|
|
2499
|
+
: {};
|
|
2500
|
+
if (typeof eventDelta.stop_reason === "string")
|
|
2501
|
+
finishReason = eventDelta.stop_reason;
|
|
2502
|
+
if (eventDelta.type === "thinking_delta" && typeof eventDelta.thinking === "string" && eventDelta.thinking.length > 0) {
|
|
2503
|
+
reasoning += eventDelta.thinking;
|
|
2504
|
+
onThinkingDelta(eventDelta.thinking);
|
|
2505
|
+
}
|
|
2506
|
+
if (eventDelta.type === "text_delta" && typeof eventDelta.text === "string" && eventDelta.text.length > 0) {
|
|
2507
|
+
content += eventDelta.text;
|
|
2508
|
+
onDelta(eventDelta.text);
|
|
2509
|
+
}
|
|
2510
|
+
return;
|
|
2511
|
+
}
|
|
2512
|
+
const streamUsage = payload.usage && typeof payload.usage === "object" && !Array.isArray(payload.usage)
|
|
2513
|
+
? payload.usage
|
|
2514
|
+
: null;
|
|
2515
|
+
if (streamUsage)
|
|
2516
|
+
usage = streamUsage;
|
|
2517
|
+
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
2518
|
+
const choice = choices[0] && typeof choices[0] === "object" && !Array.isArray(choices[0])
|
|
2519
|
+
? choices[0]
|
|
2520
|
+
: null;
|
|
2521
|
+
if (typeof choice?.finish_reason === "string")
|
|
2436
2522
|
finishReason = choice.finish_reason;
|
|
2437
|
-
const
|
|
2523
|
+
const deltaRecord = choice?.delta && typeof choice.delta === "object" && !Array.isArray(choice.delta)
|
|
2524
|
+
? choice.delta
|
|
2525
|
+
: {};
|
|
2526
|
+
const thinkingDelta = deltaRecord.reasoning_content;
|
|
2438
2527
|
if (typeof thinkingDelta === "string" && thinkingDelta.length > 0) {
|
|
2439
2528
|
reasoning += thinkingDelta;
|
|
2440
2529
|
onThinkingDelta(thinkingDelta);
|
|
2441
2530
|
}
|
|
2442
|
-
const delta =
|
|
2531
|
+
const delta = deltaRecord.content;
|
|
2443
2532
|
if (typeof delta === "string" && delta.length > 0) {
|
|
2444
2533
|
content += delta;
|
|
2445
2534
|
onDelta(delta);
|
|
@@ -2458,7 +2547,7 @@ export class AiManager {
|
|
|
2458
2547
|
if (buffer.trim())
|
|
2459
2548
|
consumeEvent(buffer);
|
|
2460
2549
|
if (!content.trim())
|
|
2461
|
-
throw new Error(
|
|
2550
|
+
throw new Error(`${protocolLabel} 流式响应缺少可用正文,finish_reason=${finishReason}`);
|
|
2462
2551
|
const cacheHitPercent = resolveCacheHitPercent(usage);
|
|
2463
2552
|
return { content, reasoning, outputTokens: resolveOutputTokens(usage, content), ...(cacheHitPercent === undefined ? {} : { cacheHitPercent }) };
|
|
2464
2553
|
}
|
|
@@ -5610,6 +5699,7 @@ export class AiManager {
|
|
|
5610
5699
|
scope: "platform",
|
|
5611
5700
|
name: stringValue(row, "name"),
|
|
5612
5701
|
baseUrl: stringValue(row, "base_url"),
|
|
5702
|
+
protocol: providerProtocol(row),
|
|
5613
5703
|
apiKey: stringValue(row, "key_hint"),
|
|
5614
5704
|
status: stringValue(row, "status"),
|
|
5615
5705
|
connectionStatus: stringValue(row, "connection_status"),
|