@klarkxy/dsh-ai-services 0.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/LICENSE +41 -0
- package/README.md +39 -0
- package/cordis.patch.yml +4 -0
- package/docs/README.zh-CN.md +9 -0
- package/lib/client-utils.d.ts +52 -0
- package/lib/client-utils.js +77 -0
- package/lib/client-utils.js.map +1 -0
- package/lib/contracts.d.ts +230 -0
- package/lib/contracts.js +21 -0
- package/lib/contracts.js.map +1 -0
- package/lib/host-rpc.d.ts +20 -0
- package/lib/host-rpc.js +95 -0
- package/lib/host-rpc.js.map +1 -0
- package/lib/index.d.ts +77 -0
- package/lib/index.js +1299 -0
- package/lib/index.js.map +1 -0
- package/lib/insert-output.d.ts +26 -0
- package/lib/insert-output.js +63 -0
- package/lib/insert-output.js.map +1 -0
- package/package.json +111 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1299 @@
|
|
|
1
|
+
import { AI_RPC_CHANNEL, CHAT_EVENTS_SLOT, MODEL_SETTINGS_SLOT, producerMessageSource } from "./contracts.js";
|
|
2
|
+
import { registerHostRpc } from "./host-rpc.js";
|
|
3
|
+
import { createInsertCollector } from "./insert-output.js";
|
|
4
|
+
import { BlockAssembler, LlmError, ReasoningEffortId, createUserMessage, isAgentLoopRequest } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
8
|
+
//#region src/errors.ts
|
|
9
|
+
var AiServicesError = class extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(message, code) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "AiServicesError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const AI_UNKNOWN_PURPOSE = "AI_UNKNOWN_PURPOSE";
|
|
18
|
+
const AI_UNKNOWN_ROLE = "AI_UNKNOWN_ROLE";
|
|
19
|
+
const AI_ROLE_UNSET = "AI_ROLE_UNSET";
|
|
20
|
+
const AI_INVALID_ROUTE = "AI_INVALID_ROUTE";
|
|
21
|
+
const AI_SESSION_UNAVAILABLE = "AI_SESSION_UNAVAILABLE";
|
|
22
|
+
const AI_SESSION_INVALID = "AI_SESSION_INVALID";
|
|
23
|
+
const AI_POLICY_CONFLICT = "AI_POLICY_CONFLICT";
|
|
24
|
+
const AI_POLICY_INVALID = "AI_POLICY_INVALID";
|
|
25
|
+
const AI_POLICY_SAVE_FAILED = "AI_POLICY_SAVE_FAILED";
|
|
26
|
+
const AI_DUPLICATE_PURPOSE = "AI_DUPLICATE_PURPOSE";
|
|
27
|
+
const AI_INACTIVE = "AI_INACTIVE";
|
|
28
|
+
function fail$1(code, message) {
|
|
29
|
+
throw new AiServicesError(message, code);
|
|
30
|
+
}
|
|
31
|
+
function isAbortError(error) {
|
|
32
|
+
if (!error || typeof error !== "object") return false;
|
|
33
|
+
const name = "name" in error ? String(error.name) : "";
|
|
34
|
+
const code = "code" in error ? String(error.code) : "";
|
|
35
|
+
return name === "AbortError" || name === "TimeoutError" || code === "ABORT_ERR" || code === "ABORTED";
|
|
36
|
+
}
|
|
37
|
+
async function abortable(work, signal) {
|
|
38
|
+
signal.throwIfAborted();
|
|
39
|
+
let off = () => {};
|
|
40
|
+
const aborted = new Promise((_, reject) => {
|
|
41
|
+
const onAbort = () => reject(signal.reason instanceof Error ? signal.reason : Object.assign(/* @__PURE__ */ new Error("This operation was aborted"), { name: "AbortError" }));
|
|
42
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
43
|
+
off = () => signal.removeEventListener("abort", onAbort);
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
return await Promise.race([Promise.resolve().then(work), aborted]);
|
|
47
|
+
} finally {
|
|
48
|
+
off();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function publicCallError(code) {
|
|
52
|
+
if (code === "NO_ADAPTER") return "指定的模型供应不可用,未改用其他模型。";
|
|
53
|
+
if (code === "UNSUPPORTED_REASONING_EFFORT") return "该模型不支持所选推理强度,未改用其他设置。";
|
|
54
|
+
if (code === "INVALID_CREDENTIAL" || code === "AUTH" || code === "MISSING_CREDENTIAL") return "模型凭据不可用。";
|
|
55
|
+
if (code === "QUOTA") return "模型额度不足。";
|
|
56
|
+
if (code === "CONTEXT_WINDOW_EXCEEDED") return "输入超出模型上下文。";
|
|
57
|
+
return "模型调用失败。";
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/storage.ts
|
|
61
|
+
const POLICY_KEY = "current";
|
|
62
|
+
const DEFAULT_LIMITS = {
|
|
63
|
+
concurrency: 1,
|
|
64
|
+
timeoutMs: 6e4,
|
|
65
|
+
maxInputChars: 32e3,
|
|
66
|
+
maxOutputTokens: 2048,
|
|
67
|
+
maxAttempts: 2
|
|
68
|
+
};
|
|
69
|
+
const roleSchema = z.enum([
|
|
70
|
+
"normal",
|
|
71
|
+
"weak",
|
|
72
|
+
"strong",
|
|
73
|
+
"fantasy"
|
|
74
|
+
]);
|
|
75
|
+
const modelRouteSchema = z.object({
|
|
76
|
+
provider: z.string().trim().min(1).max(128),
|
|
77
|
+
model: z.string().trim().min(1).max(256),
|
|
78
|
+
reasoningEffort: z.string().trim().min(1).max(64).optional()
|
|
79
|
+
}).strict();
|
|
80
|
+
const modelTargetSchema = z.discriminatedUnion("kind", [
|
|
81
|
+
z.object({
|
|
82
|
+
kind: z.literal("role"),
|
|
83
|
+
role: roleSchema
|
|
84
|
+
}).strict(),
|
|
85
|
+
z.object({ kind: z.literal("session") }).strict(),
|
|
86
|
+
z.object({ kind: z.literal("model") }).merge(modelRouteSchema).strict()
|
|
87
|
+
]);
|
|
88
|
+
const limitsSchema = z.object({
|
|
89
|
+
concurrency: z.number().int().min(1).max(8),
|
|
90
|
+
timeoutMs: z.number().int().min(1e3).max(3e5),
|
|
91
|
+
maxInputChars: z.number().int().min(1).max(2e5),
|
|
92
|
+
maxOutputTokens: z.number().int().min(1).max(8192),
|
|
93
|
+
maxAttempts: z.number().int().min(1).max(5)
|
|
94
|
+
}).strict();
|
|
95
|
+
const policyDataSchema = z.object({
|
|
96
|
+
roles: z.object({
|
|
97
|
+
normal: modelRouteSchema.optional(),
|
|
98
|
+
weak: modelRouteSchema.optional(),
|
|
99
|
+
strong: modelRouteSchema.optional(),
|
|
100
|
+
fantasy: modelRouteSchema.optional()
|
|
101
|
+
}).strict(),
|
|
102
|
+
purposes: z.record(z.string().min(1).max(80), modelTargetSchema).refine((value) => Object.keys(value).length <= 200),
|
|
103
|
+
limits: limitsSchema
|
|
104
|
+
}).strict();
|
|
105
|
+
const storedPolicySchema = policyDataSchema.extend({ revision: z.number().int().nonnegative() }).strict().extend({ imports: z.array(z.string().min(1).max(80)).max(100).optional() }).strict();
|
|
106
|
+
const updatePolicySchema = z.object({
|
|
107
|
+
expectedRevision: z.number().int().nonnegative(),
|
|
108
|
+
policy: policyDataSchema
|
|
109
|
+
}).strict();
|
|
110
|
+
const resolveRpcSchema = z.object({
|
|
111
|
+
purpose: z.string().min(1).max(80),
|
|
112
|
+
sessionId: z.string().min(1).max(128).optional(),
|
|
113
|
+
override: modelTargetSchema.optional()
|
|
114
|
+
}).strict();
|
|
115
|
+
const usageReceiptSchema = z.object({
|
|
116
|
+
id: z.string().min(1).max(80),
|
|
117
|
+
plugin: z.string().min(1).max(80),
|
|
118
|
+
purpose: z.string().min(1).max(80),
|
|
119
|
+
sessionId: z.string().max(128).optional(),
|
|
120
|
+
sourceVersion: z.string().max(128),
|
|
121
|
+
contractVersion: z.number().int().optional(),
|
|
122
|
+
promptVersion: z.string().max(80).optional(),
|
|
123
|
+
schemaVersion: z.string().max(80).optional(),
|
|
124
|
+
route: z.object({
|
|
125
|
+
provider: z.string(),
|
|
126
|
+
model: z.string(),
|
|
127
|
+
reasoningEffort: z.string().optional(),
|
|
128
|
+
source: z.enum([
|
|
129
|
+
"override",
|
|
130
|
+
"purpose",
|
|
131
|
+
"default"
|
|
132
|
+
]),
|
|
133
|
+
target: modelTargetSchema,
|
|
134
|
+
policyRevision: z.number().int().nonnegative(),
|
|
135
|
+
inheritedRole: roleSchema.optional()
|
|
136
|
+
}).strict().optional(),
|
|
137
|
+
status: z.enum([
|
|
138
|
+
"success",
|
|
139
|
+
"failed",
|
|
140
|
+
"cancelled",
|
|
141
|
+
"superseded",
|
|
142
|
+
"skipped"
|
|
143
|
+
]),
|
|
144
|
+
attempts: z.number().int().nonnegative(),
|
|
145
|
+
inputTokens: z.number().int().nonnegative().optional(),
|
|
146
|
+
outputTokens: z.number().int().nonnegative().optional(),
|
|
147
|
+
cost: z.null(),
|
|
148
|
+
startedAt: z.number(),
|
|
149
|
+
finishedAt: z.number(),
|
|
150
|
+
error: z.string().max(240).optional()
|
|
151
|
+
}).strict();
|
|
152
|
+
const storedReceiptsSchema = z.object({ items: z.array(usageReceiptSchema).max(100) }).strict();
|
|
153
|
+
const purposeSpecSchema = z.object({
|
|
154
|
+
id: z.string().regex(/^[A-Za-z][A-Za-z0-9._:-]{0,79}$/),
|
|
155
|
+
label: z.string().trim().min(1).max(80),
|
|
156
|
+
defaultTarget: modelTargetSchema,
|
|
157
|
+
maxOutputTokens: z.number().int().min(1).max(8192).optional(),
|
|
158
|
+
maxInputChars: z.number().int().min(1).max(2e5).optional(),
|
|
159
|
+
timeoutMs: z.number().int().min(1e3).max(3e5).optional()
|
|
160
|
+
}).strict();
|
|
161
|
+
function defaultPolicy() {
|
|
162
|
+
return {
|
|
163
|
+
revision: 0,
|
|
164
|
+
roles: {},
|
|
165
|
+
purposes: {},
|
|
166
|
+
limits: { ...DEFAULT_LIMITS }
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function clonePolicy(policy) {
|
|
170
|
+
return structuredClone(policy);
|
|
171
|
+
}
|
|
172
|
+
function parsePolicyData(value) {
|
|
173
|
+
const parsed = policyDataSchema.safeParse(value);
|
|
174
|
+
if (!parsed.success) fail$1(AI_POLICY_INVALID, "模型策略格式无效。");
|
|
175
|
+
return parsed.data;
|
|
176
|
+
}
|
|
177
|
+
function sanitizeReceipt(receipt) {
|
|
178
|
+
return usageReceiptSchema.parse({
|
|
179
|
+
...receipt,
|
|
180
|
+
cost: null,
|
|
181
|
+
error: receipt.error?.slice(0, 240)
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function boundedReceipts(items) {
|
|
185
|
+
return storedReceiptsSchema.parse({ items: items.map(sanitizeReceipt).slice(-100) }).items;
|
|
186
|
+
}
|
|
187
|
+
const aiServicesDomain = defineDomain({
|
|
188
|
+
name: "dsh_editor_ai_services",
|
|
189
|
+
version: 1,
|
|
190
|
+
tables: {
|
|
191
|
+
policy: domainTable(storedPolicySchema),
|
|
192
|
+
receipts: domainTable(storedReceiptsSchema)
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
function isModelRole(value) {
|
|
196
|
+
return value === "normal" || value === "weak" || value === "strong" || value === "fantasy";
|
|
197
|
+
}
|
|
198
|
+
function cloneRoute(route) {
|
|
199
|
+
return route.reasoningEffort ? {
|
|
200
|
+
provider: route.provider,
|
|
201
|
+
model: route.model,
|
|
202
|
+
reasoningEffort: route.reasoningEffort
|
|
203
|
+
} : {
|
|
204
|
+
provider: route.provider,
|
|
205
|
+
model: route.model
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function clonePurpose(spec) {
|
|
209
|
+
return structuredClone(spec);
|
|
210
|
+
}
|
|
211
|
+
//#endregion
|
|
212
|
+
//#region src/rpc.ts
|
|
213
|
+
function fail(code, message) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
error: {
|
|
217
|
+
code,
|
|
218
|
+
message
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function mapError(error) {
|
|
223
|
+
if (error instanceof AiServicesError) return fail(error.code, error.message);
|
|
224
|
+
return fail("AI_REQUEST_FAILED", "操作失败,请刷新后重试。");
|
|
225
|
+
}
|
|
226
|
+
async function handleAiRpc(service, endpoint, payload, signal) {
|
|
227
|
+
try {
|
|
228
|
+
signal.throwIfAborted();
|
|
229
|
+
if (endpoint === "status") {
|
|
230
|
+
if (payload !== void 0 && payload !== null && (typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length > 0)) return fail("AI_INVALID_REQUEST", "未知操作。");
|
|
231
|
+
return {
|
|
232
|
+
ok: true,
|
|
233
|
+
value: {
|
|
234
|
+
policy: service.getPolicy(),
|
|
235
|
+
purposes: service.purposes(),
|
|
236
|
+
storageFailed: Boolean(service.storageFailedFlag)
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
if (endpoint === "update") {
|
|
241
|
+
const parsed = updatePolicySchema.safeParse(payload);
|
|
242
|
+
if (!parsed.success) return fail("AI_INVALID_REQUEST", "策略格式无效。");
|
|
243
|
+
return {
|
|
244
|
+
ok: true,
|
|
245
|
+
value: await service.updatePolicy(parsed.data.policy, parsed.data.expectedRevision)
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (endpoint === "resolve") {
|
|
249
|
+
const parsed = resolveRpcSchema.safeParse(payload);
|
|
250
|
+
if (!parsed.success) return fail("AI_INVALID_REQUEST", "路由请求无效。");
|
|
251
|
+
return {
|
|
252
|
+
ok: true,
|
|
253
|
+
value: await service.resolve(parsed.data.purpose, parsed.data.sessionId, parsed.data.override)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (endpoint === "usage") {
|
|
257
|
+
if (payload !== void 0 && payload !== null && (typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length > 0)) return fail("AI_INVALID_REQUEST", "未知操作。");
|
|
258
|
+
return {
|
|
259
|
+
ok: true,
|
|
260
|
+
value: service.usage()
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
return fail("AI_INVALID_REQUEST", "未知操作。");
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (signal.aborted) return fail("AI_CANCELLED", "请求已取消。");
|
|
266
|
+
return mapError(error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
//#endregion
|
|
270
|
+
//#region src/generate.ts
|
|
271
|
+
const PERMANENT_CODES = /* @__PURE__ */ new Set([
|
|
272
|
+
"NO_ADAPTER",
|
|
273
|
+
"UNSUPPORTED_REASONING_EFFORT",
|
|
274
|
+
"INVALID_PREPARED_CALL",
|
|
275
|
+
"INVALID_CREDENTIAL",
|
|
276
|
+
"AUTH",
|
|
277
|
+
"MISSING_CREDENTIAL",
|
|
278
|
+
"QUOTA",
|
|
279
|
+
"CONTEXT_WINDOW_EXCEEDED"
|
|
280
|
+
]);
|
|
281
|
+
function joinText(assembler) {
|
|
282
|
+
return assembler.blocks().filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
283
|
+
}
|
|
284
|
+
async function consume(stream, signal, live, insert) {
|
|
285
|
+
const assembler = new BlockAssembler();
|
|
286
|
+
let sawFinish = false;
|
|
287
|
+
const collector = insert ? createInsertCollector(insert.maxChars) : void 0;
|
|
288
|
+
let boundedText;
|
|
289
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
290
|
+
try {
|
|
291
|
+
while (live()) {
|
|
292
|
+
const item = await abortable(() => iterator.next(), signal);
|
|
293
|
+
if (item.done) break;
|
|
294
|
+
if (item.value.type === "finish") sawFinish = true;
|
|
295
|
+
assembler.push(item.value);
|
|
296
|
+
if (collector && item.value.type === "text-delta") {
|
|
297
|
+
collector.append(item.value.text);
|
|
298
|
+
if (collector.full && !sawFinish && !assembler.blocks().some((block) => block.type === "tool-call")) {
|
|
299
|
+
boundedText = collector.text();
|
|
300
|
+
insert.stop();
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} finally {
|
|
306
|
+
try {
|
|
307
|
+
const closing = iterator.return?.();
|
|
308
|
+
if (closing) closing.catch(() => {});
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
return {
|
|
312
|
+
assembler,
|
|
313
|
+
sawFinish,
|
|
314
|
+
boundedText,
|
|
315
|
+
visibleText: collector?.text()
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
function outcomeFromAssembler(assembler, signal, sawFinish) {
|
|
319
|
+
if (signal.aborted) return {
|
|
320
|
+
text: "",
|
|
321
|
+
status: "cancelled",
|
|
322
|
+
error: "调用已取消。"
|
|
323
|
+
};
|
|
324
|
+
const finish = assembler.finish;
|
|
325
|
+
const usage = assembler.usage;
|
|
326
|
+
const tokens = usage ? {
|
|
327
|
+
inputTokens: usage.inputTokens,
|
|
328
|
+
outputTokens: usage.outputTokens
|
|
329
|
+
} : {};
|
|
330
|
+
if (finish.kind === "aborted") return {
|
|
331
|
+
text: "",
|
|
332
|
+
status: "cancelled",
|
|
333
|
+
error: "调用已取消。",
|
|
334
|
+
...tokens
|
|
335
|
+
};
|
|
336
|
+
if (finish.kind === "error") return {
|
|
337
|
+
text: "",
|
|
338
|
+
status: "failed",
|
|
339
|
+
error: publicCallError(finish.failure.code),
|
|
340
|
+
...tokens
|
|
341
|
+
};
|
|
342
|
+
if (!sawFinish) return {
|
|
343
|
+
text: "",
|
|
344
|
+
status: "failed",
|
|
345
|
+
error: "模型调用未完成。",
|
|
346
|
+
...tokens
|
|
347
|
+
};
|
|
348
|
+
if (finish.kind === "max-tokens") return {
|
|
349
|
+
text: "",
|
|
350
|
+
status: "failed",
|
|
351
|
+
error: "输出被截断。",
|
|
352
|
+
...tokens
|
|
353
|
+
};
|
|
354
|
+
if (finish.kind === "tool-calls" || assembler.blocks().some((block) => block.type === "tool-call")) return {
|
|
355
|
+
text: "",
|
|
356
|
+
status: "failed",
|
|
357
|
+
error: "辅助调用不使用工具。",
|
|
358
|
+
...tokens
|
|
359
|
+
};
|
|
360
|
+
if (finish.kind !== "stop") return {
|
|
361
|
+
text: "",
|
|
362
|
+
status: "failed",
|
|
363
|
+
error: "模型调用失败。",
|
|
364
|
+
...tokens
|
|
365
|
+
};
|
|
366
|
+
return {
|
|
367
|
+
text: joinText(assembler),
|
|
368
|
+
status: "success",
|
|
369
|
+
...tokens
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
async function generateAuxiliary(input) {
|
|
373
|
+
const { llm, plugin, route, system, text, maxTokens, maxAttempts, signal, sessionId, live } = input;
|
|
374
|
+
let last = {
|
|
375
|
+
text: "",
|
|
376
|
+
attempts: 0,
|
|
377
|
+
status: "failed",
|
|
378
|
+
error: "模型调用失败。"
|
|
379
|
+
};
|
|
380
|
+
const boundAttempts = Math.max(1, Math.min(5, maxAttempts));
|
|
381
|
+
for (let attempt = 1; attempt <= boundAttempts; attempt += 1) {
|
|
382
|
+
if (signal.aborted) return {
|
|
383
|
+
text: "",
|
|
384
|
+
attempts: attempt - 1,
|
|
385
|
+
status: "cancelled",
|
|
386
|
+
error: "调用已取消。"
|
|
387
|
+
};
|
|
388
|
+
if (!live()) return {
|
|
389
|
+
...last,
|
|
390
|
+
attempts: attempt - 1
|
|
391
|
+
};
|
|
392
|
+
try {
|
|
393
|
+
const effort = route.reasoningEffort ? ReasoningEffortId(route.reasoningEffort) : void 0;
|
|
394
|
+
const prepared = await llm.prepareCall({
|
|
395
|
+
provider: route.provider,
|
|
396
|
+
model: route.model,
|
|
397
|
+
...effort ? { reasoningEffort: effort } : {},
|
|
398
|
+
maxTokens
|
|
399
|
+
}, signal);
|
|
400
|
+
if (signal.aborted) return {
|
|
401
|
+
text: "",
|
|
402
|
+
attempts: attempt - 1,
|
|
403
|
+
status: "cancelled",
|
|
404
|
+
error: "调用已取消。"
|
|
405
|
+
};
|
|
406
|
+
if (!live()) return {
|
|
407
|
+
...last,
|
|
408
|
+
attempts: attempt - 1
|
|
409
|
+
};
|
|
410
|
+
const outputStop = new AbortController();
|
|
411
|
+
const outputSignal = AbortSignal.any([signal, outputStop.signal]);
|
|
412
|
+
const options = {
|
|
413
|
+
provider: prepared.config.provider,
|
|
414
|
+
model: prepared.config.model,
|
|
415
|
+
...prepared.config.reasoningEffort ? { reasoningEffort: prepared.config.reasoningEffort } : {},
|
|
416
|
+
...prepared.config.maxTokens !== void 0 ? { maxTokens: Math.min(maxTokens, prepared.config.maxTokens) } : { maxTokens },
|
|
417
|
+
...prepared.config.temperature !== void 0 ? { temperature: prepared.config.temperature } : {},
|
|
418
|
+
...prepared.config.stop ? { stop: [...prepared.config.stop] } : {},
|
|
419
|
+
system,
|
|
420
|
+
messages: [createUserMessage({
|
|
421
|
+
source: producerMessageSource(plugin),
|
|
422
|
+
content: [{
|
|
423
|
+
type: "text",
|
|
424
|
+
text
|
|
425
|
+
}]
|
|
426
|
+
})],
|
|
427
|
+
signal: outputSignal,
|
|
428
|
+
...sessionId ? { sessionId } : {}
|
|
429
|
+
};
|
|
430
|
+
const { assembler, sawFinish, boundedText, visibleText } = await consume(prepared.stream(options), signal, live, input.insert ? {
|
|
431
|
+
...input.insert,
|
|
432
|
+
stop: () => outputStop.abort()
|
|
433
|
+
} : void 0);
|
|
434
|
+
if (boundedText !== void 0 && !signal.aborted && live()) return {
|
|
435
|
+
text: boundedText,
|
|
436
|
+
status: "success",
|
|
437
|
+
attempts: attempt,
|
|
438
|
+
...assembler.usage ? {
|
|
439
|
+
inputTokens: assembler.usage.inputTokens,
|
|
440
|
+
outputTokens: assembler.usage.outputTokens
|
|
441
|
+
} : {}
|
|
442
|
+
};
|
|
443
|
+
last = {
|
|
444
|
+
...outcomeFromAssembler(assembler, signal, sawFinish),
|
|
445
|
+
attempts: attempt
|
|
446
|
+
};
|
|
447
|
+
if (last.status === "success" && visibleText !== void 0) last.text = visibleText;
|
|
448
|
+
if (last.status !== "failed") return last;
|
|
449
|
+
if (assembler.finish.kind !== "error") return last;
|
|
450
|
+
const code = assembler.finish.failure.code;
|
|
451
|
+
if (PERMANENT_CODES.has(code)) return last;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
if (signal.aborted || isAbortError(error)) return {
|
|
454
|
+
text: "",
|
|
455
|
+
attempts: attempt,
|
|
456
|
+
status: "cancelled",
|
|
457
|
+
error: "调用已取消。"
|
|
458
|
+
};
|
|
459
|
+
if (error instanceof LlmError) {
|
|
460
|
+
last = {
|
|
461
|
+
text: "",
|
|
462
|
+
attempts: attempt,
|
|
463
|
+
status: "failed",
|
|
464
|
+
error: publicCallError(error.code)
|
|
465
|
+
};
|
|
466
|
+
if (PERMANENT_CODES.has(error.code)) return last;
|
|
467
|
+
} else last = {
|
|
468
|
+
text: "",
|
|
469
|
+
attempts: attempt,
|
|
470
|
+
status: "failed",
|
|
471
|
+
error: "模型调用失败。"
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return last;
|
|
476
|
+
}
|
|
477
|
+
//#endregion
|
|
478
|
+
//#region src/queue.ts
|
|
479
|
+
function abortError(signal) {
|
|
480
|
+
if (signal.reason instanceof Error) return signal.reason;
|
|
481
|
+
return Object.assign(/* @__PURE__ */ new Error("This operation was aborted"), { name: "AbortError" });
|
|
482
|
+
}
|
|
483
|
+
/** Per-provider limiter: interactive waiters before background; background yields to in-flight agent streams. */
|
|
484
|
+
var ProviderQueue = class {
|
|
485
|
+
limit;
|
|
486
|
+
running = 0;
|
|
487
|
+
agent = 0;
|
|
488
|
+
waiters = [];
|
|
489
|
+
constructor(limit) {
|
|
490
|
+
this.limit = limit;
|
|
491
|
+
}
|
|
492
|
+
noteAgent(delta) {
|
|
493
|
+
if (delta !== 0) this.agent = Math.max(0, this.agent + delta);
|
|
494
|
+
this.pump();
|
|
495
|
+
}
|
|
496
|
+
wake() {
|
|
497
|
+
this.pump();
|
|
498
|
+
}
|
|
499
|
+
get agentCount() {
|
|
500
|
+
return this.agent;
|
|
501
|
+
}
|
|
502
|
+
get runningCount() {
|
|
503
|
+
return this.running;
|
|
504
|
+
}
|
|
505
|
+
get waiterCount() {
|
|
506
|
+
return this.waiters.length;
|
|
507
|
+
}
|
|
508
|
+
async acquire(priority, signal) {
|
|
509
|
+
signal.throwIfAborted();
|
|
510
|
+
if (this.tryAcquire(priority)) return () => this.release();
|
|
511
|
+
await new Promise((resolve, reject) => {
|
|
512
|
+
let waiter;
|
|
513
|
+
const onAbort = () => {
|
|
514
|
+
const index = this.waiters.indexOf(waiter);
|
|
515
|
+
if (index < 0) return;
|
|
516
|
+
this.waiters.splice(index, 1);
|
|
517
|
+
waiter.dispose();
|
|
518
|
+
reject(abortError(signal));
|
|
519
|
+
};
|
|
520
|
+
waiter = {
|
|
521
|
+
priority,
|
|
522
|
+
signal,
|
|
523
|
+
resume: resolve,
|
|
524
|
+
reject,
|
|
525
|
+
dispose: () => signal.removeEventListener("abort", onAbort)
|
|
526
|
+
};
|
|
527
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
528
|
+
this.enqueue(waiter);
|
|
529
|
+
this.pump();
|
|
530
|
+
});
|
|
531
|
+
return () => this.release();
|
|
532
|
+
}
|
|
533
|
+
enqueue(waiter) {
|
|
534
|
+
if (waiter.priority === "interactive") {
|
|
535
|
+
const index = this.waiters.findIndex((item) => item.priority === "background");
|
|
536
|
+
this.waiters.splice(index === -1 ? this.waiters.length : index, 0, waiter);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this.waiters.push(waiter);
|
|
540
|
+
}
|
|
541
|
+
canStart(priority) {
|
|
542
|
+
if (this.running >= this.limit()) return false;
|
|
543
|
+
if (priority === "background" && this.agent > 0) return false;
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
tryAcquire(priority) {
|
|
547
|
+
if (!this.canStart(priority)) return false;
|
|
548
|
+
this.running += 1;
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
release() {
|
|
552
|
+
this.running = Math.max(0, this.running - 1);
|
|
553
|
+
this.pump();
|
|
554
|
+
}
|
|
555
|
+
pump() {
|
|
556
|
+
while (true) {
|
|
557
|
+
const index = this.waiters.findIndex((waiter) => this.canStart(waiter.priority));
|
|
558
|
+
if (index < 0) return;
|
|
559
|
+
const waiter = this.waiters[index];
|
|
560
|
+
if (!this.tryAcquire(waiter.priority)) return;
|
|
561
|
+
this.waiters.splice(index, 1);
|
|
562
|
+
waiter.dispose();
|
|
563
|
+
waiter.resume();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
var ProviderQueues = class {
|
|
568
|
+
limit;
|
|
569
|
+
queues = /* @__PURE__ */ new Map();
|
|
570
|
+
constructor(limit) {
|
|
571
|
+
this.limit = limit;
|
|
572
|
+
}
|
|
573
|
+
forProvider(provider) {
|
|
574
|
+
let queue = this.queues.get(provider);
|
|
575
|
+
if (!queue) {
|
|
576
|
+
queue = new ProviderQueue(this.limit);
|
|
577
|
+
this.queues.set(provider, queue);
|
|
578
|
+
}
|
|
579
|
+
return queue;
|
|
580
|
+
}
|
|
581
|
+
noteAgent(provider, delta) {
|
|
582
|
+
this.forProvider(provider).noteAgent(delta);
|
|
583
|
+
}
|
|
584
|
+
wake() {
|
|
585
|
+
for (const queue of this.queues.values()) queue.wake();
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/routing.ts
|
|
590
|
+
function streamReasoningEffort(value) {
|
|
591
|
+
if (typeof value !== "string") return void 0;
|
|
592
|
+
const id = value.trim();
|
|
593
|
+
if (!id) return void 0;
|
|
594
|
+
return id;
|
|
595
|
+
}
|
|
596
|
+
function asCallConfig(route) {
|
|
597
|
+
const effort = streamReasoningEffort(route.reasoningEffort);
|
|
598
|
+
return effort ? {
|
|
599
|
+
provider: route.provider,
|
|
600
|
+
model: route.model,
|
|
601
|
+
reasoningEffort: ReasoningEffortId(effort)
|
|
602
|
+
} : {
|
|
603
|
+
provider: route.provider,
|
|
604
|
+
model: route.model
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
async function validateRoute(llm, route, signal) {
|
|
608
|
+
if (!route.provider.trim() || !route.model.trim()) fail$1(AI_INVALID_ROUTE, "指定的模型路由不完整,未改用其他模型。");
|
|
609
|
+
try {
|
|
610
|
+
const work = () => llm.resolveCallConfig(asCallConfig(route), signal);
|
|
611
|
+
await (signal ? abortable(work, signal) : work());
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (signal?.aborted || isAbortError(error)) throw error;
|
|
614
|
+
if (error instanceof LlmError) fail$1(AI_INVALID_ROUTE, publicCallError(error.code));
|
|
615
|
+
fail$1(AI_INVALID_ROUTE, "指定的模型路由无效,未改用其他模型。");
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
function configuredRole(policy, role) {
|
|
619
|
+
const route = policy.roles[role];
|
|
620
|
+
if (!route) return void 0;
|
|
621
|
+
if (!route.provider.trim() || !route.model.trim()) return void 0;
|
|
622
|
+
return cloneRoute(route);
|
|
623
|
+
}
|
|
624
|
+
async function resolveTarget(input) {
|
|
625
|
+
const { llm, policy, target, source, sessionId, sessionModels, defaultModel, modelCenterAvailable, signal } = input;
|
|
626
|
+
const policyRevision = policy.revision;
|
|
627
|
+
if (target.kind === "model") {
|
|
628
|
+
const route = cloneRoute({
|
|
629
|
+
provider: target.provider,
|
|
630
|
+
model: target.model,
|
|
631
|
+
...streamReasoningEffort(target.reasoningEffort) ? { reasoningEffort: streamReasoningEffort(target.reasoningEffort) } : {}
|
|
632
|
+
});
|
|
633
|
+
await validateRoute(llm, route, signal);
|
|
634
|
+
return {
|
|
635
|
+
...route,
|
|
636
|
+
source,
|
|
637
|
+
target,
|
|
638
|
+
policyRevision
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
if (target.kind === "session") {
|
|
642
|
+
if (!sessionId) fail$1(AI_SESSION_UNAVAILABLE, "会话模型需要当前会话。");
|
|
643
|
+
if (!sessionModels) fail$1(AI_SESSION_UNAVAILABLE, "无法读取当前会话模型。");
|
|
644
|
+
let selected;
|
|
645
|
+
try {
|
|
646
|
+
selected = await sessionModels(sessionId, signal);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (signal?.aborted || isAbortError(error)) throw error;
|
|
649
|
+
if (error instanceof AiServicesError) throw error;
|
|
650
|
+
if (error instanceof Error && error.message) fail$1(AI_SESSION_INVALID, error.message);
|
|
651
|
+
fail$1(AI_SESSION_UNAVAILABLE, "无法读取当前会话模型。");
|
|
652
|
+
}
|
|
653
|
+
const effort = streamReasoningEffort(selected.reasoningEffort);
|
|
654
|
+
const route = effort ? {
|
|
655
|
+
provider: selected.provider,
|
|
656
|
+
model: selected.model,
|
|
657
|
+
reasoningEffort: effort
|
|
658
|
+
} : {
|
|
659
|
+
provider: selected.provider,
|
|
660
|
+
model: selected.model
|
|
661
|
+
};
|
|
662
|
+
await validateRoute(llm, route, signal);
|
|
663
|
+
return {
|
|
664
|
+
...route,
|
|
665
|
+
source,
|
|
666
|
+
target,
|
|
667
|
+
policyRevision
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
if (!isModelRole(target.role)) fail$1(AI_UNKNOWN_ROLE, "未知模型角色。");
|
|
671
|
+
let inheritedRole;
|
|
672
|
+
let route = modelCenterAvailable === false ? void 0 : configuredRole(policy, target.role);
|
|
673
|
+
if (!route) {
|
|
674
|
+
route = configuredRole(policy, "normal") ?? defaultModel?.();
|
|
675
|
+
if (!route) fail$1(AI_ROLE_UNSET, "请先设置默认对话模型。");
|
|
676
|
+
route = cloneRoute(route);
|
|
677
|
+
if (target.role !== "normal") inheritedRole = "normal";
|
|
678
|
+
}
|
|
679
|
+
await validateRoute(llm, route, signal);
|
|
680
|
+
return inheritedRole ? {
|
|
681
|
+
...route,
|
|
682
|
+
source,
|
|
683
|
+
target,
|
|
684
|
+
policyRevision,
|
|
685
|
+
inheritedRole
|
|
686
|
+
} : {
|
|
687
|
+
...route,
|
|
688
|
+
source,
|
|
689
|
+
target,
|
|
690
|
+
policyRevision
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
async function resolvePurposeRoute(input) {
|
|
694
|
+
const { llm, policy, purpose, spec, sessionId, override, sessionModels, defaultModel, modelCenterAvailable, signal } = input;
|
|
695
|
+
if (override) return resolveTarget({
|
|
696
|
+
llm,
|
|
697
|
+
policy,
|
|
698
|
+
target: override,
|
|
699
|
+
source: "override",
|
|
700
|
+
sessionId,
|
|
701
|
+
sessionModels,
|
|
702
|
+
defaultModel,
|
|
703
|
+
modelCenterAvailable,
|
|
704
|
+
signal
|
|
705
|
+
});
|
|
706
|
+
const mapped = policy.purposes[purpose];
|
|
707
|
+
if (mapped) return resolveTarget({
|
|
708
|
+
llm,
|
|
709
|
+
policy,
|
|
710
|
+
target: mapped,
|
|
711
|
+
source: "purpose",
|
|
712
|
+
sessionId,
|
|
713
|
+
sessionModels,
|
|
714
|
+
defaultModel,
|
|
715
|
+
modelCenterAvailable,
|
|
716
|
+
signal
|
|
717
|
+
});
|
|
718
|
+
if (spec) return resolveTarget({
|
|
719
|
+
llm,
|
|
720
|
+
policy,
|
|
721
|
+
target: spec.defaultTarget,
|
|
722
|
+
source: "default",
|
|
723
|
+
sessionId,
|
|
724
|
+
sessionModels,
|
|
725
|
+
defaultModel,
|
|
726
|
+
modelCenterAvailable,
|
|
727
|
+
signal
|
|
728
|
+
});
|
|
729
|
+
fail$1(AI_UNKNOWN_PURPOSE, "用途未注册。");
|
|
730
|
+
}
|
|
731
|
+
function routeFromSelection(value, message) {
|
|
732
|
+
if (typeof value?.provider !== "string" || typeof value.model !== "string") fail$1(AI_SESSION_INVALID, message);
|
|
733
|
+
const effort = streamReasoningEffort(value.reasoningEffort);
|
|
734
|
+
return effort ? {
|
|
735
|
+
provider: value.provider,
|
|
736
|
+
model: value.model,
|
|
737
|
+
reasoningEffort: effort
|
|
738
|
+
} : {
|
|
739
|
+
provider: value.provider,
|
|
740
|
+
model: value.model
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Read the live host picker. A durable pending choice wins, then the route
|
|
745
|
+
* recorded by the current request, then the host's default model.
|
|
746
|
+
*/
|
|
747
|
+
function sessionModelsFromHost(getHost) {
|
|
748
|
+
return async (sessionId, signal) => {
|
|
749
|
+
signal?.throwIfAborted();
|
|
750
|
+
const host = getHost();
|
|
751
|
+
if (!host?.agents || !host.sessionProjections) fail$1(AI_SESSION_UNAVAILABLE, "无法读取当前会话模型。");
|
|
752
|
+
const session = host.agents.get(sessionId)?.session;
|
|
753
|
+
if (!session) fail$1(AI_SESSION_UNAVAILABLE, "当前会话不在 Host 中,无法读取模型。");
|
|
754
|
+
let projection;
|
|
755
|
+
try {
|
|
756
|
+
projection = host.sessionProjections.stateOf(session, "modelSelection");
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (signal?.aborted || isAbortError(error)) throw error;
|
|
759
|
+
fail$1(AI_SESSION_UNAVAILABLE, "无法读取当前会话模型。");
|
|
760
|
+
}
|
|
761
|
+
signal?.throwIfAborted();
|
|
762
|
+
if (!projection) fail$1(AI_SESSION_UNAVAILABLE, "当前会话模型投影不可用。");
|
|
763
|
+
if (projection.pending !== null && projection.pending !== void 0) return routeFromSelection(projection.pending, "当前会话模型选择无效。");
|
|
764
|
+
const header = session.requestHeader?.();
|
|
765
|
+
const recorded = header?.config;
|
|
766
|
+
if (recorded !== void 0) return routeFromSelection(header?.adapterDefaults?.reasoningEffort === true ? {
|
|
767
|
+
provider: recorded.provider,
|
|
768
|
+
model: recorded.model
|
|
769
|
+
} : recorded, "当前会话模型记录无效。");
|
|
770
|
+
return routeFromSelection(host.agentDefaultModel?.currentSelection(), "默认对话模型不可用。");
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/service.ts
|
|
775
|
+
const PLUGIN_NAME = /^(?:@[A-Za-z0-9._-]+\/)?[A-Za-z0-9._-]+$/;
|
|
776
|
+
function pluginName(value) {
|
|
777
|
+
const id = value.trim();
|
|
778
|
+
if (!id || id.length > 80 || id.includes("..") || !PLUGIN_NAME.test(id)) fail$1(AI_POLICY_INVALID, "插件标识无效。");
|
|
779
|
+
return id;
|
|
780
|
+
}
|
|
781
|
+
function minLimit(policyValue, purposeValue) {
|
|
782
|
+
return purposeValue === void 0 ? policyValue : Math.min(policyValue, purposeValue);
|
|
783
|
+
}
|
|
784
|
+
var AiServicesRuntime = class {
|
|
785
|
+
options;
|
|
786
|
+
policy;
|
|
787
|
+
imports;
|
|
788
|
+
receipts;
|
|
789
|
+
storageFailed = false;
|
|
790
|
+
disposed = false;
|
|
791
|
+
pending = Promise.resolve();
|
|
792
|
+
purposeRegistry = /* @__PURE__ */ new Map();
|
|
793
|
+
lifetimes = /* @__PURE__ */ new Set();
|
|
794
|
+
queues;
|
|
795
|
+
now;
|
|
796
|
+
id;
|
|
797
|
+
constructor(options) {
|
|
798
|
+
this.options = options;
|
|
799
|
+
this.imports = [...options.initialImports ?? []];
|
|
800
|
+
this.policy = clonePolicy(options.initialPolicy ?? defaultPolicy());
|
|
801
|
+
this.receipts = boundedReceipts(options.initialReceipts ?? []);
|
|
802
|
+
this.now = options.now ?? Date.now;
|
|
803
|
+
this.id = options.id ?? (() => randomUUID());
|
|
804
|
+
this.queues = new ProviderQueues(() => this.policy.limits.concurrency);
|
|
805
|
+
}
|
|
806
|
+
get storageFailedFlag() {
|
|
807
|
+
return this.storageFailed;
|
|
808
|
+
}
|
|
809
|
+
noteAgent(provider, delta) {
|
|
810
|
+
this.queues.noteAgent(provider, delta);
|
|
811
|
+
}
|
|
812
|
+
getPolicy() {
|
|
813
|
+
return clonePolicy(this.policy);
|
|
814
|
+
}
|
|
815
|
+
purposes() {
|
|
816
|
+
return [...this.purposeRegistry.values()].map((item) => ({
|
|
817
|
+
...clonePurpose(item),
|
|
818
|
+
plugin: item.plugin
|
|
819
|
+
}));
|
|
820
|
+
}
|
|
821
|
+
usage() {
|
|
822
|
+
return this.receipts.map((item) => structuredClone(item));
|
|
823
|
+
}
|
|
824
|
+
importPurposes(migrationId, defaults, roles = {}) {
|
|
825
|
+
const task = this.pending.then(async () => {
|
|
826
|
+
this.assertOpen();
|
|
827
|
+
if (!/^[A-Za-z][A-Za-z0-9._:-]{0,79}$/.test(migrationId)) fail$1(AI_POLICY_INVALID, "迁移标识无效。");
|
|
828
|
+
if (this.imports.includes(migrationId)) return this.getPolicy();
|
|
829
|
+
if (this.imports.length >= 100) fail$1(AI_POLICY_INVALID, "迁移记录已满。");
|
|
830
|
+
const { revision: _revision, ...current } = this.policy;
|
|
831
|
+
const parsed = parsePolicyData({
|
|
832
|
+
...current,
|
|
833
|
+
roles: {
|
|
834
|
+
...roles,
|
|
835
|
+
...current.roles
|
|
836
|
+
},
|
|
837
|
+
purposes: {
|
|
838
|
+
...defaults,
|
|
839
|
+
...this.policy.purposes
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
const purposes = {
|
|
843
|
+
...parsed.purposes,
|
|
844
|
+
...this.policy.purposes
|
|
845
|
+
};
|
|
846
|
+
const next = {
|
|
847
|
+
...this.getPolicy(),
|
|
848
|
+
roles: parsed.roles,
|
|
849
|
+
purposes,
|
|
850
|
+
revision: this.policy.revision + 1
|
|
851
|
+
};
|
|
852
|
+
const imports = [...this.imports, migrationId];
|
|
853
|
+
try {
|
|
854
|
+
await this.options.store.savePolicy(next, imports);
|
|
855
|
+
} catch {
|
|
856
|
+
this.storageFailed = true;
|
|
857
|
+
fail$1(AI_POLICY_SAVE_FAILED, "旧模型配置迁移失败,已保留原设置。");
|
|
858
|
+
}
|
|
859
|
+
this.policy = next;
|
|
860
|
+
this.imports = imports;
|
|
861
|
+
this.storageFailed = false;
|
|
862
|
+
return this.getPolicy();
|
|
863
|
+
});
|
|
864
|
+
this.pending = task.then(() => {}, () => {});
|
|
865
|
+
return task;
|
|
866
|
+
}
|
|
867
|
+
updatePolicy(policy, expectedRevision) {
|
|
868
|
+
const task = this.pending.then(async () => {
|
|
869
|
+
if (this.disposed) fail$1(AI_INACTIVE, "AI 服务已停止。");
|
|
870
|
+
if (expectedRevision !== this.policy.revision) fail$1(AI_POLICY_CONFLICT, "策略已被其他页面修改,请刷新后重试。");
|
|
871
|
+
const parsed = parsePolicyData(policy);
|
|
872
|
+
const next = {
|
|
873
|
+
...clonePolicy({
|
|
874
|
+
revision: expectedRevision + 1,
|
|
875
|
+
...parsed
|
|
876
|
+
}),
|
|
877
|
+
revision: expectedRevision + 1
|
|
878
|
+
};
|
|
879
|
+
try {
|
|
880
|
+
await this.options.store.savePolicy(next, this.imports);
|
|
881
|
+
} catch {
|
|
882
|
+
this.storageFailed = true;
|
|
883
|
+
fail$1(AI_POLICY_SAVE_FAILED, "策略保存失败,已保留原设置。");
|
|
884
|
+
}
|
|
885
|
+
this.policy = next;
|
|
886
|
+
this.storageFailed = false;
|
|
887
|
+
this.queues.wake();
|
|
888
|
+
return clonePolicy(this.policy);
|
|
889
|
+
});
|
|
890
|
+
this.pending = task.then(() => {}, () => {});
|
|
891
|
+
return task;
|
|
892
|
+
}
|
|
893
|
+
async resolve(purpose, sessionId, override) {
|
|
894
|
+
this.assertOpen();
|
|
895
|
+
const spec = this.purposeRegistry.get(purpose);
|
|
896
|
+
return resolvePurposeRoute({
|
|
897
|
+
llm: this.options.llm,
|
|
898
|
+
policy: this.policy,
|
|
899
|
+
purpose,
|
|
900
|
+
spec,
|
|
901
|
+
sessionId,
|
|
902
|
+
override,
|
|
903
|
+
sessionModels: this.options.sessionModels,
|
|
904
|
+
defaultModel: this.options.defaultModel,
|
|
905
|
+
modelCenterAvailable: this.options.modelCenterAvailable?.()
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
activate(plugin) {
|
|
909
|
+
this.assertOpen();
|
|
910
|
+
const name = pluginName(plugin);
|
|
911
|
+
const lifetime = {
|
|
912
|
+
plugin: name,
|
|
913
|
+
controller: new AbortController(),
|
|
914
|
+
active: true,
|
|
915
|
+
purposes: /* @__PURE__ */ new Map()
|
|
916
|
+
};
|
|
917
|
+
this.lifetimes.add(lifetime);
|
|
918
|
+
return {
|
|
919
|
+
plugin: name,
|
|
920
|
+
get signal() {
|
|
921
|
+
return lifetime.controller.signal;
|
|
922
|
+
},
|
|
923
|
+
get active() {
|
|
924
|
+
return lifetime.active && !lifetime.controller.signal.aborted;
|
|
925
|
+
},
|
|
926
|
+
registerPurpose: (spec) => this.registerPurpose(lifetime, spec),
|
|
927
|
+
run: (request) => this.run(lifetime, request),
|
|
928
|
+
dispose: () => this.disposeLifetime(lifetime)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async dispose() {
|
|
932
|
+
this.disposed = true;
|
|
933
|
+
for (const lifetime of [...this.lifetimes]) this.disposeLifetime(lifetime);
|
|
934
|
+
await this.pending;
|
|
935
|
+
}
|
|
936
|
+
assertOpen() {
|
|
937
|
+
if (this.disposed) fail$1(AI_INACTIVE, "AI 服务已停止。");
|
|
938
|
+
}
|
|
939
|
+
registerPurpose(lifetime, spec) {
|
|
940
|
+
if (!lifetime.active) fail$1(AI_INACTIVE, "插件范围已停用。");
|
|
941
|
+
let parsed;
|
|
942
|
+
try {
|
|
943
|
+
parsed = purposeSpecSchema.parse(spec);
|
|
944
|
+
} catch {
|
|
945
|
+
fail$1(AI_POLICY_INVALID, "用途声明无效。");
|
|
946
|
+
}
|
|
947
|
+
const existing = this.purposeRegistry.get(parsed.id);
|
|
948
|
+
if (existing && existing.plugin !== lifetime.plugin) fail$1(AI_DUPLICATE_PURPOSE, "用途已被其他插件注册。");
|
|
949
|
+
const owned = {
|
|
950
|
+
...clonePurpose(parsed),
|
|
951
|
+
plugin: lifetime.plugin
|
|
952
|
+
};
|
|
953
|
+
lifetime.purposes.set(parsed.id, owned);
|
|
954
|
+
this.purposeRegistry.set(parsed.id, owned);
|
|
955
|
+
let removed = false;
|
|
956
|
+
return () => {
|
|
957
|
+
if (removed) return;
|
|
958
|
+
removed = true;
|
|
959
|
+
lifetime.purposes.delete(parsed.id);
|
|
960
|
+
this.refreshPurpose(parsed.id);
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
disposeLifetime(lifetime) {
|
|
964
|
+
if (!lifetime.active && !this.lifetimes.has(lifetime)) return;
|
|
965
|
+
lifetime.active = false;
|
|
966
|
+
if (!lifetime.controller.signal.aborted) lifetime.controller.abort();
|
|
967
|
+
const ids = [...lifetime.purposes.keys()];
|
|
968
|
+
lifetime.purposes.clear();
|
|
969
|
+
this.lifetimes.delete(lifetime);
|
|
970
|
+
for (const id of ids) this.refreshPurpose(id);
|
|
971
|
+
}
|
|
972
|
+
refreshPurpose(id) {
|
|
973
|
+
for (const other of this.lifetimes) {
|
|
974
|
+
const spec = other.purposes.get(id);
|
|
975
|
+
if (spec && other.active) {
|
|
976
|
+
this.purposeRegistry.set(id, {
|
|
977
|
+
...spec,
|
|
978
|
+
plugin: other.plugin
|
|
979
|
+
});
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
this.purposeRegistry.delete(id);
|
|
984
|
+
}
|
|
985
|
+
live(lifetime, request) {
|
|
986
|
+
if (!lifetime.active || lifetime.controller.signal.aborted || request.signal?.aborted) return false;
|
|
987
|
+
try {
|
|
988
|
+
if (request.isCurrent && !request.isCurrent()) return false;
|
|
989
|
+
} catch {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
classify(lifetime, request, queued) {
|
|
995
|
+
const aborted = lifetime.controller.signal.aborted || Boolean(request.signal?.aborted);
|
|
996
|
+
let current = true;
|
|
997
|
+
try {
|
|
998
|
+
current = !request.isCurrent || request.isCurrent();
|
|
999
|
+
} catch {
|
|
1000
|
+
current = false;
|
|
1001
|
+
}
|
|
1002
|
+
if (aborted) return "cancelled";
|
|
1003
|
+
if (!lifetime.active || !current) return queued ? "superseded" : "skipped";
|
|
1004
|
+
}
|
|
1005
|
+
async run(lifetime, request) {
|
|
1006
|
+
const startedAt = this.now();
|
|
1007
|
+
const base = {
|
|
1008
|
+
id: this.id(),
|
|
1009
|
+
plugin: lifetime.plugin,
|
|
1010
|
+
purpose: request.purpose,
|
|
1011
|
+
...request.sessionId ? { sessionId: request.sessionId } : {},
|
|
1012
|
+
sourceVersion: request.sourceVersion,
|
|
1013
|
+
...request.contractVersion !== void 0 ? { contractVersion: request.contractVersion } : {},
|
|
1014
|
+
...request.promptVersion ? { promptVersion: request.promptVersion } : {},
|
|
1015
|
+
...request.schemaVersion ? { schemaVersion: request.schemaVersion } : {},
|
|
1016
|
+
cost: null,
|
|
1017
|
+
startedAt
|
|
1018
|
+
};
|
|
1019
|
+
let queued = false;
|
|
1020
|
+
let timeout;
|
|
1021
|
+
const finish = async (receipt, text = "") => {
|
|
1022
|
+
let sealed = sanitizeReceipt({
|
|
1023
|
+
...receipt,
|
|
1024
|
+
finishedAt: this.now(),
|
|
1025
|
+
cost: null
|
|
1026
|
+
});
|
|
1027
|
+
await this.record(sealed);
|
|
1028
|
+
const again = this.classify(lifetime, request, queued);
|
|
1029
|
+
if (again && (sealed.status === "success" || text !== "")) {
|
|
1030
|
+
const { error: _error, ...rest } = sealed;
|
|
1031
|
+
const timedOut = Boolean(timeout?.aborted) && !request.signal?.aborted && !lifetime.controller.signal.aborted;
|
|
1032
|
+
sealed = sanitizeReceipt({
|
|
1033
|
+
...rest,
|
|
1034
|
+
status: again,
|
|
1035
|
+
finishedAt: this.now(),
|
|
1036
|
+
cost: null,
|
|
1037
|
+
...again === "cancelled" ? { error: timedOut ? "调用超时。" : "调用已取消。" } : {}
|
|
1038
|
+
});
|
|
1039
|
+
await this.record(sealed);
|
|
1040
|
+
return {
|
|
1041
|
+
text: "",
|
|
1042
|
+
receipt: sealed
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
return {
|
|
1046
|
+
text: sealed.status === "success" ? text : "",
|
|
1047
|
+
receipt: sealed
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
1050
|
+
const early = this.classify(lifetime, request, false);
|
|
1051
|
+
if (early) return finish({
|
|
1052
|
+
...base,
|
|
1053
|
+
status: early,
|
|
1054
|
+
attempts: 0,
|
|
1055
|
+
finishedAt: this.now(),
|
|
1056
|
+
...early === "cancelled" ? { error: "调用已取消。" } : {}
|
|
1057
|
+
});
|
|
1058
|
+
const spec = lifetime.purposes.get(request.purpose);
|
|
1059
|
+
if (!spec) return finish({
|
|
1060
|
+
...base,
|
|
1061
|
+
status: "failed",
|
|
1062
|
+
attempts: 0,
|
|
1063
|
+
finishedAt: this.now(),
|
|
1064
|
+
error: "用途未注册。"
|
|
1065
|
+
});
|
|
1066
|
+
if (request.insert && (!Number.isInteger(request.insert.maxChars) || request.insert.maxChars < 1 || request.insert.maxChars > 12e3)) return finish({
|
|
1067
|
+
...base,
|
|
1068
|
+
status: "failed",
|
|
1069
|
+
attempts: 0,
|
|
1070
|
+
finishedAt: this.now(),
|
|
1071
|
+
error: "输出字符上限无效。"
|
|
1072
|
+
});
|
|
1073
|
+
const policy = this.policy;
|
|
1074
|
+
const maxInputChars = minLimit(policy.limits.maxInputChars, spec.maxInputChars);
|
|
1075
|
+
if (request.input.length + request.system.length > maxInputChars) return finish({
|
|
1076
|
+
...base,
|
|
1077
|
+
status: "failed",
|
|
1078
|
+
attempts: 0,
|
|
1079
|
+
finishedAt: this.now(),
|
|
1080
|
+
error: "输入超出上限。"
|
|
1081
|
+
});
|
|
1082
|
+
const timeoutMs = minLimit(policy.limits.timeoutMs, spec.timeoutMs);
|
|
1083
|
+
timeout = AbortSignal.timeout(timeoutMs);
|
|
1084
|
+
const combined = AbortSignal.any([
|
|
1085
|
+
lifetime.controller.signal,
|
|
1086
|
+
timeout,
|
|
1087
|
+
...request.signal ? [request.signal] : []
|
|
1088
|
+
]);
|
|
1089
|
+
let route;
|
|
1090
|
+
try {
|
|
1091
|
+
route = await abortable(() => resolvePurposeRoute({
|
|
1092
|
+
llm: this.options.llm,
|
|
1093
|
+
policy,
|
|
1094
|
+
purpose: request.purpose,
|
|
1095
|
+
spec,
|
|
1096
|
+
sessionId: request.sessionId,
|
|
1097
|
+
override: request.override,
|
|
1098
|
+
sessionModels: this.options.sessionModels,
|
|
1099
|
+
defaultModel: this.options.defaultModel,
|
|
1100
|
+
modelCenterAvailable: this.options.modelCenterAvailable?.(),
|
|
1101
|
+
signal: combined
|
|
1102
|
+
}), combined);
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
const classified = this.classify(lifetime, request, false) ?? (isAbortError(error) || combined.aborted ? "cancelled" : void 0);
|
|
1105
|
+
if (classified) {
|
|
1106
|
+
const timedOut = timeout.aborted && !request.signal?.aborted && !lifetime.controller.signal.aborted;
|
|
1107
|
+
return finish({
|
|
1108
|
+
...base,
|
|
1109
|
+
status: classified,
|
|
1110
|
+
attempts: 0,
|
|
1111
|
+
finishedAt: this.now(),
|
|
1112
|
+
...classified === "cancelled" ? { error: timedOut ? "调用超时。" : "调用已取消。" } : {}
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
const message = error instanceof AiServicesError ? error.message : "模型路由无效,未改用其他模型。";
|
|
1116
|
+
return finish({
|
|
1117
|
+
...base,
|
|
1118
|
+
status: "failed",
|
|
1119
|
+
attempts: 0,
|
|
1120
|
+
finishedAt: this.now(),
|
|
1121
|
+
error: message
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
const afterResolve = this.classify(lifetime, request, false);
|
|
1125
|
+
if (afterResolve) return finish({
|
|
1126
|
+
...base,
|
|
1127
|
+
status: afterResolve,
|
|
1128
|
+
attempts: 0,
|
|
1129
|
+
finishedAt: this.now(),
|
|
1130
|
+
route,
|
|
1131
|
+
...afterResolve === "cancelled" ? { error: "调用已取消。" } : {}
|
|
1132
|
+
});
|
|
1133
|
+
const maxOutputTokens = minLimit(policy.limits.maxOutputTokens, spec.maxOutputTokens);
|
|
1134
|
+
const maxAttempts = policy.limits.maxAttempts;
|
|
1135
|
+
const priority = request.priority === "interactive" ? "interactive" : "background";
|
|
1136
|
+
queued = true;
|
|
1137
|
+
let release = () => {};
|
|
1138
|
+
try {
|
|
1139
|
+
release = await this.queues.forProvider(route.provider).acquire(priority, combined);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
const classified = this.classify(lifetime, request, true) ?? (isAbortError(error) || combined.aborted ? "cancelled" : "failed");
|
|
1142
|
+
return finish({
|
|
1143
|
+
...base,
|
|
1144
|
+
status: classified,
|
|
1145
|
+
attempts: 0,
|
|
1146
|
+
finishedAt: this.now(),
|
|
1147
|
+
route,
|
|
1148
|
+
error: classified === "cancelled" ? timeout.aborted && !request.signal?.aborted && !lifetime.controller.signal.aborted ? "调用超时。" : "调用已取消。" : "排队失败。"
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
try {
|
|
1152
|
+
const afterQueue = this.classify(lifetime, request, true);
|
|
1153
|
+
if (afterQueue) return finish({
|
|
1154
|
+
...base,
|
|
1155
|
+
status: afterQueue,
|
|
1156
|
+
attempts: 0,
|
|
1157
|
+
finishedAt: this.now(),
|
|
1158
|
+
route,
|
|
1159
|
+
...afterQueue === "cancelled" ? { error: "调用已取消。" } : {}
|
|
1160
|
+
});
|
|
1161
|
+
const generated = await generateAuxiliary({
|
|
1162
|
+
llm: this.options.llm,
|
|
1163
|
+
plugin: lifetime.plugin,
|
|
1164
|
+
route,
|
|
1165
|
+
system: request.system,
|
|
1166
|
+
text: request.input,
|
|
1167
|
+
maxTokens: maxOutputTokens,
|
|
1168
|
+
maxAttempts,
|
|
1169
|
+
insert: request.insert,
|
|
1170
|
+
signal: combined,
|
|
1171
|
+
sessionId: request.sessionId,
|
|
1172
|
+
live: () => this.live(lifetime, request)
|
|
1173
|
+
});
|
|
1174
|
+
const classified = this.classify(lifetime, request, true);
|
|
1175
|
+
if (classified === "superseded") return finish({
|
|
1176
|
+
...base,
|
|
1177
|
+
status: "superseded",
|
|
1178
|
+
attempts: generated.attempts,
|
|
1179
|
+
finishedAt: this.now(),
|
|
1180
|
+
route,
|
|
1181
|
+
...generated.inputTokens !== void 0 ? { inputTokens: generated.inputTokens } : {},
|
|
1182
|
+
...generated.outputTokens !== void 0 ? { outputTokens: generated.outputTokens } : {}
|
|
1183
|
+
});
|
|
1184
|
+
const timedOut = timeout.aborted && !request.signal?.aborted && !lifetime.controller.signal.aborted;
|
|
1185
|
+
if (classified === "cancelled" || generated.status === "cancelled") return finish({
|
|
1186
|
+
...base,
|
|
1187
|
+
status: "cancelled",
|
|
1188
|
+
attempts: generated.attempts,
|
|
1189
|
+
finishedAt: this.now(),
|
|
1190
|
+
route,
|
|
1191
|
+
...generated.inputTokens !== void 0 ? { inputTokens: generated.inputTokens } : {},
|
|
1192
|
+
...generated.outputTokens !== void 0 ? { outputTokens: generated.outputTokens } : {},
|
|
1193
|
+
error: timedOut ? "调用超时。" : "调用已取消。"
|
|
1194
|
+
});
|
|
1195
|
+
if (generated.status !== "success") return finish({
|
|
1196
|
+
...base,
|
|
1197
|
+
status: generated.status,
|
|
1198
|
+
attempts: generated.attempts,
|
|
1199
|
+
finishedAt: this.now(),
|
|
1200
|
+
route,
|
|
1201
|
+
...generated.inputTokens !== void 0 ? { inputTokens: generated.inputTokens } : {},
|
|
1202
|
+
...generated.outputTokens !== void 0 ? { outputTokens: generated.outputTokens } : {},
|
|
1203
|
+
...generated.error ? { error: generated.error } : {}
|
|
1204
|
+
});
|
|
1205
|
+
return finish({
|
|
1206
|
+
...base,
|
|
1207
|
+
status: "success",
|
|
1208
|
+
attempts: generated.attempts,
|
|
1209
|
+
finishedAt: this.now(),
|
|
1210
|
+
route,
|
|
1211
|
+
...generated.inputTokens !== void 0 ? { inputTokens: generated.inputTokens } : {},
|
|
1212
|
+
...generated.outputTokens !== void 0 ? { outputTokens: generated.outputTokens } : {}
|
|
1213
|
+
}, generated.text);
|
|
1214
|
+
} finally {
|
|
1215
|
+
release();
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
record(receipt) {
|
|
1219
|
+
const task = this.pending.then(async () => {
|
|
1220
|
+
const index = this.receipts.findIndex((row) => row.id === receipt.id);
|
|
1221
|
+
const list = [...this.receipts];
|
|
1222
|
+
if (index >= 0) list[index] = receipt;
|
|
1223
|
+
else list.push(receipt);
|
|
1224
|
+
const next = boundedReceipts(list);
|
|
1225
|
+
try {
|
|
1226
|
+
await this.options.store.saveReceipts(next);
|
|
1227
|
+
this.receipts = next;
|
|
1228
|
+
} catch {
|
|
1229
|
+
this.storageFailed = true;
|
|
1230
|
+
this.receipts = next;
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
this.pending = task.then(() => {}, () => {});
|
|
1234
|
+
return task;
|
|
1235
|
+
}
|
|
1236
|
+
};
|
|
1237
|
+
//#endregion
|
|
1238
|
+
//#region src/index.ts
|
|
1239
|
+
const name = "@klarkxy/dsh-ai-services";
|
|
1240
|
+
const inject = [
|
|
1241
|
+
"llm",
|
|
1242
|
+
"storageDomain",
|
|
1243
|
+
"connection",
|
|
1244
|
+
"webServer",
|
|
1245
|
+
"agents",
|
|
1246
|
+
"sessionProjections",
|
|
1247
|
+
"agentDefaultModel"
|
|
1248
|
+
];
|
|
1249
|
+
function installForegroundPriority(ctx, service) {
|
|
1250
|
+
return ctx.on.call(ctx, "llm/stream", (options, next) => {
|
|
1251
|
+
if (!isAgentLoopRequest(options)) return next();
|
|
1252
|
+
return (async function* () {
|
|
1253
|
+
service.noteAgent(options.provider, 1);
|
|
1254
|
+
try {
|
|
1255
|
+
yield* next();
|
|
1256
|
+
} finally {
|
|
1257
|
+
service.noteAgent(options.provider, -1);
|
|
1258
|
+
}
|
|
1259
|
+
})();
|
|
1260
|
+
}, {
|
|
1261
|
+
global: true,
|
|
1262
|
+
prepend: true
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
async function apply(ctx) {
|
|
1266
|
+
const host = ctx;
|
|
1267
|
+
const domain = await ctx.storageDomain.open(aiServicesDomain);
|
|
1268
|
+
const policyTable = domain.table("policy");
|
|
1269
|
+
const receiptTable = domain.table("receipts");
|
|
1270
|
+
const stored = policyTable.get(POLICY_KEY);
|
|
1271
|
+
const { imports = [], ...initialPolicy } = stored ?? {};
|
|
1272
|
+
const service = new AiServicesRuntime({
|
|
1273
|
+
llm: ctx.llm,
|
|
1274
|
+
initialPolicy: stored ? initialPolicy : void 0,
|
|
1275
|
+
initialImports: imports,
|
|
1276
|
+
initialReceipts: receiptTable.get("log")?.items,
|
|
1277
|
+
store: {
|
|
1278
|
+
savePolicy: (policy, imports) => policyTable.put(POLICY_KEY, {
|
|
1279
|
+
...policy,
|
|
1280
|
+
imports
|
|
1281
|
+
}),
|
|
1282
|
+
saveReceipts: (items) => receiptTable.put("log", { items: boundedReceipts(items) })
|
|
1283
|
+
},
|
|
1284
|
+
sessionModels: sessionModelsFromHost(() => ctx),
|
|
1285
|
+
defaultModel: () => ctx.get("agentDefaultModel")?.currentSelection(),
|
|
1286
|
+
modelCenterAvailable: () => Boolean(ctx.get("modelCenter"))
|
|
1287
|
+
});
|
|
1288
|
+
ctx.effect(() => async () => {
|
|
1289
|
+
await service.dispose();
|
|
1290
|
+
await domain.close();
|
|
1291
|
+
}, "ai-services.dispose");
|
|
1292
|
+
ctx.effect(() => installForegroundPriority(ctx, service), "ai-services.foreground");
|
|
1293
|
+
ctx.provide("aiServices", service);
|
|
1294
|
+
ctx.effect(() => registerHostRpc(host, AI_RPC_CHANNEL, (endpoint, payload, signal) => handleAiRpc(service, endpoint, payload, signal)), "ai-services.rpc");
|
|
1295
|
+
}
|
|
1296
|
+
//#endregion
|
|
1297
|
+
export { AI_RPC_CHANNEL, AiServicesRuntime, CHAT_EVENTS_SLOT, MODEL_SETTINGS_SLOT, apply, inject, name, registerHostRpc };
|
|
1298
|
+
|
|
1299
|
+
//# sourceMappingURL=index.js.map
|