chore: init main branch

This commit is contained in:
954801926@qq.com committed 2026-07-29 23:36:41 +08:00
commit 549c8b5b13
231 files changed
+27286

No files matched your search

+88
View File
@@ -0,0 +1,88 @@
import type { AgentSettings } from "../types";
const DEFAULT_SETTINGS: AgentSettings = {
thinking: {
enabled: true,
effort: "medium",
keep: "all",
},
loop_control: {
max_retries_per_step: 3,
reserved_context_size: 50000,
},
background: {
keep_alive_on_exit: false,
},
permission: { rules: [] },
hooks: [],
};
function asRecord(value: unknown): Record<string, unknown> {
if (value && typeof value === "object" && !Array.isArray(value)) {
return value as Record<string, unknown>;
}
return {};
}
function getSection<T>(rawOther: unknown, key: string): T | undefined {
const root = asRecord(rawOther);
const section = root[key];
if (section === undefined || section === null) return undefined;
return section as T;
}
export function getAgentSettings(rawOther: unknown): AgentSettings {
return {
thinking: {
...DEFAULT_SETTINGS.thinking,
...getSection<AgentSettings["thinking"]>(rawOther, "thinking"),
},
loop_control: {
...DEFAULT_SETTINGS.loop_control,
...getSection<AgentSettings["loop_control"]>(rawOther, "loop_control"),
},
background: {
...DEFAULT_SETTINGS.background,
...getSection<AgentSettings["background"]>(rawOther, "background"),
},
permission: {
rules:
getSection<AgentSettings["permission"]>(rawOther, "permission")?.rules ??
[],
},
hooks: getSection<AgentSettings["hooks"]>(rawOther, "hooks") ?? [],
};
}
export function setAgentSettings(
rawOther: unknown,
patch: Partial<AgentSettings>
): unknown {
const root = { ...asRecord(rawOther) };
const current = getAgentSettings(rawOther);
const next: AgentSettings = {
thinking: { ...current.thinking, ...patch.thinking },
loop_control: { ...current.loop_control, ...patch.loop_control },
background: { ...current.background, ...patch.background },
permission: {
rules: patch.permission?.rules ?? current.permission?.rules ?? [],
},
hooks: patch.hooks ?? current.hooks ?? [],
};
if (next.thinking) root.thinking = next.thinking;
if (next.loop_control) root.loop_control = next.loop_control;
if (next.background) root.background = next.background;
if (next.permission?.rules && next.permission.rules.length > 0) {
root.permission = next.permission;
} else {
delete root.permission;
}
if (next.hooks && next.hooks.length > 0) {
root.hooks = next.hooks;
} else {
delete root.hooks;
}
return root;
}
+38
View File
@@ -0,0 +1,38 @@
// Formatting helpers for the dashboard — ported from kimicode-dashboard/web/src/format.js
export function fmtInt(n: number, locale = "en"): string {
return new Intl.NumberFormat(locale === "zh" ? "zh-CN" : "en-US").format(
Math.round(Number(n) || 0)
);
}
export function fmtTokens(n: number): string {
const v = Number(n) || 0;
if (v >= 1e9) return (v / 1e9).toFixed(2) + "B";
if (v >= 1e6) return (v / 1e6).toFixed(2) + "M";
if (v >= 1e3) return (v / 1e3).toFixed(1) + "K";
return String(Math.round(v));
}
export function fmtUsd(n: number): string {
const v = Number(n) || 0;
if (v === 0) return "$0.00";
if (v < 0.01) return "$" + v.toFixed(4);
if (v < 1) return "$" + v.toFixed(3);
return "$" + v.toFixed(2);
}
export function fmtPct(n: number): string {
return ((Number(n) || 0) * 100).toFixed(1) + "%";
}
export function fmtTime(ms: number, locale = "en"): string {
if (!ms) return "—";
return new Date(ms).toLocaleString(locale === "zh" ? "zh-CN" : "en-US", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
+90
View File
@@ -0,0 +1,90 @@
export const DEFAULT_MAX_CONTEXT_SIZE = 256000;
interface ModelContextRule {
pattern: RegExp;
max_context_size: number;
}
const MODEL_CONTEXT_RULES: ModelContextRule[] = [
// Kimi
{ pattern: /^kimi-for-coding$/i, max_context_size: 262144 },
{ pattern: /^kimi-k2\.5/i, max_context_size: 256000 },
{ pattern: /^kimi-k2/i, max_context_size: 256000 },
{ pattern: /^kimi-/i, max_context_size: 256000 },
// GLM
{ pattern: /^glm-5\.2/i, max_context_size: 1000000 },
{ pattern: /^glm-5\.1/i, max_context_size: 256000 },
{ pattern: /^glm-5/i, max_context_size: 256000 },
{ pattern: /^glm-4/i, max_context_size: 128000 },
{ pattern: /^glm-/i, max_context_size: 128000 },
// MiniMax
{ pattern: /^MiniMax-M3/i, max_context_size: 1000000 },
{ pattern: /^MiniMax-Text-01/i, max_context_size: 400000 },
{ pattern: /^MiniMax-/i, max_context_size: 256000 },
// Qwen
{ pattern: /^qwen2\.5/i, max_context_size: 128000 },
{ pattern: /^qwen-max/i, max_context_size: 128000 },
{ pattern: /^qwen-plus/i, max_context_size: 128000 },
{ pattern: /^qwen-turbo/i, max_context_size: 128000 },
{ pattern: /^qwen-coder/i, max_context_size: 128000 },
{ pattern: /^qwen-/i, max_context_size: 128000 },
// DeepSeek
{ pattern: /^deepseek-r1/i, max_context_size: 64000 },
{ pattern: /^deepseek-v3/i, max_context_size: 64000 },
{ pattern: /^deepseek-coder/i, max_context_size: 64000 },
{ pattern: /^deepseek-/i, max_context_size: 64000 },
// Hunyuan
{ pattern: /^hunyuan-pro/i, max_context_size: 32000 },
{ pattern: /^hunyuan-standard/i, max_context_size: 32000 },
{ pattern: /^hunyuan-lite/i, max_context_size: 32000 },
{ pattern: /^hunyuan-/i, max_context_size: 32000 },
// Doubao
{ pattern: /^doubao-pro/i, max_context_size: 128000 },
{ pattern: /^doubao-lite/i, max_context_size: 128000 },
{ pattern: /^doubao-vision/i, max_context_size: 128000 },
{ pattern: /^doubao-/i, max_context_size: 128000 },
// ERNIE
{ pattern: /^ernie-4\.0/i, max_context_size: 128000 },
{ pattern: /^ernie-3\.5/i, max_context_size: 128000 },
{ pattern: /^ernie-speed/i, max_context_size: 128000 },
{ pattern: /^ernie-lite/i, max_context_size: 128000 },
{ pattern: /^ernie-/i, max_context_size: 128000 },
// Spark
{ pattern: /^spark-v4/i, max_context_size: 32000 },
{ pattern: /^spark-v3\.5/i, max_context_size: 32000 },
{ pattern: /^spark-pro/i, max_context_size: 32000 },
{ pattern: /^spark-max/i, max_context_size: 32000 },
{ pattern: /^spark-/i, max_context_size: 32000 },
// SenseChat
{ pattern: /^sensechat-/i, max_context_size: 128000 },
// Baichuan
{ pattern: /^baichuan-4/i, max_context_size: 128000 },
{ pattern: /^baichuan-3/i, max_context_size: 128000 },
{ pattern: /^baichuan-/i, max_context_size: 128000 },
// Yi
{ pattern: /^yi-/i, max_context_size: 128000 },
// Claude
{ pattern: /^claude-opus/i, max_context_size: 200000 },
{ pattern: /^claude-sonnet/i, max_context_size: 200000 },
{ pattern: /^claude-haiku/i, max_context_size: 200000 },
{ pattern: /^claude-/i, max_context_size: 200000 },
// OpenAI
{ pattern: /^gpt-4\.1/i, max_context_size: 1047576 },
{ pattern: /^gpt-4o/i, max_context_size: 128000 },
{ pattern: /^gpt-4-turbo/i, max_context_size: 128000 },
{ pattern: /^gpt-4-/i, max_context_size: 128000 },
// Gemini
{ pattern: /^gemini-2\.0-flash/i, max_context_size: 1048576 },
{ pattern: /^gemini-1\.5-pro/i, max_context_size: 2097152 },
{ pattern: /^gemini-1\.5-flash/i, max_context_size: 1048576 },
{ pattern: /^gemini-/i, max_context_size: 1048576 },
];
export function getDefaultMaxContextSize(modelId: string): number {
for (const rule of MODEL_CONTEXT_RULES) {
if (rule.pattern.test(modelId)) {
return rule.max_context_size;
}
}
return DEFAULT_MAX_CONTEXT_SIZE;
}
File diff suppressed because it is too large. Load diff
+57
View File
@@ -0,0 +1,57 @@
import snapshot from "./models-dev.json";
/**
* Compact per-model reference data from models.dev
* (see scripts/fetch-models-dev.mjs).
*/
export interface ModelRef {
name?: string;
context?: number;
reasoning?: boolean;
tool_call?: boolean;
structured_output?: boolean;
image?: boolean;
video?: boolean;
}
const data: Record<string, ModelRef> = snapshot;
// models.dev keys are "<lab>/<model>" and may be mixed-case (e.g.
// "minimax/MiniMax-M3"), while provider APIs return bare model ids
// (e.g. "kimi-k2.5"). Index lowercase for tolerant matching.
const byLowerKey: Record<string, ModelRef> = {};
for (const [key, value] of Object.entries(data)) {
byLowerKey[key.toLowerCase()] = value;
}
/**
* Look up reference data for a provider model id.
* Matches exact key first, then any "<lab>/<id>" suffix.
* Returns undefined when nothing matches — callers fall back to
* getDefaultMaxContextSize / manual capabilities.
*/
export function getModelRef(modelId: string): ModelRef | undefined {
const id = modelId.trim().toLowerCase();
if (!id) return undefined;
const exact = byLowerKey[id];
if (exact) return exact;
const suffix = `/${id}`;
for (const [key, value] of Object.entries(byLowerKey)) {
if (key.endsWith(suffix)) return value;
}
return undefined;
}
/**
* Map reference data to Kimi Code capability flags.
* Multimodal (image_in/video_in) comes from models.dev input modalities;
* always_thinking cannot be derived and stays manual.
*/
export function capabilitiesFromRef(ref: ModelRef): string[] {
const caps: string[] = [];
if (ref.reasoning) caps.push("thinking");
if (ref.tool_call) caps.push("tool_use");
if (ref.image) caps.push("image_in");
if (ref.video) caps.push("video_in");
return caps;
}
+86
View File
@@ -0,0 +1,86 @@
import type { Config, Model, ProviderType } from "../types";
/**
* Provider types that must have a base_url set in order to function.
* Anthropic and Vertexai intentionally have no default — they read the
* agent's CLI environment / SDK defaults, so leaving base_url empty is
* a valid (and the only) state.
*/
const TYPES_REQUIRING_BASE_URL: ReadonlySet<ProviderType> = new Set<ProviderType>([
"openai",
"openai_responses",
"kimi",
"google-genai",
]);
export interface ProviderIssue {
name: string;
reasons: string[];
}
/**
* Inspect a config and report any enabled providers that are not ready to
* use. Used by the save flow to warn the user before persisting an
* incomplete state to config.toml / SQLite.
*
* "Incomplete" means any of:
* - missing API key (and not a managed OAuth provider)
* - missing base_url for provider types that need one
* - zero configured models
*
* Disabled providers are skipped — the user explicitly turned them off.
*/
export function validateProviders(
config: Config,
modelIndex: Record<string, Model>,
// The i18n `t` from useTranslation() is strictly typed to its known key
// union; a generic (key: string) signature is the right shape for a
// library-style helper that doesn't want to import the project's i18n
// module directly.
t: (key: string, vars?: Record<string, unknown>) => string,
// When set, only inspect this single provider. Used when activating a
// specific provider: sibling providers merely listed in the config
// must not block the switch.
target?: string,
): ProviderIssue[] {
const issues: ProviderIssue[] = [];
for (const provider of Object.values(config.providers)) {
if (target && provider.name !== target) continue;
if (!provider.enabled) continue;
const reasons: string[] = [];
const apiKey = provider.api_key?.trim();
if (!apiKey && !provider.managed) {
reasons.push(t("validationNoApiKey"));
}
const baseUrl = provider.base_url?.trim();
if (!baseUrl && TYPES_REQUIRING_BASE_URL.has(provider.provider_type)) {
reasons.push(t("validationNoBaseUrl"));
}
const providerModels = Object.values(modelIndex).filter(
(m) => m.provider === provider.name,
);
if (providerModels.length === 0) {
reasons.push(t("validationNoModels"));
} else {
// A model entry exists but its `model` field may be blank (e.g. the
// user hit "+ 添加模型" in the edit page and saved before filling
// in the upstream model id). Those still break the provider.
const blankAliases = providerModels
.filter((m) => !m.model || !m.model.trim())
.map((m) => m.alias);
if (blankAliases.length > 0) {
reasons.push(
t("validationBlankModels", { aliases: blankAliases.join(", ") }),
);
}
}
if (reasons.length > 0) {
issues.push({ name: provider.name, reasons });
}
}
return issues;
}