@minko-ai/minko 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +577 -2
- package/assets/mascot-preview.png +0 -0
- package/assets/mascot-preview.txt +46 -0
- package/assets/mascot-source.png +0 -0
- package/assets/skills/README.md +21 -0
- package/assets/skills/brainstorming/LICENSE +21 -0
- package/assets/skills/brainstorming/SKILL.md +316 -0
- package/assets/skills/buddhist-method/LICENSE +21 -0
- package/assets/skills/buddhist-method/SKILL.md +137 -0
- package/assets/skills/buddhist-method/references/ariyasacca-debug.md +57 -0
- package/assets/skills/buddhist-method/references/extended-principles.md +100 -0
- package/assets/skills/code-humanizer/LICENSE +21 -0
- package/assets/skills/code-humanizer/SKILL.md +174 -0
- package/assets/skills/code-review/LICENSE +21 -0
- package/assets/skills/code-review/SKILL.md +232 -0
- package/assets/skills/code-review/reference/angular.md +768 -0
- package/assets/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/assets/skills/code-review/reference/c.md +890 -0
- package/assets/skills/code-review/reference/code-quality-universal.md +488 -0
- package/assets/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/assets/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/assets/skills/code-review/reference/cpp.md +893 -0
- package/assets/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/assets/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/assets/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/assets/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/assets/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/assets/skills/code-review/reference/csharp.md +525 -0
- package/assets/skills/code-review/reference/css-less-sass.md +661 -0
- package/assets/skills/code-review/reference/dart.md +670 -0
- package/assets/skills/code-review/reference/django.md +985 -0
- package/assets/skills/code-review/reference/fastapi.md +580 -0
- package/assets/skills/code-review/reference/go.md +993 -0
- package/assets/skills/code-review/reference/java.md +409 -0
- package/assets/skills/code-review/reference/java8.md +586 -0
- package/assets/skills/code-review/reference/kotlin.md +1018 -0
- package/assets/skills/code-review/reference/nestjs.md +593 -0
- package/assets/skills/code-review/reference/performance-review-guide.md +816 -0
- package/assets/skills/code-review/reference/php.md +684 -0
- package/assets/skills/code-review/reference/python.md +1073 -0
- package/assets/skills/code-review/reference/qt.md +757 -0
- package/assets/skills/code-review/reference/react.md +871 -0
- package/assets/skills/code-review/reference/ruby.md +964 -0
- package/assets/skills/code-review/reference/rust.md +846 -0
- package/assets/skills/code-review/reference/security-review-guide.md +494 -0
- package/assets/skills/code-review/reference/svelte.md +1064 -0
- package/assets/skills/code-review/reference/swift.md +936 -0
- package/assets/skills/code-review/reference/typescript.md +1016 -0
- package/assets/skills/code-review/reference/vue.md +924 -0
- package/assets/skills/code-review/reference/zig.md +440 -0
- package/assets/skills/cyclomatic-complexity/LICENSE +201 -0
- package/assets/skills/cyclomatic-complexity/SKILL.md +69 -0
- package/assets/skills/five-whys/LICENSE +21 -0
- package/assets/skills/five-whys/SKILL.md +247 -0
- package/assets/skills/five-whys/examples/performance-regression.md +161 -0
- package/assets/skills/five-whys/examples/production-incident.md +134 -0
- package/assets/skills/five-whys/references/software-patterns.md +284 -0
- package/assets/skills/five-whys/references/toyota-origins.md +257 -0
- package/assets/skills/systematic-debugging/LICENSE +21 -0
- package/assets/skills/systematic-debugging/SKILL.md +283 -0
- package/assets/skills/test-driven-development/LICENSE +21 -0
- package/assets/skills/test-driven-development/SKILL.md +330 -0
- package/assets/skills/verification-before-completion/LICENSE +21 -0
- package/assets/skills/verification-before-completion/SKILL.md +120 -0
- package/assets/skills/vibe-sec/LICENSE +201 -0
- package/assets/skills/vibe-sec/SKILL.md +758 -0
- package/assets/skills/writing-plans/LICENSE +21 -0
- package/assets/skills/writing-plans/SKILL.md +204 -0
- package/bin/minko.js +2 -2
- package/dist/agent/approvalTokens.js +224 -0
- package/dist/agent/branchLock.js +315 -0
- package/dist/agent/compact.js +182 -0
- package/dist/agent/context.js +183 -0
- package/dist/agent/cron.js +326 -0
- package/dist/agent/hooks.js +211 -0
- package/dist/agent/lanes.js +511 -0
- package/dist/agent/loop.js +337 -0
- package/dist/agent/memory.js +159 -0
- package/dist/agent/permissions.js +71 -0
- package/dist/agent/plan.js +118 -0
- package/dist/agent/prompt.js +120 -0
- package/dist/agent/subagent.js +67 -0
- package/dist/agent/tasks.js +449 -0
- package/dist/agent/transcript.js +62 -0
- package/dist/agent/workerStore.js +172 -0
- package/dist/agent/workerTypes.js +1 -0
- package/dist/agent/workers.js +503 -0
- package/dist/cli/app.js +261 -0
- package/dist/cli/argv.js +206 -0
- package/dist/cli/commandsCommand.js +12 -0
- package/dist/cli/configCommand.js +170 -0
- package/dist/cli/doctorCommand.js +112 -0
- package/dist/cli/exportCommand.js +85 -0
- package/dist/cli/forkCommand.js +24 -0
- package/dist/cli/linkCommand.js +272 -0
- package/dist/cli/loginCommand.js +69 -0
- package/dist/cli/mcpLoginCommand.js +68 -0
- package/dist/cli/pluginCommand.js +113 -0
- package/dist/cli/powerCommand.js +174 -0
- package/dist/cli/promptFooter.js +74 -0
- package/dist/cli/relayCommand.js +38 -0
- package/dist/cli/sandboxCommand.js +31 -0
- package/dist/cli/serveCommand.js +104 -0
- package/dist/cli/statusCommand.js +120 -0
- package/dist/cli/tui.js +1104 -0
- package/dist/commands/registry.js +99 -0
- package/dist/config/paths.js +81 -0
- package/dist/config/store.js +605 -0
- package/dist/index.js +199 -0
- package/dist/link/protocol.js +159 -0
- package/dist/link/relay.js +184 -0
- package/dist/link/server.js +566 -0
- package/dist/link/tunnel.js +90 -0
- package/dist/lsp/client.js +340 -0
- package/dist/lsp/tool.js +177 -0
- package/dist/mcp/client.js +469 -0
- package/dist/mcp/httpClient.js +191 -0
- package/dist/mcp/server.js +206 -0
- package/dist/mcp/tools.js +173 -0
- package/dist/oauth/device.js +111 -0
- package/dist/oauth/store.js +56 -0
- package/dist/plugins/loader.js +291 -0
- package/dist/plugins/market.js +98 -0
- package/dist/providers/anthropic.js +279 -0
- package/dist/providers/factory.js +109 -0
- package/dist/providers/openai.js +182 -0
- package/dist/providers/retry.js +95 -0
- package/dist/providers/sse.js +67 -0
- package/dist/providers/types.js +10 -0
- package/dist/rag/embeddings.js +171 -0
- package/dist/rag/index.js +373 -0
- package/dist/rag/server.js +167 -0
- package/dist/rag/tool.js +148 -0
- package/dist/sandbox/index.js +55 -0
- package/dist/sdk/server.js +150 -0
- package/dist/sdk/session.js +104 -0
- package/dist/session/liveness.js +148 -0
- package/dist/session/recorder.js +49 -0
- package/dist/session/store.js +171 -0
- package/dist/skills/index.js +428 -0
- package/dist/theme.js +30 -0
- package/dist/tools/fs.js +195 -0
- package/dist/tools/notebook.js +109 -0
- package/dist/tools/registry.js +32 -0
- package/dist/tools/search.js +130 -0
- package/dist/tools/shell.js +200 -0
- package/dist/tools/structured.js +83 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/ansi.js +135 -0
- package/dist/ui/banner.js +103 -0
- package/dist/ui/input.js +49 -0
- package/dist/ui/inputModel.js +113 -0
- package/dist/ui/keyboard.js +142 -0
- package/dist/ui/mascot.js +34 -0
- package/dist/ui/mascotData.js +96 -0
- package/dist/ui/md.js +183 -0
- package/dist/ui/screen.js +49 -0
- package/dist/ui/spinner.js +52 -0
- package/dist/ui/transcript.js +287 -0
- package/dist/util/clipboard.js +53 -0
- package/dist/util/errors.js +50 -0
- package/dist/util/version.js +12 -0
- package/dist/web/fetch.js +168 -0
- package/dist/web/search.js +97 -0
- package/package.json +45 -36
- package/src/agent/loop.ts +0 -159
- package/src/agent/prompt.ts +0 -45
- package/src/cli/app.ts +0 -396
- package/src/cli/argv.ts +0 -139
- package/src/cli/configCommand.ts +0 -175
- package/src/config/paths.ts +0 -20
- package/src/config/store.ts +0 -149
- package/src/index.ts +0 -120
- package/src/providers/anthropic.ts +0 -215
- package/src/providers/openai.ts +0 -158
- package/src/providers/sse.ts +0 -57
- package/src/providers/types.ts +0 -59
- package/src/session/store.ts +0 -125
- package/src/theme.ts +0 -41
- package/src/tools/fs.ts +0 -143
- package/src/tools/registry.ts +0 -19
- package/src/tools/shell.ts +0 -120
- package/src/tools/types.ts +0 -26
- package/src/ui/ansi.ts +0 -110
- package/src/ui/banner.ts +0 -39
- package/src/ui/input.ts +0 -54
- package/src/ui/md.ts +0 -160
- package/src/ui/spinner.ts +0 -58
- package/src/util/errors.ts +0 -48
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { activeProvider } from "../config/store.js";
|
|
2
|
+
import { HttpError } from "../util/errors.js";
|
|
3
|
+
import { isRetryable } from "./retry.js";
|
|
4
|
+
import { createAnthropicProvider } from "./anthropic.js";
|
|
5
|
+
import { createOpenAiProvider } from "./openai.js";
|
|
6
|
+
const BUILTIN_ANTHROPIC_ALIASES = {
|
|
7
|
+
sonnet: 'claude-sonnet-5-5',
|
|
8
|
+
opus: 'claude-opus-5-5',
|
|
9
|
+
haiku: 'claude-haiku-5-5',
|
|
10
|
+
};
|
|
11
|
+
export function createSingleProvider(config) {
|
|
12
|
+
return config.kind === 'anthropic'
|
|
13
|
+
? createAnthropicProvider(config)
|
|
14
|
+
: createOpenAiProvider(config);
|
|
15
|
+
}
|
|
16
|
+
/** Applies `modelAliases` and built-in Anthropic short names. */
|
|
17
|
+
export function resolveModelAlias(config, provider) {
|
|
18
|
+
const custom = config.modelAliases[provider.model];
|
|
19
|
+
if (custom) {
|
|
20
|
+
return custom;
|
|
21
|
+
}
|
|
22
|
+
if (provider.kind === 'anthropic') {
|
|
23
|
+
return BUILTIN_ANTHROPIC_ALIASES[provider.model.toLowerCase()] ?? provider.model;
|
|
24
|
+
}
|
|
25
|
+
return provider.model;
|
|
26
|
+
}
|
|
27
|
+
/** Provider failures that another provider may be able to handle. */
|
|
28
|
+
export function shouldFallback(error) {
|
|
29
|
+
if (error instanceof HttpError) {
|
|
30
|
+
if (error.status === 401 || error.status === 403) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return isRetryable(error);
|
|
34
|
+
}
|
|
35
|
+
return isRetryable(error);
|
|
36
|
+
}
|
|
37
|
+
export class FallbackProvider {
|
|
38
|
+
entries;
|
|
39
|
+
constructor(entries) {
|
|
40
|
+
this.entries = entries;
|
|
41
|
+
}
|
|
42
|
+
async stream(messages, tools, handlers, signal) {
|
|
43
|
+
let lastError;
|
|
44
|
+
for (let index = 0; index < this.entries.length; index += 1) {
|
|
45
|
+
const entry = this.entries[index];
|
|
46
|
+
try {
|
|
47
|
+
return await entry.provider.stream(messages, tools, handlers, signal);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
lastError = error;
|
|
51
|
+
if (signal.aborted || !shouldFallback(error) || index === this.entries.length - 1) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
const next = this.entries[index + 1];
|
|
55
|
+
handlers.onFallback?.({ from: entry.name, to: next.name, error });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw lastError;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Builds the active provider plus its ordered fallback chain. Every provider
|
|
63
|
+
* keeps its own model; `modelOverride` only affects the primary.
|
|
64
|
+
*/
|
|
65
|
+
export function providerChain(config, modelOverride) {
|
|
66
|
+
const active = activeProvider(config);
|
|
67
|
+
if (!active) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
const names = [];
|
|
72
|
+
for (const name of [active.name, ...config.fallbackProviders]) {
|
|
73
|
+
if (seen.has(name)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!config.providers[name]) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
seen.add(name);
|
|
80
|
+
names.push(name);
|
|
81
|
+
}
|
|
82
|
+
const entries = [];
|
|
83
|
+
let primaryModel = '';
|
|
84
|
+
for (const name of names) {
|
|
85
|
+
const providerConfig = config.providers[name];
|
|
86
|
+
const model = name === active.name && modelOverride
|
|
87
|
+
? resolveModelAlias(config, { ...providerConfig, model: modelOverride })
|
|
88
|
+
: resolveModelAlias(config, providerConfig);
|
|
89
|
+
entries.push({
|
|
90
|
+
name,
|
|
91
|
+
provider: createSingleProvider({
|
|
92
|
+
...providerConfig,
|
|
93
|
+
model,
|
|
94
|
+
maxTokens: config.maxTokens,
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
if (name === active.name) {
|
|
98
|
+
primaryModel = model;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (entries.length === 0) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
name: active.name,
|
|
106
|
+
model: primaryModel || active.provider.model,
|
|
107
|
+
provider: entries.length === 1 ? entries[0].provider : new FallbackProvider(entries),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { HttpError } from "../util/errors.js";
|
|
2
|
+
import { retryAfterFromHeaders, withRetry } from "./retry.js";
|
|
3
|
+
import { sseEvents } from "./sse.js";
|
|
4
|
+
export function toOpenAiMessages(messages, fillEmptyReasoning = false) {
|
|
5
|
+
return messages.map((message) => {
|
|
6
|
+
if (message.role === 'tool') {
|
|
7
|
+
return { role: 'tool', tool_call_id: message.toolCallId, content: message.content };
|
|
8
|
+
}
|
|
9
|
+
if (message.role === 'assistant') {
|
|
10
|
+
const out = {
|
|
11
|
+
role: 'assistant',
|
|
12
|
+
content: message.content.length > 0 ? message.content : null,
|
|
13
|
+
};
|
|
14
|
+
// Thinking models reject history that drops their reasoning.
|
|
15
|
+
if (message.reasoning && message.reasoning.length > 0) {
|
|
16
|
+
out.reasoning_content = message.reasoning;
|
|
17
|
+
}
|
|
18
|
+
else if (fillEmptyReasoning) {
|
|
19
|
+
out.reasoning_content = '';
|
|
20
|
+
}
|
|
21
|
+
if (message.toolCalls.length > 0) {
|
|
22
|
+
out.tool_calls = message.toolCalls.map((call) => ({
|
|
23
|
+
id: call.id,
|
|
24
|
+
type: 'function',
|
|
25
|
+
function: { name: call.name, arguments: call.arguments },
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
return { role: message.role, content: message.content };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export class OpenAiProvider {
|
|
34
|
+
config;
|
|
35
|
+
reasoningMode = false;
|
|
36
|
+
retry;
|
|
37
|
+
constructor(config, retry = {}) {
|
|
38
|
+
this.config = config;
|
|
39
|
+
this.retry = retry;
|
|
40
|
+
}
|
|
41
|
+
async request(messages, tools, signal, fillEmptyReasoning, onRetry) {
|
|
42
|
+
const body = {
|
|
43
|
+
model: this.config.model,
|
|
44
|
+
messages: toOpenAiMessages(messages, fillEmptyReasoning),
|
|
45
|
+
stream: true,
|
|
46
|
+
stream_options: { include_usage: true },
|
|
47
|
+
};
|
|
48
|
+
if (this.config.maxTokens && this.config.maxTokens > 0) {
|
|
49
|
+
body.max_tokens = this.config.maxTokens;
|
|
50
|
+
}
|
|
51
|
+
if (tools.length > 0) {
|
|
52
|
+
body.tools = tools.map((tool) => ({
|
|
53
|
+
type: 'function',
|
|
54
|
+
function: { name: tool.name, description: tool.description, parameters: tool.parameters },
|
|
55
|
+
}));
|
|
56
|
+
body.tool_choice = 'auto';
|
|
57
|
+
}
|
|
58
|
+
return await withRetry(async () => {
|
|
59
|
+
const headers = { 'content-type': 'application/json' };
|
|
60
|
+
if (this.config.apiKey.length > 0) {
|
|
61
|
+
headers.authorization = 'Bearer ' + this.config.apiKey;
|
|
62
|
+
}
|
|
63
|
+
const response = await fetch(joinUrl(this.config.baseUrl, 'chat/completions'), {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers,
|
|
66
|
+
body: JSON.stringify(body),
|
|
67
|
+
signal,
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new HttpError(response.status, await response.text(), retryAfterFromHeaders(response.headers));
|
|
71
|
+
}
|
|
72
|
+
return response;
|
|
73
|
+
}, { ...this.retry, signal, onRetry });
|
|
74
|
+
}
|
|
75
|
+
async stream(messages, tools, handlers, signal) {
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await this.request(messages, tools, signal, this.reasoningMode, handlers.onRetry);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof HttpError && error.status === 400 && /reasoning_content/i.test(error.body)) {
|
|
82
|
+
// Thinking models want the field on every assistant turn, even when a
|
|
83
|
+
// particular reply carried no reasoning text.
|
|
84
|
+
this.reasoningMode = true;
|
|
85
|
+
response = await this.request(messages, tools, signal, true, handlers.onRetry);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!response.body) {
|
|
92
|
+
throw new Error('服务端没有返回可读的流');
|
|
93
|
+
}
|
|
94
|
+
const textParts = [];
|
|
95
|
+
const reasoningParts = [];
|
|
96
|
+
const calls = new Map();
|
|
97
|
+
let usage = { input: 0, output: 0 };
|
|
98
|
+
for await (const event of sseEvents(response.body)) {
|
|
99
|
+
if (event.data === '[DONE]') {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
let payload;
|
|
103
|
+
try {
|
|
104
|
+
payload = JSON.parse(event.data);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const rawUsage = payload.usage;
|
|
110
|
+
if (rawUsage) {
|
|
111
|
+
const details = rawUsage.prompt_tokens_details;
|
|
112
|
+
const cached = numberOrZero(details?.cached_tokens)
|
|
113
|
+
|| numberOrZero(rawUsage.prompt_cache_hit_tokens);
|
|
114
|
+
const next = {
|
|
115
|
+
input: numberOrZero(rawUsage.prompt_tokens),
|
|
116
|
+
output: numberOrZero(rawUsage.completion_tokens),
|
|
117
|
+
};
|
|
118
|
+
if (cached > 0) {
|
|
119
|
+
next.cached = cached;
|
|
120
|
+
}
|
|
121
|
+
usage = next;
|
|
122
|
+
handlers.onUsage?.(usage);
|
|
123
|
+
}
|
|
124
|
+
const choices = payload.choices;
|
|
125
|
+
const choice = choices && choices.length > 0 ? choices[0] : undefined;
|
|
126
|
+
if (!choice) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const delta = (choice.delta ?? {});
|
|
130
|
+
const content = delta.content;
|
|
131
|
+
if (typeof content === 'string' && content.length > 0) {
|
|
132
|
+
textParts.push(content);
|
|
133
|
+
handlers.onText?.(content);
|
|
134
|
+
}
|
|
135
|
+
const reasoning = delta.reasoning_content;
|
|
136
|
+
if (typeof reasoning === 'string' && reasoning.length > 0) {
|
|
137
|
+
reasoningParts.push(reasoning);
|
|
138
|
+
handlers.onReasoning?.(reasoning);
|
|
139
|
+
}
|
|
140
|
+
const toolDeltas = delta.tool_calls;
|
|
141
|
+
if (Array.isArray(toolDeltas)) {
|
|
142
|
+
for (const raw of toolDeltas) {
|
|
143
|
+
const index = typeof raw.index === 'number' ? raw.index : 0;
|
|
144
|
+
const entry = calls.get(index) ?? { id: '', name: '', args: '' };
|
|
145
|
+
if (typeof raw.id === 'string' && raw.id.length > 0) {
|
|
146
|
+
entry.id = raw.id;
|
|
147
|
+
}
|
|
148
|
+
if (raw.function) {
|
|
149
|
+
if (typeof raw.function.name === 'string') {
|
|
150
|
+
entry.name += raw.function.name;
|
|
151
|
+
}
|
|
152
|
+
if (typeof raw.function.arguments === 'string') {
|
|
153
|
+
entry.args += raw.function.arguments;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
calls.set(index, entry);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const toolCalls = [...calls.entries()]
|
|
161
|
+
.sort((a, b) => a[0] - b[0])
|
|
162
|
+
.flatMap(([index, entry]) => entry.name.length === 0
|
|
163
|
+
? []
|
|
164
|
+
: [{ id: entry.id || 'call_' + index, name: entry.name, arguments: entry.args || '{}' }]);
|
|
165
|
+
return {
|
|
166
|
+
text: textParts.join(''),
|
|
167
|
+
reasoning: reasoningParts.join(''),
|
|
168
|
+
toolCalls,
|
|
169
|
+
usage,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function numberOrZero(value) {
|
|
174
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
175
|
+
}
|
|
176
|
+
export function joinUrl(baseUrl, suffix) {
|
|
177
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
178
|
+
return base + '/' + suffix.replace(/^\/+/, '');
|
|
179
|
+
}
|
|
180
|
+
export function createOpenAiProvider(config) {
|
|
181
|
+
return new OpenAiProvider(config);
|
|
182
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { HttpError } from "../util/errors.js";
|
|
2
|
+
/** HTTP statuses that are worth retrying. */
|
|
3
|
+
export function isRetryable(error) {
|
|
4
|
+
if (error instanceof HttpError) {
|
|
5
|
+
if (error.status === 408 || error.status === 409 || error.status === 425 || error.status === 429) {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
return error.status >= 500;
|
|
9
|
+
}
|
|
10
|
+
if (error instanceof Error) {
|
|
11
|
+
if (error.name === 'AbortError') {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return error instanceof TypeError
|
|
15
|
+
|| /fetch failed|ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|socket hang up/i.test(error.message);
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
export function retryAfterMs(error) {
|
|
20
|
+
return error instanceof HttpError ? error.retryAfterMs : undefined;
|
|
21
|
+
}
|
|
22
|
+
/** Reads `Retry-After` as seconds or an HTTP date. */
|
|
23
|
+
export function retryAfterFromHeaders(headers) {
|
|
24
|
+
const raw = headers.get('retry-after');
|
|
25
|
+
if (!raw) {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
const seconds = Number(raw.trim());
|
|
29
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
30
|
+
return Math.floor(seconds * 1000);
|
|
31
|
+
}
|
|
32
|
+
const date = Date.parse(raw);
|
|
33
|
+
if (Number.isNaN(date)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return Math.max(0, date - Date.now());
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Runs an operation with retries for transient provider failures.
|
|
40
|
+
* `Retry-After` wins over the computed backoff but is still capped.
|
|
41
|
+
*/
|
|
42
|
+
export async function withRetry(operation, options = {}) {
|
|
43
|
+
const maxRetries = options.maxRetries ?? 3;
|
|
44
|
+
const initialDelayMs = options.initialDelayMs ?? 500;
|
|
45
|
+
const maxDelayMs = options.maxDelayMs ?? 8000;
|
|
46
|
+
const random = options.random ?? Math.random;
|
|
47
|
+
const sleep = options.sleep ?? ((ms) => abortableSleep(ms, options.signal));
|
|
48
|
+
let attempt = 0;
|
|
49
|
+
for (;;) {
|
|
50
|
+
if (options.signal?.aborted) {
|
|
51
|
+
throw abortError();
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return await operation();
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (attempt >= maxRetries || !isRetryable(error)) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
attempt += 1;
|
|
61
|
+
const base = retryAfterMs(error) ?? Math.min(maxDelayMs, initialDelayMs * 2 ** (attempt - 1));
|
|
62
|
+
const jitter = Math.floor(base * 0.2 * random());
|
|
63
|
+
const delayMs = Math.min(maxDelayMs, base + jitter);
|
|
64
|
+
options.onRetry?.({ attempt, delayMs, error });
|
|
65
|
+
await sleep(delayMs);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function abortError() {
|
|
70
|
+
const error = new Error('已中断');
|
|
71
|
+
error.name = 'AbortError';
|
|
72
|
+
return error;
|
|
73
|
+
}
|
|
74
|
+
function abortableSleep(ms, signal) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
if (signal?.aborted) {
|
|
77
|
+
reject(abortError());
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let onAbort;
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
if (onAbort) {
|
|
83
|
+
signal?.removeEventListener('abort', onAbort);
|
|
84
|
+
}
|
|
85
|
+
resolve();
|
|
86
|
+
}, ms);
|
|
87
|
+
if (signal) {
|
|
88
|
+
onAbort = () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
reject(abortError());
|
|
91
|
+
};
|
|
92
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a Server-Sent Events body. Handles CRLF, split chunks and comments.
|
|
3
|
+
*/
|
|
4
|
+
export async function* sseEvents(body) {
|
|
5
|
+
const reader = body.getReader();
|
|
6
|
+
const decoder = new TextDecoder();
|
|
7
|
+
let buffer = '';
|
|
8
|
+
let event = '';
|
|
9
|
+
let data = [];
|
|
10
|
+
const flush = () => {
|
|
11
|
+
if (data.length === 0) {
|
|
12
|
+
event = '';
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const payload = { event: event || 'message', data: data.join('\n') };
|
|
16
|
+
event = '';
|
|
17
|
+
data = [];
|
|
18
|
+
return payload;
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
for (;;) {
|
|
22
|
+
const { done, value } = await reader.read();
|
|
23
|
+
if (done) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
buffer += decoder.decode(value, { stream: true });
|
|
27
|
+
let index = buffer.indexOf('\n');
|
|
28
|
+
while (index >= 0) {
|
|
29
|
+
let line = buffer.slice(0, index);
|
|
30
|
+
buffer = buffer.slice(index + 1);
|
|
31
|
+
if (line.endsWith('\r')) {
|
|
32
|
+
line = line.slice(0, -1);
|
|
33
|
+
}
|
|
34
|
+
if (line.length === 0) {
|
|
35
|
+
const parsed = flush();
|
|
36
|
+
if (parsed) {
|
|
37
|
+
yield parsed;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else if (line.startsWith(':')) {
|
|
41
|
+
// heartbeat / comment
|
|
42
|
+
}
|
|
43
|
+
else if (line.startsWith('data:')) {
|
|
44
|
+
data.push(line.slice(5).replace(/^ /, ''));
|
|
45
|
+
}
|
|
46
|
+
else if (line.startsWith('event:')) {
|
|
47
|
+
event = line.slice(6).trim();
|
|
48
|
+
}
|
|
49
|
+
index = buffer.indexOf('\n');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
buffer += decoder.decode();
|
|
53
|
+
if (buffer.trim().length > 0) {
|
|
54
|
+
const line = buffer.replace(/\r$/, '');
|
|
55
|
+
if (line.startsWith('data:')) {
|
|
56
|
+
data.push(line.slice(5).replace(/^ /, ''));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const tail = flush();
|
|
60
|
+
if (tail) {
|
|
61
|
+
yield tail;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
reader.releaseLock();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function parseToolArguments(raw) {
|
|
2
|
+
if (raw.trim().length === 0) {
|
|
3
|
+
return {};
|
|
4
|
+
}
|
|
5
|
+
const parsed = JSON.parse(raw);
|
|
6
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
7
|
+
throw new Error('工具参数必须是 JSON 对象');
|
|
8
|
+
}
|
|
9
|
+
return parsed;
|
|
10
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { activeProvider } from "../config/store.js";
|
|
5
|
+
import { minkoHome } from "../config/paths.js";
|
|
6
|
+
import { HttpError } from "../util/errors.js";
|
|
7
|
+
import { retryAfterFromHeaders, withRetry } from "../providers/retry.js";
|
|
8
|
+
const DEFAULT_BATCH_SIZE = 32;
|
|
9
|
+
const REQUEST_TIMEOUT_MS = 60_000;
|
|
10
|
+
/** OpenAI-compatible /embeddings client with batching and retries. */
|
|
11
|
+
export class EmbeddingClient {
|
|
12
|
+
model;
|
|
13
|
+
baseUrl;
|
|
14
|
+
apiKey;
|
|
15
|
+
batchSize;
|
|
16
|
+
dimensions;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
19
|
+
this.apiKey = options.apiKey ?? '';
|
|
20
|
+
this.model = options.model;
|
|
21
|
+
this.batchSize = Math.max(1, Math.min(128, options.batchSize ?? DEFAULT_BATCH_SIZE));
|
|
22
|
+
this.dimensions = options.dimensions;
|
|
23
|
+
}
|
|
24
|
+
async embed(texts) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (let index = 0; index < texts.length; index += this.batchSize) {
|
|
27
|
+
const batch = texts.slice(index, index + this.batchSize);
|
|
28
|
+
out.push(...await this.request(batch));
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
async request(input) {
|
|
33
|
+
return await withRetry(async () => {
|
|
34
|
+
const headers = { 'content-type': 'application/json' };
|
|
35
|
+
if (this.apiKey.length > 0) {
|
|
36
|
+
headers.authorization = 'Bearer ' + this.apiKey;
|
|
37
|
+
}
|
|
38
|
+
const response = await fetch(joinUrl(this.baseUrl, 'embeddings'), {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers,
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
model: this.model,
|
|
43
|
+
input,
|
|
44
|
+
...(this.dimensions ? { dimensions: this.dimensions } : {}),
|
|
45
|
+
}),
|
|
46
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
47
|
+
});
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new HttpError(response.status, await response.text(), retryAfterFromHeaders(response.headers));
|
|
50
|
+
}
|
|
51
|
+
const payload = await response.json();
|
|
52
|
+
const data = Array.isArray(payload.data) ? payload.data.slice() : [];
|
|
53
|
+
data.sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
54
|
+
if (data.length !== input.length) {
|
|
55
|
+
throw new Error('embedding 返回数量不匹配:期望 ' + input.length + ',实际 ' + data.length);
|
|
56
|
+
}
|
|
57
|
+
return data.map((item) => {
|
|
58
|
+
if (!Array.isArray(item.embedding) || item.embedding.length === 0) {
|
|
59
|
+
throw new Error('embedding 返回了空向量');
|
|
60
|
+
}
|
|
61
|
+
return new Float32Array(item.embedding);
|
|
62
|
+
});
|
|
63
|
+
}, { maxRetries: 2, initialDelayMs: 500, maxDelayMs: 4_000 });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** On-disk cache keyed by content hash; avoids re-embedding unchanged chunks. */
|
|
67
|
+
export class EmbeddingCache {
|
|
68
|
+
file;
|
|
69
|
+
model;
|
|
70
|
+
entries = new Map();
|
|
71
|
+
constructor(file, model) {
|
|
72
|
+
this.file = file;
|
|
73
|
+
this.model = model;
|
|
74
|
+
}
|
|
75
|
+
load() {
|
|
76
|
+
try {
|
|
77
|
+
const raw = JSON.parse(fs.readFileSync(this.file, 'utf8'));
|
|
78
|
+
if (raw.model !== this.model) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (raw.entries === null || typeof raw.entries !== 'object') {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
for (const [hash, encoded] of Object.entries(raw.entries)) {
|
|
85
|
+
if (typeof encoded === 'string') {
|
|
86
|
+
this.entries.set(hash, decodeVector(encoded));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// A missing or corrupt cache merely means re-embedding.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
get(hash) {
|
|
95
|
+
return this.entries.get(hash);
|
|
96
|
+
}
|
|
97
|
+
set(hash, vector) {
|
|
98
|
+
if (this.entries.size >= 20_000) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.entries.set(hash, vector);
|
|
102
|
+
}
|
|
103
|
+
save() {
|
|
104
|
+
try {
|
|
105
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
106
|
+
const entries = {};
|
|
107
|
+
for (const [hash, vector] of this.entries) {
|
|
108
|
+
entries[hash] = encodeVector(vector);
|
|
109
|
+
}
|
|
110
|
+
const temporary = this.file + '.tmp';
|
|
111
|
+
const payload = JSON.stringify({ model: this.model, entries });
|
|
112
|
+
fs.writeFileSync(temporary, payload, 'utf8');
|
|
113
|
+
try {
|
|
114
|
+
fs.renameSync(temporary, this.file);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Windows may refuse to replace an existing file; write directly.
|
|
118
|
+
fs.writeFileSync(this.file, payload, 'utf8');
|
|
119
|
+
try {
|
|
120
|
+
fs.unlinkSync(temporary);
|
|
121
|
+
}
|
|
122
|
+
catch { /* best effort */ }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// The cache is an optimization; never break retrieval over it.
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolves embedding settings. If baseUrl/apiKey are omitted, the active
|
|
132
|
+
* OpenAI-compatible provider is reused, so one API key is enough.
|
|
133
|
+
*/
|
|
134
|
+
export function resolveEmbedding(config, root, cacheDir) {
|
|
135
|
+
const settings = config?.rag?.embeddings;
|
|
136
|
+
if (!settings || settings.enabled === false) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
const active = config ? activeProvider(config) : null;
|
|
140
|
+
const provider = active && active.provider.kind === 'openai' ? active.provider : null;
|
|
141
|
+
const baseUrl = settings.baseUrl ?? provider?.baseUrl ?? '';
|
|
142
|
+
if (baseUrl.length === 0) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const apiKey = settings.apiKey ?? provider?.apiKey ?? '';
|
|
146
|
+
const model = settings.model ?? 'text-embedding-3-small';
|
|
147
|
+
const embedder = new EmbeddingClient({
|
|
148
|
+
baseUrl,
|
|
149
|
+
apiKey,
|
|
150
|
+
model,
|
|
151
|
+
batchSize: settings.batchSize,
|
|
152
|
+
dimensions: settings.dimensions,
|
|
153
|
+
});
|
|
154
|
+
const key = crypto.createHash('sha1')
|
|
155
|
+
.update(path.resolve(root) + '|' + model)
|
|
156
|
+
.digest('hex')
|
|
157
|
+
.slice(0, 16);
|
|
158
|
+
const cache = new EmbeddingCache(path.join(cacheDir ?? minkoHome(), 'rag', key + '.json'), model);
|
|
159
|
+
cache.load();
|
|
160
|
+
return { embedder, cache, maxChunks: settings.maxChunks ?? 1_000 };
|
|
161
|
+
}
|
|
162
|
+
function joinUrl(baseUrl, suffix) {
|
|
163
|
+
return baseUrl.replace(/\/+$/, '') + '/' + suffix.replace(/^\/+/, '');
|
|
164
|
+
}
|
|
165
|
+
function encodeVector(vector) {
|
|
166
|
+
return Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength).toString('base64');
|
|
167
|
+
}
|
|
168
|
+
function decodeVector(encoded) {
|
|
169
|
+
const buffer = Buffer.from(encoded, 'base64');
|
|
170
|
+
return new Float32Array(buffer.buffer, buffer.byteOffset, Math.floor(buffer.byteLength / 4));
|
|
171
|
+
}
|