mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 06:24:32 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5816ca8ae3 | |||
| f6cee96394 | |||
| ecd5927769 | |||
| 1bac3922fb | |||
| 920b675ec2 | |||
| b5ace10066 | |||
| 6dcc3803b4 | |||
| 158a679b8b | |||
| c303cc8acf | |||
| 71dc459eb2 | |||
| 7fee0d8fa6 | |||
| 657b7c29c2 | |||
| 750e1060e8 | |||
| 3d25350cbf | |||
| 860d6ec8dd | |||
| 8371e9e683 | |||
| acbeb03917 | |||
| 7cee0de784 | |||
| 39ec987396 | |||
| 9b71a280ea | |||
| c5668165a8 | |||
| d9a7c78138 | |||
| c8a6aab5b2 | |||
| 49de0896b3 | |||
| e1e2bdef2a | |||
| c380528a27 | |||
| a8dbea1398 | |||
| 7138aca310 | |||
| 992dda5c5c |
@@ -156,6 +156,7 @@ Claude Code / Codex / Gemini official channels at 38% / 2% / 9% of original pric
|
||||
**Core Capabilities**
|
||||
|
||||
- **Provider Management**: One-click switching between Claude Code, Codex, and Gemini API configurations
|
||||
- **AWS Bedrock Support**: Built-in AWS Bedrock provider presets with AKSK and API Key authentication, cross-region inference support (global/us/eu/apac), covering Claude Code and OpenCode
|
||||
- **Speed Testing**: Measure API endpoint latency with visual quality indicators
|
||||
- **Import/Export**: Backup and restore configs with auto-rotation (keep 10 most recent)
|
||||
- **i18n Support**: Complete Chinese/English localization (UI, errors, tray)
|
||||
|
||||
@@ -156,6 +156,7 @@ Claude Code / Codex / Gemini 公式チャンネルが最安で元価格の 38% /
|
||||
**コア機能**
|
||||
|
||||
- **プロバイダ管理**:Claude Code、Codex、Gemini の API 設定をワンクリックで切り替え
|
||||
- **AWS Bedrock 対応**:AWS Bedrock プロバイダプリセットを内蔵、AKSK および API Key 認証に対応、クロスリージョン推論(global/us/eu/apac)をサポート、Claude Code と OpenCode に対応
|
||||
- **速度テスト**:エンドポイント遅延を計測し、品質を可視化
|
||||
- **インポート/エクスポート**:設定をバックアップ・復元(最新 10 件を自動ローテーション)
|
||||
- **多言語対応**:UI/エラー/トレイを含む中国語・英語・日本語ローカライズ
|
||||
|
||||
@@ -157,6 +157,7 @@ Claude Code / Codex / Gemini 官方渠道低至 3.8 / 0.2 / 0.9 折,充值更
|
||||
**核心功能**
|
||||
|
||||
- **供应商管理**:一键切换 Claude Code、Codex 与 Gemini 的 API 配置
|
||||
- **AWS Bedrock 支持**:内置 AWS Bedrock 供应商预设,支持 AKSK 和 API Key 两种认证方式,支持跨区域推理(global/us/eu/apac),覆盖 Claude Code 和 OpenCode
|
||||
- **速度测试**:测量 API 端点延迟,可视化连接质量指示器
|
||||
- **导入导出**:备份和恢复配置,自动轮换(保留最近 10 个)
|
||||
- **国际化支持**:完整的中英文本地化(UI、错误、托盘)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use regex::Regex;
|
||||
use std::sync::LazyLock;
|
||||
use tauri::AppHandle;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
use crate::config::write_text_file;
|
||||
use crate::openclaw_config::get_openclaw_dir;
|
||||
@@ -147,6 +149,141 @@ pub async fn write_daily_memory_file(filename: String, content: String) -> Resul
|
||||
.map_err(|e| format!("Failed to write daily memory file {filename}: {e}"))
|
||||
}
|
||||
|
||||
/// Find the largest index `<= i` that is a valid UTF-8 char boundary.
|
||||
/// Equivalent to the unstable `str::floor_char_boundary` (stabilized in 1.91).
|
||||
fn floor_char_boundary(s: &str, mut i: usize) -> usize {
|
||||
if i >= s.len() {
|
||||
return s.len();
|
||||
}
|
||||
while !s.is_char_boundary(i) {
|
||||
i -= 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
/// Find the smallest index `>= i` that is a valid UTF-8 char boundary.
|
||||
/// Equivalent to the unstable `str::ceil_char_boundary` (stabilized in 1.91).
|
||||
fn ceil_char_boundary(s: &str, mut i: usize) -> usize {
|
||||
if i >= s.len() {
|
||||
return s.len();
|
||||
}
|
||||
while !s.is_char_boundary(i) {
|
||||
i += 1;
|
||||
}
|
||||
i
|
||||
}
|
||||
|
||||
/// Search result for daily memory full-text search.
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DailyMemorySearchResult {
|
||||
pub filename: String,
|
||||
pub date: String,
|
||||
pub size_bytes: u64,
|
||||
pub modified_at: u64,
|
||||
pub snippet: String,
|
||||
pub match_count: usize,
|
||||
}
|
||||
|
||||
/// Full-text search across all daily memory files.
|
||||
///
|
||||
/// Performs case-insensitive search on both the date field and file content.
|
||||
/// Returns results sorted by filename descending (newest first), each with a
|
||||
/// snippet showing ~120 characters of context around the first match.
|
||||
#[tauri::command]
|
||||
pub async fn search_daily_memory_files(
|
||||
query: String,
|
||||
) -> Result<Vec<DailyMemorySearchResult>, String> {
|
||||
let memory_dir = get_openclaw_dir().join("workspace").join("memory");
|
||||
|
||||
if !memory_dir.exists() || query.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let query_lower = query.to_lowercase();
|
||||
let mut results: Vec<DailyMemorySearchResult> = Vec::new();
|
||||
|
||||
let entries = std::fs::read_dir(&memory_dir)
|
||||
.map_err(|e| format!("Failed to read memory directory: {e}"))?;
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
if !name.ends_with(".md") {
|
||||
continue;
|
||||
}
|
||||
let meta = match entry.metadata() {
|
||||
Ok(m) if m.is_file() => m,
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
let date = name.trim_end_matches(".md").to_string();
|
||||
let content = std::fs::read_to_string(entry.path()).unwrap_or_default();
|
||||
let content_lower = content.to_lowercase();
|
||||
|
||||
// Count matches in content
|
||||
let content_matches: Vec<usize> = content_lower
|
||||
.match_indices(&query_lower)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
|
||||
// Also check date field
|
||||
let date_matches = date.to_lowercase().contains(&query_lower);
|
||||
|
||||
if content_matches.is_empty() && !date_matches {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build snippet around first content match (~120 chars of context)
|
||||
let snippet = if let Some(&first_pos) = content_matches.first() {
|
||||
let start = if first_pos > 50 {
|
||||
floor_char_boundary(&content, first_pos - 50)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let end = ceil_char_boundary(&content, (first_pos + 70).min(content.len()));
|
||||
let mut s = String::new();
|
||||
if start > 0 {
|
||||
s.push_str("...");
|
||||
}
|
||||
s.push_str(&content[start..end]);
|
||||
if end < content.len() {
|
||||
s.push_str("...");
|
||||
}
|
||||
s
|
||||
} else {
|
||||
// Date-only match — use beginning of file as preview
|
||||
let end = ceil_char_boundary(&content, 120.min(content.len()));
|
||||
let mut s = content[..end].to_string();
|
||||
if end < content.len() {
|
||||
s.push_str("...");
|
||||
}
|
||||
s
|
||||
};
|
||||
|
||||
let size_bytes = meta.len();
|
||||
let modified_at = meta
|
||||
.modified()
|
||||
.ok()
|
||||
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
|
||||
results.push(DailyMemorySearchResult {
|
||||
filename: name,
|
||||
date,
|
||||
size_bytes,
|
||||
modified_at,
|
||||
snippet,
|
||||
match_count: content_matches.len(),
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by filename descending (newest date first)
|
||||
results.sort_by(|a, b| b.filename.cmp(&a.filename));
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Delete a daily memory file (idempotent).
|
||||
#[tauri::command]
|
||||
pub async fn delete_daily_memory_file(filename: String) -> Result<(), String> {
|
||||
@@ -201,3 +338,25 @@ pub async fn write_workspace_file(filename: String, content: String) -> Result<(
|
||||
write_text_file(&path, &content)
|
||||
.map_err(|e| format!("Failed to write workspace file {filename}: {e}"))
|
||||
}
|
||||
|
||||
/// Open the workspace or memory directory in the system file manager.
|
||||
/// `subdir`: "workspace" opens `~/.openclaw/workspace/`,
|
||||
/// "memory" opens `~/.openclaw/workspace/memory/`.
|
||||
#[tauri::command]
|
||||
pub async fn open_workspace_directory(handle: AppHandle, subdir: String) -> Result<bool, String> {
|
||||
let dir = match subdir.as_str() {
|
||||
"memory" => get_openclaw_dir().join("workspace").join("memory"),
|
||||
_ => get_openclaw_dir().join("workspace"),
|
||||
};
|
||||
|
||||
if !dir.exists() {
|
||||
std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create directory: {e}"))?;
|
||||
}
|
||||
|
||||
handle
|
||||
.opener()
|
||||
.open_path(dir.to_string_lossy().to_string(), None::<String>)
|
||||
.map_err(|e| format!("Failed to open directory: {e}"))?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -1051,6 +1051,8 @@ pub fn run() {
|
||||
commands::read_daily_memory_file,
|
||||
commands::write_daily_memory_file,
|
||||
commands::delete_daily_memory_file,
|
||||
commands::search_daily_memory_files,
|
||||
commands::open_workspace_directory,
|
||||
]);
|
||||
|
||||
let app = builder
|
||||
|
||||
@@ -198,8 +198,8 @@ export function ClaudeFormFields({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* API 格式选择(仅非官方供应商显示) */}
|
||||
{shouldShowModelSelector && (
|
||||
{/* API 格式选择(仅非官方、非云服务商显示) */}
|
||||
{shouldShowModelSelector && category !== "cloud_provider" && (
|
||||
<div className="space-y-2">
|
||||
<FormLabel htmlFor="apiFormat">
|
||||
{t("providerForm.apiFormat", { defaultValue: "API 格式" })}
|
||||
|
||||
@@ -40,7 +40,10 @@ import {
|
||||
import { OpenCodeFormFields } from "./OpenCodeFormFields";
|
||||
import { OpenClawFormFields } from "./OpenClawFormFields";
|
||||
import type { UniversalProviderPreset } from "@/config/universalProviderPresets";
|
||||
import { applyTemplateValues } from "@/utils/providerConfigUtils";
|
||||
import {
|
||||
applyTemplateValues,
|
||||
hasApiKeyField,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
|
||||
import { getCodexCustomTemplate } from "@/config/codexTemplates";
|
||||
import CodexConfigEditor from "./CodexConfigEditor";
|
||||
@@ -594,7 +597,8 @@ export function ProviderForm({
|
||||
}
|
||||
|
||||
// 非官方供应商必填校验:端点和 API Key
|
||||
if (category !== "official") {
|
||||
// cloud_provider(如 Bedrock)通过模板变量处理认证,跳过通用校验
|
||||
if (category !== "official" && category !== "cloud_provider") {
|
||||
if (appId === "claude") {
|
||||
if (!baseUrl.trim()) {
|
||||
toast.error(
|
||||
@@ -843,7 +847,8 @@ export function ProviderForm({
|
||||
);
|
||||
}, [groupedPresets]);
|
||||
|
||||
const shouldShowSpeedTest = category !== "official";
|
||||
const shouldShowSpeedTest =
|
||||
category !== "official" && category !== "cloud_provider";
|
||||
|
||||
const {
|
||||
shouldShowApiKeyLink: shouldShowClaudeApiKeyLink,
|
||||
@@ -1233,10 +1238,13 @@ export function ProviderForm({
|
||||
{appId === "claude" && (
|
||||
<ClaudeFormFields
|
||||
providerId={providerId}
|
||||
shouldShowApiKey={shouldShowApiKey(
|
||||
form.getValues("settingsConfig"),
|
||||
isEditMode,
|
||||
)}
|
||||
shouldShowApiKey={
|
||||
hasApiKeyField(form.getValues("settingsConfig"), "claude") &&
|
||||
shouldShowApiKey(
|
||||
form.getValues("settingsConfig"),
|
||||
isEditMode,
|
||||
)
|
||||
}
|
||||
apiKey={apiKey}
|
||||
onApiKeyChange={handleApiKeyChange}
|
||||
category={category}
|
||||
|
||||
@@ -525,15 +525,14 @@ export function AboutSection({ isPortable }: AboutSectionProps) {
|
||||
{isLoadingTools || loadingTools[toolName] ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : tool?.version ? (
|
||||
<div className="flex items-center gap-1.5">
|
||||
{tool.latest_version &&
|
||||
tool.version !== tool.latest_version && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border border-yellow-500/20">
|
||||
{tool.latest_version}
|
||||
</span>
|
||||
)}
|
||||
tool.latest_version &&
|
||||
tool.version !== tool.latest_version ? (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 border border-yellow-500/20">
|
||||
{tool.latest_version}
|
||||
</span>
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<AlertCircle className="h-4 w-4 text-yellow-500" />
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useRef,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { Calendar, Trash2, Plus } from "lucide-react";
|
||||
import { Calendar, Trash2, Plus, Search, X, FolderOpen } from "lucide-react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import MarkdownEditor from "@/components/MarkdownEditor";
|
||||
import { workspaceApi, type DailyMemoryFileInfo } from "@/lib/api/workspace";
|
||||
import {
|
||||
workspaceApi,
|
||||
type DailyMemoryFileInfo,
|
||||
type DailyMemorySearchResult,
|
||||
} from "@/lib/api/workspace";
|
||||
|
||||
interface DailyMemoryPanelProps {
|
||||
isOpen: boolean;
|
||||
@@ -46,6 +58,16 @@ const DailyMemoryPanel: React.FC<DailyMemoryPanelProps> = ({
|
||||
// Delete state
|
||||
const [deletingFile, setDeletingFile] = useState<string | null>(null);
|
||||
|
||||
// Search state
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
const [searchResults, setSearchResults] = useState<DailyMemorySearchResult[]>(
|
||||
[],
|
||||
);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
// Dark mode
|
||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||
|
||||
@@ -61,6 +83,100 @@ const DailyMemoryPanel: React.FC<DailyMemoryPanelProps> = ({
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Whether we are in active search mode (search open with a non-empty term)
|
||||
const isActiveSearch = useMemo(
|
||||
() => isSearchOpen && searchTerm.trim().length > 0,
|
||||
[isSearchOpen, searchTerm],
|
||||
);
|
||||
|
||||
// Debounced search execution
|
||||
const executeSearch = useCallback(
|
||||
async (query: string) => {
|
||||
if (!query.trim()) {
|
||||
setSearchResults([]);
|
||||
setSearching(false);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
try {
|
||||
const results = await workspaceApi.searchDailyMemoryFiles(query.trim());
|
||||
setSearchResults(results);
|
||||
} catch (err) {
|
||||
console.error("Failed to search daily memory files:", err);
|
||||
toast.error(t("workspace.dailyMemory.searchFailed"));
|
||||
} finally {
|
||||
setSearching(false);
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
// Handle search input change with debounce
|
||||
const handleSearchChange = useCallback(
|
||||
(value: string) => {
|
||||
setSearchTerm(value);
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
debounceTimerRef.current = setTimeout(() => {
|
||||
void executeSearch(value);
|
||||
}, 300);
|
||||
},
|
||||
[executeSearch],
|
||||
);
|
||||
|
||||
// Open search bar
|
||||
const openSearch = useCallback(() => {
|
||||
setIsSearchOpen(true);
|
||||
// Focus input on next frame
|
||||
requestAnimationFrame(() => {
|
||||
searchInputRef.current?.focus();
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Close search bar and clear state
|
||||
const closeSearch = useCallback(() => {
|
||||
setIsSearchOpen(false);
|
||||
setSearchTerm("");
|
||||
setSearchResults([]);
|
||||
setSearching(false);
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Keyboard shortcut: Cmd/Ctrl+F to open search, Escape to close
|
||||
useEffect(() => {
|
||||
if (!isOpen || editingFile) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "f") {
|
||||
e.preventDefault();
|
||||
if (!isSearchOpen) {
|
||||
openSearch();
|
||||
} else {
|
||||
searchInputRef.current?.focus();
|
||||
}
|
||||
}
|
||||
if (e.key === "Escape" && isSearchOpen) {
|
||||
e.preventDefault();
|
||||
closeSearch();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [isOpen, editingFile, isSearchOpen, openSearch, closeSearch]);
|
||||
|
||||
// Clean up debounce timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (debounceTimerRef.current) {
|
||||
clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Load file list
|
||||
const loadFiles = useCallback(async () => {
|
||||
setLoadingList(true);
|
||||
@@ -142,24 +258,44 @@ const DailyMemoryPanel: React.FC<DailyMemoryPanelProps> = ({
|
||||
setEditingFile(null);
|
||||
}
|
||||
await loadFiles();
|
||||
// Re-trigger search if active
|
||||
if (isSearchOpen && searchTerm.trim()) {
|
||||
void executeSearch(searchTerm);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to delete daily memory file:", err);
|
||||
toast.error(t("workspace.dailyMemory.deleteFailed"));
|
||||
setDeletingFile(null);
|
||||
}
|
||||
}, [deletingFile, editingFile, loadFiles, t]);
|
||||
}, [
|
||||
deletingFile,
|
||||
editingFile,
|
||||
loadFiles,
|
||||
t,
|
||||
isSearchOpen,
|
||||
searchTerm,
|
||||
executeSearch,
|
||||
]);
|
||||
|
||||
// Back from edit mode to list mode
|
||||
// Back from edit mode to list mode — preserve search state
|
||||
const handleBackToList = useCallback(() => {
|
||||
setEditingFile(null);
|
||||
setContent("");
|
||||
void loadFiles();
|
||||
}, [loadFiles]);
|
||||
// Re-trigger search if active (file content may have changed)
|
||||
if (isSearchOpen && searchTerm.trim()) {
|
||||
void executeSearch(searchTerm);
|
||||
}
|
||||
}, [loadFiles, isSearchOpen, searchTerm, executeSearch]);
|
||||
|
||||
// Close panel entirely
|
||||
// Close panel entirely — clear search state
|
||||
const handleClose = useCallback(() => {
|
||||
setEditingFile(null);
|
||||
setContent("");
|
||||
setIsSearchOpen(false);
|
||||
setSearchTerm("");
|
||||
setSearchResults([]);
|
||||
setSearching(false);
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
@@ -214,24 +350,142 @@ const DailyMemoryPanel: React.FC<DailyMemoryPanelProps> = ({
|
||||
onClose={handleClose}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Header with path and create button */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
~/.openclaw/workspace/memory/
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateToday}
|
||||
className="gap-1.5"
|
||||
{/* Header with path, search, and create button */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p
|
||||
className="text-sm text-muted-foreground shrink-0 cursor-pointer hover:text-foreground transition-colors inline-flex items-center gap-1"
|
||||
onClick={() => workspaceApi.openDirectory("memory")}
|
||||
title={t("workspace.openDirectory")}
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
{t("workspace.dailyMemory.createToday")}
|
||||
</Button>
|
||||
~/.openclaw/workspace/memory/
|
||||
<FolderOpen className="w-3.5 h-3.5" />
|
||||
</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={isSearchOpen ? closeSearch : openSearch}
|
||||
title={t("workspace.dailyMemory.searchScopeHint")}
|
||||
>
|
||||
<Search className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCreateToday}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
{t("workspace.dailyMemory.createToday")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* File list */}
|
||||
{loadingList ? (
|
||||
{/* Search bar */}
|
||||
<AnimatePresence>
|
||||
{isSearchOpen && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground pointer-events-none" />
|
||||
<Input
|
||||
ref={searchInputRef}
|
||||
value={searchTerm}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
placeholder={t("workspace.dailyMemory.searchPlaceholder")}
|
||||
className="pl-8 pr-8 h-8 text-sm"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => handleSearchChange("")}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={closeSearch}
|
||||
className="text-xs text-muted-foreground h-8 px-2 shrink-0"
|
||||
>
|
||||
{t("workspace.dailyMemory.searchCloseHint")}
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Content: search results or normal file list */}
|
||||
{isActiveSearch ? (
|
||||
// --- Search results ---
|
||||
searching ? (
|
||||
<div className="flex items-center justify-center h-48 text-muted-foreground">
|
||||
{t("workspace.dailyMemory.searching")}
|
||||
</div>
|
||||
) : searchResults.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center h-48 text-muted-foreground gap-3 border-2 border-dashed border-border rounded-xl">
|
||||
<Search className="w-10 h-10 opacity-40" />
|
||||
<p className="text-sm">
|
||||
{t("workspace.dailyMemory.noSearchResults")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{searchResults.map((result) => (
|
||||
<button
|
||||
key={result.filename}
|
||||
onClick={() => openFile(result.filename)}
|
||||
className="w-full flex items-start gap-3 p-4 rounded-xl border border-border bg-card hover:bg-accent/50 transition-colors text-left group"
|
||||
>
|
||||
<div className="mt-0.5 text-muted-foreground group-hover:text-foreground transition-colors">
|
||||
<Calendar className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-sm text-foreground">
|
||||
{result.date}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{formatFileSize(result.sizeBytes)}
|
||||
</span>
|
||||
{result.matchCount > 0 && (
|
||||
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-primary/10 text-primary font-medium">
|
||||
{t("workspace.dailyMemory.matchCount", {
|
||||
count: result.matchCount,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{result.snippet && (
|
||||
<p className="text-xs text-muted-foreground mt-1 line-clamp-2 whitespace-pre-line">
|
||||
{result.snippet}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setDeletingFile(result.filename);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-muted-foreground hover:text-destructive transition-colors" />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
) : // --- Normal file list ---
|
||||
loadingList ? (
|
||||
<div className="flex items-center justify-center h-48 text-muted-foreground">
|
||||
{t("prompts.loading")}
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
Circle,
|
||||
Calendar,
|
||||
ChevronRight,
|
||||
FolderOpen,
|
||||
} from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { workspaceApi } from "@/lib/api/workspace";
|
||||
@@ -83,8 +84,13 @@ const WorkspaceFilesPanel: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="px-6 pt-4 pb-8">
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
<p
|
||||
className="text-sm text-muted-foreground mb-6 cursor-pointer hover:text-foreground transition-colors inline-flex items-center gap-1"
|
||||
onClick={() => workspaceApi.openDirectory("workspace")}
|
||||
title={t("workspace.openDirectory")}
|
||||
>
|
||||
~/.openclaw/workspace/
|
||||
<FolderOpen className="w-3.5 h-3.5" />
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
|
||||
@@ -92,10 +92,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://open.bigmodel.cn/api/anthropic",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "glm-4.7",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.7",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.7",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.7",
|
||||
ANTHROPIC_MODEL: "glm-5",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-5",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-5",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-5",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -110,10 +110,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.z.ai/api/anthropic",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "glm-4.7",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-4.7",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-4.7",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-4.7",
|
||||
ANTHROPIC_MODEL: "glm-5",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "glm-5",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "glm-5",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "glm-5",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -170,10 +170,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api-inference.modelscope.cn",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "ZhipuAI/GLM-4.7",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "ZhipuAI/GLM-4.7",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "ZhipuAI/GLM-4.7",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "ZhipuAI/GLM-4.7",
|
||||
ANTHROPIC_MODEL: "ZhipuAI/GLM-5",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "ZhipuAI/GLM-5",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "ZhipuAI/GLM-5",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "ZhipuAI/GLM-5",
|
||||
},
|
||||
},
|
||||
category: "aggregator",
|
||||
@@ -236,10 +236,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
API_TIMEOUT_MS: "3000000",
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
||||
ANTHROPIC_MODEL: "MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2.1",
|
||||
ANTHROPIC_MODEL: "MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2.5",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -262,10 +262,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
API_TIMEOUT_MS: "3000000",
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
||||
ANTHROPIC_MODEL: "MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2.1",
|
||||
ANTHROPIC_MODEL: "MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMax-M2.5",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -287,10 +287,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
ANTHROPIC_BASE_URL: "https://ark.cn-beijing.volces.com/api/coding",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
API_TIMEOUT_MS: "3000000",
|
||||
ANTHROPIC_MODEL: "doubao-seed-code-preview-latest",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "doubao-seed-code-preview-latest",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "doubao-seed-code-preview-latest",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "doubao-seed-code-preview-latest",
|
||||
ANTHROPIC_MODEL: "doubao-seed-2-0-code-preview-latest",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "doubao-seed-2-0-code-preview-latest",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "doubao-seed-2-0-code-preview-latest",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "doubao-seed-2-0-code-preview-latest",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -304,10 +304,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.tbox.cn/api/anthropic",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "Ling-1T",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "Ling-1T",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "Ling-1T",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "Ling-1T",
|
||||
ANTHROPIC_MODEL: "Ling-2.5-1T",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "Ling-2.5-1T",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "Ling-2.5-1T",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "Ling-2.5-1T",
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -336,10 +336,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.siliconflow.cn",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1",
|
||||
ANTHROPIC_MODEL: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||
},
|
||||
},
|
||||
category: "aggregator",
|
||||
@@ -354,10 +354,10 @@ export const providerPresets: ProviderPreset[] = [
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL: "https://api.siliconflow.com",
|
||||
ANTHROPIC_AUTH_TOKEN: "",
|
||||
ANTHROPIC_MODEL: "MiniMaxAI/MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMaxAI/MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMaxAI/MiniMax-M2.1",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMaxAI/MiniMax-M2.1",
|
||||
ANTHROPIC_MODEL: "MiniMaxAI/MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMaxAI/MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMaxAI/MiniMax-M2.5",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMaxAI/MiniMax-M2.5",
|
||||
},
|
||||
},
|
||||
category: "aggregator",
|
||||
@@ -531,4 +531,73 @@ export const providerPresets: ProviderPreset[] = [
|
||||
icon: "xiaomimimo",
|
||||
iconColor: "#000000",
|
||||
},
|
||||
{
|
||||
name: "AWS Bedrock (AKSK)",
|
||||
websiteUrl: "https://aws.amazon.com/bedrock/",
|
||||
settingsConfig: {
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL:
|
||||
"https://bedrock-runtime.${AWS_REGION}.amazonaws.com",
|
||||
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}",
|
||||
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}",
|
||||
AWS_REGION: "${AWS_REGION}",
|
||||
ANTHROPIC_MODEL: "global.anthropic.claude-opus-4-6-v1",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL:
|
||||
"global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL:
|
||||
"global.anthropic.claude-sonnet-4-6",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "global.anthropic.claude-opus-4-6-v1",
|
||||
CLAUDE_CODE_USE_BEDROCK: "1",
|
||||
},
|
||||
},
|
||||
category: "cloud_provider",
|
||||
templateValues: {
|
||||
AWS_REGION: {
|
||||
label: "AWS Region",
|
||||
placeholder: "us-west-2",
|
||||
editorValue: "us-west-2",
|
||||
},
|
||||
AWS_ACCESS_KEY_ID: {
|
||||
label: "Access Key ID",
|
||||
placeholder: "AKIA...",
|
||||
editorValue: "",
|
||||
},
|
||||
AWS_SECRET_ACCESS_KEY: {
|
||||
label: "Secret Access Key",
|
||||
placeholder: "your-secret-key",
|
||||
editorValue: "",
|
||||
},
|
||||
},
|
||||
icon: "aws",
|
||||
iconColor: "#FF9900",
|
||||
},
|
||||
{
|
||||
name: "AWS Bedrock (API Key)",
|
||||
websiteUrl: "https://aws.amazon.com/bedrock/",
|
||||
settingsConfig: {
|
||||
apiKey: "",
|
||||
env: {
|
||||
ANTHROPIC_BASE_URL:
|
||||
"https://bedrock-runtime.${AWS_REGION}.amazonaws.com",
|
||||
AWS_REGION: "${AWS_REGION}",
|
||||
ANTHROPIC_MODEL: "global.anthropic.claude-opus-4-6-v1",
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL:
|
||||
"global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL:
|
||||
"global.anthropic.claude-sonnet-4-6",
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: "global.anthropic.claude-opus-4-6-v1",
|
||||
CLAUDE_CODE_USE_BEDROCK: "1",
|
||||
},
|
||||
},
|
||||
category: "cloud_provider",
|
||||
templateValues: {
|
||||
AWS_REGION: {
|
||||
label: "AWS Region",
|
||||
placeholder: "us-west-2",
|
||||
editorValue: "us-west-2",
|
||||
},
|
||||
},
|
||||
icon: "aws",
|
||||
iconColor: "#FF9900",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -112,8 +112,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "glm-4.7",
|
||||
name: "GLM-4.7",
|
||||
id: "glm-5",
|
||||
name: "GLM-5",
|
||||
contextWindow: 128000,
|
||||
cost: { input: 0.001, output: 0.001 },
|
||||
},
|
||||
@@ -138,8 +138,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "zhipu/glm-4.7" },
|
||||
modelCatalog: { "zhipu/glm-4.7": { alias: "GLM" } },
|
||||
model: { primary: "zhipu/glm-5" },
|
||||
modelCatalog: { "zhipu/glm-5": { alias: "GLM" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -152,8 +152,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "glm-4.7",
|
||||
name: "GLM-4.7",
|
||||
id: "glm-5",
|
||||
name: "GLM-5",
|
||||
contextWindow: 128000,
|
||||
cost: { input: 0.001, output: 0.001 },
|
||||
},
|
||||
@@ -178,8 +178,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "zhipu-en/glm-4.7" },
|
||||
modelCatalog: { "zhipu-en/glm-4.7": { alias: "GLM" } },
|
||||
model: { primary: "zhipu-en/glm-5" },
|
||||
modelCatalog: { "zhipu-en/glm-5": { alias: "GLM" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -192,8 +192,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "qwen3-max",
|
||||
name: "Qwen3 Max",
|
||||
id: "qwen3.5-plus",
|
||||
name: "Qwen3.5 Plus",
|
||||
contextWindow: 32000,
|
||||
cost: { input: 0.002, output: 0.006 },
|
||||
},
|
||||
@@ -216,8 +216,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "qwen/qwen3-max" },
|
||||
modelCatalog: { "qwen/qwen3-max": { alias: "Qwen" } },
|
||||
model: { primary: "qwen/qwen3.5-plus" },
|
||||
modelCatalog: { "qwen/qwen3.5-plus": { alias: "Qwen" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -306,8 +306,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "MiniMax-M2.1",
|
||||
name: "MiniMax M2.1",
|
||||
id: "MiniMax-M2.5",
|
||||
name: "MiniMax M2.5",
|
||||
contextWindow: 200000,
|
||||
cost: { input: 0.001, output: 0.004 },
|
||||
},
|
||||
@@ -330,8 +330,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "minimax/MiniMax-M2.1" },
|
||||
modelCatalog: { "minimax/MiniMax-M2.1": { alias: "MiniMax" } },
|
||||
model: { primary: "minimax/MiniMax-M2.5" },
|
||||
modelCatalog: { "minimax/MiniMax-M2.5": { alias: "MiniMax" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -344,8 +344,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "MiniMax-M2.1",
|
||||
name: "MiniMax M2.1",
|
||||
id: "MiniMax-M2.5",
|
||||
name: "MiniMax M2.5",
|
||||
contextWindow: 200000,
|
||||
cost: { input: 0.001, output: 0.004 },
|
||||
},
|
||||
@@ -368,8 +368,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "minimax-en/MiniMax-M2.1" },
|
||||
modelCatalog: { "minimax-en/MiniMax-M2.1": { alias: "MiniMax" } },
|
||||
model: { primary: "minimax-en/MiniMax-M2.5" },
|
||||
modelCatalog: { "minimax-en/MiniMax-M2.5": { alias: "MiniMax" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -465,7 +465,7 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "doubao-seed-code-preview-latest",
|
||||
id: "doubao-seed-2-0-code-preview-latest",
|
||||
name: "DouBao Seed Code Preview",
|
||||
contextWindow: 128000,
|
||||
cost: { input: 0.002, output: 0.006 },
|
||||
@@ -483,9 +483,9 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "doubaoseed/doubao-seed-code-preview-latest" },
|
||||
model: { primary: "doubaoseed/doubao-seed-2-0-code-preview-latest" },
|
||||
modelCatalog: {
|
||||
"doubaoseed/doubao-seed-code-preview-latest": { alias: "DouBao" },
|
||||
"doubaoseed/doubao-seed-2-0-code-preview-latest": { alias: "DouBao" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -498,8 +498,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "Ling-1T",
|
||||
name: "Ling 1T",
|
||||
id: "Ling-2.5-1T",
|
||||
name: "Ling 2.5 1T",
|
||||
contextWindow: 128000,
|
||||
cost: { input: 0.001, output: 0.004 },
|
||||
},
|
||||
@@ -514,8 +514,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "bailing/Ling-1T" },
|
||||
modelCatalog: { "bailing/Ling-1T": { alias: "BaiLing" } },
|
||||
model: { primary: "bailing/Ling-2.5-1T" },
|
||||
modelCatalog: { "bailing/Ling-2.5-1T": { alias: "BaiLing" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -694,8 +694,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "ZhipuAI/GLM-4.7",
|
||||
name: "GLM-4.7",
|
||||
id: "ZhipuAI/GLM-5",
|
||||
name: "GLM-5",
|
||||
contextWindow: 128000,
|
||||
cost: { input: 0.001, output: 0.001 },
|
||||
},
|
||||
@@ -718,8 +718,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "modelscope/ZhipuAI/GLM-4.7" },
|
||||
modelCatalog: { "modelscope/ZhipuAI/GLM-4.7": { alias: "GLM" } },
|
||||
model: { primary: "modelscope/ZhipuAI/GLM-5" },
|
||||
modelCatalog: { "modelscope/ZhipuAI/GLM-5": { alias: "GLM" } },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -732,8 +732,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "Pro/MiniMaxAI/MiniMax-M2.1",
|
||||
name: "MiniMax M2.1",
|
||||
id: "Pro/MiniMaxAI/MiniMax-M2.5",
|
||||
name: "MiniMax M2.5",
|
||||
contextWindow: 200000,
|
||||
cost: { input: 0.001, output: 0.004 },
|
||||
},
|
||||
@@ -750,9 +750,9 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "siliconflow/Pro/MiniMaxAI/MiniMax-M2.1" },
|
||||
model: { primary: "siliconflow/Pro/MiniMaxAI/MiniMax-M2.5" },
|
||||
modelCatalog: {
|
||||
"siliconflow/Pro/MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax" },
|
||||
"siliconflow/Pro/MiniMaxAI/MiniMax-M2.5": { alias: "MiniMax" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -766,8 +766,8 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: "MiniMaxAI/MiniMax-M2.1",
|
||||
name: "MiniMax M2.1",
|
||||
id: "MiniMaxAI/MiniMax-M2.5",
|
||||
name: "MiniMax M2.5",
|
||||
contextWindow: 200000,
|
||||
cost: { input: 0.001, output: 0.004 },
|
||||
},
|
||||
@@ -784,9 +784,9 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
suggestedDefaults: {
|
||||
model: { primary: "siliconflow-en/MiniMaxAI/MiniMax-M2.1" },
|
||||
model: { primary: "siliconflow-en/MiniMaxAI/MiniMax-M2.5" },
|
||||
modelCatalog: {
|
||||
"siliconflow-en/MiniMaxAI/MiniMax-M2.1": { alias: "MiniMax" },
|
||||
"siliconflow-en/MiniMaxAI/MiniMax-M2.5": { alias: "MiniMax" },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1054,6 +1054,41 @@ export const openclawProviderPresets: OpenClawProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
|
||||
// ========== Cloud Providers ==========
|
||||
{
|
||||
name: "AWS Bedrock",
|
||||
websiteUrl: "https://aws.amazon.com/bedrock/",
|
||||
settingsConfig: {
|
||||
// 请将 us-west-2 替换为你的 AWS Region
|
||||
baseUrl: "https://bedrock-runtime.us-west-2.amazonaws.com",
|
||||
apiKey: "",
|
||||
api: "bedrock-converse-stream",
|
||||
models: [
|
||||
{
|
||||
id: "anthropic.claude-opus-4-6-20250514-v1:0",
|
||||
name: "Claude Opus 4.6",
|
||||
contextWindow: 200000,
|
||||
cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
||||
},
|
||||
{
|
||||
id: "anthropic.claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
contextWindow: 200000,
|
||||
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
},
|
||||
{
|
||||
id: "anthropic.claude-haiku-4-5-20251022-v1:0",
|
||||
name: "Claude Haiku 4.5",
|
||||
contextWindow: 200000,
|
||||
cost: { input: 0.8, output: 4, cacheRead: 0.08, cacheWrite: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
category: "cloud_provider",
|
||||
icon: "aws",
|
||||
iconColor: "#FF9900",
|
||||
},
|
||||
|
||||
// ========== Custom Template ==========
|
||||
{
|
||||
name: "OpenAI Compatible",
|
||||
|
||||
@@ -21,6 +21,7 @@ export const opencodeNpmPackages = [
|
||||
{ value: "@ai-sdk/openai", label: "OpenAI" },
|
||||
{ value: "@ai-sdk/openai-compatible", label: "OpenAI Compatible" },
|
||||
{ value: "@ai-sdk/anthropic", label: "Anthropic" },
|
||||
{ value: "@ai-sdk/amazon-bedrock", label: "Amazon Bedrock" },
|
||||
{ value: "@ai-sdk/google", label: "Google (Gemini)" },
|
||||
] as const;
|
||||
|
||||
@@ -40,15 +41,15 @@ export const OPENCODE_PRESET_MODEL_VARIANTS: Record<
|
||||
> = {
|
||||
"@ai-sdk/openai-compatible": [
|
||||
{
|
||||
id: "MiniMax-M2.1",
|
||||
name: "MiniMax M2.1",
|
||||
id: "MiniMax-M2.5",
|
||||
name: "MiniMax M2.5",
|
||||
contextLimit: 204800,
|
||||
outputLimit: 131072,
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
},
|
||||
{
|
||||
id: "glm-4.7",
|
||||
name: "GLM 4.7",
|
||||
id: "glm-5",
|
||||
name: "GLM 5",
|
||||
contextLimit: 204800,
|
||||
outputLimit: 131072,
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
@@ -315,6 +316,50 @@ export const OPENCODE_PRESET_MODEL_VARIANTS: Record<
|
||||
},
|
||||
},
|
||||
],
|
||||
"@ai-sdk/amazon-bedrock": [
|
||||
{
|
||||
id: "global.anthropic.claude-opus-4-6-v1",
|
||||
name: "Claude Opus 4.6",
|
||||
contextLimit: 1000000,
|
||||
outputLimit: 128000,
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
{
|
||||
id: "global.anthropic.claude-sonnet-4-6",
|
||||
name: "Claude Sonnet 4.6",
|
||||
contextLimit: 200000,
|
||||
outputLimit: 64000,
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
{
|
||||
id: "global.anthropic.claude-haiku-4-5-20251001-v1:0",
|
||||
name: "Claude Haiku 4.5",
|
||||
contextLimit: 200000,
|
||||
outputLimit: 64000,
|
||||
modalities: { input: ["text", "image", "pdf"], output: ["text"] },
|
||||
},
|
||||
{
|
||||
id: "us.amazon.nova-pro-v1:0",
|
||||
name: "Amazon Nova Pro",
|
||||
contextLimit: 300000,
|
||||
outputLimit: 5000,
|
||||
modalities: { input: ["text", "image"], output: ["text"] },
|
||||
},
|
||||
{
|
||||
id: "us.meta.llama4-maverick-17b-instruct-v1:0",
|
||||
name: "Meta Llama 4 Maverick",
|
||||
contextLimit: 131072,
|
||||
outputLimit: 131072,
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
},
|
||||
{
|
||||
id: "us.deepseek.r1-v1:0",
|
||||
name: "DeepSeek R1",
|
||||
contextLimit: 131072,
|
||||
outputLimit: 131072,
|
||||
modalities: { input: ["text"], output: ["text"] },
|
||||
},
|
||||
],
|
||||
"@ai-sdk/anthropic": [
|
||||
{
|
||||
id: "claude-sonnet-4-5-20250929",
|
||||
@@ -441,7 +486,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"glm-4.7": { name: "GLM-4.7" },
|
||||
"glm-5": { name: "GLM-5" },
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -473,7 +518,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"glm-4.7": { name: "GLM-4.7" },
|
||||
"glm-5": { name: "GLM-5" },
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -599,7 +644,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"ZhipuAI/GLM-4.7": { name: "GLM-4.7" },
|
||||
"ZhipuAI/GLM-5": { name: "GLM-5" },
|
||||
},
|
||||
},
|
||||
category: "aggregator",
|
||||
@@ -703,7 +748,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"MiniMax-M2.1": { name: "MiniMax M2.1" },
|
||||
"MiniMax-M2.5": { name: "MiniMax M2.5" },
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -735,7 +780,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"MiniMax-M2.1": { name: "MiniMax M2.1" },
|
||||
"MiniMax-M2.5": { name: "MiniMax M2.5" },
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -767,7 +812,9 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"doubao-seed-code-preview-latest": { name: "Doubao Seed Code Preview" },
|
||||
"doubao-seed-2-0-code-preview-latest": {
|
||||
name: "Doubao Seed Code Preview",
|
||||
},
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -792,7 +839,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
apiKey: "",
|
||||
},
|
||||
models: {
|
||||
"Ling-1T": { name: "Ling 1T" },
|
||||
"Ling-2.5-1T": { name: "Ling 2.5-1T" },
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
@@ -1087,6 +1134,54 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "AWS Bedrock",
|
||||
websiteUrl: "https://aws.amazon.com/bedrock/",
|
||||
settingsConfig: {
|
||||
npm: "@ai-sdk/amazon-bedrock",
|
||||
name: "AWS Bedrock",
|
||||
options: {
|
||||
region: "${region}",
|
||||
accessKeyId: "${accessKeyId}",
|
||||
secretAccessKey: "${secretAccessKey}",
|
||||
},
|
||||
models: {
|
||||
"global.anthropic.claude-opus-4-6-v1": { name: "Claude Opus 4.6" },
|
||||
"global.anthropic.claude-sonnet-4-6": {
|
||||
name: "Claude Sonnet 4.6",
|
||||
},
|
||||
"global.anthropic.claude-haiku-4-5-20251001-v1:0": {
|
||||
name: "Claude Haiku 4.5",
|
||||
},
|
||||
"us.amazon.nova-pro-v1:0": { name: "Amazon Nova Pro" },
|
||||
"us.meta.llama4-maverick-17b-instruct-v1:0": {
|
||||
name: "Meta Llama 4 Maverick",
|
||||
},
|
||||
"us.deepseek.r1-v1:0": { name: "DeepSeek R1" },
|
||||
},
|
||||
},
|
||||
category: "cloud_provider",
|
||||
icon: "aws",
|
||||
iconColor: "#FF9900",
|
||||
templateValues: {
|
||||
region: {
|
||||
label: "AWS Region",
|
||||
placeholder: "us-west-2",
|
||||
defaultValue: "us-west-2",
|
||||
editorValue: "us-west-2",
|
||||
},
|
||||
accessKeyId: {
|
||||
label: "Access Key ID",
|
||||
placeholder: "AKIA...",
|
||||
editorValue: "",
|
||||
},
|
||||
secretAccessKey: {
|
||||
label: "Secret Access Key",
|
||||
placeholder: "your-secret-key",
|
||||
editorValue: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "OpenAI Compatible",
|
||||
websiteUrl: "",
|
||||
|
||||
@@ -1193,19 +1193,27 @@
|
||||
"saveSuccess": "Saved successfully",
|
||||
"saveFailed": "Failed to save",
|
||||
"loadFailed": "Failed to load",
|
||||
"openDirectory": "Open in file manager",
|
||||
"dailyMemory": {
|
||||
"title": "Daily Memory",
|
||||
"sectionTitle": "Daily Memory",
|
||||
"cardTitle": "Daily Memory Files",
|
||||
"cardDescription": "Browse & manage daily memories",
|
||||
"createToday": "Today's Note",
|
||||
"createToday": "Add Memory",
|
||||
"empty": "No daily memory files yet",
|
||||
"loadFailed": "Failed to load daily memory files",
|
||||
"createFailed": "Failed to create daily memory file",
|
||||
"deleteSuccess": "Daily memory file deleted",
|
||||
"deleteFailed": "Failed to delete daily memory file",
|
||||
"confirmDeleteTitle": "Delete Daily Memory",
|
||||
"confirmDeleteMessage": "Delete the daily memory for {{date}}? This cannot be undone."
|
||||
"confirmDeleteMessage": "Delete the daily memory for {{date}}? This cannot be undone.",
|
||||
"searchPlaceholder": "Search full content...",
|
||||
"searchScopeHint": "Full-text search across all daily memories. ⌘F",
|
||||
"searchCloseHint": "Esc to close",
|
||||
"noSearchResults": "No daily memories match your search.",
|
||||
"searching": "Searching...",
|
||||
"searchFailed": "Search failed",
|
||||
"matchCount": "{{count}} match(es)"
|
||||
}
|
||||
},
|
||||
"openclaw": {
|
||||
|
||||
@@ -1193,19 +1193,27 @@
|
||||
"saveSuccess": "保存しました",
|
||||
"saveFailed": "保存に失敗しました",
|
||||
"loadFailed": "読み込みに失敗しました",
|
||||
"openDirectory": "ファイルマネージャーで開く",
|
||||
"dailyMemory": {
|
||||
"title": "デイリーメモリー",
|
||||
"sectionTitle": "デイリーメモリー",
|
||||
"cardTitle": "デイリーメモリーファイル",
|
||||
"cardDescription": "デイリーメモリーの閲覧・管理",
|
||||
"createToday": "今日のノート",
|
||||
"createToday": "メモリーを追加",
|
||||
"empty": "デイリーメモリーファイルはまだありません",
|
||||
"loadFailed": "デイリーメモリーファイルの読み込みに失敗しました",
|
||||
"createFailed": "デイリーメモリーファイルの作成に失敗しました",
|
||||
"deleteSuccess": "デイリーメモリーファイルを削除しました",
|
||||
"deleteFailed": "デイリーメモリーファイルの削除に失敗しました",
|
||||
"confirmDeleteTitle": "デイリーメモリーを削除",
|
||||
"confirmDeleteMessage": "{{date}} のデイリーメモリーを削除しますか?この操作は取り消せません。"
|
||||
"confirmDeleteMessage": "{{date}} のデイリーメモリーを削除しますか?この操作は取り消せません。",
|
||||
"searchPlaceholder": "全文検索...",
|
||||
"searchScopeHint": "すべてのデイリーメモリーを全文検索 ⌘F",
|
||||
"searchCloseHint": "Escで閉じる",
|
||||
"noSearchResults": "検索に一致するデイリーメモリーはありません。",
|
||||
"searching": "検索中...",
|
||||
"searchFailed": "検索に失敗しました",
|
||||
"matchCount": "{{count}}件一致"
|
||||
}
|
||||
},
|
||||
"openclaw": {
|
||||
|
||||
@@ -1193,19 +1193,27 @@
|
||||
"saveSuccess": "保存成功",
|
||||
"saveFailed": "保存失败",
|
||||
"loadFailed": "读取失败",
|
||||
"openDirectory": "在文件管理器中打开",
|
||||
"dailyMemory": {
|
||||
"title": "每日记忆",
|
||||
"sectionTitle": "每日记忆",
|
||||
"cardTitle": "每日记忆文件",
|
||||
"cardDescription": "浏览管理每日记忆",
|
||||
"createToday": "今日笔记",
|
||||
"createToday": "添加记忆",
|
||||
"empty": "暂无每日记忆文件",
|
||||
"loadFailed": "加载每日记忆文件失败",
|
||||
"createFailed": "创建每日记忆文件失败",
|
||||
"deleteSuccess": "每日记忆文件已删除",
|
||||
"deleteFailed": "删除每日记忆文件失败",
|
||||
"confirmDeleteTitle": "删除每日记忆",
|
||||
"confirmDeleteMessage": "确定删除 {{date}} 的每日记忆吗?此操作不可撤销。"
|
||||
"confirmDeleteMessage": "确定删除 {{date}} 的每日记忆吗?此操作不可撤销。",
|
||||
"searchPlaceholder": "搜索全文内容...",
|
||||
"searchScopeHint": "全文搜索所有每日记忆 ⌘F",
|
||||
"searchCloseHint": "Esc 关闭",
|
||||
"noSearchResults": "没有找到匹配的每日记忆。",
|
||||
"searching": "搜索中...",
|
||||
"searchFailed": "搜索失败",
|
||||
"matchCount": "{{count}} 处匹配"
|
||||
}
|
||||
},
|
||||
"openclaw": {
|
||||
|
||||
@@ -8,6 +8,15 @@ export interface DailyMemoryFileInfo {
|
||||
preview: string;
|
||||
}
|
||||
|
||||
export interface DailyMemorySearchResult {
|
||||
filename: string;
|
||||
date: string;
|
||||
sizeBytes: number;
|
||||
modifiedAt: number;
|
||||
snippet: string;
|
||||
matchCount: number;
|
||||
}
|
||||
|
||||
export const workspaceApi = {
|
||||
async readFile(filename: string): Promise<string | null> {
|
||||
return invoke<string | null>("read_workspace_file", { filename });
|
||||
@@ -32,4 +41,16 @@ export const workspaceApi = {
|
||||
async deleteDailyMemoryFile(filename: string): Promise<void> {
|
||||
return invoke<void>("delete_daily_memory_file", { filename });
|
||||
},
|
||||
|
||||
async searchDailyMemoryFiles(
|
||||
query: string,
|
||||
): Promise<DailyMemorySearchResult[]> {
|
||||
return invoke<DailyMemorySearchResult[]>("search_daily_memory_files", {
|
||||
query,
|
||||
});
|
||||
},
|
||||
|
||||
async openDirectory(subdir: "workspace" | "memory"): Promise<void> {
|
||||
await invoke("open_workspace_directory", { subdir });
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export type ProviderCategory =
|
||||
| "official" // 官方
|
||||
| "cn_official" // 开源官方(原"国产官方")
|
||||
| "cloud_provider" // 云服务商(AWS Bedrock 等)
|
||||
| "aggregator" // 聚合网站
|
||||
| "third_party" // 第三方供应商
|
||||
| "custom" // 自定义
|
||||
|
||||
@@ -29,6 +29,16 @@ export const getApiKeyFromConfig = (
|
||||
): string => {
|
||||
try {
|
||||
const config = JSON.parse(jsonString);
|
||||
|
||||
// 优先检查顶层 apiKey 字段(用于 Bedrock API Key 等预设)
|
||||
if (
|
||||
typeof config?.apiKey === "string" &&
|
||||
config.apiKey &&
|
||||
!config.apiKey.includes("${")
|
||||
) {
|
||||
return config.apiKey;
|
||||
}
|
||||
|
||||
const env = config?.env;
|
||||
|
||||
if (!env) return "";
|
||||
@@ -112,6 +122,12 @@ export const hasApiKeyField = (
|
||||
): boolean => {
|
||||
try {
|
||||
const config = JSON.parse(jsonString);
|
||||
|
||||
// 检查顶层 apiKey 字段(用于 Bedrock API Key 等预设)
|
||||
if (Object.prototype.hasOwnProperty.call(config, "apiKey")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const env = config?.env ?? {};
|
||||
|
||||
if (appType === "gemini") {
|
||||
@@ -144,6 +160,13 @@ export const setApiKeyInConfig = (
|
||||
const { createIfMissing = false, appType, apiKeyField } = options;
|
||||
try {
|
||||
const config = JSON.parse(jsonString);
|
||||
|
||||
// 优先检查顶层 apiKey 字段(用于 Bedrock API Key 等预设)
|
||||
if (Object.prototype.hasOwnProperty.call(config, "apiKey")) {
|
||||
config.apiKey = apiKey;
|
||||
return JSON.stringify(config, null, 2);
|
||||
}
|
||||
|
||||
if (!config.env) {
|
||||
if (!createIfMissing) return jsonString;
|
||||
config.env = {};
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { providerPresets } from "@/config/claudeProviderPresets";
|
||||
|
||||
describe("AWS Bedrock Provider Presets", () => {
|
||||
const bedrockAksk = providerPresets.find(
|
||||
(p) => p.name === "AWS Bedrock (AKSK)",
|
||||
);
|
||||
|
||||
it("should include AWS Bedrock (AKSK) preset", () => {
|
||||
expect(bedrockAksk).toBeDefined();
|
||||
});
|
||||
|
||||
it("AKSK preset should have required AWS env variables", () => {
|
||||
const env = (bedrockAksk!.settingsConfig as any).env;
|
||||
expect(env).toHaveProperty("AWS_ACCESS_KEY_ID");
|
||||
expect(env).toHaveProperty("AWS_SECRET_ACCESS_KEY");
|
||||
expect(env).toHaveProperty("AWS_REGION");
|
||||
expect(env).toHaveProperty("CLAUDE_CODE_USE_BEDROCK", "1");
|
||||
});
|
||||
|
||||
it("AKSK preset should have template values for AWS credentials", () => {
|
||||
expect(bedrockAksk!.templateValues).toBeDefined();
|
||||
expect(bedrockAksk!.templateValues!.AWS_ACCESS_KEY_ID).toBeDefined();
|
||||
expect(bedrockAksk!.templateValues!.AWS_SECRET_ACCESS_KEY).toBeDefined();
|
||||
expect(bedrockAksk!.templateValues!.AWS_REGION).toBeDefined();
|
||||
expect(bedrockAksk!.templateValues!.AWS_REGION.editorValue).toBe(
|
||||
"us-west-2",
|
||||
);
|
||||
});
|
||||
|
||||
it("AKSK preset should have correct base URL template", () => {
|
||||
const env = (bedrockAksk!.settingsConfig as any).env;
|
||||
expect(env.ANTHROPIC_BASE_URL).toContain("bedrock-runtime");
|
||||
expect(env.ANTHROPIC_BASE_URL).toContain("${AWS_REGION}");
|
||||
});
|
||||
|
||||
it("AKSK preset should have cloud_provider category", () => {
|
||||
expect(bedrockAksk!.category).toBe("cloud_provider");
|
||||
});
|
||||
|
||||
it("AKSK preset should have Bedrock model as default", () => {
|
||||
const env = (bedrockAksk!.settingsConfig as any).env;
|
||||
expect(env.ANTHROPIC_MODEL).toContain("anthropic.claude");
|
||||
});
|
||||
|
||||
const bedrockApiKey = providerPresets.find(
|
||||
(p) => p.name === "AWS Bedrock (API Key)",
|
||||
);
|
||||
|
||||
it("should include AWS Bedrock (API Key) preset", () => {
|
||||
expect(bedrockApiKey).toBeDefined();
|
||||
});
|
||||
|
||||
it("API Key preset should have apiKey field and AWS env variables", () => {
|
||||
const config = bedrockApiKey!.settingsConfig as any;
|
||||
expect(config).toHaveProperty("apiKey", "");
|
||||
expect(config.env).toHaveProperty("AWS_REGION");
|
||||
expect(config.env).toHaveProperty("CLAUDE_CODE_USE_BEDROCK", "1");
|
||||
});
|
||||
|
||||
it("API Key preset should NOT have AKSK env variables", () => {
|
||||
const env = (bedrockApiKey!.settingsConfig as any).env;
|
||||
expect(env).not.toHaveProperty("AWS_ACCESS_KEY_ID");
|
||||
expect(env).not.toHaveProperty("AWS_SECRET_ACCESS_KEY");
|
||||
});
|
||||
|
||||
it("API Key preset should have template values for region only", () => {
|
||||
expect(bedrockApiKey!.templateValues).toBeDefined();
|
||||
expect(bedrockApiKey!.templateValues!.AWS_REGION).toBeDefined();
|
||||
expect(bedrockApiKey!.templateValues!.AWS_REGION.editorValue).toBe(
|
||||
"us-west-2",
|
||||
);
|
||||
});
|
||||
|
||||
it("API Key preset should have cloud_provider category", () => {
|
||||
expect(bedrockApiKey!.category).toBe("cloud_provider");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
opencodeProviderPresets,
|
||||
opencodeNpmPackages,
|
||||
OPENCODE_PRESET_MODEL_VARIANTS,
|
||||
} from "@/config/opencodeProviderPresets";
|
||||
|
||||
describe("AWS Bedrock OpenCode Provider Presets", () => {
|
||||
it("should include @ai-sdk/amazon-bedrock in npm packages", () => {
|
||||
const bedrockPkg = opencodeNpmPackages.find(
|
||||
(p) => p.value === "@ai-sdk/amazon-bedrock",
|
||||
);
|
||||
expect(bedrockPkg).toBeDefined();
|
||||
expect(bedrockPkg!.label).toBe("Amazon Bedrock");
|
||||
});
|
||||
|
||||
it("should include Bedrock model variants", () => {
|
||||
const variants = OPENCODE_PRESET_MODEL_VARIANTS["@ai-sdk/amazon-bedrock"];
|
||||
expect(variants).toBeDefined();
|
||||
expect(variants.length).toBeGreaterThan(0);
|
||||
|
||||
const opusModel = variants.find((v) =>
|
||||
v.id.includes("anthropic.claude-opus-4-6"),
|
||||
);
|
||||
expect(opusModel).toBeDefined();
|
||||
});
|
||||
|
||||
const bedrockPreset = opencodeProviderPresets.find(
|
||||
(p) => p.name === "AWS Bedrock",
|
||||
);
|
||||
|
||||
it("should include AWS Bedrock preset", () => {
|
||||
expect(bedrockPreset).toBeDefined();
|
||||
});
|
||||
|
||||
it("Bedrock preset should use @ai-sdk/amazon-bedrock npm package", () => {
|
||||
expect(bedrockPreset!.settingsConfig.npm).toBe(
|
||||
"@ai-sdk/amazon-bedrock",
|
||||
);
|
||||
});
|
||||
|
||||
it("Bedrock preset should have region in options", () => {
|
||||
expect(bedrockPreset!.settingsConfig.options).toHaveProperty("region");
|
||||
});
|
||||
|
||||
it("Bedrock preset should have cloud_provider category", () => {
|
||||
expect(bedrockPreset!.category).toBe("cloud_provider");
|
||||
});
|
||||
|
||||
it("Bedrock preset should have template values for AWS credentials", () => {
|
||||
expect(bedrockPreset!.templateValues).toBeDefined();
|
||||
expect(bedrockPreset!.templateValues!.region).toBeDefined();
|
||||
expect(bedrockPreset!.templateValues!.region.editorValue).toBe(
|
||||
"us-west-2",
|
||||
);
|
||||
expect(bedrockPreset!.templateValues!.accessKeyId).toBeDefined();
|
||||
expect(bedrockPreset!.templateValues!.secretAccessKey).toBeDefined();
|
||||
});
|
||||
|
||||
it("Bedrock preset should include Claude models", () => {
|
||||
const models = bedrockPreset!.settingsConfig.models;
|
||||
expect(models).toBeDefined();
|
||||
const modelIds = Object.keys(models!);
|
||||
expect(
|
||||
modelIds.some((id) => id.includes("anthropic.claude")),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user