diff --git a/launch/setup-ai-rules.sh b/launch/setup-ai-rules.sh new file mode 100755 index 00000000..e5d0a98a --- /dev/null +++ b/launch/setup-ai-rules.sh @@ -0,0 +1,252 @@ +#!/bin/bash + +# ============================================= +# AI Coding Assistant Rules Setup +# AI 编程助手规则配置 +# ============================================= + +set -e + +# Get script directory and project root +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# Source colors +source "$SCRIPT_DIR/colors.sh" + +# ============================================= +# Language Detection / 语言检测 +# ============================================= + +detect_language() { + local lang="${LANG:-en_US}" + if [[ "$lang" == zh_CN* ]] || [[ "$lang" == zh_TW* ]] || [[ "$lang" == zh_HK* ]]; then + echo "zh" + else + echo "en" + fi +} + +LANGUAGE=$(detect_language) + +# ============================================= +# Bilingual Messages / 双语消息 +# ============================================= + +msg() { + local en="$1" + local zh="$2" + if [[ "$LANGUAGE" == "zh" ]]; then + echo -e "$zh" + else + echo -e "$en" + fi +} + +# ============================================= +# Tool definitions (compatible with bash 3.x) +# ============================================= + +get_tool_name() { + case "$1" in + 1) echo "Claude Code" ;; + 2) echo "Cursor" ;; + 3) echo "Windsurf" ;; + 4) echo "GitHub Copilot" ;; + 5) echo "Cline/Roo Code" ;; + *) echo "" ;; + esac +} + +get_tool_target() { + case "$1" in + 1) echo "CLAUDE.md" ;; + 2) echo ".cursorrules" ;; + 3) echo ".windsurfrules" ;; + 4) echo ".github/copilot-instructions.md" ;; + 5) echo ".clinerules" ;; + *) echo "" ;; + esac +} + +# ============================================= +# Print Banner +# ============================================= + +print_banner() { + echo "" + echo -e "${BRIGHT_CYAN}╔════════════════════════════════════════════════════════════╗${RESET}" + if [[ "$LANGUAGE" == "zh" ]]; then + echo -e "${BRIGHT_CYAN}║${RESET} ${BOLD}AI 编程助手规则配置工具${RESET} ${BRIGHT_CYAN}║${RESET}" + echo -e "${BRIGHT_CYAN}║${RESET} 将 AGENTS.md 链接到各个 AI 工具的配置文件 ${BRIGHT_CYAN}║${RESET}" + else + echo -e "${BRIGHT_CYAN}║${RESET} ${BOLD}AI Coding Assistant Rules Setup${RESET} ${BRIGHT_CYAN}║${RESET}" + echo -e "${BRIGHT_CYAN}║${RESET} Link AGENTS.md to AI tool config files ${BRIGHT_CYAN}║${RESET}" + fi + echo -e "${BRIGHT_CYAN}╚════════════════════════════════════════════════════════════╝${RESET}" + echo "" +} + +# ============================================= +# Check source file exists +# ============================================= + +check_source() { + if [[ ! -f "$PROJECT_ROOT/AGENTS.md" ]]; then + msg "${RED}Error: AGENTS.md not found in project root${RESET}" \ + "${RED}错误: 项目根目录未找到 AGENTS.md${RESET}" + exit 1 + fi +} + +# ============================================= +# Create symlink +# ============================================= + +create_link() { + local target="$1" + local target_path="$PROJECT_ROOT/$target" + local target_dir=$(dirname "$target_path") + + # Create parent directory if needed + if [[ ! -d "$target_dir" ]]; then + mkdir -p "$target_dir" + fi + + # Remove existing file/link + if [[ -e "$target_path" ]] || [[ -L "$target_path" ]]; then + rm -f "$target_path" + fi + + # Create relative symlink + local rel_path="AGENTS.md" + if [[ "$target_dir" != "$PROJECT_ROOT" ]]; then + # Calculate relative path for nested targets + local depth=$(echo "$target" | tr -cd '/' | wc -c | tr -d ' ') + rel_path="" + for ((i=0; i " selection + + process_selection "$selection" +} + +main "$@" diff --git a/service/app/core/consume_strategy.py b/service/app/core/consume_strategy.py index 7d3abc99..74369400 100644 --- a/service/app/core/consume_strategy.py +++ b/service/app/core/consume_strategy.py @@ -67,7 +67,7 @@ class TierBasedConsumptionStrategy(ConsumptionStrategy): - Tier rate multiplies ALL costs (base + tokens + files) """ - BASE_COST = 3 + BASE_COST = 1 INPUT_TOKEN_RATE = 0.2 / 1000 # per token OUTPUT_TOKEN_RATE = 1 / 1000 # per token FILE_GENERATION_COST = 10 diff --git a/service/tests/unit/test_core/test_consume_strategy.py b/service/tests/unit/test_core/test_consume_strategy.py index 2bcda4ee..41a43ddd 100644 --- a/service/tests/unit/test_core/test_consume_strategy.py +++ b/service/tests/unit/test_core/test_consume_strategy.py @@ -78,10 +78,8 @@ def test_standard_tier_base_multiplier(self) -> None: # STANDARD rate is 1.0 assert TIER_MODEL_CONSUMPTION_RATE[ModelTier.STANDARD] == 1.0 - # Calculate expected: base(3) + tokens(1000*0.2/1000 + 1000*1/1000) = 3 + 0.2 + 1 = 4.2 - # With multiplier 1.0 = int(4.2) = 4 - expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) # 0.2 + 1 = 1.2 - expected = int((3 + expected_token_cost) * 1.0) + expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) + expected = int((1 + expected_token_cost) * 1.0) assert result.amount == expected assert result.breakdown["tier_rate"] == 1.0 @@ -101,8 +99,8 @@ def test_pro_tier_multiplier(self) -> None: # PRO rate is 3.0 assert TIER_MODEL_CONSUMPTION_RATE[ModelTier.PRO] == 3.0 - expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) # 1.2 - expected = int((3 + expected_token_cost) * 3.0) # 4.2 * 3 = 12.6 -> 12 + expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) + expected = int((1 + expected_token_cost) * 3.0) assert result.amount == expected assert result.breakdown["tier_rate"] == 3.0 @@ -122,8 +120,8 @@ def test_ultra_tier_multiplier(self) -> None: # ULTRA rate is 6.8 assert TIER_MODEL_CONSUMPTION_RATE[ModelTier.ULTRA] == 6.8 - expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) # 1.2 - expected = int((3 + expected_token_cost) * 6.8) # 4.2 * 6.8 = 28.56 -> 28 + expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) + expected = int((1 + expected_token_cost) * 6.8) assert result.amount == expected assert result.breakdown["tier_rate"] == 6.8 @@ -140,8 +138,7 @@ def test_file_generation_cost(self) -> None: ) result = strategy.calculate(context) - # Base(3) + files(2*10) = 23, with rate 1.0 = 23 - expected = int((3 + 20) * 1.0) + expected = int((1 + 20) * 1.0) assert result.amount == expected assert result.breakdown["file_cost"] == 20 @@ -159,8 +156,8 @@ def test_no_tier_defaults_to_1(self) -> None: result = strategy.calculate(context) # Should use default rate 1.0 - expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) # 1.2 - expected = int((3 + expected_token_cost) * 1.0) # 4 + expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) + expected = int((1 + expected_token_cost) * 1.0) assert result.amount == expected assert result.breakdown["tier_rate"] == 1.0 assert result.breakdown["tier"] == "default" @@ -213,8 +210,8 @@ def test_calculate_pro_tier(self) -> None: result = ConsumptionCalculator.calculate(context) # PRO rate is 3.0 - expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) # 1.2 - expected = int((3 + expected_token_cost) * 3.0) # 12 + expected_token_cost = (1000 * 0.2 / 1000) + (1000 * 1 / 1000) + expected = int((1 + expected_token_cost) * 3.0) assert result.amount == expected assert result.breakdown["tier_rate"] == 3.0 diff --git a/web/src/components/layouts/components/TierInfoModal.tsx b/web/src/components/layouts/components/TierInfoModal.tsx new file mode 100644 index 00000000..1c4b284e --- /dev/null +++ b/web/src/components/layouts/components/TierInfoModal.tsx @@ -0,0 +1,261 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useTranslation } from "react-i18next"; +import { cn } from "@/lib/utils"; + +interface TierInfoModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +// Tier configuration with models +interface TierInfo { + key: string; + icon: string; + color: string; + bgColor: string; + borderColor: string; + rate: number; + models: ModelInfo[]; +} + +interface ModelInfo { + name: string; + provider: "anthropic" | "openai" | "google" | "qwen" | "deepseek"; + hasImageGen?: boolean; +} + +const PROVIDER_COLORS: Record = { + anthropic: { + bg: "bg-orange-100 dark:bg-orange-900/30", + text: "text-orange-700 dark:text-orange-400", + }, + openai: { + bg: "bg-emerald-100 dark:bg-emerald-900/30", + text: "text-emerald-700 dark:text-emerald-400", + }, + google: { + bg: "bg-blue-100 dark:bg-blue-900/30", + text: "text-blue-700 dark:text-blue-400", + }, + qwen: { + bg: "bg-purple-100 dark:bg-purple-900/30", + text: "text-purple-700 dark:text-purple-400", + }, + deepseek: { + bg: "bg-cyan-100 dark:bg-cyan-900/30", + text: "text-cyan-700 dark:text-cyan-400", + }, +}; + +const TIERS: TierInfo[] = [ + { + key: "ultra", + icon: "👑", + color: "text-purple-600 dark:text-purple-400", + bgColor: "bg-purple-50 dark:bg-purple-900/20", + borderColor: "border-purple-200 dark:border-purple-800", + rate: 6.8, + models: [ + { name: "Claude Opus 4.5", provider: "anthropic" }, + { name: "GPT-5.2 Pro", provider: "openai" }, + { name: "GPT-5 Pro", provider: "openai" }, + ], + }, + { + key: "pro", + icon: "💼", + color: "text-blue-600 dark:text-blue-400", + bgColor: "bg-blue-50 dark:bg-blue-900/20", + borderColor: "border-blue-200 dark:border-blue-800", + rate: 3.0, + models: [ + { name: "Claude Sonnet 4.5", provider: "anthropic" }, + { name: "Gemini 3 Pro", provider: "google" }, + { name: "Qwen3 Max", provider: "qwen" }, + { name: "GPT-5.2", provider: "openai" }, + ], + }, + { + key: "standard", + icon: "☕", + color: "text-green-600 dark:text-green-400", + bgColor: "bg-green-50 dark:bg-green-900/20", + borderColor: "border-green-200 dark:border-green-800", + rate: 1.0, + models: [ + { name: "Gemini 3 Flash", provider: "google" }, + { name: "DeepSeek V3.1", provider: "deepseek" }, + { name: "GPT-5 Mini", provider: "openai" }, + ], + }, + { + key: "lite", + icon: "🧠", + color: "text-orange-600 dark:text-orange-400", + bgColor: "bg-orange-50 dark:bg-orange-900/20", + borderColor: "border-orange-200 dark:border-orange-800", + rate: 0.0, + models: [ + { name: "Qwen3 30B A3B", provider: "qwen" }, + { name: "Gemini 2.5 Flash Lite", provider: "google" }, + { name: "GPT-5 Nano", provider: "openai" }, + ], + }, +]; + +export function TierInfoModal({ open, onOpenChange }: TierInfoModalProps) { + const { t } = useTranslation(); + + const formatRate = (rate: number): string => { + if (rate === 0) return t("app.tierSelector.free"); + return `${rate}x`; + }; + + return ( + + + + + 📊 + {t("app.tierSelector.infoModal.title")} + + + +
+
+ {TIERS.map((tier, index) => ( +
+ {/* Dashed Connector Line */} + {index !== TIERS.length - 1 && ( +
+ )} + +
+ {/* 1. Identity Component */} +
+
+
+ {tier.icon} +
+
+

+ {t(`app.tierSelector.tiers.${tier.key}.name`)} +

+

+ {t(`app.tierSelector.tiers.${tier.key}.description`)} +

+
+
+
+ + {/* Arrow */} +
+ → +
+ + {/* 2. Rate Component */} +
+
+ + Multiplier + + + {formatRate(tier.rate)} + +
+
+ + {/* Arrow */} +
+ → +
+ + {/* 3. Models Component */} +
+
+ {tier.models.map((model, idx) => { + const providerStyle = PROVIDER_COLORS[ + model.provider + ] || { + bg: "bg-gray-100", + text: "text-gray-700", + }; + return ( +
+
+ {model.name} + {model.hasImageGen && ( + + 📷 + + )} +
+ ); + })} +
+
+
+
+ ))} +
+
+ + {/* Footer */} +
+
+ {t("app.tierSelector.infoModal.disclaimer")} +
+
+ +
+ ); +} diff --git a/web/src/components/layouts/components/TierSelector.tsx b/web/src/components/layouts/components/TierSelector.tsx index 8fd0e73e..cc3318d9 100644 --- a/web/src/components/layouts/components/TierSelector.tsx +++ b/web/src/components/layouts/components/TierSelector.tsx @@ -1,9 +1,14 @@ "use client"; -import { ChevronDownIcon, CpuChipIcon } from "@heroicons/react/24/outline"; +import { + ChevronDownIcon, + CpuChipIcon, + InformationCircleIcon, +} from "@heroicons/react/24/outline"; import { AnimatePresence, motion } from "motion/react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { TierInfoModal } from "./TierInfoModal"; export type ModelTier = "ultra" | "pro" | "standard" | "lite"; @@ -18,6 +23,7 @@ interface TierConfig { bgColor: string; textColor: string; dotColor: string; + rate: number; // Consumption rate multiplier } const TIER_CONFIGS: TierConfig[] = [ @@ -26,24 +32,28 @@ const TIER_CONFIGS: TierConfig[] = [ bgColor: "bg-purple-500/10 dark:bg-purple-500/20", textColor: "text-purple-700 dark:text-purple-400", dotColor: "bg-purple-500", + rate: 6.8, }, { key: "pro", bgColor: "bg-blue-500/10 dark:bg-blue-500/20", textColor: "text-blue-700 dark:text-blue-400", dotColor: "bg-blue-500", + rate: 3.0, }, { key: "standard", bgColor: "bg-green-500/10 dark:bg-green-500/20", textColor: "text-green-700 dark:text-green-400", dotColor: "bg-green-500", + rate: 1.0, }, { key: "lite", bgColor: "bg-orange-500/10 dark:bg-orange-500/20", textColor: "text-orange-700 dark:text-orange-400", dotColor: "bg-orange-500", + rate: 0.0, }, ]; @@ -54,6 +64,7 @@ export function TierSelector({ }: TierSelectorProps) { const { t } = useTranslation(); const [isOpen, setIsOpen] = useState(false); + const [isInfoModalOpen, setIsInfoModalOpen] = useState(false); // Default to standard if no tier is selected const effectiveTier = currentTier || "standard"; @@ -65,73 +76,101 @@ export function TierSelector({ setIsOpen(false); }; + // Format rate for display + const formatRate = (rate: number): string => { + if (rate === 0) return t("app.tierSelector.free"); + return t("app.tierSelector.rateFormat", { rate: rate.toFixed(1) }); + }; + return ( -
!disabled && setIsOpen(true)} - onMouseLeave={() => setIsOpen(false)} - > - {/* Main Trigger Button */} - !disabled && setIsOpen(!isOpen)} + <> +
!disabled && setIsOpen(true)} + onMouseLeave={() => setIsOpen(false)} > - - - {t(`app.tierSelector.tiers.${effectiveTier}.name`)} - - - + {/* Main Trigger Button */} + !disabled && setIsOpen(!isOpen)} + > + + + {t(`app.tierSelector.tiers.${effectiveTier}.name`)} + + + - {/* Dropdown */} - - {isOpen && ( - -
- {t("app.tierSelector.title")} -
-
- {TIER_CONFIGS.map((config, index) => ( - handleTierClick(config.key)} - className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-left transition-colors ${ - effectiveTier === config.key - ? `${config.bgColor} ${config.textColor}` - : "hover:bg-neutral-100 dark:hover:bg-neutral-800" - }`} + {/* Dropdown */} + + {isOpen && ( + +
+ + {t("app.tierSelector.title")} + + +
+
+ {TIER_CONFIGS.map((config, index) => ( + handleTierClick(config.key)} + className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-left transition-colors ${ + effectiveTier === config.key + ? `${config.bgColor} ${config.textColor}` + : "hover:bg-neutral-100 dark:hover:bg-neutral-800" + }`} + > +
+
+
+ + {t(`app.tierSelector.tiers.${config.key}.name`)} + + + {formatRate(config.rate)} + +
+
+ {t(`app.tierSelector.tiers.${config.key}.description`)} +
-
- {t(`app.tierSelector.tiers.${config.key}.description`)} -
-
-
- ))} -
-
- )} -
-
+ + ))} +
+ + )} + +
+ + {/* Tier Info Modal */} + + ); } diff --git a/web/src/i18n/locales/en/app.json b/web/src/i18n/locales/en/app.json index 2bbae6e5..555e1fdd 100644 --- a/web/src/i18n/locales/en/app.json +++ b/web/src/i18n/locales/en/app.json @@ -61,6 +61,13 @@ }, "tierSelector": { "title": "Select Model Tier", + "free": "Free", + "rateFormat": "{{rate}}x", + "infoModal": { + "title": "Model Tier Guide", + "imageGen": "Image Generation", + "disclaimer": "* Models listed are for capability reference only and do not guarantee the actual model used." + }, "tiers": { "ultra": { "name": "Xyzen Ultra", diff --git a/web/src/i18n/locales/ja/app.json b/web/src/i18n/locales/ja/app.json index 9c8dd9f6..e94882cc 100644 --- a/web/src/i18n/locales/ja/app.json +++ b/web/src/i18n/locales/ja/app.json @@ -61,6 +61,12 @@ }, "tierSelector": { "title": "モデルティアを選択", + "free": "無料", + "rateFormat": "{{rate}}x", + "infoModal": { + "title": "モデルティアガイド", + "imageGen": "画像生成" + }, "tiers": { "ultra": { "name": "Xyzen ウルトラ", diff --git a/web/src/i18n/locales/zh/app.json b/web/src/i18n/locales/zh/app.json index 5cb321e0..1b44b6b4 100644 --- a/web/src/i18n/locales/zh/app.json +++ b/web/src/i18n/locales/zh/app.json @@ -61,6 +61,13 @@ }, "tierSelector": { "title": "选择模型等级", + "free": "0x", + "rateFormat": "{{rate}}x", + "infoModal": { + "title": "模型等级说明", + "imageGen": "图像生成", + "disclaimer": "* 此列表仅作为模型能力参考,不代表实际调用的具体模型。" + }, "tiers": { "ultra": { "name": "Xyzen Ultra", diff --git a/web/src/store/slices/chatSlice.ts b/web/src/store/slices/chatSlice.ts index 29344edb..8b9e3736 100644 --- a/web/src/store/slices/chatSlice.ts +++ b/web/src/store/slices/chatSlice.ts @@ -185,6 +185,7 @@ export const createChatSlice: StateCreator< agentId: session.agent_id, provider_id: session.provider_id, model: session.model, + model_tier: session.model_tier, connected: false, error: null, }; @@ -268,6 +269,7 @@ export const createChatSlice: StateCreator< let sessionAgentId = undefined; let sessionProviderId = undefined; let sessionModel = undefined; + let sessionModelTier = undefined; let googleSearchEnabled = undefined; for (const session of sessions) { @@ -278,6 +280,7 @@ export const createChatSlice: StateCreator< sessionAgentId = session.agent_id; // 获取 session 的 agent_id sessionProviderId = session.provider_id; sessionModel = session.model; + sessionModelTier = session.model_tier; googleSearchEnabled = session.google_search_enabled; break; } @@ -292,6 +295,7 @@ export const createChatSlice: StateCreator< agentId: sessionAgentId, // 使用从 session 获取的 agentId provider_id: sessionProviderId, model: sessionModel, + model_tier: sessionModelTier, google_search_enabled: googleSearchEnabled, connected: false, error: null, @@ -459,6 +463,7 @@ export const createChatSlice: StateCreator< agentId: session.agent_id, provider_id: session.provider_id, model: session.model, + model_tier: session.model_tier, google_search_enabled: session.google_search_enabled, connected: false, error: null, @@ -494,6 +499,7 @@ export const createChatSlice: StateCreator< agentId: session.agent_id, provider_id: session.provider_id, model: session.model, + model_tier: session.model_tier, google_search_enabled: session.google_search_enabled, connected: false, error: null, @@ -1734,6 +1740,7 @@ export const createChatSlice: StateCreator< agentId: existingSession.agent_id, provider_id: existingSession.provider_id, model: existingSession.model, + model_tier: existingSession.model_tier, google_search_enabled: existingSession.google_search_enabled, connected: false, error: null, @@ -1837,6 +1844,7 @@ export const createChatSlice: StateCreator< agentId: newSession.agent_id, provider_id: newSession.provider_id, model: newSession.model, + model_tier: newSession.model_tier, google_search_enabled: newSession.google_search_enabled, connected: false, error: null, @@ -1897,6 +1905,8 @@ export const createChatSlice: StateCreator< agentId: newSession.agent_id, provider_id: newSession.provider_id, model: newSession.model, + model_tier: newSession.model_tier, + google_search_enabled: newSession.google_search_enabled, connected: false, error: null, };