feat(i18n): establish english-first locale baseline
- split UI locale packs into en-US, zh-CN, and zh-TW modules - switch runtime defaults, docs, and repository guidance toward an English-first baseline - update core, MCP, and UI runtime copy to remove hardcoded Chinese fallbacks - add locale parity and no-hardcoded-runtime guardrails across the repo
This commit is contained in:
1 parent
d60b11a6c2
commit
c75afa89e9
170 files changed
+11852
-1700
No files matched your search
@@ -19,30 +19,30 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: 安装 pnpm
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: 设置 Node.js 环境
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: 安装依赖
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: 运行门禁(不含 E2E)
|
||||
- name: Run repo and gate checks
|
||||
run: pnpm test:gate
|
||||
|
||||
- name: 安装 Playwright 浏览器
|
||||
- name: Install Playwright browsers
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: 运行门禁 E2E(关键用例,VCR 回放模式)
|
||||
- name: Run gate E2E checks (VCR replay)
|
||||
run: pnpm test:gate:e2e
|
||||
env:
|
||||
E2E_VCR_MODE: replay
|
||||
@@ -1,8 +1,8 @@
|
||||
# Prompt Optimizer (提示词优化器) 🚀
|
||||
# Prompt Optimizer 🚀
|
||||
|
||||
<div align="center">
|
||||
|
||||
[English](README_EN.md) | [中文](README.md)
|
||||
[English](README.md) | [中文](README.zh-CN.md)
|
||||
|
||||
[](https://github.com/linshenkx/prompt-optimizer/stargazers)
|
||||

|
||||
@@ -14,194 +14,197 @@
|
||||

|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flinshenkx%2Fprompt-optimizer)
|
||||
|
||||
[在线体验](https://prompt.always200.com) | [快速开始](#快速开始) | [常见问题](#常见问题) | [Chrome插件](https://chromewebstore.google.com/detail/prompt-optimizer/cakkkhboolfnadechdlgdcnjammejlna) | [💖赞助支持](images/other/sponsor_wx.jpg)
|
||||
[Live Demo](https://prompt.always200.com) | [Quick Start](#quick-start) | [FAQ](#faq) | [Chrome Extension](https://chromewebstore.google.com/detail/prompt-optimizer/cakkkhboolfnadechdlgdcnjammejlna)
|
||||
|
||||
[开发文档](dev.md) | [Vercel部署指南](docs/user/deployment/vercel.md) | [MCP部署使用说明](docs/user/mcp-server.md) | [DeepWiki文档](https://deepwiki.com/linshenkx/prompt-optimizer) | [ZRead文档](https://zread.ai/linshenkx/prompt-optimizer)
|
||||
[Development Docs](dev.md) | [Vercel Deployment Guide](docs/user/deployment/vercel_en.md) | [MCP Deployment Guide](docs/user/mcp-server_en.md) | [DeepWiki Docs](https://deepwiki.com/linshenkx/prompt-optimizer) | [ZRead Docs](https://zread.ai/linshenkx/prompt-optimizer)
|
||||
|
||||
</div>
|
||||
|
||||
## 📖 项目简介
|
||||
## 📖 Project Introduction
|
||||
|
||||
Prompt Optimizer是一个强大的AI提示词优化工具,帮助你编写更好的AI提示词,提升AI输出质量。支持Web应用、桌面应用、Chrome插件和Docker部署四种使用方式。
|
||||
Prompt Optimizer is a powerful AI prompt optimization tool that helps you write better AI prompts and improve the quality of AI outputs. It supports four usage methods: web application, desktop application, Chrome extension, and Docker deployment.
|
||||
|
||||
### 🎥 功能演示
|
||||
### 🎥 Feature Demonstration
|
||||
|
||||
<div align="center">
|
||||
<p><b>1. 红队审稿:让模型不再顺着说</b></p>
|
||||
<p>同样的输入下,系统提示词优化能让小模型从泛泛而谈的附和式回答,转向更有立场、更有结构的批判式审查,明确指出论点中的漏洞、风险与隐含假设。</p>
|
||||
<img src="images/demo/red-team-reviewer-fullpage.png" alt="红队审稿人整页演示" width="85%">
|
||||
<p><b>1. Hard-Nosed Reviewer: Turn Agreement into Useful Critique</b></p>
|
||||
<p>Starting from a minimal English role prompt, optimization pushes a small model away from generic pushback and toward a clearer, more structured review that surfaces weak assumptions, evidence gaps, and concrete revision advice.</p>
|
||||
<img src="images/demo/hard-nosed-reviewer-fullpage-en.png" alt="Hard-nosed reviewer full-page demo" width="85%">
|
||||
<br>
|
||||
<p><b>2. 闲鱼砍价回复:让变量真正决定回复策略</b></p>
|
||||
<p>同一套提示词模板里,只需替换商品、报价、底线和语气等变量,就能快速复用到不同交易场景。优化后的提示词会明显减少“助手腔”和多余解释,让小模型更像真人卖家一样,围绕价格分歧、商品情况和成交底线直接组织回复。</p>
|
||||
<img src="images/demo/pro-variable-bargaining-reply.png" alt="闲鱼砍价回复变量模式演示" width="85%">
|
||||
<p><b>2. Marketplace Bargaining Reply: Let Variables Change the Strategy</b></p>
|
||||
<p>With a single reusable prompt template, you can swap in item details, price anchors, buyer offers, tone, and negotiation goals for different marketplace conversations. After optimization, the same small model does a better job turning those variables into a clearer, more transaction-ready reply instead of a generic helper-style response.</p>
|
||||
<img src="images/demo/pro-variable-bargaining-reply-en.png" alt="Marketplace bargaining reply variable-mode demo" width="85%">
|
||||
<br>
|
||||
<p><b>3. 文生图:把一句想法优化成更可控的主视觉提示词</b></p>
|
||||
<p>这不是单纯把提示词写得更长,而是把一句模糊念头拆成更清晰的视觉主体、空间关系和情绪锚点。左侧只有“夜空中的漂浮图书馆”这个概念,右侧则通过优化补足了更明确的奇幻结构和画面重心,让生成结果更像可继续定制的主视觉,而不是只靠模型自由发挥。</p>
|
||||
<img src="images/demo/text2image-floating-library-creative.png" alt="漂浮图书馆文生图演示" width="85%">
|
||||
<p><b>3. Text-to-Image: Optimize a One-Line Idea into a More Directable Key Visual Prompt</b></p>
|
||||
<p>This is not just prompt expansion. Starting from a vague one-line idea, Prompt Optimizer adds clearer subject cues, spatial relationships, and mood anchors. The left side is simply “a floating library in the night sky,” while the optimized version gives the model a more directed fantasy composition that feels closer to a reusable key visual than a lucky generic image.</p>
|
||||
<img src="images/demo/text2image-floating-library-creative-en.png" alt="Floating library text-to-image demo" width="85%">
|
||||
</div>
|
||||
|
||||
## ✨ 核心特性
|
||||
## ✨ Core Features
|
||||
|
||||
- 🎯 **智能优化**:一键优化提示词,支持多轮迭代改进,提升AI回复准确度
|
||||
- 📝 **双模式优化**:支持系统提示词优化和用户提示词优化,满足不同使用场景
|
||||
- 🔄 **分析与对比评估**:支持分析、单结果评估和多结果对比评估,帮助判断提示词是否真的改进
|
||||
- 🤖 **多模型集成**:支持OpenAI、Gemini、DeepSeek、智谱AI、SiliconFlow等主流AI模型
|
||||
- 🖼️ **图像生成**:支持文生图(T2I)和图生图(I2I),集成Gemini、Seedream等图像模型
|
||||
- 📊 **高级测试模式**:上下文变量管理、多轮会话测试、工具调用(Function Calling)支持
|
||||
- 🔒 **安全架构**:纯客户端处理,数据直接与AI服务商交互,不经过中间服务器
|
||||
- 📱 **多端支持**:同时提供Web应用、桌面应用、Chrome插件和Docker部署四种使用方式
|
||||
- 🔐 **访问控制**:支持密码保护功能,保障部署安全
|
||||
- 🧩 **MCP协议支持**:支持Model Context Protocol (MCP) 协议,可与Claude Desktop等MCP兼容应用集成
|
||||
- 🎯 **Intelligent Optimization**: One-click prompt optimization with multi-round iterative improvements to enhance AI response accuracy
|
||||
- 📝 **Dual Mode Optimization**: Support for both system prompt optimization and user prompt optimization to meet different usage scenarios
|
||||
- 🔄 **Analysis and Compare Evaluation**: Supports analysis, single-result evaluation, and multi-result compare evaluation to help determine whether a prompt has truly improved
|
||||
- 🤖 **Multi-model Integration**: Support for mainstream AI models including OpenAI, Gemini, DeepSeek, Zhipu AI, SiliconFlow, etc.
|
||||
- 🖼️ **Image Generation**: Support for Text-to-Image (T2I) and Image-to-Image (I2I) with models like Gemini, Seedream
|
||||
- 📊 **Advanced Testing Mode**: Context variable management, multi-turn conversation testing, Function Calling support
|
||||
- 🔒 **Secure Architecture**: Pure client-side processing with direct data interaction with AI service providers, bypassing intermediate servers
|
||||
- 📱 **Multi-platform Support**: Available as web application, desktop application, Chrome extension, and Docker deployment
|
||||
- 🔐 **Access Control**: Password protection feature for secure deployment
|
||||
- 🧩 **MCP Protocol Support**: Supports Model Context Protocol (MCP), enabling integration with MCP-compatible AI applications like Claude Desktop
|
||||
|
||||
## 🚀 高级功能
|
||||
## 🚀 Advanced Features
|
||||
|
||||
### 图像生成模式
|
||||
- 🖼️ **文生图(T2I)**:通过文本提示词生成图像
|
||||
- 🎨 **图生图(I2I)**:基于本地图片进行图像变换和优化
|
||||
- 🔌 **多模型支持**:集成Gemini、Seedream等主流图像生成模型
|
||||
- ⚙️ **模型参数**:支持各模型特有参数配置(如尺寸、风格等)
|
||||
- 📥 **预览与下载**:实时预览生成结果,支持下载保存
|
||||
### Image Generation Mode
|
||||
- 🖼️ **Text-to-Image (T2I)**: Generate images from text prompts
|
||||
- 🎨 **Image-to-Image (I2I)**: Transform and optimize images based on local files
|
||||
- 🔌 **Multi-model Support**: Integrated with mainstream image generation models like Gemini, Seedream
|
||||
- ⚙️ **Model Parameters**: Support model-specific parameter configuration (size, style, etc.)
|
||||
- 📥 **Preview & Download**: Real-time preview of generated results with download support
|
||||
|
||||
### 高级测试模式
|
||||
- 📊 **上下文变量管理**:自定义变量、批量替换、变量预览
|
||||
- 💬 **多轮会话测试**:模拟真实对话场景,测试提示词在多轮交互中的表现
|
||||
- 🛠️ **工具调用支持**:Function Calling集成,支持OpenAI和Gemini工具调用
|
||||
- 🔍 **分析与评估链路**:在文本模式下支持分析、评估、对比评估和基于评估的智能改写
|
||||
### Advanced Testing Mode
|
||||
- 📊 **Context Variable Management**: Custom variables, batch replacement, variable preview
|
||||
- 💬 **Multi-turn Conversation Testing**: Simulate real conversation scenarios to test prompt performance in multi-turn interactions
|
||||
- 🛠️ **Function Calling Support**: Function Calling integration with support for OpenAI and Gemini tool calling
|
||||
- 🔍 **Analysis and Evaluation Pipeline**: Supports analysis, evaluation, compare evaluation, and evaluation-driven smart rewrite in text modes
|
||||
|
||||
详细使用说明请查看 [图像模式文档](docs/image-mode.md)
|
||||
For detailed usage instructions, please refer to the [Image Mode Documentation](docs/image-mode.md)
|
||||
|
||||
## 快速开始
|
||||
## Quick Start
|
||||
|
||||
### 1. 使用在线版本(推荐)
|
||||
### 1. Use Online Version (Recommended)
|
||||
|
||||
直接访问:[https://prompt.always200.com](https://prompt.always200.com)
|
||||
Direct access: [https://prompt.always200.com](https://prompt.always200.com)
|
||||
|
||||
项目是纯前端项目,所有数据只存储在浏览器本地,不会上传至任何服务器,因此直接使用在线版本也是安全可靠的
|
||||
This is a pure frontend project with all data stored locally in your browser and never uploaded to any server, making the online version both safe and reliable to use.
|
||||
|
||||
### 2. Vercel部署
|
||||
方式1:一键部署到自己的Vercel(方便,但后续无法自动更新):
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flinshenkx%2Fprompt-optimizer)
|
||||
### 2. Vercel Deployment
|
||||
Method 1: One-click deployment to your own Vercel:
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flinshenkx%2Fprompt-optimizer)
|
||||
|
||||
方式2: Fork项目后在Vercel中导入(推荐,但需参考部署文档进行手动设置):
|
||||
- 先Fork项目到自己的GitHub
|
||||
- 然后在Vercel中导入该项目
|
||||
- 可跟踪源项目更新,便于同步最新功能和修复
|
||||
- 配置环境变量:
|
||||
- `ACCESS_PASSWORD`:设置访问密码,启用访问限制
|
||||
- `VITE_OPENAI_API_KEY`等:配置各AI服务商的API密钥
|
||||
Method 2: Fork the project and import to Vercel (Recommended):
|
||||
- First fork the project to your GitHub account
|
||||
- Then import the project to Vercel
|
||||
- This allows tracking of source project updates for easy syncing of new features and fixes
|
||||
- Configure environment variables:
|
||||
- `ACCESS_PASSWORD`: Set access password to enable access restriction
|
||||
- `VITE_OPENAI_API_KEY` etc.: Configure API keys for various AI service providers
|
||||
|
||||
For more detailed deployment steps and important notes, please check:
|
||||
- [Vercel Deployment Guide](docs/user/deployment/vercel_en.md)
|
||||
|
||||
更多详细的部署步骤和注意事项,请查看:
|
||||
- [Vercel部署指南](docs/user/deployment/vercel.md)
|
||||
### 3. Download Desktop Application
|
||||
Download the latest version from [GitHub Releases](https://github.com/linshenkx/prompt-optimizer/releases). We provide both **installer** and **archive** formats for each platform.
|
||||
|
||||
### 3. 下载桌面应用
|
||||
从 [GitHub Releases](https://github.com/linshenkx/prompt-optimizer/releases) 下载最新版本。我们为各平台提供**安装程序**和**压缩包**两种格式。
|
||||
- **Installer (Recommended)**: Such as `*.exe`, `*.dmg`, `*.AppImage`, etc. **Strongly recommended as it supports automatic updates**.
|
||||
- **Archive**: Such as `*.zip`. Extract and use, but cannot auto-update.
|
||||
|
||||
- **安装程序 (推荐)**: 如 `*.exe`, `*.dmg`, `*.AppImage` 等。**强烈推荐使用此方式,因为它支持自动更新**。
|
||||
- **压缩包**: 如 `*.zip`。解压即用,但无法自动更新。
|
||||
**Core Advantages of Desktop Application**:
|
||||
- ✅ **No CORS Limitations**: As a native desktop application, it completely eliminates browser Cross-Origin Resource Sharing (CORS) issues. This means you can directly connect to any AI service provider's API, including locally deployed Ollama or commercial APIs with strict security policies, for the most complete and stable functional experience.
|
||||
- ✅ **Automatic Updates**: Versions installed through installers (like `.exe`, `.dmg`) can automatically check and update to the latest version.
|
||||
- ✅ **Independent Operation**: No browser dependency, providing faster response and better performance.
|
||||
|
||||
**桌面应用核心优势**:
|
||||
- ✅ **无跨域限制**:作为原生桌面应用,它能彻底摆脱浏览器跨域(CORS)问题的困扰。这意味着您可以直接连接任何AI服务提供商的API,包括本地部署的Ollama或有严格安全策略的商业API,获得最完整、最稳定的功能体验。
|
||||
- ✅ **自动更新**:通过安装程序(如 `.exe`, `.dmg`)安装的版本,能够自动检查并更新到最新版。
|
||||
- ✅ **独立运行**:无需依赖浏览器,提供更快的响应和更佳的性能。
|
||||
### 4. Install Chrome Extension
|
||||
1. Install from Chrome Web Store (may not be the latest version due to approval delays): [Chrome Web Store](https://chromewebstore.google.com/detail/prompt-optimizer/cakkkhboolfnadechdlgdcnjammejlna)
|
||||
2. Click the icon to open the Prompt Optimizer
|
||||
|
||||
### 4. 安装Chrome插件
|
||||
1. 从Chrome商店安装(由于审批较慢,可能不是最新的):[Chrome商店地址](https://chromewebstore.google.com/detail/prompt-optimizer/cakkkhboolfnadechdlgdcnjammejlna)
|
||||
2. 点击图标即可打开提示词优化器
|
||||
|
||||
### 5. Docker部署
|
||||
### 5. Docker Deployment
|
||||
<details>
|
||||
<summary>点击查看 Docker 部署命令</summary>
|
||||
|
||||
<summary>Click to view Docker deployment commands</summary>
|
||||
```bash
|
||||
# 运行容器(默认配置)
|
||||
# Run container (default configuration)
|
||||
docker run -d -p 8081:80 --restart unless-stopped --name prompt-optimizer linshen/prompt-optimizer
|
||||
|
||||
# 运行容器(配置API密钥和访问密码)
|
||||
# Run container (with API key configuration and password protection)
|
||||
docker run -d -p 8081:80 \
|
||||
-e VITE_OPENAI_API_KEY=your_key \
|
||||
-e ACCESS_USERNAME=your_username \ # 可选,默认为"admin"
|
||||
-e ACCESS_PASSWORD=your_password \ # 设置访问密码
|
||||
-e ACCESS_USERNAME=your_username \ # Optional, defaults to "admin"
|
||||
-e ACCESS_PASSWORD=your_password \ # Set access password
|
||||
--restart unless-stopped \
|
||||
--name prompt-optimizer \
|
||||
linshen/prompt-optimizer
|
||||
```
|
||||
</details>
|
||||
|
||||
> **国内镜像**: 如果Docker Hub访问较慢,可以将上述命令中的 `linshen/prompt-optimizer` 替换为 `registry.cn-guangzhou.aliyuncs.com/prompt-optimizer/prompt-optimizer`
|
||||
|
||||
### 6. Docker Compose部署
|
||||
### 6. Docker Compose Deployment
|
||||
<details>
|
||||
<summary>点击查看 Docker Compose 部署步骤</summary>
|
||||
|
||||
<summary>Click to view Docker Compose deployment steps</summary>
|
||||
```bash
|
||||
# 1. 克隆仓库
|
||||
# 1. Clone the repository
|
||||
git clone https://github.com/linshenkx/prompt-optimizer.git
|
||||
cd prompt-optimizer
|
||||
|
||||
# 2. 可选:创建.env文件配置API密钥和访问认证
|
||||
cp env.local.example .env
|
||||
# 编辑 .env 文件,填入实际的 API 密钥和配置
|
||||
# 2. Optional: Create .env file for API keys and authentication
|
||||
cat > .env << EOF
|
||||
# API Key Configuration
|
||||
VITE_OPENAI_API_KEY=your_openai_api_key
|
||||
VITE_GEMINI_API_KEY=your_gemini_api_key
|
||||
VITE_DEEPSEEK_API_KEY=your_deepseek_api_key
|
||||
VITE_ZHIPU_API_KEY=your_zhipu_api_key
|
||||
VITE_SILICONFLOW_API_KEY=your_siliconflow_api_key
|
||||
|
||||
# 3. 启动服务
|
||||
# Basic Authentication (Password Protection)
|
||||
ACCESS_USERNAME=your_username # Optional, defaults to "admin"
|
||||
ACCESS_PASSWORD=your_password # Set access password
|
||||
EOF
|
||||
|
||||
# 3. Start the service
|
||||
docker compose up -d
|
||||
|
||||
# 4. 查看日志
|
||||
# 4. View logs
|
||||
docker compose logs -f
|
||||
|
||||
# 5. 访问服务
|
||||
Web 界面:http://localhost:8081
|
||||
MCP 服务器:http://localhost:8081/mcp
|
||||
# 5. Access the service
|
||||
Web Interface: http://localhost:8081
|
||||
MCP Server: http://localhost:8081/mcp
|
||||
```
|
||||
</details>
|
||||
|
||||
你还可以直接编辑docker-compose.yml文件,自定义配置:
|
||||
You can also directly edit the docker-compose.yml file to customize your configuration:
|
||||
<details>
|
||||
<summary>点击查看 docker-compose.yml 示例</summary>
|
||||
<summary>Click to view docker-compose.yml example</summary>
|
||||
|
||||
```yaml
|
||||
services:
|
||||
prompt-optimizer:
|
||||
# 使用Docker Hub镜像
|
||||
# Use Docker Hub image
|
||||
image: linshen/prompt-optimizer:latest
|
||||
# 或使用阿里云镜像(国内用户推荐)
|
||||
# image: registry.cn-guangzhou.aliyuncs.com/prompt-optimizer/prompt-optimizer:latest
|
||||
container_name: prompt-optimizer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8081:80" # Web应用端口(包含MCP服务器,通过/mcp路径访问)
|
||||
- "8081:80" # Web application port (MCP server accessible via /mcp path)
|
||||
environment:
|
||||
# API密钥配置
|
||||
- VITE_OPENAI_API_KEY=your_openai_key
|
||||
- VITE_GEMINI_API_KEY=your_gemini_key
|
||||
# 访问控制(可选)
|
||||
# Access Control (Optional)
|
||||
- ACCESS_USERNAME=admin
|
||||
- ACCESS_PASSWORD=your_password
|
||||
```
|
||||
</details>
|
||||
|
||||
### 7. MCP Server 使用说明
|
||||
### 7. MCP Server Usage Instructions
|
||||
<details>
|
||||
<summary>点击查看 MCP Server 使用说明</summary>
|
||||
<summary>Click to view MCP Server usage instructions</summary>
|
||||
|
||||
Prompt Optimizer 现在支持 Model Context Protocol (MCP) 协议,可以与 Claude Desktop 等支持 MCP 的 AI 应用集成。
|
||||
Prompt Optimizer now supports the Model Context Protocol (MCP), enabling integration with AI applications that support MCP such as Claude Desktop.
|
||||
|
||||
当通过 Docker 运行时,MCP Server 会自动启动,并可通过 `http://ip:port/mcp` 访问。
|
||||
When running via Docker, the MCP Server automatically starts and can be accessed via `http://ip:port/mcp`.
|
||||
|
||||
#### 环境变量配置
|
||||
#### Environment Variable Configuration
|
||||
|
||||
MCP Server 需要配置 API 密钥才能正常工作。主要的 MCP 专属配置:
|
||||
MCP Server requires API key configuration to function properly. Main MCP-specific configurations:
|
||||
|
||||
```bash
|
||||
# MCP 服务器配置
|
||||
MCP_DEFAULT_MODEL_PROVIDER=openai # 可选值:openai, gemini, anthropic, deepseek, siliconflow, zhipu, dashscope, openrouter, modelscope, custom
|
||||
MCP_LOG_LEVEL=info # 日志级别
|
||||
# MCP Server Configuration
|
||||
MCP_DEFAULT_MODEL_PROVIDER=openai # Options: openai, gemini, anthropic, deepseek, siliconflow, zhipu, dashscope, openrouter, modelscope, custom
|
||||
MCP_LOG_LEVEL=info # Log level
|
||||
```
|
||||
|
||||
#### Docker 环境下使用 MCP
|
||||
#### Using MCP in Docker Environment
|
||||
|
||||
在 Docker 环境中,MCP Server 会与 Web 应用一起运行,您可以通过 Web 应用的相同端口访问 MCP 服务,路径为 `/mcp`。
|
||||
In a Docker environment, the MCP Server runs alongside the web application. You can access the MCP service through the same port as the web application at the `/mcp` path.
|
||||
|
||||
例如,如果您将容器的 80 端口映射到主机的 8081 端口:
|
||||
For example, if you map the container's port 80 to port 8081 on the host:
|
||||
```bash
|
||||
docker run -d -p 8081:80 \
|
||||
-e VITE_OPENAI_API_KEY=your-openai-key \
|
||||
@@ -210,18 +213,18 @@ docker run -d -p 8081:80 \
|
||||
linshen/prompt-optimizer
|
||||
```
|
||||
|
||||
那么 MCP Server 将可以通过 `http://localhost:8081/mcp` 访问。
|
||||
The MCP Server will then be accessible at `http://localhost:8081/mcp`.
|
||||
|
||||
#### Claude Desktop 集成示例
|
||||
#### Claude Desktop Integration Example
|
||||
|
||||
要在 Claude Desktop 中使用 Prompt Optimizer,您需要在 Claude Desktop 的配置文件中添加服务配置。
|
||||
To use Prompt Optimizer in Claude Desktop, you need to add the service configuration to Claude Desktop's configuration file.
|
||||
|
||||
1. 找到 Claude Desktop 的配置目录:
|
||||
1. Find Claude Desktop's configuration directory:
|
||||
- Windows: `%APPDATA%\Claude\services`
|
||||
- macOS: `~/Library/Application Support/Claude/services`
|
||||
- Linux: `~/.config/Claude/services`
|
||||
|
||||
2. 编辑或创建 `services.json` 文件,添加以下内容:
|
||||
2. Edit or create the `services.json` file, adding the following content:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -234,42 +237,42 @@ docker run -d -p 8081:80 \
|
||||
}
|
||||
```
|
||||
|
||||
请确保将 `localhost:8081` 替换为您实际部署 Prompt Optimizer 的地址和端口。
|
||||
Make sure to replace `localhost:8081` with the actual address and port where you've deployed Prompt Optimizer.
|
||||
|
||||
#### 可用工具
|
||||
#### Available Tools
|
||||
|
||||
- **optimize-user-prompt**: 优化用户提示词以提高 LLM 性能
|
||||
- **optimize-system-prompt**: 优化系统提示词以提高 LLM 性能
|
||||
- **iterate-prompt**: 对已经成熟/完善的提示词进行定向迭代优化
|
||||
- **optimize-user-prompt**: Optimize user prompts to improve LLM performance
|
||||
- **optimize-system-prompt**: Optimize system prompts to improve LLM performance
|
||||
- **iterate-prompt**: Iteratively improve mature prompts based on specific requirements
|
||||
|
||||
更多详细信息,请查看 [MCP 服务器用户指南](docs/user/mcp-server.md)。
|
||||
For more detailed information, please refer to the [MCP Server User Guide](docs/user/mcp-server_en.md).
|
||||
</details>
|
||||
|
||||
## ⚙️ API密钥配置
|
||||
## ⚙️ API Key Configuration
|
||||
|
||||
<details>
|
||||
<summary>点击查看API密钥配置方法</summary>
|
||||
<summary>Click to view API key configuration methods</summary>
|
||||
|
||||
### 方式一:通过界面配置(推荐)
|
||||
1. 点击界面右上角的"⚙️设置"按钮
|
||||
2. 选择"模型管理"选项卡
|
||||
3. 点击需要配置的模型(如OpenAI、Gemini、DeepSeek等)
|
||||
4. 在弹出的配置框中输入对应的API密钥
|
||||
5. 点击"保存"即可
|
||||
### Method 1: Via Interface (Recommended)
|
||||
1. Click the "⚙️Settings" button in the upper right corner
|
||||
2. Select the "Model Management" tab
|
||||
3. Click on the model you need to configure (such as OpenAI, Gemini, DeepSeek, etc.)
|
||||
4. Enter the corresponding API key in the configuration box
|
||||
5. Click "Save"
|
||||
|
||||
支持的模型:OpenAI、Gemini、DeepSeek、Zhipu智谱、SiliconFlow、自定义API(OpenAI兼容接口)
|
||||
Supported models: OpenAI, Gemini, DeepSeek, Zhipu AI, SiliconFlow, Custom API (OpenAI compatible interface)
|
||||
|
||||
除了API密钥,您还可以在模型配置界面为每个模型单独设置高级LLM参数。这些参数通过一个名为 `llmParams` 的字段进行配置,它允许您以键值对的形式指定LLM SDK支持的任何参数,从而更精细地控制模型行为。
|
||||
In addition to API keys, you can configure advanced LLM parameters for each model individually. These parameters are configured through a field called `llmParams`, which allows you to specify any parameters supported by the LLM SDK in key-value pairs for fine-grained control over model behavior.
|
||||
|
||||
**高级LLM参数配置示例:**
|
||||
- **OpenAI/兼容API**: `{"temperature": 0.7, "max_tokens": 4096, "timeout": 60000}`
|
||||
**Advanced LLM Parameter Configuration Examples:**
|
||||
- **OpenAI/Compatible APIs**: `{"temperature": 0.7, "max_tokens": 4096, "timeout": 60000}`
|
||||
- **Gemini**: `{"temperature": 0.8, "maxOutputTokens": 2048, "topP": 0.95}`
|
||||
- **DeepSeek**: `{"temperature": 0.5, "top_p": 0.9, "frequency_penalty": 0.1}`
|
||||
|
||||
有关 `llmParams` 的更详细说明和配置指南,请参阅 [LLM参数配置指南](docs/developer/llm-params-guide.md)。
|
||||
For more detailed information about `llmParams` configuration, please refer to the [LLM Parameters Configuration Guide](docs/developer/llm-params-guide.md).
|
||||
|
||||
### 方式二:通过环境变量配置
|
||||
Docker部署时通过 `-e` 参数配置环境变量:
|
||||
### Method 2: Via Environment Variables
|
||||
Configure environment variables through the `-e` parameter when deploying with Docker:
|
||||
|
||||
```bash
|
||||
-e VITE_OPENAI_API_KEY=your_key
|
||||
@@ -278,62 +281,62 @@ Docker部署时通过 `-e` 参数配置环境变量:
|
||||
-e VITE_ZHIPU_API_KEY=your_key
|
||||
-e VITE_SILICONFLOW_API_KEY=your_key
|
||||
|
||||
# 多自定义模型配置(支持无限数量)
|
||||
# Multiple Custom Models Configuration (Unlimited Quantity)
|
||||
-e VITE_CUSTOM_API_KEY_ollama=dummy_key
|
||||
-e VITE_CUSTOM_API_BASE_URL_ollama=http://localhost:11434/v1
|
||||
-e VITE_CUSTOM_API_MODEL_ollama=qwen2.5:7b
|
||||
```
|
||||
|
||||
> 📖 **详细配置指南**: 查看 [多自定义模型配置文档](./docs/user/multi-custom-models.md) 了解完整的配置方法和高级用法
|
||||
> 📖 **Detailed Configuration Guide**: See [Multiple Custom Models Documentation](./docs/user/multi-custom-models_en.md) for complete configuration methods and advanced usage
|
||||
|
||||
</details>
|
||||
|
||||
## 本地开发
|
||||
详细文档可查看 [开发文档](dev.md)
|
||||
## Local Development
|
||||
For detailed documentation, see [Development Documentation](dev.md)
|
||||
|
||||
<details>
|
||||
<summary>点击查看本地开发命令</summary>
|
||||
<summary>Click to view local development commands</summary>
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
# 1. Clone the project
|
||||
git clone https://github.com/linshenkx/prompt-optimizer.git
|
||||
cd prompt-optimizer
|
||||
|
||||
# 2. 安装依赖
|
||||
# 2. Install dependencies
|
||||
pnpm install
|
||||
|
||||
# 3. 启动开发服务
|
||||
pnpm dev # 主开发命令:构建 core/ui 并运行 web 应用
|
||||
pnpm dev:fresh # 完整重置并重新启动开发环境
|
||||
# 3. Start development server
|
||||
pnpm dev # Main development command: build core/ui and run web app
|
||||
pnpm dev:web # Run web app only
|
||||
pnpm dev:fresh # Complete reset and restart development environment
|
||||
```
|
||||
</details>
|
||||
|
||||
## 🗺️ 开发路线
|
||||
## 🗺️ Roadmap
|
||||
|
||||
- [x] 基础功能开发
|
||||
- [x] Web应用发布
|
||||
- [x] Chrome插件发布
|
||||
- [x] 国际化支持
|
||||
- [x] 支持系统提示词优化和用户提示词优化
|
||||
- [x] 桌面应用发布
|
||||
- [x] MCP服务发布
|
||||
- [x] 高级模式:变量管理、上下文测试、工具调用
|
||||
- [x] 图像生成:文生图(T2I)和图生图(I2I)支持
|
||||
- [x] 提示词收藏和模板管理
|
||||
- [ ] 支持工作区/项目管理
|
||||
- [x] Basic feature development
|
||||
- [x] Web application release
|
||||
- [x] Chrome extension release
|
||||
- [x] Internationalization support
|
||||
- [x] Support for system prompt optimization and user prompt optimization
|
||||
- [x] Desktop application release
|
||||
- [x] MCP service release
|
||||
- [x] Advanced mode: Variable management, context testing, function calling
|
||||
- [x] Image generation: Text-to-Image (T2I) and Image-to-Image (I2I) support
|
||||
- [x] Prompt favorites and template management
|
||||
- [ ] Support for workspace/project management
|
||||
|
||||
详细的项目状态可查看 [项目状态文档](docs/project/project-status.md)
|
||||
For detailed project status, see [Project Status Document](docs/project/project-status.md)
|
||||
|
||||
## 📖 相关文档
|
||||
|
||||
- [文档索引](docs/README.md) - 所有文档的索引
|
||||
- [技术开发指南](docs/developer/technical-development-guide.md) - 技术栈和开发规范
|
||||
- [LLM参数配置指南](docs/developer/llm-params-guide.md) - 高级LLM参数配置详细说明
|
||||
- [项目结构](docs/developer/project-structure.md) - 详细的项目结构说明
|
||||
- [项目状态](docs/project/project-status.md) - 当前进度和计划
|
||||
- [产品需求](docs/project/prd.md) - 产品需求文档
|
||||
- [Vercel部署指南](docs/user/deployment/vercel.md) - Vercel部署详细说明
|
||||
## 📖 Related Documentation
|
||||
|
||||
- [Documentation Index](docs/README.md) - Index of all documentation
|
||||
- [Technical Development Guide](docs/developer/technical-development-guide.md) - Technology stack and development specifications
|
||||
- [LLM Parameters Configuration Guide](docs/developer/llm-params-guide.md) - Detailed guide for advanced LLM parameter configuration
|
||||
- [Project Structure](docs/developer/project-structure.md) - Detailed project structure description
|
||||
- [Project Status](docs/project/project-status.md) - Current progress and plans
|
||||
- [Product Requirements](docs/project/prd.md) - Product requirements document
|
||||
- [Vercel Deployment Guide](docs/user/deployment/vercel_en.md) - Detailed instructions for Vercel deployment
|
||||
|
||||
## Star History
|
||||
|
||||
@@ -345,131 +348,131 @@ pnpm dev:fresh # 完整重置并重新启动开发环境
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 常见问题
|
||||
## FAQ
|
||||
|
||||
<details>
|
||||
<summary>点击查看常见问题解答</summary>
|
||||
<summary>Click to view frequently asked questions</summary>
|
||||
|
||||
### API连接问题
|
||||
### API Connection Issues
|
||||
|
||||
#### Q1: 为什么配置好API密钥后仍然无法连接到模型服务?
|
||||
**A**: 大多数连接失败是由**跨域问题**(CORS)导致的。由于本项目是纯前端应用,浏览器出于安全考虑会阻止直接访问不同源的API服务。模型服务如未正确配置CORS策略,会拒绝来自浏览器的直接请求。
|
||||
#### Q1: Why can't I connect to the model service after configuring the API key?
|
||||
**A**: Most connection failures are caused by **Cross-Origin Resource Sharing (CORS)** issues. As this project is a pure frontend application, browsers block direct access to API services from different origins for security reasons. Model services will reject direct requests from browsers if CORS policies are not correctly configured.
|
||||
|
||||
#### Q2: 如何解决本地Ollama的连接问题?
|
||||
**A**: Ollama完全支持OpenAI标准接口,只需配置正确的跨域策略:
|
||||
1. 设置环境变量 `OLLAMA_ORIGINS=*` 允许任意来源的请求
|
||||
2. 如仍有问题,设置 `OLLAMA_HOST=0.0.0.0:11434` 监听任意IP地址
|
||||
#### Q2: How to solve Ollama connection issues?
|
||||
**A**: Ollama fully supports the OpenAI standard interface, just configure the correct CORS policy:
|
||||
1. Set environment variable `OLLAMA_ORIGINS=*` to allow requests from any origin
|
||||
2. If issues persist, set `OLLAMA_HOST=0.0.0.0:11434` to listen on any IP address
|
||||
|
||||
#### Q3: 如何解决商业API(如Nvidia的DS API、字节跳动的火山API)的跨域问题?
|
||||
**A**: 这些平台通常有严格的跨域限制,推荐以下解决方案:
|
||||
#### Q3: How to solve CORS issues with commercial APIs (such as Nvidia's DS API, ByteDance's Volcano API)?
|
||||
**A**: These platforms typically have strict CORS restrictions. Recommended solutions:
|
||||
|
||||
1. **使用桌面版应用**(最推荐)
|
||||
- 桌面应用作为原生应用,完全没有跨域限制
|
||||
- 可以直接连接任何API服务,包括本地部署的模型
|
||||
- 提供最完整、最稳定的功能体验
|
||||
- 从 [GitHub Releases](https://github.com/linshenkx/prompt-optimizer/releases) 下载
|
||||
1. **Use Desktop Application** (Most Recommended)
|
||||
- Desktop app has no CORS restrictions as a native application
|
||||
- Can directly connect to any API service, including locally deployed models
|
||||
- Provides the most complete and stable feature experience
|
||||
- Download from [GitHub Releases](https://github.com/linshenkx/prompt-optimizer/releases)
|
||||
|
||||
2. **使用自部署的API中转服务**(专业方案)
|
||||
- 部署如OneAPI、NewAPI等开源API聚合/代理工具
|
||||
- 在设置中配置为自定义API端点
|
||||
- 请求流向:浏览器→中转服务→模型服务提供商
|
||||
- 完全控制安全策略和访问权限
|
||||
2. **Use Self-deployed API Proxy Service** (Professional solution)
|
||||
- Deploy open-source API aggregation/proxy tools like OneAPI, NewAPI
|
||||
- Configure as custom API endpoint in settings
|
||||
- Request flow: Browser → Proxy service → Model service provider
|
||||
- Full control over security policies and access permissions
|
||||
|
||||
**注意**:Web版(包括在线版、Vercel部署、Docker部署)都是纯前端应用,都会受到浏览器CORS限制。只有桌面版或使用API中转服务才能解决跨域问题。
|
||||
**Note**: All web versions (including online version, Vercel deployment, Docker deployment) are pure frontend applications and subject to browser CORS restrictions. Only the desktop version or using an API proxy service can solve CORS issues.
|
||||
|
||||
#### Q4: 我已正确配置本地模型(如Ollama)的跨域策略,为什么使用在线版依然无法连接?
|
||||
**A**: 这是由浏览器的**混合内容(Mixed Content)安全策略**导致的。出于安全考虑,浏览器会阻止安全的HTTPS页面(如在线版)向不安全的HTTP地址(如您的本地Ollama服务)发送请求。
|
||||
#### Q4: I have correctly configured CORS policies for my local model (like Ollama), why can't I still connect using the online version?
|
||||
**A**: This is caused by the browser's **Mixed Content security policy**. For security reasons, browsers block secure HTTPS pages (like the online version) from sending requests to insecure HTTP addresses (like your local Ollama service).
|
||||
|
||||
**解决方案**:
|
||||
为了绕过此限制,您需要让应用和API处于同一种协议下(例如,都是HTTP)。推荐以下方式:
|
||||
1. **使用桌面版**:桌面应用没有浏览器限制,是连接本地模型最稳定可靠的方式
|
||||
2. **使用Docker部署(HTTP)**:通过 `http://localhost:8081` 访问,与本地Ollama都是HTTP
|
||||
3. **使用Chrome插件**:插件在某些情况下也可以绕过部分安全限制
|
||||
**Solutions**:
|
||||
To bypass this limitation, you need to have the application and API under the same protocol (e.g., both HTTP). We recommend the following approaches:
|
||||
1. **Use the desktop version**: Desktop applications have no browser restrictions and are the most stable and reliable way to connect to local models
|
||||
2. **Use Docker deployment (HTTP)**: Access via `http://localhost:8081`, both the app and local Ollama use HTTP
|
||||
3. **Use Chrome extension**: Extensions can bypass some security restrictions in certain situations
|
||||
|
||||
### macOS 桌面应用问题
|
||||
### macOS Desktop Application Issues
|
||||
|
||||
#### Q5: macOS 打开应用时提示「已损坏」或「无法验证开发者」怎么办?
|
||||
**A**: 这是因为应用未经过 Apple 签名认证。由于 Apple 开发者账号费用较高,目前桌面应用暂未进行签名。
|
||||
#### Q5: macOS shows "damaged" or "unverified developer" when opening the app?
|
||||
**A**: This is because the application has not been signed with an Apple Developer certificate. Due to the high cost of Apple Developer accounts, the desktop application is currently unsigned.
|
||||
|
||||
**解决方案**:
|
||||
在终端中执行以下命令移除安全隔离属性:
|
||||
**Solution**:
|
||||
Run the following command in Terminal to remove the quarantine attribute:
|
||||
|
||||
```bash
|
||||
# 对于已安装的应用
|
||||
# For installed applications
|
||||
xattr -rd com.apple.quarantine /Applications/PromptOptimizer.app
|
||||
|
||||
# 对于下载的 .dmg 文件(安装前执行)
|
||||
# For downloaded .dmg files (run before installation)
|
||||
xattr -rd com.apple.quarantine ~/Downloads/PromptOptimizer-*.dmg
|
||||
```
|
||||
|
||||
执行后重新打开应用即可正常使用。
|
||||
After running the command, you can open the application normally.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 🤝 参与贡献
|
||||
## 🤝 Contributing
|
||||
|
||||
<details>
|
||||
<summary>点击查看贡献指南</summary>
|
||||
<summary>Click to view contribution guidelines</summary>
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m '添加某个特性'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 提交 Pull Request
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
提示:使用cursor工具开发时,建议在提交前:
|
||||
1. 使用"code_review"规则进行代码审查
|
||||
2. 按照审查报告格式检查:
|
||||
- 变更的整体一致性
|
||||
- 代码质量和实现方式
|
||||
- 测试覆盖情况
|
||||
- 文档完善程度
|
||||
3. 根据审查结果进行优化后再提交
|
||||
Tip: When developing with Cursor tool, it is recommended to do the following before committing:
|
||||
1. Use the "CodeReview" rule for review
|
||||
2. Check according to the review report format:
|
||||
- Overall consistency of changes
|
||||
- Code quality and implementation method
|
||||
- Test coverage
|
||||
- Documentation completeness
|
||||
3. Optimize based on review results before submitting
|
||||
|
||||
</details>
|
||||
|
||||
## 👏 贡献者名单
|
||||
## 👏 Contributors
|
||||
|
||||
感谢所有为项目做出贡献的开发者!
|
||||
Thanks to all the developers who have contributed to this project!
|
||||
|
||||
<a href="https://github.com/linshenkx/prompt-optimizer/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=linshenkx/prompt-optimizer" alt="贡献者" />
|
||||
<img src="https://contrib.rocks/image?repo=linshenkx/prompt-optimizer" alt="Contributors" />
|
||||
</a>
|
||||
|
||||
## 🙏 鸣谢
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
本项目在提示词工程与结构化提示词设计的探索中,受到了 [LangGPT](https://github.com/langgptai/LangGPT) 的启发。感谢 LangGPT 项目及其社区的开源分享与持续探索。
|
||||
This project was partly inspired by [LangGPT](https://github.com/langgptai/LangGPT) in prompt engineering and structured prompt design. Thanks to the LangGPT project and community for their open-source sharing and continued exploration.
|
||||
|
||||
## 📄 开源协议
|
||||
## 📄 License
|
||||
|
||||
本项目采用 [AGPL-3.0](LICENSE) 协议开源。
|
||||
This project is licensed under [AGPL-3.0](LICENSE).
|
||||
|
||||
**简单来说**:你可以自由使用、修改和商用本项目,但如果你把它做成网站或服务给别人用,需要公开你的源代码。
|
||||
**In simple terms**: You can freely use, modify, and commercialize this project, but if you turn it into a website or service for others, you must share your source code.
|
||||
|
||||
<details>
|
||||
<summary>👉 点击查看详细说明</summary>
|
||||
<summary>👉 Click for detailed explanation</summary>
|
||||
|
||||
**允许做什么:**
|
||||
- ✅ 个人使用、学习、研究
|
||||
- ✅ 公司内部使用(不对外提供服务)
|
||||
- ✅ 修改代码并用于商业项目
|
||||
- ✅ 收费销售或提供服务
|
||||
**What you can do:**
|
||||
- ✅ Personal use, learning, and research
|
||||
- ✅ Internal company use (not offering public services)
|
||||
- ✅ Modify code for commercial projects
|
||||
- ✅ Charge for products or services
|
||||
|
||||
**需要做什么:**
|
||||
- 📖 如果分发软件或提供网络服务,必须公开源代码
|
||||
- 📝 保留原作者的版权声明
|
||||
**What you must do:**
|
||||
- 📖 If distributing software or offering network services, disclose source code
|
||||
- 📝 Preserve original author's copyright notices
|
||||
|
||||
**一句话核心**:可以商用,但不能闭源。
|
||||
**Core principle**: Commercial use is allowed, but not closed-source.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
如果这个项目对你有帮助,请考虑给它一个 Star ⭐️
|
||||
If this project is helpful to you, please consider giving it a Star ⭐️
|
||||
|
||||
## 👥 联系我们
|
||||
## 👥 Contact Us
|
||||
|
||||
- 提交 Issue
|
||||
- 发起 Pull Request
|
||||
- 加入讨论组
|
||||
- Submit an Issue
|
||||
- Create a Pull Request
|
||||
- Join the discussion group
|
||||
+475
@@ -0,0 +1,475 @@
|
||||
# Prompt Optimizer (提示词优化器) 🚀
|
||||
|
||||
<div align="center">
|
||||
|
||||
[English](README.md) | [中文](README.zh-CN.md)
|
||||
|
||||
[](https://github.com/linshenkx/prompt-optimizer/stargazers)
|
||||

|
||||
|
||||
<a href="https://trendshift.io/repositories/13813" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13813" alt="linshenkx%2Fprompt-optimizer | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
[](LICENSE)
|
||||
[](https://hub.docker.com/r/linshen/prompt-optimizer)
|
||||

|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flinshenkx%2Fprompt-optimizer)
|
||||
|
||||
[在线体验](https://prompt.always200.com) | [快速开始](#快速开始) | [常见问题](#常见问题) | [Chrome插件](https://chromewebstore.google.com/detail/prompt-optimizer/cakkkhboolfnadechdlgdcnjammejlna) | [💖赞助支持](images/other/sponsor_wx.jpg)
|
||||
|
||||
[开发文档](dev.md) | [Vercel部署指南](docs/user/deployment/vercel.md) | [MCP部署使用说明](docs/user/mcp-server.md) | [DeepWiki文档](https://deepwiki.com/linshenkx/prompt-optimizer) | [ZRead文档](https://zread.ai/linshenkx/prompt-optimizer)
|
||||
|
||||
</div>
|
||||
|
||||
## 📖 项目简介
|
||||
|
||||
Prompt Optimizer是一个强大的AI提示词优化工具,帮助你编写更好的AI提示词,提升AI输出质量。支持Web应用、桌面应用、Chrome插件和Docker部署四种使用方式。
|
||||
|
||||
### 🎥 功能演示
|
||||
|
||||
<div align="center">
|
||||
<p><b>1. 红队审稿:让模型不再顺着说</b></p>
|
||||
<p>同样的输入下,系统提示词优化能让小模型从泛泛而谈的附和式回答,转向更有立场、更有结构的批判式审查,明确指出论点中的漏洞、风险与隐含假设。</p>
|
||||
<img src="images/demo/red-team-reviewer-fullpage.png" alt="红队审稿人整页演示" width="85%">
|
||||
<br>
|
||||
<p><b>2. 闲鱼砍价回复:让变量真正决定回复策略</b></p>
|
||||
<p>同一套提示词模板里,只需替换商品、报价、底线和语气等变量,就能快速复用到不同交易场景。优化后的提示词会明显减少“助手腔”和多余解释,让小模型更像真人卖家一样,围绕价格分歧、商品情况和成交底线直接组织回复。</p>
|
||||
<img src="images/demo/pro-variable-bargaining-reply.png" alt="闲鱼砍价回复变量模式演示" width="85%">
|
||||
<br>
|
||||
<p><b>3. 文生图:把一句想法优化成更可控的主视觉提示词</b></p>
|
||||
<p>这不是单纯把提示词写得更长,而是把一句模糊念头拆成更清晰的视觉主体、空间关系和情绪锚点。左侧只有“夜空中的漂浮图书馆”这个概念,右侧则通过优化补足了更明确的奇幻结构和画面重心,让生成结果更像可继续定制的主视觉,而不是只靠模型自由发挥。</p>
|
||||
<img src="images/demo/text2image-floating-library-creative.png" alt="漂浮图书馆文生图演示" width="85%">
|
||||
</div>
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
- 🎯 **智能优化**:一键优化提示词,支持多轮迭代改进,提升AI回复准确度
|
||||
- 📝 **双模式优化**:支持系统提示词优化和用户提示词优化,满足不同使用场景
|
||||
- 🔄 **分析与对比评估**:支持分析、单结果评估和多结果对比评估,帮助判断提示词是否真的改进
|
||||
- 🤖 **多模型集成**:支持OpenAI、Gemini、DeepSeek、智谱AI、SiliconFlow等主流AI模型
|
||||
- 🖼️ **图像生成**:支持文生图(T2I)和图生图(I2I),集成Gemini、Seedream等图像模型
|
||||
- 📊 **高级测试模式**:上下文变量管理、多轮会话测试、工具调用(Function Calling)支持
|
||||
- 🔒 **安全架构**:纯客户端处理,数据直接与AI服务商交互,不经过中间服务器
|
||||
- 📱 **多端支持**:同时提供Web应用、桌面应用、Chrome插件和Docker部署四种使用方式
|
||||
- 🔐 **访问控制**:支持密码保护功能,保障部署安全
|
||||
- 🧩 **MCP协议支持**:支持Model Context Protocol (MCP) 协议,可与Claude Desktop等MCP兼容应用集成
|
||||
|
||||
## 🚀 高级功能
|
||||
|
||||
### 图像生成模式
|
||||
- 🖼️ **文生图(T2I)**:通过文本提示词生成图像
|
||||
- 🎨 **图生图(I2I)**:基于本地图片进行图像变换和优化
|
||||
- 🔌 **多模型支持**:集成Gemini、Seedream等主流图像生成模型
|
||||
- ⚙️ **模型参数**:支持各模型特有参数配置(如尺寸、风格等)
|
||||
- 📥 **预览与下载**:实时预览生成结果,支持下载保存
|
||||
|
||||
### 高级测试模式
|
||||
- 📊 **上下文变量管理**:自定义变量、批量替换、变量预览
|
||||
- 💬 **多轮会话测试**:模拟真实对话场景,测试提示词在多轮交互中的表现
|
||||
- 🛠️ **工具调用支持**:Function Calling集成,支持OpenAI和Gemini工具调用
|
||||
- 🔍 **分析与评估链路**:在文本模式下支持分析、评估、对比评估和基于评估的智能改写
|
||||
|
||||
详细使用说明请查看 [图像模式文档](docs/image-mode.md)
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 使用在线版本(推荐)
|
||||
|
||||
直接访问:[https://prompt.always200.com](https://prompt.always200.com)
|
||||
|
||||
项目是纯前端项目,所有数据只存储在浏览器本地,不会上传至任何服务器,因此直接使用在线版本也是安全可靠的
|
||||
|
||||
### 2. Vercel部署
|
||||
方式1:一键部署到自己的Vercel(方便,但后续无法自动更新):
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flinshenkx%2Fprompt-optimizer)
|
||||
|
||||
方式2: Fork项目后在Vercel中导入(推荐,但需参考部署文档进行手动设置):
|
||||
- 先Fork项目到自己的GitHub
|
||||
- 然后在Vercel中导入该项目
|
||||
- 可跟踪源项目更新,便于同步最新功能和修复
|
||||
- 配置环境变量:
|
||||
- `ACCESS_PASSWORD`:设置访问密码,启用访问限制
|
||||
- `VITE_OPENAI_API_KEY`等:配置各AI服务商的API密钥
|
||||
|
||||
更多详细的部署步骤和注意事项,请查看:
|
||||
- [Vercel部署指南](docs/user/deployment/vercel.md)
|
||||
|
||||
### 3. 下载桌面应用
|
||||
从 [GitHub Releases](https://github.com/linshenkx/prompt-optimizer/releases) 下载最新版本。我们为各平台提供**安装程序**和**压缩包**两种格式。
|
||||
|
||||
- **安装程序 (推荐)**: 如 `*.exe`, `*.dmg`, `*.AppImage` 等。**强烈推荐使用此方式,因为它支持自动更新**。
|
||||
- **压缩包**: 如 `*.zip`。解压即用,但无法自动更新。
|
||||
|
||||
**桌面应用核心优势**:
|
||||
- ✅ **无跨域限制**:作为原生桌面应用,它能彻底摆脱浏览器跨域(CORS)问题的困扰。这意味着您可以直接连接任何AI服务提供商的API,包括本地部署的Ollama或有严格安全策略的商业API,获得最完整、最稳定的功能体验。
|
||||
- ✅ **自动更新**:通过安装程序(如 `.exe`, `.dmg`)安装的版本,能够自动检查并更新到最新版。
|
||||
- ✅ **独立运行**:无需依赖浏览器,提供更快的响应和更佳的性能。
|
||||
|
||||
### 4. 安装Chrome插件
|
||||
1. 从Chrome商店安装(由于审批较慢,可能不是最新的):[Chrome商店地址](https://chromewebstore.google.com/detail/prompt-optimizer/cakkkhboolfnadechdlgdcnjammejlna)
|
||||
2. 点击图标即可打开提示词优化器
|
||||
|
||||
### 5. Docker部署
|
||||
<details>
|
||||
<summary>点击查看 Docker 部署命令</summary>
|
||||
|
||||
```bash
|
||||
# 运行容器(默认配置)
|
||||
docker run -d -p 8081:80 --restart unless-stopped --name prompt-optimizer linshen/prompt-optimizer
|
||||
|
||||
# 运行容器(配置API密钥和访问密码)
|
||||
docker run -d -p 8081:80 \
|
||||
-e VITE_OPENAI_API_KEY=your_key \
|
||||
-e ACCESS_USERNAME=your_username \ # 可选,默认为"admin"
|
||||
-e ACCESS_PASSWORD=your_password \ # 设置访问密码
|
||||
--restart unless-stopped \
|
||||
--name prompt-optimizer \
|
||||
linshen/prompt-optimizer
|
||||
```
|
||||
</details>
|
||||
|
||||
> **国内镜像**: 如果Docker Hub访问较慢,可以将上述命令中的 `linshen/prompt-optimizer` 替换为 `registry.cn-guangzhou.aliyuncs.com/prompt-optimizer/prompt-optimizer`
|
||||
|
||||
### 6. Docker Compose部署
|
||||
<details>
|
||||
<summary>点击查看 Docker Compose 部署步骤</summary>
|
||||
|
||||
```bash
|
||||
# 1. 克隆仓库
|
||||
git clone https://github.com/linshenkx/prompt-optimizer.git
|
||||
cd prompt-optimizer
|
||||
|
||||
# 2. 可选:创建.env文件配置API密钥和访问认证
|
||||
cp env.local.example .env
|
||||
# 编辑 .env 文件,填入实际的 API 密钥和配置
|
||||
|
||||
# 3. 启动服务
|
||||
docker compose up -d
|
||||
|
||||
# 4. 查看日志
|
||||
docker compose logs -f
|
||||
|
||||
# 5. 访问服务
|
||||
Web 界面:http://localhost:8081
|
||||
MCP 服务器:http://localhost:8081/mcp
|
||||
```
|
||||
</details>
|
||||
|
||||
你还可以直接编辑docker-compose.yml文件,自定义配置:
|
||||
<details>
|
||||
<summary>点击查看 docker-compose.yml 示例</summary>
|
||||
|
||||
```yaml
|
||||
services:
|
||||
prompt-optimizer:
|
||||
# 使用Docker Hub镜像
|
||||
image: linshen/prompt-optimizer:latest
|
||||
# 或使用阿里云镜像(国内用户推荐)
|
||||
# image: registry.cn-guangzhou.aliyuncs.com/prompt-optimizer/prompt-optimizer:latest
|
||||
container_name: prompt-optimizer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8081:80" # Web应用端口(包含MCP服务器,通过/mcp路径访问)
|
||||
environment:
|
||||
# API密钥配置
|
||||
- VITE_OPENAI_API_KEY=your_openai_key
|
||||
- VITE_GEMINI_API_KEY=your_gemini_key
|
||||
# 访问控制(可选)
|
||||
- ACCESS_USERNAME=admin
|
||||
- ACCESS_PASSWORD=your_password
|
||||
```
|
||||
</details>
|
||||
|
||||
### 7. MCP Server 使用说明
|
||||
<details>
|
||||
<summary>点击查看 MCP Server 使用说明</summary>
|
||||
|
||||
Prompt Optimizer 现在支持 Model Context Protocol (MCP) 协议,可以与 Claude Desktop 等支持 MCP 的 AI 应用集成。
|
||||
|
||||
当通过 Docker 运行时,MCP Server 会自动启动,并可通过 `http://ip:port/mcp` 访问。
|
||||
|
||||
#### 环境变量配置
|
||||
|
||||
MCP Server 需要配置 API 密钥才能正常工作。主要的 MCP 专属配置:
|
||||
|
||||
```bash
|
||||
# MCP 服务器配置
|
||||
MCP_DEFAULT_MODEL_PROVIDER=openai # 可选值:openai, gemini, anthropic, deepseek, siliconflow, zhipu, dashscope, openrouter, modelscope, custom
|
||||
MCP_LOG_LEVEL=info # 日志级别
|
||||
```
|
||||
|
||||
#### Docker 环境下使用 MCP
|
||||
|
||||
在 Docker 环境中,MCP Server 会与 Web 应用一起运行,您可以通过 Web 应用的相同端口访问 MCP 服务,路径为 `/mcp`。
|
||||
|
||||
例如,如果您将容器的 80 端口映射到主机的 8081 端口:
|
||||
```bash
|
||||
docker run -d -p 8081:80 \
|
||||
-e VITE_OPENAI_API_KEY=your-openai-key \
|
||||
-e MCP_DEFAULT_MODEL_PROVIDER=openai \
|
||||
--name prompt-optimizer \
|
||||
linshen/prompt-optimizer
|
||||
```
|
||||
|
||||
那么 MCP Server 将可以通过 `http://localhost:8081/mcp` 访问。
|
||||
|
||||
#### Claude Desktop 集成示例
|
||||
|
||||
要在 Claude Desktop 中使用 Prompt Optimizer,您需要在 Claude Desktop 的配置文件中添加服务配置。
|
||||
|
||||
1. 找到 Claude Desktop 的配置目录:
|
||||
- Windows: `%APPDATA%\Claude\services`
|
||||
- macOS: `~/Library/Application Support/Claude/services`
|
||||
- Linux: `~/.config/Claude/services`
|
||||
|
||||
2. 编辑或创建 `services.json` 文件,添加以下内容:
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"name": "Prompt Optimizer",
|
||||
"url": "http://localhost:8081/mcp"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
请确保将 `localhost:8081` 替换为您实际部署 Prompt Optimizer 的地址和端口。
|
||||
|
||||
#### 可用工具
|
||||
|
||||
- **optimize-user-prompt**: 优化用户提示词以提高 LLM 性能
|
||||
- **optimize-system-prompt**: 优化系统提示词以提高 LLM 性能
|
||||
- **iterate-prompt**: 对已经成熟/完善的提示词进行定向迭代优化
|
||||
|
||||
更多详细信息,请查看 [MCP 服务器用户指南](docs/user/mcp-server.md)。
|
||||
</details>
|
||||
|
||||
## ⚙️ API密钥配置
|
||||
|
||||
<details>
|
||||
<summary>点击查看API密钥配置方法</summary>
|
||||
|
||||
### 方式一:通过界面配置(推荐)
|
||||
1. 点击界面右上角的"⚙️设置"按钮
|
||||
2. 选择"模型管理"选项卡
|
||||
3. 点击需要配置的模型(如OpenAI、Gemini、DeepSeek等)
|
||||
4. 在弹出的配置框中输入对应的API密钥
|
||||
5. 点击"保存"即可
|
||||
|
||||
支持的模型:OpenAI、Gemini、DeepSeek、Zhipu智谱、SiliconFlow、自定义API(OpenAI兼容接口)
|
||||
|
||||
除了API密钥,您还可以在模型配置界面为每个模型单独设置高级LLM参数。这些参数通过一个名为 `llmParams` 的字段进行配置,它允许您以键值对的形式指定LLM SDK支持的任何参数,从而更精细地控制模型行为。
|
||||
|
||||
**高级LLM参数配置示例:**
|
||||
- **OpenAI/兼容API**: `{"temperature": 0.7, "max_tokens": 4096, "timeout": 60000}`
|
||||
- **Gemini**: `{"temperature": 0.8, "maxOutputTokens": 2048, "topP": 0.95}`
|
||||
- **DeepSeek**: `{"temperature": 0.5, "top_p": 0.9, "frequency_penalty": 0.1}`
|
||||
|
||||
有关 `llmParams` 的更详细说明和配置指南,请参阅 [LLM参数配置指南](docs/developer/llm-params-guide.md)。
|
||||
|
||||
### 方式二:通过环境变量配置
|
||||
Docker部署时通过 `-e` 参数配置环境变量:
|
||||
|
||||
```bash
|
||||
-e VITE_OPENAI_API_KEY=your_key
|
||||
-e VITE_GEMINI_API_KEY=your_key
|
||||
-e VITE_DEEPSEEK_API_KEY=your_key
|
||||
-e VITE_ZHIPU_API_KEY=your_key
|
||||
-e VITE_SILICONFLOW_API_KEY=your_key
|
||||
|
||||
# 多自定义模型配置(支持无限数量)
|
||||
-e VITE_CUSTOM_API_KEY_ollama=dummy_key
|
||||
-e VITE_CUSTOM_API_BASE_URL_ollama=http://localhost:11434/v1
|
||||
-e VITE_CUSTOM_API_MODEL_ollama=qwen2.5:7b
|
||||
```
|
||||
|
||||
> 📖 **详细配置指南**: 查看 [多自定义模型配置文档](./docs/user/multi-custom-models.md) 了解完整的配置方法和高级用法
|
||||
|
||||
</details>
|
||||
|
||||
## 本地开发
|
||||
详细文档可查看 [开发文档](dev.md)
|
||||
|
||||
<details>
|
||||
<summary>点击查看本地开发命令</summary>
|
||||
|
||||
```bash
|
||||
# 1. 克隆项目
|
||||
git clone https://github.com/linshenkx/prompt-optimizer.git
|
||||
cd prompt-optimizer
|
||||
|
||||
# 2. 安装依赖
|
||||
pnpm install
|
||||
|
||||
# 3. 启动开发服务
|
||||
pnpm dev # 主开发命令:构建 core/ui 并运行 web 应用
|
||||
pnpm dev:fresh # 完整重置并重新启动开发环境
|
||||
```
|
||||
</details>
|
||||
|
||||
## 🗺️ 开发路线
|
||||
|
||||
- [x] 基础功能开发
|
||||
- [x] Web应用发布
|
||||
- [x] Chrome插件发布
|
||||
- [x] 国际化支持
|
||||
- [x] 支持系统提示词优化和用户提示词优化
|
||||
- [x] 桌面应用发布
|
||||
- [x] MCP服务发布
|
||||
- [x] 高级模式:变量管理、上下文测试、工具调用
|
||||
- [x] 图像生成:文生图(T2I)和图生图(I2I)支持
|
||||
- [x] 提示词收藏和模板管理
|
||||
- [ ] 支持工作区/项目管理
|
||||
|
||||
详细的项目状态可查看 [项目状态文档](docs/project/project-status.md)
|
||||
|
||||
## 📖 相关文档
|
||||
|
||||
- [文档索引](docs/README.md) - 所有文档的索引
|
||||
- [技术开发指南](docs/developer/technical-development-guide.md) - 技术栈和开发规范
|
||||
- [LLM参数配置指南](docs/developer/llm-params-guide.md) - 高级LLM参数配置详细说明
|
||||
- [项目结构](docs/developer/project-structure.md) - 详细的项目结构说明
|
||||
- [项目状态](docs/project/project-status.md) - 当前进度和计划
|
||||
- [产品需求](docs/project/prd.md) - 产品需求文档
|
||||
- [Vercel部署指南](docs/user/deployment/vercel.md) - Vercel部署详细说明
|
||||
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#linshenkx/prompt-optimizer&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=linshenkx/prompt-optimizer&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=linshenkx/prompt-optimizer&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=linshenkx/prompt-optimizer&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 常见问题
|
||||
|
||||
<details>
|
||||
<summary>点击查看常见问题解答</summary>
|
||||
|
||||
### API连接问题
|
||||
|
||||
#### Q1: 为什么配置好API密钥后仍然无法连接到模型服务?
|
||||
**A**: 大多数连接失败是由**跨域问题**(CORS)导致的。由于本项目是纯前端应用,浏览器出于安全考虑会阻止直接访问不同源的API服务。模型服务如未正确配置CORS策略,会拒绝来自浏览器的直接请求。
|
||||
|
||||
#### Q2: 如何解决本地Ollama的连接问题?
|
||||
**A**: Ollama完全支持OpenAI标准接口,只需配置正确的跨域策略:
|
||||
1. 设置环境变量 `OLLAMA_ORIGINS=*` 允许任意来源的请求
|
||||
2. 如仍有问题,设置 `OLLAMA_HOST=0.0.0.0:11434` 监听任意IP地址
|
||||
|
||||
#### Q3: 如何解决商业API(如Nvidia的DS API、字节跳动的火山API)的跨域问题?
|
||||
**A**: 这些平台通常有严格的跨域限制,推荐以下解决方案:
|
||||
|
||||
1. **使用桌面版应用**(最推荐)
|
||||
- 桌面应用作为原生应用,完全没有跨域限制
|
||||
- 可以直接连接任何API服务,包括本地部署的模型
|
||||
- 提供最完整、最稳定的功能体验
|
||||
- 从 [GitHub Releases](https://github.com/linshenkx/prompt-optimizer/releases) 下载
|
||||
|
||||
2. **使用自部署的API中转服务**(专业方案)
|
||||
- 部署如OneAPI、NewAPI等开源API聚合/代理工具
|
||||
- 在设置中配置为自定义API端点
|
||||
- 请求流向:浏览器→中转服务→模型服务提供商
|
||||
- 完全控制安全策略和访问权限
|
||||
|
||||
**注意**:Web版(包括在线版、Vercel部署、Docker部署)都是纯前端应用,都会受到浏览器CORS限制。只有桌面版或使用API中转服务才能解决跨域问题。
|
||||
|
||||
#### Q4: 我已正确配置本地模型(如Ollama)的跨域策略,为什么使用在线版依然无法连接?
|
||||
**A**: 这是由浏览器的**混合内容(Mixed Content)安全策略**导致的。出于安全考虑,浏览器会阻止安全的HTTPS页面(如在线版)向不安全的HTTP地址(如您的本地Ollama服务)发送请求。
|
||||
|
||||
**解决方案**:
|
||||
为了绕过此限制,您需要让应用和API处于同一种协议下(例如,都是HTTP)。推荐以下方式:
|
||||
1. **使用桌面版**:桌面应用没有浏览器限制,是连接本地模型最稳定可靠的方式
|
||||
2. **使用Docker部署(HTTP)**:通过 `http://localhost:8081` 访问,与本地Ollama都是HTTP
|
||||
3. **使用Chrome插件**:插件在某些情况下也可以绕过部分安全限制
|
||||
|
||||
### macOS 桌面应用问题
|
||||
|
||||
#### Q5: macOS 打开应用时提示「已损坏」或「无法验证开发者」怎么办?
|
||||
**A**: 这是因为应用未经过 Apple 签名认证。由于 Apple 开发者账号费用较高,目前桌面应用暂未进行签名。
|
||||
|
||||
**解决方案**:
|
||||
在终端中执行以下命令移除安全隔离属性:
|
||||
|
||||
```bash
|
||||
# 对于已安装的应用
|
||||
xattr -rd com.apple.quarantine /Applications/PromptOptimizer.app
|
||||
|
||||
# 对于下载的 .dmg 文件(安装前执行)
|
||||
xattr -rd com.apple.quarantine ~/Downloads/PromptOptimizer-*.dmg
|
||||
```
|
||||
|
||||
执行后重新打开应用即可正常使用。
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 🤝 参与贡献
|
||||
|
||||
<details>
|
||||
<summary>点击查看贡献指南</summary>
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m '添加某个特性'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 提交 Pull Request
|
||||
|
||||
提示:使用cursor工具开发时,建议在提交前:
|
||||
1. 使用"code_review"规则进行代码审查
|
||||
2. 按照审查报告格式检查:
|
||||
- 变更的整体一致性
|
||||
- 代码质量和实现方式
|
||||
- 测试覆盖情况
|
||||
- 文档完善程度
|
||||
3. 根据审查结果进行优化后再提交
|
||||
|
||||
</details>
|
||||
|
||||
## 👏 贡献者名单
|
||||
|
||||
感谢所有为项目做出贡献的开发者!
|
||||
|
||||
<a href="https://github.com/linshenkx/prompt-optimizer/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=linshenkx/prompt-optimizer" alt="贡献者" />
|
||||
</a>
|
||||
|
||||
## 🙏 鸣谢
|
||||
|
||||
本项目在提示词工程与结构化提示词设计的探索中,受到了 [LangGPT](https://github.com/langgptai/LangGPT) 的启发。感谢 LangGPT 项目及其社区的开源分享与持续探索。
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
本项目采用 [AGPL-3.0](LICENSE) 协议开源。
|
||||
|
||||
**简单来说**:你可以自由使用、修改和商用本项目,但如果你把它做成网站或服务给别人用,需要公开你的源代码。
|
||||
|
||||
<details>
|
||||
<summary>👉 点击查看详细说明</summary>
|
||||
|
||||
**允许做什么:**
|
||||
- ✅ 个人使用、学习、研究
|
||||
- ✅ 公司内部使用(不对外提供服务)
|
||||
- ✅ 修改代码并用于商业项目
|
||||
- ✅ 收费销售或提供服务
|
||||
|
||||
**需要做什么:**
|
||||
- 📖 如果分发软件或提供网络服务,必须公开源代码
|
||||
- 📝 保留原作者的版权声明
|
||||
|
||||
**一句话核心**:可以商用,但不能闭源。
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
如果这个项目对你有帮助,请考虑给它一个 Star ⭐️
|
||||
|
||||
## 👥 联系我们
|
||||
|
||||
- 提交 Issue
|
||||
- 发起 Pull Request
|
||||
- 加入讨论组
|
||||
-478
@@ -1,478 +0,0 @@
|
||||
# Prompt Optimizer 🚀
|
||||
|
||||
<div align="center">
|
||||
|
||||
[English](README_EN.md) | [中文](README.md)
|
||||
|
||||
[](https://github.com/linshenkx/prompt-optimizer/stargazers)
|
||||

|
||||
|
||||
<a href="https://trendshift.io/repositories/13813" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13813" alt="linshenkx%2Fprompt-optimizer | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
[](LICENSE)
|
||||
[](https://hub.docker.com/r/linshen/prompt-optimizer)
|
||||

|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flinshenkx%2Fprompt-optimizer)
|
||||
|
||||
[Live Demo](https://prompt.always200.com) | [Quick Start](#quick-start) | [FAQ](#faq) | [Chrome Extension](https://chromewebstore.google.com/detail/prompt-optimizer/cakkkhboolfnadechdlgdcnjammejlna)
|
||||
|
||||
[Development Docs](dev.md) | [Vercel Deployment Guide](docs/user/deployment/vercel_en.md) | [MCP Deployment Guide](docs/user/mcp-server_en.md) | [DeepWiki Docs](https://deepwiki.com/linshenkx/prompt-optimizer) | [ZRead Docs](https://zread.ai/linshenkx/prompt-optimizer)
|
||||
|
||||
</div>
|
||||
|
||||
## 📖 Project Introduction
|
||||
|
||||
Prompt Optimizer is a powerful AI prompt optimization tool that helps you write better AI prompts and improve the quality of AI outputs. It supports four usage methods: web application, desktop application, Chrome extension, and Docker deployment.
|
||||
|
||||
### 🎥 Feature Demonstration
|
||||
|
||||
<div align="center">
|
||||
<p><b>1. Hard-Nosed Reviewer: Turn Agreement into Useful Critique</b></p>
|
||||
<p>Starting from a minimal English role prompt, optimization pushes a small model away from generic pushback and toward a clearer, more structured review that surfaces weak assumptions, evidence gaps, and concrete revision advice.</p>
|
||||
<img src="images/demo/hard-nosed-reviewer-fullpage-en.png" alt="Hard-nosed reviewer full-page demo" width="85%">
|
||||
<br>
|
||||
<p><b>2. Marketplace Bargaining Reply: Let Variables Change the Strategy</b></p>
|
||||
<p>With a single reusable prompt template, you can swap in item details, price anchors, buyer offers, tone, and negotiation goals for different marketplace conversations. After optimization, the same small model does a better job turning those variables into a clearer, more transaction-ready reply instead of a generic helper-style response.</p>
|
||||
<img src="images/demo/pro-variable-bargaining-reply-en.png" alt="Marketplace bargaining reply variable-mode demo" width="85%">
|
||||
<br>
|
||||
<p><b>3. Text-to-Image: Optimize a One-Line Idea into a More Directable Key Visual Prompt</b></p>
|
||||
<p>This is not just prompt expansion. Starting from a vague one-line idea, Prompt Optimizer adds clearer subject cues, spatial relationships, and mood anchors. The left side is simply “a floating library in the night sky,” while the optimized version gives the model a more directed fantasy composition that feels closer to a reusable key visual than a lucky generic image.</p>
|
||||
<img src="images/demo/text2image-floating-library-creative-en.png" alt="Floating library text-to-image demo" width="85%">
|
||||
</div>
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
- 🎯 **Intelligent Optimization**: One-click prompt optimization with multi-round iterative improvements to enhance AI response accuracy
|
||||
- 📝 **Dual Mode Optimization**: Support for both system prompt optimization and user prompt optimization to meet different usage scenarios
|
||||
- 🔄 **Analysis and Compare Evaluation**: Supports analysis, single-result evaluation, and multi-result compare evaluation to help determine whether a prompt has truly improved
|
||||
- 🤖 **Multi-model Integration**: Support for mainstream AI models including OpenAI, Gemini, DeepSeek, Zhipu AI, SiliconFlow, etc.
|
||||
- 🖼️ **Image Generation**: Support for Text-to-Image (T2I) and Image-to-Image (I2I) with models like Gemini, Seedream
|
||||
- 📊 **Advanced Testing Mode**: Context variable management, multi-turn conversation testing, Function Calling support
|
||||
- 🔒 **Secure Architecture**: Pure client-side processing with direct data interaction with AI service providers, bypassing intermediate servers
|
||||
- 📱 **Multi-platform Support**: Available as web application, desktop application, Chrome extension, and Docker deployment
|
||||
- 🔐 **Access Control**: Password protection feature for secure deployment
|
||||
- 🧩 **MCP Protocol Support**: Supports Model Context Protocol (MCP), enabling integration with MCP-compatible AI applications like Claude Desktop
|
||||
|
||||
## 🚀 Advanced Features
|
||||
|
||||
### Image Generation Mode
|
||||
- 🖼️ **Text-to-Image (T2I)**: Generate images from text prompts
|
||||
- 🎨 **Image-to-Image (I2I)**: Transform and optimize images based on local files
|
||||
- 🔌 **Multi-model Support**: Integrated with mainstream image generation models like Gemini, Seedream
|
||||
- ⚙️ **Model Parameters**: Support model-specific parameter configuration (size, style, etc.)
|
||||
- 📥 **Preview & Download**: Real-time preview of generated results with download support
|
||||
|
||||
### Advanced Testing Mode
|
||||
- 📊 **Context Variable Management**: Custom variables, batch replacement, variable preview
|
||||
- 💬 **Multi-turn Conversation Testing**: Simulate real conversation scenarios to test prompt performance in multi-turn interactions
|
||||
- 🛠️ **Function Calling Support**: Function Calling integration with support for OpenAI and Gemini tool calling
|
||||
- 🔍 **Analysis and Evaluation Pipeline**: Supports analysis, evaluation, compare evaluation, and evaluation-driven smart rewrite in text modes
|
||||
|
||||
For detailed usage instructions, please refer to the [Image Mode Documentation](docs/image-mode.md)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Use Online Version (Recommended)
|
||||
|
||||
Direct access: [https://prompt.always200.com](https://prompt.always200.com)
|
||||
|
||||
This is a pure frontend project with all data stored locally in your browser and never uploaded to any server, making the online version both safe and reliable to use.
|
||||
|
||||
### 2. Vercel Deployment
|
||||
Method 1: One-click deployment to your own Vercel:
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flinshenkx%2Fprompt-optimizer)
|
||||
|
||||
Method 2: Fork the project and import to Vercel (Recommended):
|
||||
- First fork the project to your GitHub account
|
||||
- Then import the project to Vercel
|
||||
- This allows tracking of source project updates for easy syncing of new features and fixes
|
||||
- Configure environment variables:
|
||||
- `ACCESS_PASSWORD`: Set access password to enable access restriction
|
||||
- `VITE_OPENAI_API_KEY` etc.: Configure API keys for various AI service providers
|
||||
|
||||
For more detailed deployment steps and important notes, please check:
|
||||
- [Vercel Deployment Guide](docs/user/deployment/vercel_en.md)
|
||||
|
||||
### 3. Download Desktop Application
|
||||
Download the latest version from [GitHub Releases](https://github.com/linshenkx/prompt-optimizer/releases). We provide both **installer** and **archive** formats for each platform.
|
||||
|
||||
- **Installer (Recommended)**: Such as `*.exe`, `*.dmg`, `*.AppImage`, etc. **Strongly recommended as it supports automatic updates**.
|
||||
- **Archive**: Such as `*.zip`. Extract and use, but cannot auto-update.
|
||||
|
||||
**Core Advantages of Desktop Application**:
|
||||
- ✅ **No CORS Limitations**: As a native desktop application, it completely eliminates browser Cross-Origin Resource Sharing (CORS) issues. This means you can directly connect to any AI service provider's API, including locally deployed Ollama or commercial APIs with strict security policies, for the most complete and stable functional experience.
|
||||
- ✅ **Automatic Updates**: Versions installed through installers (like `.exe`, `.dmg`) can automatically check and update to the latest version.
|
||||
- ✅ **Independent Operation**: No browser dependency, providing faster response and better performance.
|
||||
|
||||
### 4. Install Chrome Extension
|
||||
1. Install from Chrome Web Store (may not be the latest version due to approval delays): [Chrome Web Store](https://chromewebstore.google.com/detail/prompt-optimizer/cakkkhboolfnadechdlgdcnjammejlna)
|
||||
2. Click the icon to open the Prompt Optimizer
|
||||
|
||||
### 5. Docker Deployment
|
||||
<details>
|
||||
<summary>Click to view Docker deployment commands</summary>
|
||||
```bash
|
||||
# Run container (default configuration)
|
||||
docker run -d -p 8081:80 --restart unless-stopped --name prompt-optimizer linshen/prompt-optimizer
|
||||
|
||||
# Run container (with API key configuration and password protection)
|
||||
docker run -d -p 8081:80 \
|
||||
-e VITE_OPENAI_API_KEY=your_key \
|
||||
-e ACCESS_USERNAME=your_username \ # Optional, defaults to "admin"
|
||||
-e ACCESS_PASSWORD=your_password \ # Set access password
|
||||
--restart unless-stopped \
|
||||
--name prompt-optimizer \
|
||||
linshen/prompt-optimizer
|
||||
```
|
||||
</details>
|
||||
|
||||
### 6. Docker Compose Deployment
|
||||
<details>
|
||||
<summary>Click to view Docker Compose deployment steps</summary>
|
||||
```bash
|
||||
# 1. Clone the repository
|
||||
git clone https://github.com/linshenkx/prompt-optimizer.git
|
||||
cd prompt-optimizer
|
||||
|
||||
# 2. Optional: Create .env file for API keys and authentication
|
||||
cat > .env << EOF
|
||||
# API Key Configuration
|
||||
VITE_OPENAI_API_KEY=your_openai_api_key
|
||||
VITE_GEMINI_API_KEY=your_gemini_api_key
|
||||
VITE_DEEPSEEK_API_KEY=your_deepseek_api_key
|
||||
VITE_ZHIPU_API_KEY=your_zhipu_api_key
|
||||
VITE_SILICONFLOW_API_KEY=your_siliconflow_api_key
|
||||
|
||||
# Basic Authentication (Password Protection)
|
||||
ACCESS_USERNAME=your_username # Optional, defaults to "admin"
|
||||
ACCESS_PASSWORD=your_password # Set access password
|
||||
EOF
|
||||
|
||||
# 3. Start the service
|
||||
docker compose up -d
|
||||
|
||||
# 4. View logs
|
||||
docker compose logs -f
|
||||
|
||||
# 5. Access the service
|
||||
Web Interface: http://localhost:8081
|
||||
MCP Server: http://localhost:8081/mcp
|
||||
```
|
||||
</details>
|
||||
|
||||
You can also directly edit the docker-compose.yml file to customize your configuration:
|
||||
<details>
|
||||
<summary>Click to view docker-compose.yml example</summary>
|
||||
|
||||
```yaml
|
||||
services:
|
||||
prompt-optimizer:
|
||||
# Use Docker Hub image
|
||||
image: linshen/prompt-optimizer:latest
|
||||
container_name: prompt-optimizer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8081:80" # Web application port (MCP server accessible via /mcp path)
|
||||
environment:
|
||||
- VITE_OPENAI_API_KEY=your_openai_key
|
||||
- VITE_GEMINI_API_KEY=your_gemini_key
|
||||
# Access Control (Optional)
|
||||
- ACCESS_USERNAME=admin
|
||||
- ACCESS_PASSWORD=your_password
|
||||
```
|
||||
</details>
|
||||
|
||||
### 7. MCP Server Usage Instructions
|
||||
<details>
|
||||
<summary>Click to view MCP Server usage instructions</summary>
|
||||
|
||||
Prompt Optimizer now supports the Model Context Protocol (MCP), enabling integration with AI applications that support MCP such as Claude Desktop.
|
||||
|
||||
When running via Docker, the MCP Server automatically starts and can be accessed via `http://ip:port/mcp`.
|
||||
|
||||
#### Environment Variable Configuration
|
||||
|
||||
MCP Server requires API key configuration to function properly. Main MCP-specific configurations:
|
||||
|
||||
```bash
|
||||
# MCP Server Configuration
|
||||
MCP_DEFAULT_MODEL_PROVIDER=openai # Options: openai, gemini, anthropic, deepseek, siliconflow, zhipu, dashscope, openrouter, modelscope, custom
|
||||
MCP_LOG_LEVEL=info # Log level
|
||||
```
|
||||
|
||||
#### Using MCP in Docker Environment
|
||||
|
||||
In a Docker environment, the MCP Server runs alongside the web application. You can access the MCP service through the same port as the web application at the `/mcp` path.
|
||||
|
||||
For example, if you map the container's port 80 to port 8081 on the host:
|
||||
```bash
|
||||
docker run -d -p 8081:80 \
|
||||
-e VITE_OPENAI_API_KEY=your-openai-key \
|
||||
-e MCP_DEFAULT_MODEL_PROVIDER=openai \
|
||||
--name prompt-optimizer \
|
||||
linshen/prompt-optimizer
|
||||
```
|
||||
|
||||
The MCP Server will then be accessible at `http://localhost:8081/mcp`.
|
||||
|
||||
#### Claude Desktop Integration Example
|
||||
|
||||
To use Prompt Optimizer in Claude Desktop, you need to add the service configuration to Claude Desktop's configuration file.
|
||||
|
||||
1. Find Claude Desktop's configuration directory:
|
||||
- Windows: `%APPDATA%\Claude\services`
|
||||
- macOS: `~/Library/Application Support/Claude/services`
|
||||
- Linux: `~/.config/Claude/services`
|
||||
|
||||
2. Edit or create the `services.json` file, adding the following content:
|
||||
|
||||
```json
|
||||
{
|
||||
"services": [
|
||||
{
|
||||
"name": "Prompt Optimizer",
|
||||
"url": "http://localhost:8081/mcp"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Make sure to replace `localhost:8081` with the actual address and port where you've deployed Prompt Optimizer.
|
||||
|
||||
#### Available Tools
|
||||
|
||||
- **optimize-user-prompt**: Optimize user prompts to improve LLM performance
|
||||
- **optimize-system-prompt**: Optimize system prompts to improve LLM performance
|
||||
- **iterate-prompt**: Iteratively improve mature prompts based on specific requirements
|
||||
|
||||
For more detailed information, please refer to the [MCP Server User Guide](docs/user/mcp-server_en.md).
|
||||
</details>
|
||||
|
||||
## ⚙️ API Key Configuration
|
||||
|
||||
<details>
|
||||
<summary>Click to view API key configuration methods</summary>
|
||||
|
||||
### Method 1: Via Interface (Recommended)
|
||||
1. Click the "⚙️Settings" button in the upper right corner
|
||||
2. Select the "Model Management" tab
|
||||
3. Click on the model you need to configure (such as OpenAI, Gemini, DeepSeek, etc.)
|
||||
4. Enter the corresponding API key in the configuration box
|
||||
5. Click "Save"
|
||||
|
||||
Supported models: OpenAI, Gemini, DeepSeek, Zhipu AI, SiliconFlow, Custom API (OpenAI compatible interface)
|
||||
|
||||
In addition to API keys, you can configure advanced LLM parameters for each model individually. These parameters are configured through a field called `llmParams`, which allows you to specify any parameters supported by the LLM SDK in key-value pairs for fine-grained control over model behavior.
|
||||
|
||||
**Advanced LLM Parameter Configuration Examples:**
|
||||
- **OpenAI/Compatible APIs**: `{"temperature": 0.7, "max_tokens": 4096, "timeout": 60000}`
|
||||
- **Gemini**: `{"temperature": 0.8, "maxOutputTokens": 2048, "topP": 0.95}`
|
||||
- **DeepSeek**: `{"temperature": 0.5, "top_p": 0.9, "frequency_penalty": 0.1}`
|
||||
|
||||
For more detailed information about `llmParams` configuration, please refer to the [LLM Parameters Configuration Guide](docs/developer/llm-params-guide.md).
|
||||
|
||||
### Method 2: Via Environment Variables
|
||||
Configure environment variables through the `-e` parameter when deploying with Docker:
|
||||
|
||||
```bash
|
||||
-e VITE_OPENAI_API_KEY=your_key
|
||||
-e VITE_GEMINI_API_KEY=your_key
|
||||
-e VITE_DEEPSEEK_API_KEY=your_key
|
||||
-e VITE_ZHIPU_API_KEY=your_key
|
||||
-e VITE_SILICONFLOW_API_KEY=your_key
|
||||
|
||||
# Multiple Custom Models Configuration (Unlimited Quantity)
|
||||
-e VITE_CUSTOM_API_KEY_ollama=dummy_key
|
||||
-e VITE_CUSTOM_API_BASE_URL_ollama=http://localhost:11434/v1
|
||||
-e VITE_CUSTOM_API_MODEL_ollama=qwen2.5:7b
|
||||
```
|
||||
|
||||
> 📖 **Detailed Configuration Guide**: See [Multiple Custom Models Documentation](./docs/user/multi-custom-models_en.md) for complete configuration methods and advanced usage
|
||||
|
||||
</details>
|
||||
|
||||
## Local Development
|
||||
For detailed documentation, see [Development Documentation](dev.md)
|
||||
|
||||
<details>
|
||||
<summary>Click to view local development commands</summary>
|
||||
|
||||
```bash
|
||||
# 1. Clone the project
|
||||
git clone https://github.com/linshenkx/prompt-optimizer.git
|
||||
cd prompt-optimizer
|
||||
|
||||
# 2. Install dependencies
|
||||
pnpm install
|
||||
|
||||
# 3. Start development server
|
||||
pnpm dev # Main development command: build core/ui and run web app
|
||||
pnpm dev:web # Run web app only
|
||||
pnpm dev:fresh # Complete reset and restart development environment
|
||||
```
|
||||
</details>
|
||||
|
||||
## 🗺️ Roadmap
|
||||
|
||||
- [x] Basic feature development
|
||||
- [x] Web application release
|
||||
- [x] Chrome extension release
|
||||
- [x] Internationalization support
|
||||
- [x] Support for system prompt optimization and user prompt optimization
|
||||
- [x] Desktop application release
|
||||
- [x] MCP service release
|
||||
- [x] Advanced mode: Variable management, context testing, function calling
|
||||
- [x] Image generation: Text-to-Image (T2I) and Image-to-Image (I2I) support
|
||||
- [x] Prompt favorites and template management
|
||||
- [ ] Support for workspace/project management
|
||||
|
||||
For detailed project status, see [Project Status Document](docs/project/project-status.md)
|
||||
|
||||
## 📖 Related Documentation
|
||||
|
||||
- [Documentation Index](docs/README.md) - Index of all documentation
|
||||
- [Technical Development Guide](docs/developer/technical-development-guide.md) - Technology stack and development specifications
|
||||
- [LLM Parameters Configuration Guide](docs/developer/llm-params-guide.md) - Detailed guide for advanced LLM parameter configuration
|
||||
- [Project Structure](docs/developer/project-structure.md) - Detailed project structure description
|
||||
- [Project Status](docs/project/project-status.md) - Current progress and plans
|
||||
- [Product Requirements](docs/project/prd.md) - Product requirements document
|
||||
- [Vercel Deployment Guide](docs/user/deployment/vercel_en.md) - Detailed instructions for Vercel deployment
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://star-history.com/#linshenkx/prompt-optimizer&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=linshenkx/prompt-optimizer&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=linshenkx/prompt-optimizer&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=linshenkx/prompt-optimizer&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## FAQ
|
||||
|
||||
<details>
|
||||
<summary>Click to view frequently asked questions</summary>
|
||||
|
||||
### API Connection Issues
|
||||
|
||||
#### Q1: Why can't I connect to the model service after configuring the API key?
|
||||
**A**: Most connection failures are caused by **Cross-Origin Resource Sharing (CORS)** issues. As this project is a pure frontend application, browsers block direct access to API services from different origins for security reasons. Model services will reject direct requests from browsers if CORS policies are not correctly configured.
|
||||
|
||||
#### Q2: How to solve Ollama connection issues?
|
||||
**A**: Ollama fully supports the OpenAI standard interface, just configure the correct CORS policy:
|
||||
1. Set environment variable `OLLAMA_ORIGINS=*` to allow requests from any origin
|
||||
2. If issues persist, set `OLLAMA_HOST=0.0.0.0:11434` to listen on any IP address
|
||||
|
||||
#### Q3: How to solve CORS issues with commercial APIs (such as Nvidia's DS API, ByteDance's Volcano API)?
|
||||
**A**: These platforms typically have strict CORS restrictions. Recommended solutions:
|
||||
|
||||
1. **Use Desktop Application** (Most Recommended)
|
||||
- Desktop app has no CORS restrictions as a native application
|
||||
- Can directly connect to any API service, including locally deployed models
|
||||
- Provides the most complete and stable feature experience
|
||||
- Download from [GitHub Releases](https://github.com/linshenkx/prompt-optimizer/releases)
|
||||
|
||||
2. **Use Self-deployed API Proxy Service** (Professional solution)
|
||||
- Deploy open-source API aggregation/proxy tools like OneAPI, NewAPI
|
||||
- Configure as custom API endpoint in settings
|
||||
- Request flow: Browser → Proxy service → Model service provider
|
||||
- Full control over security policies and access permissions
|
||||
|
||||
**Note**: All web versions (including online version, Vercel deployment, Docker deployment) are pure frontend applications and subject to browser CORS restrictions. Only the desktop version or using an API proxy service can solve CORS issues.
|
||||
|
||||
#### Q4: I have correctly configured CORS policies for my local model (like Ollama), why can't I still connect using the online version?
|
||||
**A**: This is caused by the browser's **Mixed Content security policy**. For security reasons, browsers block secure HTTPS pages (like the online version) from sending requests to insecure HTTP addresses (like your local Ollama service).
|
||||
|
||||
**Solutions**:
|
||||
To bypass this limitation, you need to have the application and API under the same protocol (e.g., both HTTP). We recommend the following approaches:
|
||||
1. **Use the desktop version**: Desktop applications have no browser restrictions and are the most stable and reliable way to connect to local models
|
||||
2. **Use Docker deployment (HTTP)**: Access via `http://localhost:8081`, both the app and local Ollama use HTTP
|
||||
3. **Use Chrome extension**: Extensions can bypass some security restrictions in certain situations
|
||||
|
||||
### macOS Desktop Application Issues
|
||||
|
||||
#### Q5: macOS shows "damaged" or "unverified developer" when opening the app?
|
||||
**A**: This is because the application has not been signed with an Apple Developer certificate. Due to the high cost of Apple Developer accounts, the desktop application is currently unsigned.
|
||||
|
||||
**Solution**:
|
||||
Run the following command in Terminal to remove the quarantine attribute:
|
||||
|
||||
```bash
|
||||
# For installed applications
|
||||
xattr -rd com.apple.quarantine /Applications/PromptOptimizer.app
|
||||
|
||||
# For downloaded .dmg files (run before installation)
|
||||
xattr -rd com.apple.quarantine ~/Downloads/PromptOptimizer-*.dmg
|
||||
```
|
||||
|
||||
After running the command, you can open the application normally.
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
<details>
|
||||
<summary>Click to view contribution guidelines</summary>
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/AmazingFeature`)
|
||||
3. Commit your changes (`git commit -m 'Add some feature'`)
|
||||
4. Push to the branch (`git push origin feature/AmazingFeature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
Tip: When developing with Cursor tool, it is recommended to do the following before committing:
|
||||
1. Use the "CodeReview" rule for review
|
||||
2. Check according to the review report format:
|
||||
- Overall consistency of changes
|
||||
- Code quality and implementation method
|
||||
- Test coverage
|
||||
- Documentation completeness
|
||||
3. Optimize based on review results before submitting
|
||||
|
||||
</details>
|
||||
|
||||
## 👏 Contributors
|
||||
|
||||
Thanks to all the developers who have contributed to this project!
|
||||
|
||||
<a href="https://github.com/linshenkx/prompt-optimizer/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=linshenkx/prompt-optimizer" alt="Contributors" />
|
||||
</a>
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
This project was partly inspired by [LangGPT](https://github.com/langgptai/LangGPT) in prompt engineering and structured prompt design. Thanks to the LangGPT project and community for their open-source sharing and continued exploration.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under [AGPL-3.0](LICENSE).
|
||||
|
||||
**In simple terms**: You can freely use, modify, and commercialize this project, but if you turn it into a website or service for others, you must share your source code.
|
||||
|
||||
<details>
|
||||
<summary>👉 Click for detailed explanation</summary>
|
||||
|
||||
**What you can do:**
|
||||
- ✅ Personal use, learning, and research
|
||||
- ✅ Internal company use (not offering public services)
|
||||
- ✅ Modify code for commercial projects
|
||||
- ✅ Charge for products or services
|
||||
|
||||
**What you must do:**
|
||||
- 📖 If distributing software or offering network services, disclose source code
|
||||
- 📝 Preserve original author's copyright notices
|
||||
|
||||
**Core principle**: Commercial use is allowed, but not closed-source.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
If this project is helpful to you, please consider giving it a Star ⭐️
|
||||
|
||||
## 👥 Contact Us
|
||||
|
||||
- Submit an Issue
|
||||
- Create a Pull Request
|
||||
- Join the discussion group
|
||||
@@ -0,0 +1,43 @@
|
||||
# 国际化约定
|
||||
|
||||
本仓库采用英文优先的国际化治理策略,目标是在不影响团队中文协作的前提下,让产品运行时文案、异常提示和默认入口保持英文主导并可持续维护。
|
||||
|
||||
## 基本规则
|
||||
|
||||
- 英文是 source locale,所有新增翻译键以 `en-US` 为基准。
|
||||
- 根 `README.md` 保持英文默认入口,中文说明放在 `README.zh-CN.md`。
|
||||
- 开发文档可以继续使用中文。
|
||||
- 注释允许保留中文,但新增或修改时优先写英文。
|
||||
|
||||
## 运行时代码约束
|
||||
|
||||
- 普通运行时代码中不允许新增中文用户可见文案。
|
||||
- UI 文案必须优先走 i18n key。
|
||||
- Core 层优先抛出结构化错误(`code + params`),不要把最终展示文案硬编码在业务逻辑里。
|
||||
- 非 UI 的内部错误和日志如果不做国际化,一律使用英文。
|
||||
|
||||
## 允许保留中文的目录
|
||||
|
||||
以下目录允许保留中文内容资产:
|
||||
|
||||
- `docs/**`
|
||||
- `mkdocs/**`
|
||||
- `packages/ui/src/i18n/**`
|
||||
- `packages/core/src/services/template/default-templates/**`
|
||||
- `packages/ui/src/docs/**`
|
||||
- `packages/ui/src/examples/**`
|
||||
- 测试目录及其夹具
|
||||
|
||||
如需新增允许目录,请同步更新中文扫描脚本中的 allowlist。
|
||||
|
||||
## Locale 结构要求
|
||||
|
||||
- locale 文件按领域拆分维护,不再使用单一超大文件直接编辑。
|
||||
- 所有语言包必须与 `en-US` 保持完全一致的 key 结构。
|
||||
- locale parity 校验和非法中文扫描属于 CI 阻断项。
|
||||
|
||||
## 默认语言策略
|
||||
|
||||
- 无用户偏好设置时,应用默认语言为 `en-US`。
|
||||
- 已保存偏好设置的用户保持原有行为。
|
||||
- 非法或未知 locale 一律回退到 `en-US`。
|
||||
+6
-3
@@ -5,8 +5,8 @@
|
||||
"packageManager": "pnpm@10.6.1",
|
||||
"engines": {
|
||||
"node": "^22.0.0",
|
||||
"npm": "请使用pnpm代替npm",
|
||||
"yarn": "请使用pnpm代替yarn"
|
||||
"npm": "Please use pnpm instead of npm",
|
||||
"yarn": "Please use pnpm instead of yarn"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node scripts/run-many.js build:core build:ui build:parallel",
|
||||
@@ -28,6 +28,9 @@
|
||||
"dev:desktop:parallel": "concurrently -k -p \"[{name}]\" -n \"WEB,DESKTOP\" \"pnpm -F @prompt-optimizer/web dev\" \"pnpm -F @prompt-optimizer/desktop dev\"",
|
||||
"test": "pnpm test:unit",
|
||||
"test:unit": "pnpm -r test --run --passWithNoTests",
|
||||
"test:repo": "node --test scripts/run-many.test.mjs scripts/check-locale-parity.test.mjs scripts/check-no-chinese-runtime.test.mjs && pnpm check:locale && pnpm check:no-chinese-runtime",
|
||||
"check:locale": "node scripts/check-locale-parity.mjs",
|
||||
"check:no-chinese-runtime": "node scripts/check-no-chinese-runtime.mjs",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:smart": "node scripts/smart-e2e.js",
|
||||
"test:e2e:gate": "node scripts/run-e2e-group.js gate",
|
||||
@@ -38,7 +41,7 @@
|
||||
"test:gate:core": "pnpm -F @prompt-optimizer/core test:gate",
|
||||
"test:gate:ui": "pnpm -F @prompt-optimizer/core build && pnpm -F @prompt-optimizer/ui test",
|
||||
"test:gate:e2e": "pnpm test:e2e:gate",
|
||||
"test:gate": "node scripts/run-many.js -s test:gate:core test:gate:ui",
|
||||
"test:gate": "node scripts/run-many.js -s test:repo test:gate:core test:gate:ui",
|
||||
"test:gate:full": "node scripts/run-many.js -s test:gate test:gate:e2e",
|
||||
"test:fast": "pnpm -r test --run --passWithNoTests",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
|
||||
@@ -47,7 +47,7 @@ export abstract class AbstractAdapterRegistry<
|
||||
* 子类可选实现:获取错误消息的提供商类型描述
|
||||
*/
|
||||
protected getProviderTypeDescription(): string {
|
||||
return '提供商';
|
||||
return 'provider';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,7 +196,7 @@ export abstract class AbstractAdapterRegistry<
|
||||
return mergedModels;
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`动态模型加载失败 (${providerId}),回退到静态模型:`,
|
||||
`Failed to load dynamic models (${providerId}); falling back to static models:`,
|
||||
error
|
||||
);
|
||||
// 降级到静态模型
|
||||
|
||||
@@ -27,7 +27,7 @@ export type PredefinedVariable = typeof PREDEFINED_VARIABLES[number];
|
||||
// 默认上下文配置
|
||||
export const DEFAULT_CONTEXT_CONFIG = {
|
||||
id: 'default',
|
||||
title: '默认上下文',
|
||||
title: 'Default Context',
|
||||
version: '1.0.0'
|
||||
} as const;
|
||||
|
||||
@@ -37,7 +37,7 @@ export const CONTEXT_STORE_VERSION = '1.0.0' as const;
|
||||
// UI文本常量
|
||||
export const CONTEXT_UI_LABELS = {
|
||||
/** 默认上下文标题模板 */
|
||||
DEFAULT_TITLE_TEMPLATE: '上下文', // 将与日期组合使用
|
||||
DEFAULT_TITLE_TEMPLATE: 'Context', // 将与日期组合使用
|
||||
/** 副本后缀 */
|
||||
DUPLICATE_SUFFIX: '(副本)'
|
||||
DUPLICATE_SUFFIX: ' (Copy)'
|
||||
} as const;
|
||||
@@ -71,7 +71,7 @@ export class DataManager implements IDataManager {
|
||||
data['userSettings'] = await this.preferenceService.exportData();
|
||||
data['contexts'] = await this.contextRepo.exportData();
|
||||
} catch (error) {
|
||||
console.error('导出数据失败:', error);
|
||||
console.error('Failed to export data:', error);
|
||||
if (typeof (error as any)?.code === 'string') {
|
||||
throw toErrorWithCode(error)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
this.initialized = true;
|
||||
this.initState = 'initialized';
|
||||
} catch (error) {
|
||||
console.error('[FavoriteManager] 初始化失败:', error);
|
||||
console.error('[FavoriteManager] Initialization failed:', error);
|
||||
// 即使初始化失败,也标记为已初始化,避免阻塞后续操作
|
||||
this.initialized = true;
|
||||
this.initState = 'initialized';
|
||||
@@ -130,7 +130,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
if (migrated) {
|
||||
// 迁移后更新统计信息
|
||||
await this.updateStats();
|
||||
console.info('[FavoriteManager] 数据迁移完成,已更新收藏项格式');
|
||||
console.info('[FavoriteManager] Legacy data migration completed; favorite entries updated');
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
@@ -184,7 +184,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
await this.storageProvider.setItem('favorite_categories_initialized', 'true');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[FavoriteManager] 确保默认分类失败:', error);
|
||||
console.warn('[FavoriteManager] Failed to ensure default categories:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +444,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
await this.updateFavorite(id, { useCount: (await this.getFavorite(id)).useCount + 1 });
|
||||
} catch (error) {
|
||||
// 静默处理使用次数增加失败,不影响主要功能
|
||||
console.warn('增加使用次数失败:', error);
|
||||
console.warn('Failed to increment use count:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,7 +614,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
try {
|
||||
await this.storageProvider.setItem(this.STORAGE_KEYS.STATS, JSON.stringify(stats));
|
||||
} catch (error) {
|
||||
console.warn('缓存统计数据失败:', error);
|
||||
console.warn('Failed to cache statistics:', error);
|
||||
}
|
||||
|
||||
return stats;
|
||||
@@ -643,7 +643,7 @@ export class FavoriteManager implements IFavoriteManager {
|
||||
const independentTags: FavoriteTag[] = storedTags ? JSON.parse(storedTags) : [];
|
||||
return independentTags.map(t => t.tag);
|
||||
} catch (error) {
|
||||
console.warn('获取独立标签失败:', error);
|
||||
console.warn('Failed to get standalone tags:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ export class ImageModelManager implements IImageModelManager {
|
||||
provider: {
|
||||
id: config.providerId || 'unknown',
|
||||
name: `Unknown Provider (${config.providerId || 'unknown'})`,
|
||||
description: '此配置损坏,无法修复',
|
||||
description: 'This configuration is corrupted and cannot be repaired.',
|
||||
requiresApiKey: false,
|
||||
supportsDynamicModels: false,
|
||||
defaultBaseURL: '',
|
||||
@@ -452,7 +452,7 @@ export class ImageModelManager implements IImageModelManager {
|
||||
model: {
|
||||
id: config.modelId || 'unknown',
|
||||
name: `Unknown Model (${config.modelId || 'unknown'})`,
|
||||
description: '此配置损坏,请删除后重新创建',
|
||||
description: 'This configuration is corrupted. Please delete it and create a new one.',
|
||||
providerId: config.providerId || 'unknown',
|
||||
capabilities: {
|
||||
text2image: false,
|
||||
|
||||
@@ -95,7 +95,7 @@ export class ImageAdapterRegistry
|
||||
* 获取错误消息的提供商类型描述
|
||||
*/
|
||||
protected getProviderTypeDescription(): string {
|
||||
return '图像提供商'
|
||||
return 'image provider'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ export class TextAdapterRegistry
|
||||
* 获取错误消息的提供商类型描述
|
||||
*/
|
||||
protected getProviderTypeDescription(): string {
|
||||
return '文本模型提供商';
|
||||
return 'text model provider';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -845,7 +845,7 @@ export class PromptService implements IPromptService {
|
||||
return JSON.stringify(
|
||||
request.inputImages.map((image, index) => ({
|
||||
index: index + 1,
|
||||
label: `图${index + 1}`,
|
||||
label: `Image ${index + 1}`,
|
||||
mimeType: image.mimeType || "image/png",
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -97,7 +97,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
const record = await this.db.storage.get(key);
|
||||
return record?.value ?? null;
|
||||
} catch (error) {
|
||||
console.error(`获取存储项失败 (${key}):`, error);
|
||||
console.error(`Failed to get storage item (${key}):`, error);
|
||||
throw new StorageError(`Failed to get item: ${key}`, 'read');
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`设置存储项失败 (${key}):`, error);
|
||||
console.error(`Failed to set storage item (${key}):`, error);
|
||||
throw new StorageError(`Failed to set item: ${key}`, 'write');
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
try {
|
||||
await this.db.storage.delete(key);
|
||||
} catch (error) {
|
||||
console.error(`删除存储项失败 (${key}):`, error);
|
||||
console.error(`Failed to remove storage item (${key}):`, error);
|
||||
throw new StorageError(`Failed to remove item: ${key}`, 'delete');
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
try {
|
||||
await this.db.storage.clear();
|
||||
} catch (error) {
|
||||
console.error('清空存储失败:', error);
|
||||
console.error('Failed to clear storage:', error);
|
||||
throw new StorageError('Failed to clear storage', 'clear');
|
||||
}
|
||||
}
|
||||
@@ -209,7 +209,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
return; // 成功,直接返回
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
console.warn(`原子更新尝试 ${attempt}/${maxRetries} 失败 (${key}):`, error);
|
||||
console.warn(`Atomic update attempt ${attempt}/${maxRetries} failed (${key}):`, error);
|
||||
|
||||
// 如果是事务错误且还有重试机会,等待一段时间后重试
|
||||
if (this.isError(error) && error.name === 'PrematureCommitError' && attempt < maxRetries) {
|
||||
@@ -220,13 +220,13 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
|
||||
// 如果是最后一次尝试或非事务错误,尝试降级到简单更新
|
||||
if (attempt === maxRetries) {
|
||||
console.warn(`所有重试失败,尝试降级到简单更新 (${key})`);
|
||||
console.warn(`All retries failed; falling back to simple update (${key})`);
|
||||
try {
|
||||
await this._performSimpleUpdate(key, updateFn);
|
||||
console.log(`降级更新成功 (${key})`);
|
||||
console.log(`Fallback update succeeded (${key})`);
|
||||
return;
|
||||
} catch (fallbackError) {
|
||||
console.error(`降级更新也失败 (${key}):`, fallbackError);
|
||||
console.error(`Fallback update also failed (${key}):`, fallbackError);
|
||||
throw lastError; // 抛出原始错误
|
||||
}
|
||||
}
|
||||
@@ -263,7 +263,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
timestamp: Date.now()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`简单更新失败 (${key}):`, error);
|
||||
console.error(`Simple update failed (${key}):`, error);
|
||||
throw new StorageError(`Failed to perform simple update: ${key}`, 'write');
|
||||
}
|
||||
}
|
||||
@@ -296,12 +296,12 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
});
|
||||
} catch (innerError) {
|
||||
// 事务内部错误,让事务回滚
|
||||
console.error(`事务内部操作失败 (${key}):`, innerError);
|
||||
console.error(`Transaction operation failed (${key}):`, innerError);
|
||||
throw innerError;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`原子更新失败 (${key}):`, error);
|
||||
console.error(`Atomic update failed (${key}):`, error);
|
||||
|
||||
// 如果是Dexie事务错误,提供更详细的错误信息
|
||||
if (this.isError(error) && error.name === 'PrematureCommitError') {
|
||||
@@ -353,7 +353,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量更新失败:', error);
|
||||
console.error('Batch update failed:', error);
|
||||
throw new StorageError('Failed to perform batch update', 'write');
|
||||
}
|
||||
}
|
||||
@@ -387,7 +387,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
lastUpdated: lastRecord?.timestamp ?? null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取存储信息失败:', error);
|
||||
console.error('Failed to get storage information:', error);
|
||||
return {
|
||||
itemCount: 0,
|
||||
estimatedSize: 0,
|
||||
@@ -412,7 +412,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('导出数据失败:', error);
|
||||
console.error('Failed to export data:', error);
|
||||
throw new StorageError('Failed to export data', 'read');
|
||||
}
|
||||
}
|
||||
@@ -432,7 +432,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
|
||||
await this.db.storage.bulkPut(records);
|
||||
} catch (error) {
|
||||
console.error('导入数据失败:', error);
|
||||
console.error('Failed to import data:', error);
|
||||
throw new StorageError('Failed to import data', 'write');
|
||||
}
|
||||
}
|
||||
@@ -444,7 +444,7 @@ export class DexieStorageProvider implements IStorageProvider {
|
||||
try {
|
||||
await this.db.close();
|
||||
} catch (error) {
|
||||
console.error('关闭数据库失败:', error);
|
||||
console.error('Failed to close database:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -458,8 +458,9 @@ export class FileStorageProvider implements IStorageProvider {
|
||||
if (error instanceof Error &&
|
||||
(error.name.includes('Error') ||
|
||||
error.constructor.name !== 'Error' ||
|
||||
error.message.includes('模型') ||
|
||||
error.message.includes('不存在'))) {
|
||||
error.message.includes('Model') ||
|
||||
error.message.includes('not found') ||
|
||||
error.message.includes('not exist'))) {
|
||||
throw error;
|
||||
}
|
||||
// 只有真正的存储错误才包装为StorageError
|
||||
|
||||
@@ -271,8 +271,8 @@ export class TemplateManager implements ITemplateManager {
|
||||
case 'zh-CN':
|
||||
return this.staticLoader.getDefaultTemplates();
|
||||
default:
|
||||
console.warn(`Unsupported language: ${language}, falling back to Chinese templates`);
|
||||
return this.staticLoader.getDefaultTemplates();
|
||||
console.warn(`Unsupported language: ${language}, falling back to English templates`);
|
||||
return this.staticLoader.getDefaultTemplatesEn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ export class TemplateManager implements ITemplateManager {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substr(2, 6);
|
||||
finalTemplateId = `user-${template.id}-${timestamp}-${random}`;
|
||||
finalTemplateName = `${template.name} (导入副本)`;
|
||||
finalTemplateName = `${template.name} (Imported copy)`;
|
||||
console.warn(`Detected conflict with built-in template ID: ${template.id}, renamed to: ${finalTemplateId}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -227,14 +227,14 @@ export class TemplateProcessor {
|
||||
return tools
|
||||
.map((tool) => {
|
||||
const func = tool.function;
|
||||
let toolText = `工具名称: ${func.name}`;
|
||||
let toolText = `Tool name: ${func.name}`;
|
||||
|
||||
if (func.description) {
|
||||
toolText += `\n描述: ${func.description}`;
|
||||
toolText += `\nDescription: ${func.description}`;
|
||||
}
|
||||
|
||||
if (func.parameters) {
|
||||
toolText += `\n参数结构: ${JSON.stringify(func.parameters, null, 2)}`;
|
||||
toolText += `\nParameters: ${JSON.stringify(func.parameters, null, 2)}`;
|
||||
}
|
||||
|
||||
return toolText;
|
||||
|
||||
@@ -68,7 +68,7 @@ export class StaticLoader {
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`🔄 静态导入开始加载模板...`);
|
||||
console.log('Starting static template loading...');
|
||||
|
||||
const all: Record<string, Template> = {};
|
||||
const byLanguage: Record<Language, Record<string, Template>> = { zh: {}, en: {} };
|
||||
@@ -95,7 +95,7 @@ export class StaticLoader {
|
||||
|
||||
// 验证内置模板必须包含language字段
|
||||
if (template.isBuiltin && !language) {
|
||||
console.error(`❌ 内置模板缺少language字段: ${id}`);
|
||||
console.error(`Built-in template is missing the language field: ${id}`);
|
||||
throw new TemplateValidationError(
|
||||
`Built-in template '${id}' is missing required 'language' field in metadata`,
|
||||
);
|
||||
@@ -157,10 +157,10 @@ export class StaticLoader {
|
||||
|
||||
const result = { all, byLanguage, byType };
|
||||
|
||||
console.log(`✅ 成功加载 ${Object.keys(all).length} 个模板`, {
|
||||
'总数': Object.keys(all).length,
|
||||
'中文': Object.keys(byLanguage.zh).length,
|
||||
'英文': Object.keys(byLanguage.en).length,
|
||||
console.log(`Loaded ${Object.keys(all).length} templates successfully`, {
|
||||
total: Object.keys(all).length,
|
||||
chinese: Object.keys(byLanguage.zh).length,
|
||||
english: Object.keys(byLanguage.en).length,
|
||||
optimize: Object.keys(byType.optimize.zh).length + Object.keys(byType.optimize.en).length,
|
||||
'user-optimize': Object.keys(byType['user-optimize'].zh).length + Object.keys(byType['user-optimize'].en).length,
|
||||
text2imageOptimize: Object.keys(byType.text2imageOptimize.zh).length + Object.keys(byType.text2imageOptimize.en).length,
|
||||
@@ -180,7 +180,7 @@ export class StaticLoader {
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 静态导入加载模板失败:', error);
|
||||
console.error('Failed to load templates via static import:', error);
|
||||
throw new TemplateLoadError(
|
||||
'static-loader',
|
||||
`Failed to load static templates: ${error instanceof Error ? error.message : String(error)}`,
|
||||
|
||||
@@ -146,7 +146,7 @@ export class VariableExtractionService implements IVariableExtractionService {
|
||||
private buildTemplateContext(request: VariableExtractionRequest): TemplateContext {
|
||||
const context: TemplateContext = {
|
||||
promptContent: request.promptContent,
|
||||
existingVariableNames: request.existingVariableNames?.join(', ') || '无',
|
||||
existingVariableNames: request.existingVariableNames?.join(', ') || 'None',
|
||||
hasExistingVariables: !!request.existingVariableNames?.length,
|
||||
};
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ export class VariableValueGenerationService implements IVariableValueGenerationS
|
||||
const variablesText = request.variables
|
||||
.map((v, idx) => {
|
||||
const parts = [`${idx + 1}. ${v.name}`];
|
||||
if (v.currentValue) parts.push(`(当前值: ${v.currentValue})`);
|
||||
if (v.currentValue) parts.push(`(current value: ${v.currentValue})`);
|
||||
if (v.source) parts.push(`[${v.source}]`);
|
||||
return parts.join(' ');
|
||||
})
|
||||
@@ -240,11 +240,11 @@ export class VariableValueGenerationService implements IVariableValueGenerationS
|
||||
if (requestedNames.has(val.name)) {
|
||||
// 🔧 检测LLM返回的同名重复
|
||||
if (valueMap.has(val.name)) {
|
||||
console.warn(`[VariableValueGeneration] LLM返回了重复的变量名: ${val.name},后者将覆盖前者`);
|
||||
console.warn(`[VariableValueGeneration] LLM returned a duplicate variable name: ${val.name}. The later value will overwrite the earlier one.`);
|
||||
}
|
||||
valueMap.set(val.name, val);
|
||||
} else {
|
||||
console.warn(`[VariableValueGeneration] LLM返回了未请求的变量: ${val.name}`);
|
||||
console.warn(`[VariableValueGeneration] LLM returned a variable that was not requested: ${val.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,7 +253,7 @@ export class VariableValueGenerationService implements IVariableValueGenerationS
|
||||
for (const req of requestedVariables) {
|
||||
const trimmedName = req.name.trim();
|
||||
if (seenRequestNames.has(trimmedName)) {
|
||||
console.warn(`[VariableValueGeneration] 请求列表中存在重复的变量名: ${trimmedName},将返回相同的生成结果`);
|
||||
console.warn(`[VariableValueGeneration] The request list contains a duplicate variable name: ${trimmedName}. The generated result will be reused.`);
|
||||
}
|
||||
seenRequestNames.add(trimmedName);
|
||||
}
|
||||
@@ -267,11 +267,11 @@ export class VariableValueGenerationService implements IVariableValueGenerationS
|
||||
return generated;
|
||||
}
|
||||
// 缺失的变量用空值补齐
|
||||
console.warn(`[VariableValueGeneration] LLM未返回变量 "${trimmedName}",已补齐空值`);
|
||||
console.warn(`[VariableValueGeneration] LLM did not return variable "${trimmedName}". Filling it with an empty value.`);
|
||||
return {
|
||||
name: trimmedName,
|
||||
value: '',
|
||||
reason: '(LLM未生成此变量的值)',
|
||||
reason: 'LLM did not generate a value for this variable.',
|
||||
confidence: 0,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const readCoreSource = (relativePath: string) =>
|
||||
readFileSync(resolve(process.cwd(), relativePath), 'utf8')
|
||||
|
||||
describe('core runtime english guards', () => {
|
||||
it('keeps prompt and template services free of hardcoded chinese runtime defaults', () => {
|
||||
const promptServiceSource = readCoreSource('src/services/prompt/service.ts')
|
||||
const templateLanguageSource = readCoreSource('src/services/template/languageService.ts')
|
||||
const templateLanguageProxySource = readCoreSource('src/services/template/electron-language-proxy.ts')
|
||||
const templateManagerSource = readCoreSource('src/services/template/manager.ts')
|
||||
const templateProcessorSource = readCoreSource('src/services/template/processor.ts')
|
||||
const variableExtractionSource = readCoreSource('src/services/variable-extraction/service.ts')
|
||||
|
||||
expect(promptServiceSource).toMatch(/label:\s*`Image \$\{index \+ 1\}`/)
|
||||
expect(promptServiceSource).not.toMatch(/label:\s*`图\$\{index \+ 1\}`/)
|
||||
|
||||
expect(templateLanguageSource).toMatch(/return '中文'/)
|
||||
expect(templateLanguageProxySource).toMatch(/return '中文'/)
|
||||
|
||||
expect(templateManagerSource).toContain('(Imported copy)')
|
||||
expect(templateManagerSource).not.toMatch(/\(导入副本\)/)
|
||||
|
||||
expect(templateProcessorSource).toMatch(/Tool name:/)
|
||||
expect(templateProcessorSource).toMatch(/Description:/)
|
||||
expect(templateProcessorSource).toMatch(/Parameters:/)
|
||||
expect(templateProcessorSource).not.toMatch(/工具名称:/)
|
||||
|
||||
expect(variableExtractionSource).toMatch(/\|\| 'None'/)
|
||||
expect(variableExtractionSource).not.toMatch(/\|\| '无'/)
|
||||
})
|
||||
})
|
||||
@@ -62,6 +62,18 @@ describe('TemplateManager with Mocked LanguageService', () => {
|
||||
expect(zhTemplate?.content).toBe('你好');
|
||||
});
|
||||
|
||||
it('should fallback to English templates for unsupported languages', async () => {
|
||||
vi.spyOn(languageService, 'getCurrentLanguage').mockReturnValue('invalid-lang' as any);
|
||||
|
||||
const templates = await templateManager.listTemplates();
|
||||
const enTemplate = templates.find(t => t.id === 'test-en');
|
||||
const zhTemplate = templates.find(t => t.id === 'test-zh');
|
||||
|
||||
expect(enTemplate).toBeDefined();
|
||||
expect(zhTemplate).toBeUndefined();
|
||||
expect(enTemplate?.content).toBe('Hello');
|
||||
});
|
||||
|
||||
it('should get current builtin template language from the language service', async () => {
|
||||
vi.spyOn(languageService, 'getCurrentLanguage').mockResolvedValue('en-US');
|
||||
const lang = await templateManager.getCurrentBuiltinTemplateLanguage();
|
||||
@@ -135,4 +147,4 @@ describe('TemplateManager with Mocked LanguageService', () => {
|
||||
await expect(templateManager.deleteTemplate('test-en'))
|
||||
.rejects.toThrow('Cannot delete built-in template: test-en');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
|
||||
import { createVariableValueGenerationService } from '../../../src/services/variable-value-generation/service'
|
||||
|
||||
describe('VariableValueGenerationService runtime messages', () => {
|
||||
it('uses English warnings and fallback reason for alignment edge cases', async () => {
|
||||
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
const service = createVariableValueGenerationService(
|
||||
{
|
||||
sendMessage: vi.fn(async () =>
|
||||
JSON.stringify({
|
||||
values: [
|
||||
{ name: 'topic', value: 'cats', reason: 'first' },
|
||||
{ name: 'topic', value: 'dogs', reason: 'second' },
|
||||
{ name: 'extra', value: 'unused', reason: 'extra' },
|
||||
],
|
||||
summary: 'done',
|
||||
}),
|
||||
),
|
||||
} as any,
|
||||
{
|
||||
getModel: vi.fn(async () => ({ key: 'model-1' })),
|
||||
} as any,
|
||||
{
|
||||
getTemplate: vi.fn(async () => ({
|
||||
id: 'variable-value-generation',
|
||||
content: 'template',
|
||||
})),
|
||||
} as any,
|
||||
)
|
||||
|
||||
const result = await service.generate({
|
||||
promptContent: 'Write about {{topic}} in a {{tone}} style.',
|
||||
generationModelKey: 'model-1',
|
||||
variables: [
|
||||
{ name: 'topic' },
|
||||
{ name: 'topic' },
|
||||
{ name: 'tone' },
|
||||
],
|
||||
})
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'[VariableValueGeneration] LLM returned a duplicate variable name: topic. The later value will overwrite the earlier one.',
|
||||
)
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'[VariableValueGeneration] LLM returned a variable that was not requested: extra',
|
||||
)
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'[VariableValueGeneration] The request list contains a duplicate variable name: topic. The generated result will be reused.',
|
||||
)
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'[VariableValueGeneration] LLM did not return variable "tone". Filling it with an empty value.',
|
||||
)
|
||||
expect(result.values).toEqual([
|
||||
{ name: 'topic', value: 'dogs', reason: 'second', confidence: undefined },
|
||||
{ name: 'topic', value: 'dogs', reason: 'second', confidence: undefined },
|
||||
{
|
||||
name: 'tone',
|
||||
value: '',
|
||||
reason: 'LLM did not generate a value for this variable.',
|
||||
confidence: 0,
|
||||
},
|
||||
])
|
||||
|
||||
consoleWarnSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
@@ -16,6 +16,13 @@ export const MCP_ERROR_CODES = {
|
||||
INVALID_REQUEST: -32600
|
||||
} as const;
|
||||
|
||||
const INVALID_PARAM_PATTERNS = [
|
||||
'must be a non-empty string',
|
||||
'must not exceed',
|
||||
'is required',
|
||||
'invalid parameter',
|
||||
] as const;
|
||||
|
||||
export class MCPErrorHandler {
|
||||
/**
|
||||
* 转换 Core 模块错误为 MCP 错误
|
||||
@@ -25,13 +32,13 @@ export class MCPErrorHandler {
|
||||
if (error.name.includes('OptimizationError') || error.name.includes('IterationError') || error.name.includes('TestError')) {
|
||||
return new McpError(
|
||||
MCP_ERROR_CODES.PROMPT_OPTIMIZATION_FAILED,
|
||||
`提示词优化失败: ${error.message}`,
|
||||
`Prompt optimization failed: ${error.message}`,
|
||||
{ originalError: error.name }
|
||||
);
|
||||
}
|
||||
|
||||
// 参数验证错误
|
||||
if (error.message.includes('必须是') || error.message.includes('不能为空') || error.message.includes('过长')) {
|
||||
if (INVALID_PARAM_PATTERNS.some((pattern) => error.message.toLowerCase().includes(pattern))) {
|
||||
return new McpError(
|
||||
MCP_ERROR_CODES.INVALID_PARAMS,
|
||||
error.message,
|
||||
@@ -43,7 +50,7 @@ export class MCPErrorHandler {
|
||||
if (error.message.includes('Model') || error.message.includes('API key') || error.message.includes('Template')) {
|
||||
return new McpError(
|
||||
MCP_ERROR_CODES.INTERNAL_ERROR,
|
||||
`配置错误: ${error.message}`,
|
||||
`Configuration error: ${error.message}`,
|
||||
{ originalError: error.name }
|
||||
);
|
||||
}
|
||||
@@ -51,7 +58,7 @@ export class MCPErrorHandler {
|
||||
// 默认内部错误
|
||||
return new McpError(
|
||||
MCP_ERROR_CODES.INTERNAL_ERROR,
|
||||
`内部错误: ${error.message}`,
|
||||
`Internal error: ${error.message}`,
|
||||
{ originalError: error.name }
|
||||
);
|
||||
}
|
||||
@@ -60,7 +67,7 @@ export class MCPErrorHandler {
|
||||
* 创建参数验证错误
|
||||
*/
|
||||
static createValidationError(message: string): McpError {
|
||||
return new McpError(MCP_ERROR_CODES.INVALID_PARAMS, `参数验证失败: ${message}`);
|
||||
return new McpError(MCP_ERROR_CODES.INVALID_PARAMS, `Parameter validation failed: ${message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@ export class SimpleLanguageService implements ITemplateLanguageService {
|
||||
private currentLanguage: BuiltinTemplateLanguage;
|
||||
private initialized = false;
|
||||
|
||||
constructor(defaultLanguage: string = 'zh-CN') {
|
||||
constructor(defaultLanguage: string = 'en-US') {
|
||||
// 映射语言代码到 Core 模块支持的格式
|
||||
const languageMap: Record<string, BuiltinTemplateLanguage> = {
|
||||
'zh': 'zh-CN',
|
||||
@@ -21,7 +21,7 @@ export class SimpleLanguageService implements ITemplateLanguageService {
|
||||
'english': 'en-US'
|
||||
};
|
||||
|
||||
this.currentLanguage = languageMap[defaultLanguage as keyof typeof languageMap] || 'zh-CN';
|
||||
this.currentLanguage = languageMap[defaultLanguage as keyof typeof languageMap] || 'en-US';
|
||||
}
|
||||
|
||||
async initialize(): Promise<void> {
|
||||
@@ -51,7 +51,7 @@ export class SimpleLanguageService implements ITemplateLanguageService {
|
||||
}
|
||||
|
||||
async getSupportedLanguages(): Promise<BuiltinTemplateLanguage[]> {
|
||||
return ['zh-CN', 'en-US'];
|
||||
return ['en-US', 'zh-CN'];
|
||||
}
|
||||
|
||||
getLanguageDisplayName(language: BuiltinTemplateLanguage): string {
|
||||
@@ -71,5 +71,5 @@ export class SimpleLanguageService implements ITemplateLanguageService {
|
||||
}
|
||||
|
||||
export function createSimpleLanguageService(defaultLanguage?: string): SimpleLanguageService {
|
||||
return new SimpleLanguageService(defaultLanguage || 'zh-CN');
|
||||
return new SimpleLanguageService(defaultLanguage || 'en-US');
|
||||
}
|
||||
@@ -10,10 +10,10 @@ export class ParameterValidator {
|
||||
*/
|
||||
static validatePrompt(prompt: string): void {
|
||||
if (!prompt || typeof prompt !== 'string' || prompt.trim().length === 0) {
|
||||
throw new Error('提示词必须是非空字符串');
|
||||
throw new Error('Prompt must be a non-empty string');
|
||||
}
|
||||
if (prompt.length > 50000) {
|
||||
throw new Error('提示词过长(最大 50,000 字符)');
|
||||
throw new Error('Prompt must not exceed 50,000 characters');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ParameterValidator {
|
||||
*/
|
||||
static validateTemplate(template?: string): void {
|
||||
if (template !== undefined && (typeof template !== 'string' || template.trim().length === 0)) {
|
||||
throw new Error('模板必须是非空字符串');
|
||||
throw new Error('Template must be a non-empty string');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,10 @@ export class ParameterValidator {
|
||||
*/
|
||||
static validateRequirements(requirements: string): void {
|
||||
if (!requirements || typeof requirements !== 'string' || requirements.trim().length === 0) {
|
||||
throw new Error('需求描述必须是非空字符串');
|
||||
throw new Error('Requirements must be a non-empty string');
|
||||
}
|
||||
if (requirements.length > 10000) {
|
||||
throw new Error('需求描述过长(最大 10,000 字符)');
|
||||
throw new Error('Requirements must not exceed 10,000 characters');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export async function getTemplateOptions(
|
||||
const options = templates.map(template => ({
|
||||
value: template.id,
|
||||
label: template.name,
|
||||
description: template.metadata.description || (template.isBuiltin ? '内置模板' : '用户模板')
|
||||
description: template.metadata.description || (template.isBuiltin ? 'Built-in template' : 'User template')
|
||||
}));
|
||||
|
||||
return options;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/*
|
||||
* Prompt Optimizer - AI提示词优化工具
|
||||
* Prompt Optimizer - AI prompt optimization toolkit
|
||||
* Copyright (C) 2025 linshenkx
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -20,14 +20,14 @@
|
||||
/**
|
||||
* MCP Server for Prompt Optimizer
|
||||
*
|
||||
* 提供3个核心工具:
|
||||
* - optimize-user-prompt: 优化用户提示词
|
||||
* - optimize-system-prompt: 优化系统提示词
|
||||
* - iterate-prompt: 迭代优化成熟提示词
|
||||
* Provides 3 core tools:
|
||||
* - optimize-user-prompt: optimize user prompts
|
||||
* - optimize-system-prompt: optimize system prompts
|
||||
* - iterate-prompt: iterate on existing prompts
|
||||
*
|
||||
* 支持 stdio 和 HTTP 两种传输方式
|
||||
* Supports both stdio and HTTP transports
|
||||
*
|
||||
* 注意:环境变量通过 environment.ts 在应用启动时加载
|
||||
* Note: environment variables are loaded by environment.ts during startup
|
||||
*/
|
||||
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
@@ -65,7 +65,7 @@ async function createServerInstance(config: MCPServerConfig) {
|
||||
async function setupServerHandlers(server: Server, coreServices: CoreServicesManager) {
|
||||
|
||||
// 获取模板选项和默认模板ID用于工具定义
|
||||
logger.info('获取模板选项...');
|
||||
logger.info('Loading template options...');
|
||||
const templateManager = coreServices.getTemplateManager();
|
||||
const [userOptimizeOptions, systemOptimizeOptions, iterateOptions, userDefaultId, systemDefaultId, iterateDefaultId] = await Promise.all([
|
||||
getTemplateOptions(templateManager, 'userOptimize'),
|
||||
@@ -77,23 +77,23 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
]);
|
||||
|
||||
// 注册工具列表处理器
|
||||
logger.info('注册 MCP 工具...');
|
||||
logger.info('Registering MCP tools...');
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "optimize-user-prompt",
|
||||
description: "优化用户提示词,提升与AI对话的效果。适用于日常对话、问答、创作等场景。\n\n主要功能:\n- 增强表达清晰度和具体性\n- 添加必要的上下文信息\n- 优化语言表达和逻辑结构\n- 提高AI理解准确性\n\n使用场景示例:\n- 将模糊问题转化为具体明确的询问\n- 为创作任务添加详细要求和约束\n- 优化技术问题的描述方式",
|
||||
description: "Optimize a user prompt to improve clarity, specificity, and overall response quality. Best for everyday chat, Q&A, writing, and task-oriented requests.\n\nKey capabilities:\n- Make the request clearer and more specific\n- Add missing context when useful\n- Improve wording and logical structure\n- Help the model understand the goal more accurately\n\nTypical use cases:\n- Turn a vague question into a concrete request\n- Add detailed constraints to a creative task\n- Improve the framing of a technical question",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
prompt: {
|
||||
type: "string",
|
||||
description: "要优化的用户提示词。例如:'帮我写个文章' 或 '解释一下机器学习'"
|
||||
description: "The user prompt to optimize. For example: 'Help me write an article' or 'Explain machine learning'."
|
||||
},
|
||||
template: {
|
||||
type: "string",
|
||||
description: `选择优化模板,不同模板适用于不同场景:\n${userOptimizeOptions.map(opt => `- ${opt.label}:${opt.description}`).join('\n')}`,
|
||||
description: `Choose an optimization template. Different templates fit different scenarios:\n${userOptimizeOptions.map(opt => `- ${opt.label}: ${opt.description}`).join('\n')}`,
|
||||
enum: userOptimizeOptions.map(opt => opt.value),
|
||||
default: userDefaultId
|
||||
}
|
||||
@@ -103,17 +103,17 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
},
|
||||
{
|
||||
name: "optimize-system-prompt",
|
||||
description: "优化系统提示词,提升AI角色扮演和行为控制效果。适用于定制AI助手、创建专业角色、设计对话系统等场景。\n\n主要功能:\n- 增强角色定义和专业性\n- 优化行为指导和约束\n- 改进指令结构和层次\n- 添加必要的专业知识\n\n使用场景示例:\n- 将简单角色描述转化为专业角色定义\n- 为AI助手添加详细的行为规则和限制\n- 优化特定领域专家的知识框架",
|
||||
description: "Optimize a system prompt to improve role definition, instruction quality, and behavior control. Best for custom assistants, expert roles, and structured dialogue systems.\n\nKey capabilities:\n- Strengthen role definition and professionalism\n- Improve behavioral guidance and constraints\n- Refine instruction structure and hierarchy\n- Add missing domain context when needed\n\nTypical use cases:\n- Turn a simple role description into a professional system prompt\n- Add clearer operating rules for an AI assistant\n- Improve a domain-specific expert prompt",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
prompt: {
|
||||
type: "string",
|
||||
description: "要优化的系统提示词。例如:'你是一个助手' 或 '你是一个医疗顾问'"
|
||||
description: "The system prompt to optimize. For example: 'You are a helpful assistant' or 'You are a medical advisor'."
|
||||
},
|
||||
template: {
|
||||
type: "string",
|
||||
description: `选择优化模板,不同模板适用于不同场景:\n${systemOptimizeOptions.map(opt => `- ${opt.label}:${opt.description}`).join('\n')}`,
|
||||
description: `Choose an optimization template. Different templates fit different scenarios:\n${systemOptimizeOptions.map(opt => `- ${opt.label}: ${opt.description}`).join('\n')}`,
|
||||
enum: systemOptimizeOptions.map(opt => opt.value),
|
||||
default: systemDefaultId
|
||||
}
|
||||
@@ -123,21 +123,21 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
},
|
||||
{
|
||||
name: "iterate-prompt",
|
||||
description: "基于具体需求迭代改进已有的提示词。适用于已经有基础提示词,但需要针对特定需求进行精细调整的场景。\n\n主要功能:\n- 保持原有提示词的核心功能\n- 根据具体需求进行针对性改进\n- 解决现有提示词的特定问题\n- 适应新的使用场景或要求\n\n使用场景示例:\n- 现有提示词效果不够理想,需要改进\n- 需要适应新的业务需求或使用场景\n- 要解决特定的输出格式或内容问题\n- 需要增强某个特定方面的表现",
|
||||
description: "Iteratively improve an existing prompt based on concrete requirements. Best when you already have a usable prompt but need targeted refinements.\n\nKey capabilities:\n- Preserve the prompt's core intent\n- Improve it against specific requirements\n- Address known weaknesses or output issues\n- Adapt it to new scenarios or constraints\n\nTypical use cases:\n- Improve a prompt that is close but not good enough\n- Adapt a prompt to new business or product needs\n- Fix output formatting or content issues\n- Strengthen a specific aspect of performance",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
prompt: {
|
||||
type: "string",
|
||||
description: "要迭代改进的现有提示词。应该是一个已经在使用但需要改进的完整提示词"
|
||||
description: "The existing prompt to refine. This should be a complete prompt that is already in use but needs improvement."
|
||||
},
|
||||
requirements: {
|
||||
type: "string",
|
||||
description: "具体的改进需求或问题描述。例如:'输出格式不够规范' 或 '需要更专业的语言风格' 或 '希望增加创意性'"
|
||||
description: "The concrete improvement requirements or problem statement. For example: 'The output format is inconsistent', 'Use a more professional tone', or 'Increase creativity'."
|
||||
},
|
||||
template: {
|
||||
type: "string",
|
||||
description: `选择迭代优化模板,不同模板有不同的改进策略:\n${iterateOptions.map(opt => `- ${opt.label}:${opt.description}`).join('\n')}`,
|
||||
description: `Choose an iteration template. Different templates use different refinement strategies:\n${iterateOptions.map(opt => `- ${opt.label}: ${opt.description}`).join('\n')}`,
|
||||
enum: iterateOptions.map(opt => opt.value),
|
||||
default: iterateDefaultId
|
||||
}
|
||||
@@ -152,7 +152,7 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
// 注册工具调用处理器
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
logger.info(`处理工具调用请求: ${name}`);
|
||||
logger.info(`Handling tool call request: ${name}`);
|
||||
|
||||
try {
|
||||
switch (name) {
|
||||
@@ -164,7 +164,7 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "错误:缺少必需参数 'prompt'"
|
||||
text: "Error: Missing required parameter 'prompt'"
|
||||
}]
|
||||
};
|
||||
}
|
||||
@@ -187,7 +187,7 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "错误:MCP 默认模型未配置或未启用,请检查环境变量配置"
|
||||
text: "Error: The MCP default model is not configured or not enabled. Check the environment configuration."
|
||||
}]
|
||||
};
|
||||
}
|
||||
@@ -216,7 +216,7 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "错误:缺少必需参数 'prompt'"
|
||||
text: "Error: Missing required parameter 'prompt'"
|
||||
}]
|
||||
};
|
||||
}
|
||||
@@ -239,7 +239,7 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "错误:MCP 默认模型未配置或未启用,请检查环境变量配置"
|
||||
text: "Error: The MCP default model is not configured or not enabled. Check the environment configuration."
|
||||
}]
|
||||
};
|
||||
}
|
||||
@@ -272,7 +272,7 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "错误:缺少必需参数 'prompt'"
|
||||
text: "Error: Missing required parameter 'prompt'"
|
||||
}]
|
||||
};
|
||||
}
|
||||
@@ -282,7 +282,7 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "错误:缺少必需参数 'requirements'"
|
||||
text: "Error: Missing required parameter 'requirements'"
|
||||
}]
|
||||
};
|
||||
}
|
||||
@@ -306,7 +306,7 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: "错误:MCP 默认模型未配置或未启用,请检查环境变量配置"
|
||||
text: "Error: The MCP default model is not configured or not enabled. Check the environment configuration."
|
||||
}]
|
||||
};
|
||||
}
|
||||
@@ -333,23 +333,23 @@ async function setupServerHandlers(server: Server, coreServices: CoreServicesMan
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `错误:未知工具 '${name}'`
|
||||
text: `Error: Unknown tool '${name}'`
|
||||
}]
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`工具执行错误 ${name}:`, error as Error);
|
||||
logger.error(`Tool execution error ${name}:`, error as Error);
|
||||
return {
|
||||
isError: true,
|
||||
content: [{
|
||||
type: "text",
|
||||
text: `工具执行错误: ${(error as Error).message}`
|
||||
text: `Tool execution error: ${(error as Error).message}`
|
||||
}]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('MCP 工具注册成功');
|
||||
logger.info('MCP tools registered successfully');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SimpleLanguageService, createSimpleLanguageService } from '../src/adapters/language-service.js';
|
||||
|
||||
describe('SimpleLanguageService', () => {
|
||||
it('defaults to en-US when no explicit language is provided', async () => {
|
||||
const service = createSimpleLanguageService();
|
||||
|
||||
await service.initialize();
|
||||
|
||||
expect(await service.getCurrentLanguage()).toBe('en-US');
|
||||
expect(await service.getSupportedLanguages()).toEqual(['en-US', 'zh-CN']);
|
||||
});
|
||||
|
||||
it('falls back to en-US for unsupported language values', async () => {
|
||||
const service = new SimpleLanguageService('fr-FR');
|
||||
|
||||
await service.initialize();
|
||||
|
||||
expect(await service.getCurrentLanguage()).toBe('en-US');
|
||||
});
|
||||
|
||||
it('keeps localized language display names', () => {
|
||||
const service = new SimpleLanguageService();
|
||||
|
||||
expect(service.getLanguageDisplayName('zh-CN')).toBe('中文');
|
||||
expect(service.getLanguageDisplayName('en-US')).toBe('English');
|
||||
});
|
||||
});
|
||||
@@ -23,66 +23,66 @@ describe('MCP Server Tools', () => {
|
||||
});
|
||||
|
||||
describe('ParameterValidator', () => {
|
||||
it('应该正确验证提示词输入', () => {
|
||||
expect(() => ParameterValidator.validatePrompt('有效的提示词')).not.toThrow();
|
||||
expect(() => ParameterValidator.validatePrompt('')).toThrow('提示词必须是非空字符串');
|
||||
expect(() => ParameterValidator.validatePrompt(' ')).toThrow('提示词必须是非空字符串');
|
||||
expect(() => ParameterValidator.validatePrompt('a'.repeat(60000))).toThrow('提示词过长');
|
||||
it('validates prompt input', () => {
|
||||
expect(() => ParameterValidator.validatePrompt('valid prompt')).not.toThrow();
|
||||
expect(() => ParameterValidator.validatePrompt('')).toThrow('Prompt must be a non-empty string');
|
||||
expect(() => ParameterValidator.validatePrompt(' ')).toThrow('Prompt must be a non-empty string');
|
||||
expect(() => ParameterValidator.validatePrompt('a'.repeat(60000))).toThrow('Prompt must not exceed 50,000 characters');
|
||||
});
|
||||
|
||||
it('应该正确验证需求输入', () => {
|
||||
expect(() => ParameterValidator.validateRequirements('有效的需求描述')).not.toThrow();
|
||||
expect(() => ParameterValidator.validateRequirements('')).toThrow('需求描述必须是非空字符串');
|
||||
expect(() => ParameterValidator.validateRequirements(' ')).toThrow('需求描述必须是非空字符串');
|
||||
expect(() => ParameterValidator.validateRequirements('a'.repeat(15000))).toThrow('需求描述过长');
|
||||
it('validates requirements input', () => {
|
||||
expect(() => ParameterValidator.validateRequirements('valid requirements')).not.toThrow();
|
||||
expect(() => ParameterValidator.validateRequirements('')).toThrow('Requirements must be a non-empty string');
|
||||
expect(() => ParameterValidator.validateRequirements(' ')).toThrow('Requirements must be a non-empty string');
|
||||
expect(() => ParameterValidator.validateRequirements('a'.repeat(15000))).toThrow('Requirements must not exceed 10,000 characters');
|
||||
});
|
||||
|
||||
it('应该正确验证模板输入', () => {
|
||||
it('validates template input', () => {
|
||||
expect(() => ParameterValidator.validateTemplate('valid-template')).not.toThrow();
|
||||
expect(() => ParameterValidator.validateTemplate(undefined)).not.toThrow();
|
||||
expect(() => ParameterValidator.validateTemplate('')).toThrow('模板必须是非空字符串');
|
||||
expect(() => ParameterValidator.validateTemplate(' ')).toThrow('模板必须是非空字符串');
|
||||
expect(() => ParameterValidator.validateTemplate('')).toThrow('Template must be a non-empty string');
|
||||
expect(() => ParameterValidator.validateTemplate(' ')).toThrow('Template must be a non-empty string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCPErrorHandler', () => {
|
||||
it('应该正确转换验证错误', () => {
|
||||
const error = new Error('提示词必须是非空字符串');
|
||||
it('converts validation errors', () => {
|
||||
const error = new Error('Prompt must be a non-empty string');
|
||||
const mcpError = MCPErrorHandler.convertCoreError(error);
|
||||
|
||||
expect(mcpError.code).toBe(MCP_ERROR_CODES.INVALID_PARAMS); // -32602
|
||||
expect(mcpError.message).toContain('提示词必须是非空字符串');
|
||||
expect(mcpError.message).toContain('Prompt must be a non-empty string');
|
||||
});
|
||||
|
||||
it('应该正确转换优化错误', () => {
|
||||
const error = new Error('优化失败');
|
||||
it('converts optimization errors', () => {
|
||||
const error = new Error('Optimization failed');
|
||||
error.name = 'OptimizationError';
|
||||
const mcpError = MCPErrorHandler.convertCoreError(error);
|
||||
|
||||
expect(mcpError.code).toBe(MCP_ERROR_CODES.PROMPT_OPTIMIZATION_FAILED); // -32001
|
||||
expect(mcpError.message).toContain('提示词优化失败');
|
||||
expect(mcpError.message).toContain('Prompt optimization failed');
|
||||
});
|
||||
|
||||
it('应该将未知错误处理为内部错误', () => {
|
||||
const error = new Error('未知错误');
|
||||
it('converts unknown errors to internal errors', () => {
|
||||
const error = new Error('Unknown failure');
|
||||
const mcpError = MCPErrorHandler.convertCoreError(error);
|
||||
|
||||
expect(mcpError.code).toBe(MCP_ERROR_CODES.INTERNAL_ERROR); // -32000
|
||||
expect(mcpError.message).toContain('内部错误');
|
||||
expect(mcpError.message).toContain('Internal error');
|
||||
});
|
||||
|
||||
it('应该正确创建验证错误', () => {
|
||||
const mcpError = MCPErrorHandler.createValidationError('测试验证错误');
|
||||
it('creates validation errors', () => {
|
||||
const mcpError = MCPErrorHandler.createValidationError('Validation issue');
|
||||
|
||||
expect(mcpError.code).toBe(MCP_ERROR_CODES.INVALID_PARAMS);
|
||||
expect(mcpError.message).toContain('参数验证失败: 测试验证错误');
|
||||
expect(mcpError.message).toContain('Parameter validation failed: Validation issue');
|
||||
});
|
||||
|
||||
it('应该正确创建内部错误', () => {
|
||||
const mcpError = MCPErrorHandler.createInternalError('测试内部错误');
|
||||
it('creates internal errors', () => {
|
||||
const mcpError = MCPErrorHandler.createInternalError('Internal failure');
|
||||
|
||||
expect(mcpError.code).toBe(MCP_ERROR_CODES.INTERNAL_ERROR);
|
||||
expect(mcpError.message).toContain('测试内部错误');
|
||||
expect(mcpError.message).toContain('Internal failure');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -20,7 +20,7 @@
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"readme": "在Windows环境下,请使用分号(;)而不是&&作为命令分隔符,或使用build:win脚本",
|
||||
"readme": "On Windows, use semicolons (;) instead of && as command separators, or run the build:win script.",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build:bundle": "vite build",
|
||||
|
||||
@@ -46,18 +46,18 @@ const toast = useToast()
|
||||
// 统一使用inject获取services
|
||||
const services = inject<Ref<AppServices | null>>('services')
|
||||
if (!services) {
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] services未正确注入,请确保在App组件中正确provide了services')
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] Services were not injected correctly. Make sure App provides the services dependency.')
|
||||
}
|
||||
|
||||
const getTemplateManager = computed(() => {
|
||||
const servicesValue = services.value
|
||||
if (!servicesValue) {
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] services未初始化,请确保应用已正确启动')
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] Services are not initialized. Make sure the app has finished bootstrapping.')
|
||||
}
|
||||
|
||||
const manager = servicesValue.templateManager
|
||||
if (!manager) {
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] templateManager未初始化,请确保服务已正确配置')
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] Template manager is not initialized. Check the service configuration.')
|
||||
}
|
||||
|
||||
return manager
|
||||
@@ -66,22 +66,33 @@ const getTemplateManager = computed(() => {
|
||||
const getTemplateLanguageService = computed(() => {
|
||||
const servicesValue = services.value
|
||||
if (!servicesValue) {
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] services未初始化,请确保应用已正确启动')
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] Services are not initialized. Make sure the app has finished bootstrapping.')
|
||||
}
|
||||
|
||||
const service = servicesValue.templateLanguageService
|
||||
if (!service) {
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] templateLanguageService未初始化,请确保服务已正确配置')
|
||||
throw new Error('[BuiltinTemplateLanguageSwitch] Template language service is not initialized. Check the service configuration.')
|
||||
}
|
||||
|
||||
return service
|
||||
})
|
||||
|
||||
// Reactive state
|
||||
const currentLanguage = ref<BuiltinTemplateLanguage>('zh-CN')
|
||||
const currentLanguage = ref<BuiltinTemplateLanguage>('en-US')
|
||||
const supportedLanguages = ref<BuiltinTemplateLanguage[]>([])
|
||||
const isChanging = ref(false)
|
||||
|
||||
const getLanguageDisplayNameFallback = (language: BuiltinTemplateLanguage): string => {
|
||||
switch (language) {
|
||||
case 'zh-CN':
|
||||
return '中文'
|
||||
case 'en-US':
|
||||
return 'English'
|
||||
default:
|
||||
return language
|
||||
}
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
const getCurrentLanguageShort = computed(() => {
|
||||
try {
|
||||
@@ -92,7 +103,7 @@ const getCurrentLanguageShort = computed(() => {
|
||||
return service.getLanguageDisplayName(currentLanguage.value)
|
||||
} catch (error) {
|
||||
console.error('Error getting current language short:', error)
|
||||
return '中文' // fallback to Chinese
|
||||
return getLanguageDisplayNameFallback(currentLanguage.value)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -122,8 +133,8 @@ onMounted(async () => {
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize builtin template language switch:', error)
|
||||
// Set fallback values
|
||||
currentLanguage.value = 'zh-CN'
|
||||
supportedLanguages.value = ['zh-CN', 'en-US']
|
||||
currentLanguage.value = 'en-US'
|
||||
supportedLanguages.value = ['en-US', 'zh-CN']
|
||||
|
||||
// Only show toast error if toast is available
|
||||
try {
|
||||
|
||||
@@ -154,6 +154,7 @@ import {
|
||||
} from '@vicons/tabler';
|
||||
import { useToast } from '../composables/ui/useToast';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { getI18nErrorMessage } from '../utils/error';
|
||||
import type { FavoriteCategory } from '@prompt-optimizer/core';
|
||||
import type { AppServices } from '../types/services';
|
||||
|
||||
@@ -374,7 +375,7 @@ const handleDeleteCategory = async (category: FavoriteCategory) => {
|
||||
try {
|
||||
deletingCategoryUsageCount.value = await servicesValue.favoriteManager.getCategoryUsage(category.id);
|
||||
} catch (error) {
|
||||
console.error('获取分类使用统计失败:', error);
|
||||
console.error('Failed to fetch category usage count:', error);
|
||||
deletingCategoryUsageCount.value = 0;
|
||||
}
|
||||
}
|
||||
@@ -388,7 +389,7 @@ const handleConfirmDelete = async () => {
|
||||
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) {
|
||||
message.warning('收藏功能暂不可用');
|
||||
message.warning('Favorites are temporarily unavailable.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -407,9 +408,8 @@ const handleConfirmDelete = async () => {
|
||||
await loadCategories();
|
||||
emit('category-updated');
|
||||
} catch (error) {
|
||||
console.error('删除分类失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.categoryManager.deleteFailed')}: ${errorMessage}`);
|
||||
console.error('Failed to delete category:', error);
|
||||
message.error(getI18nErrorMessage(error, t('favorites.categoryManager.deleteFailed')));
|
||||
} finally {
|
||||
deleteDialogVisible.value = false;
|
||||
deletingCategory.value = null;
|
||||
@@ -426,7 +426,7 @@ const handleSaveCategory = async () => {
|
||||
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) {
|
||||
message.warning('收藏功能暂不可用');
|
||||
message.warning(t('favorites.manager.messages.unavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -457,9 +457,8 @@ const handleSaveCategory = async () => {
|
||||
await loadCategories();
|
||||
emit('category-updated');
|
||||
} catch (error) {
|
||||
console.error('保存分类失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.categoryManager.saveFailed')}: ${errorMessage}`);
|
||||
console.error('Failed to save category:', error);
|
||||
message.error(getI18nErrorMessage(error, t('favorites.categoryManager.saveFailed')));
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
@@ -494,16 +493,15 @@ const handleUpdateExpandedKeys = (keys: string[]) => {
|
||||
const loadCategories = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) {
|
||||
console.warn('收藏管理器未初始化');
|
||||
console.warn('Favorite manager is not initialized.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
categories.value = await servicesValue.favoriteManager.getCategories();
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.categoryManager.loadFailed')}: ${errorMessage}`);
|
||||
console.error('Failed to load categories:', error);
|
||||
message.error(getI18nErrorMessage(error, t('favorites.categoryManager.loadFailed')));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -120,14 +120,14 @@ const computedStyle = computed(() => props.style);
|
||||
const loadCategories = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue?.favoriteManager) {
|
||||
console.warn('收藏管理器未初始化,跳过分类加载');
|
||||
console.warn('Favorite manager is not initialized; skipping category loading.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
categories.value = await servicesValue.favoriteManager.getCategories();
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error);
|
||||
console.error('Failed to load categories:', error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -227,18 +227,18 @@ const dataManagerModalStyle = {
|
||||
|
||||
const services = inject<Ref<AppServices | null>>('services')
|
||||
if (!services) {
|
||||
throw new Error('[DataManager] services未正确注入,请确保在App组件中正确provide了services')
|
||||
throw new Error('[DataManager] Services were not injected correctly. Make sure App provides the services instance.')
|
||||
}
|
||||
|
||||
const getDataManager = computed(() => {
|
||||
const servicesValue = services.value
|
||||
if (!servicesValue) {
|
||||
throw new Error('[DataManager] services未初始化,请确保应用已正确启动')
|
||||
throw new Error('[DataManager] Services are not initialized. Make sure the application has started correctly.')
|
||||
}
|
||||
|
||||
const manager = servicesValue.dataManager
|
||||
if (!manager) {
|
||||
throw new Error('[DataManager] dataManager未初始化,请确保服务已正确配置')
|
||||
throw new Error('[DataManager] DataManager is not initialized. Make sure the service is configured correctly.')
|
||||
}
|
||||
|
||||
return manager
|
||||
@@ -285,7 +285,7 @@ const refreshStorageSummary = async () => {
|
||||
isRefreshingStorage.value = true
|
||||
const servicesValue = services.value
|
||||
if (!servicesValue) {
|
||||
throw new Error('[DataManager] services未初始化,请确保应用已正确启动')
|
||||
throw new Error('[DataManager] Services are not initialized. Make sure the application has started correctly.')
|
||||
}
|
||||
|
||||
storageSummary.value = await resolveDataManagerStorageBreakdown({
|
||||
@@ -357,7 +357,7 @@ const handleExport = async () => {
|
||||
|
||||
toast.success(t('dataManager.export.success'))
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
console.error('Export failed:', error)
|
||||
toast.error(t('dataManager.export.failed'))
|
||||
} finally {
|
||||
isExporting.value = false
|
||||
@@ -389,7 +389,7 @@ const handleImport = async () => {
|
||||
emit('close')
|
||||
clearSelectedFile()
|
||||
} catch (error) {
|
||||
console.error('导入失败:', error)
|
||||
console.error('Import failed:', error)
|
||||
toast.error(`${t('dataManager.import.failed')}: ${(error as Error).message}`)
|
||||
} finally {
|
||||
isImporting.value = false
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:size="size"
|
||||
:disabled="loading"
|
||||
@click="handleToggleFavorite"
|
||||
:title="isFavorited ? '取消收藏' : '添加到收藏'"
|
||||
:title="isFavorited ? t('favorites.button.removeTitle') : t('favorites.button.addTitle')"
|
||||
class="favorite-button"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -13,14 +13,14 @@
|
||||
<Star v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ isFavorited ? '已收藏' : '收藏' }}
|
||||
{{ isFavorited ? t('favorites.button.favorited') : t('favorites.button.favorite') }}
|
||||
</n-button>
|
||||
|
||||
<!-- 收藏对话框 -->
|
||||
<n-modal v-model:show="showFavoriteModal">
|
||||
<n-card
|
||||
style="max-width: 500px"
|
||||
title="添加到收藏"
|
||||
:title="t('favorites.dialog.saveTitle')"
|
||||
:bordered="false"
|
||||
size="huge"
|
||||
role="dialog"
|
||||
@@ -32,40 +32,40 @@
|
||||
:rules="formRules"
|
||||
label-placement="top"
|
||||
>
|
||||
<n-form-item label="标题" path="title">
|
||||
<n-form-item :label="t('favorites.dialog.titleLabel')" path="title">
|
||||
<n-input
|
||||
v-model:value="favoriteForm.title"
|
||||
placeholder="为这个提示词起个名字"
|
||||
:placeholder="t('favorites.dialog.titlePlaceholder')"
|
||||
maxlength="100"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="描述" path="description">
|
||||
<n-form-item :label="t('favorites.dialog.descriptionLabel')" path="description">
|
||||
<n-input
|
||||
v-model:value="favoriteForm.description"
|
||||
type="textarea"
|
||||
placeholder="描述这个提示词的用途和特点"
|
||||
:placeholder="t('favorites.dialog.descriptionPlaceholder')"
|
||||
:rows="3"
|
||||
maxlength="300"
|
||||
show-count
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="分类" path="category">
|
||||
<n-form-item :label="t('favorites.dialog.categoryLabel')" path="category">
|
||||
<n-select
|
||||
v-model:value="favoriteForm.category"
|
||||
:options="categoryOptions"
|
||||
placeholder="选择分类"
|
||||
:placeholder="t('favorites.dialog.categoryPlaceholder')"
|
||||
clearable
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item label="标签" path="tags">
|
||||
<n-form-item :label="t('favorites.dialog.tagsLabel')" path="tags">
|
||||
<n-dynamic-tags
|
||||
v-model:value="favoriteForm.tags"
|
||||
:max="10"
|
||||
placeholder="输入标签后按回车添加"
|
||||
:placeholder="t('favorites.dialog.tagsPlaceholder')"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
@@ -74,14 +74,14 @@
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<n-button @click="showFavoriteModal = false">
|
||||
取消
|
||||
{{ t('favorites.dialog.cancel') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
type="primary"
|
||||
:loading="loading"
|
||||
@click="handleSaveFavorite"
|
||||
>
|
||||
保存
|
||||
{{ t('favorites.dialog.save') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -92,6 +92,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, inject, watch, type Ref } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
NButton,
|
||||
NIcon,
|
||||
@@ -106,6 +107,7 @@ import {
|
||||
type FormRules
|
||||
} from 'naive-ui';
|
||||
import { useToast } from '../composables/ui/useToast';
|
||||
import { getI18nErrorMessage } from '../utils/error';
|
||||
import { Star, Stars } from '@vicons/tabler';
|
||||
import type { FavoriteCategory } from '@prompt-optimizer/core';
|
||||
import type { AppServices } from '../types/services';
|
||||
@@ -134,6 +136,7 @@ const emit = defineEmits<{
|
||||
const services = inject<Ref<AppServices | null> | null>('services', null);
|
||||
|
||||
const message = useToast();
|
||||
const { t } = useI18n();
|
||||
|
||||
// 表单相关
|
||||
const formRef = ref<FormInst | null>(null);
|
||||
@@ -158,16 +161,9 @@ const formRules: FormRules = {
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入标题',
|
||||
message: t('favorites.dialog.validation.titleRequired'),
|
||||
trigger: ['input', 'blur']
|
||||
}
|
||||
],
|
||||
category: [
|
||||
{
|
||||
required: false,
|
||||
message: '请选择分类',
|
||||
trigger: ['change', 'blur']
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -186,7 +182,7 @@ const checkFavoriteStatus = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue) return;
|
||||
if (!servicesValue.favoriteManager) {
|
||||
console.warn('收藏管理器未初始化,跳过收藏状态检查');
|
||||
console.warn('Favorite manager is not initialized. Skipping favorite status check.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,7 +198,7 @@ const checkFavoriteStatus = async () => {
|
||||
favoriteId.value = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查收藏状态失败:', error);
|
||||
console.error('Failed to check favorite status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -212,15 +208,15 @@ const loadCategories = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue) return;
|
||||
if (!servicesValue.favoriteManager) {
|
||||
console.warn('收藏管理器未初始化,跳过分类加载');
|
||||
console.warn('Favorite manager is not initialized. Skipping category loading.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
categories.value = await servicesValue.favoriteManager.getCategories();
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error);
|
||||
message.error('加载分类失败');
|
||||
console.error('Failed to load categories:', error);
|
||||
message.error(getI18nErrorMessage(error, t('favorites.manager.messages.loadCategoryFailed')));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -242,17 +238,10 @@ const initFavoriteForm = () => {
|
||||
title += '...';
|
||||
}
|
||||
|
||||
// 根据内容智能分类
|
||||
let defaultCategory = '';
|
||||
if (props.originalContent) {
|
||||
// 如果有原始内容,说明是优化后的提示词
|
||||
defaultCategory = categories.value.find(c => c.name === '系统提示词')?.id || '';
|
||||
}
|
||||
|
||||
favoriteForm.value = {
|
||||
title,
|
||||
description: '',
|
||||
category: defaultCategory,
|
||||
category: '',
|
||||
tags: []
|
||||
};
|
||||
};
|
||||
@@ -263,8 +252,8 @@ const handleSaveFavorite = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue) return;
|
||||
if (!servicesValue.favoriteManager) {
|
||||
console.warn('收藏管理器未初始化,无法执行收藏操作');
|
||||
message.warning('收藏功能暂不可用,请稍后再试');
|
||||
console.warn('Favorite manager is not initialized. Cannot save favorite.');
|
||||
message.warning(t('favorites.dialog.messages.unavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -292,12 +281,11 @@ const handleSaveFavorite = async () => {
|
||||
favoriteId.value = id;
|
||||
showFavoriteModal.value = false;
|
||||
|
||||
message.success('添加到收藏成功');
|
||||
message.success(t('favorites.dialog.messages.saveSuccess'));
|
||||
emit('favorited', id);
|
||||
} catch (error) {
|
||||
console.error('添加收藏失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`添加收藏失败: ${errorMessage}`);
|
||||
console.error('Failed to save favorite:', error);
|
||||
message.error(getI18nErrorMessage(error, t('favorites.dialog.messages.saveFailed')));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -308,8 +296,8 @@ const handleRemoveFavorite = async () => {
|
||||
const servicesValue = services?.value;
|
||||
if (!servicesValue || !favoriteId.value) return;
|
||||
if (!servicesValue.favoriteManager) {
|
||||
console.warn('收藏管理器未初始化,无法执行取消收藏操作');
|
||||
message.warning('收藏功能暂不可用,请稍后再试');
|
||||
console.warn('Favorite manager is not initialized. Cannot remove favorite.');
|
||||
message.warning(t('favorites.dialog.messages.unavailable'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -319,12 +307,11 @@ const handleRemoveFavorite = async () => {
|
||||
isFavorited.value = false;
|
||||
favoriteId.value = null;
|
||||
|
||||
message.success('取消收藏成功');
|
||||
message.success(t('favorites.button.removeSuccess'));
|
||||
emit('unfavorited');
|
||||
} catch (error) {
|
||||
console.error('取消收藏失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`取消收藏失败: ${errorMessage}`);
|
||||
console.error('Failed to remove favorite:', error);
|
||||
message.error(getI18nErrorMessage(error, t('favorites.manager.actions.deleteFailed')));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<n-button
|
||||
quaternary
|
||||
@click="$emit('copy', favorite)"
|
||||
title="复制"
|
||||
:title="t('favorites.library.card.copyContent')"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon><Copy /></n-icon>
|
||||
@@ -37,7 +37,7 @@
|
||||
<n-button
|
||||
quaternary
|
||||
@click="$emit('use', favorite)"
|
||||
title="使用"
|
||||
:title="t('favorites.library.card.useNow')"
|
||||
type="primary"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -106,7 +106,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { h } from 'vue'
|
||||
import { computed, h } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import {
|
||||
NListItem,
|
||||
@@ -142,6 +143,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
isSelected: false
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'select': [favorite: FavoritePrompt, selected: boolean];
|
||||
'edit': [favorite: FavoritePrompt];
|
||||
@@ -152,24 +155,24 @@ const emit = defineEmits<{
|
||||
'share': [favorite: FavoritePrompt];
|
||||
}>();
|
||||
|
||||
const actionMenuOptions = [
|
||||
const actionMenuOptions = computed(() => [
|
||||
{
|
||||
label: '编辑',
|
||||
label: t('favorites.library.card.edit'),
|
||||
key: 'edit',
|
||||
icon: () => h(NIcon, null, { default: () => h(Edit) })
|
||||
},
|
||||
{
|
||||
label: '复制',
|
||||
label: t('favorites.library.card.copyContent'),
|
||||
key: 'copy',
|
||||
icon: () => h(NIcon, null, { default: () => h(Copy) })
|
||||
},
|
||||
{
|
||||
label: '分享',
|
||||
label: t('prompt.share'),
|
||||
key: 'share',
|
||||
icon: () => h(NIcon, null, { default: () => h(Share) })
|
||||
},
|
||||
{
|
||||
label: '切换分类',
|
||||
label: t('favorites.library.card.toggleCategory'),
|
||||
key: 'category',
|
||||
icon: () => h(NIcon, null, { default: () => h(Tag) })
|
||||
},
|
||||
@@ -177,11 +180,11 @@ const actionMenuOptions = [
|
||||
type: 'divider'
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
label: t('favorites.library.card.delete'),
|
||||
key: 'delete',
|
||||
icon: () => h(NIcon, null, { default: () => h(Trash) })
|
||||
}
|
||||
];
|
||||
]);
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
@@ -193,13 +196,15 @@ const formatDate = (timestamp: number) => {
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
if (hours === 0) {
|
||||
const minutes = Math.floor(diff / (1000 * 60));
|
||||
return minutes <= 1 ? '刚刚' : `${minutes}分钟前`;
|
||||
return minutes <= 1
|
||||
? t('favorites.library.time.justNow')
|
||||
: t('favorites.library.time.minutesAgo', { minutes });
|
||||
}
|
||||
return `${hours}小时前`;
|
||||
return t('favorites.library.time.hoursAgo', { hours });
|
||||
} else if (days === 1) {
|
||||
return '昨天';
|
||||
return t('favorites.library.time.yesterday');
|
||||
} else if (days < 7) {
|
||||
return `${days}天前`;
|
||||
return t('favorites.library.time.daysAgo', { days });
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
:page-slot="7"
|
||||
>
|
||||
<template #prefix="{ itemCount }">
|
||||
<NText depth="3">共 {{ itemCount }} 项</NText>
|
||||
<NText depth="3">{{ t('favorites.manager.totalCount', { count: itemCount }) }}</NText>
|
||||
</template>
|
||||
</NPagination>
|
||||
</NSpace>
|
||||
@@ -280,6 +280,7 @@ import {
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useToast } from '../composables/ui/useToast';
|
||||
import { useFavoriteInitializer } from '../composables/storage/useFavoriteInitializer';
|
||||
import { getI18nErrorMessage } from '../utils/error';
|
||||
import ToastUI from './Toast.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -559,6 +560,12 @@ const readFileAsText = (file: File) =>
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
const buildErrorMessage = (summary: string, error: unknown) => {
|
||||
const fallback = t('common.error');
|
||||
const detail = getI18nErrorMessage(error, fallback);
|
||||
return detail === fallback ? summary : `${summary}: ${detail}`;
|
||||
};
|
||||
|
||||
const tryCopyToClipboard = async (text: string, successMessage: string) => {
|
||||
try {
|
||||
if (navigator?.clipboard?.writeText) {
|
||||
@@ -576,7 +583,7 @@ const tryCopyToClipboard = async (text: string, successMessage: string) => {
|
||||
message.success(successMessage);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('复制失败:', error);
|
||||
console.error('[FavoriteManager] Failed to copy favorite content:', error);
|
||||
message.error(t('favorites.manager.actions.copyFailed'));
|
||||
return false;
|
||||
}
|
||||
@@ -629,8 +636,7 @@ const handleImportConfirm = async () => {
|
||||
try {
|
||||
payload = await readFileAsText(file);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.manager.importDialog.readFileFailed')}: ${errorMessage}`);
|
||||
message.error(buildErrorMessage(t('favorites.manager.importDialog.readFileFailed'), error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -648,13 +654,12 @@ const handleImportConfirm = async () => {
|
||||
});
|
||||
message.success(t('favorites.manager.importDialog.importSuccess', { imported: result.imported, skipped: result.skipped }));
|
||||
if (result.errors.length > 0) {
|
||||
message.warning(`${t('favorites.manager.importDialog.importPartialFailed')}:\n${result.errors.join('\n')}`);
|
||||
message.warning(`${t('favorites.manager.importDialog.importPartialFailed')}:\n${result.errors.join('\n')}`);
|
||||
}
|
||||
await loadFavorites();
|
||||
closeImportDialog();
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.manager.importDialog.importFailed')}: ${errorMessage}`);
|
||||
message.error(buildErrorMessage(t('favorites.manager.importDialog.importFailed'), error));
|
||||
} finally {
|
||||
importState.importing = false;
|
||||
}
|
||||
@@ -706,9 +711,8 @@ const loadFavorites = async () => {
|
||||
previewFavorite.value = updated ? { ...updated } : null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载收藏失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.manager.messages.loadFailed')}: ${errorMessage}`);
|
||||
console.error('[FavoriteManager] Failed to load favorites:', error);
|
||||
message.error(buildErrorMessage(t('favorites.manager.messages.loadFailed'), error));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -725,9 +729,8 @@ const loadCategories = async () => {
|
||||
try {
|
||||
categories.value = await servicesValue.favoriteManager.getCategories();
|
||||
} catch (error) {
|
||||
console.error('加载分类失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.manager.messages.loadCategoryFailed')}: ${errorMessage}`);
|
||||
console.error('[FavoriteManager] Failed to load categories:', error);
|
||||
message.error(buildErrorMessage(t('favorites.manager.messages.loadCategoryFailed'), error));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -773,8 +776,7 @@ const handleDeleteFavorite = (favorite: FavoritePrompt) => {
|
||||
message.warning(t('favorites.manager.messages.unavailable'));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.manager.actions.deleteFailed')}: ${errorMessage}`);
|
||||
message.error(buildErrorMessage(t('favorites.manager.actions.deleteFailed'), error));
|
||||
}
|
||||
})();
|
||||
if (previewFavorite.value?.id === favorite.id) {
|
||||
@@ -788,7 +790,9 @@ const handleUseFavorite = (favorite: FavoritePrompt) => {
|
||||
// 增加使用次数
|
||||
const servicesValue = services?.value;
|
||||
if (servicesValue?.favoriteManager) {
|
||||
servicesValue.favoriteManager.incrementUseCount(favorite.id).catch(console.error);
|
||||
servicesValue.favoriteManager.incrementUseCount(favorite.id).catch((error) => {
|
||||
console.error('[FavoriteManager] Failed to increment favorite usage count:', error);
|
||||
});
|
||||
}
|
||||
bumpUseCountLocally(favorite.id);
|
||||
if (previewFavorite.value?.id === favorite.id) {
|
||||
@@ -828,8 +832,7 @@ const handleActionMenuSelect = (key: string) => {
|
||||
message.warning(t('favorites.manager.messages.unavailable'));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.manager.actions.clearFailed')}: ${errorMessage}`);
|
||||
message.error(buildErrorMessage(t('favorites.manager.actions.clearFailed'), error));
|
||||
}
|
||||
})();
|
||||
break;
|
||||
@@ -856,8 +859,7 @@ const handleExportFavorites = async () => {
|
||||
message.warning(t('favorites.manager.messages.unavailable'));
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
message.error(`${t('favorites.manager.actions.exportFailed')}: ${errorMessage}`);
|
||||
message.error(buildErrorMessage(t('favorites.manager.actions.exportFailed'), error));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -517,7 +517,7 @@ const save = async () => {
|
||||
emit('saved')
|
||||
close()
|
||||
} catch (_error) {
|
||||
console.error('保存配置失败:', _error)
|
||||
console.error('[ImageModelEditModal] Failed to save config:', _error)
|
||||
toast.error(t('image.config.saveFailed'))
|
||||
}
|
||||
}
|
||||
@@ -544,7 +544,7 @@ watch(() => props.show, async (newShow) => {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载配置失败:', e)
|
||||
console.error('[ImageModelEditModal] Failed to load config:', e)
|
||||
}
|
||||
} else {
|
||||
resetFormData()
|
||||
@@ -563,7 +563,7 @@ watch(() => ({
|
||||
await applyDraftConfig(initialConfig)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('处理图像模型弹窗数据变化失败:', e)
|
||||
console.error('[ImageModelEditModal] Failed to process modal data changes:', e)
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
@@ -228,7 +228,7 @@ const selectTestType = (model: ImageModel): 'text2image' | 'image2image' => {
|
||||
return 'text2image' // 两种都支持,优先文生图
|
||||
}
|
||||
|
||||
throw new Error('模型不支持任何图像生成功能')
|
||||
throw new Error('The model does not support image generation')
|
||||
}
|
||||
|
||||
const getPreviewImageSrc = (configId: string): string | null => {
|
||||
@@ -284,7 +284,7 @@ const testConnection = async (configId: string) => {
|
||||
|
||||
// 获取选中的模型信息
|
||||
if (!config.model) {
|
||||
throw new Error('选中的模型未找到')
|
||||
throw new Error('The selected model could not be found')
|
||||
}
|
||||
|
||||
// 根据模型能力确定测试类型
|
||||
@@ -347,8 +347,8 @@ const toggleConfig = async (config: { id: string; enabled: boolean }) => {
|
||||
await loadConfigs()
|
||||
toast.success(config.enabled ? t('modelManager.disableSuccess') : t('modelManager.enableSuccess'))
|
||||
} catch (error) {
|
||||
console.error('切换模型状态失败:', error)
|
||||
toast.error(t('modelManager.toggleFailed', { error: getI18nErrorMessage(error, 'Unknown error') }))
|
||||
console.error('[ImageModelManager] Failed to toggle model status:', error)
|
||||
toast.error(t('modelManager.toggleFailed', { error: getI18nErrorMessage(error, t('common.error')) }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,8 +359,8 @@ const deleteConfig = async (configId: string) => {
|
||||
await loadConfigs()
|
||||
toast.success(t('modelManager.deleteSuccess'))
|
||||
} catch (error) {
|
||||
console.error('删除模型失败:', error)
|
||||
toast.error(t('modelManager.deleteFailed', { error: getI18nErrorMessage(error, 'Unknown error') }))
|
||||
console.error('[ImageModelManager] Failed to delete model:', error)
|
||||
toast.error(t('modelManager.deleteFailed', { error: getI18nErrorMessage(error, t('common.error')) }))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,7 +370,7 @@ onMounted(async () => {
|
||||
try {
|
||||
await initialize()
|
||||
} catch (error) {
|
||||
console.error('初始化图像模型管理器失败:', error)
|
||||
console.error('[ImageModelManager] Failed to initialize:', error)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -42,12 +42,12 @@
|
||||
font-size="8"
|
||||
font-weight="bold">A</text>
|
||||
|
||||
<!-- 中文符号 - 清晰的"中"字 -->
|
||||
<!-- Secondary character to hint multi-language support -->
|
||||
<text x="8" y="25"
|
||||
fill="currentColor"
|
||||
font-family="system-ui"
|
||||
font-size="7"
|
||||
font-weight="bold">中</text>
|
||||
font-weight="bold">B</text>
|
||||
</svg>
|
||||
</template>
|
||||
</NButton>
|
||||
@@ -57,8 +57,9 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, type Ref } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NButton, NDropdown, type DropdownOption } from 'naive-ui'
|
||||
import { i18n } from '../plugins/i18n'
|
||||
import { i18n, type SupportedLocale } from '../plugins/i18n'
|
||||
import { UI_SETTINGS_KEYS } from '@prompt-optimizer/core'
|
||||
import { usePreferences } from '../composables/storage/usePreferenceManager'
|
||||
import type { AppServices } from '../types/services'
|
||||
@@ -66,9 +67,7 @@ import type { AppServices } from '../types/services'
|
||||
// 服务注入
|
||||
const services = inject<Ref<AppServices | null>>('services')!
|
||||
const { setPreference } = usePreferences(services)
|
||||
|
||||
// 语言选项配置 - 为未来扩展预留接口
|
||||
type SupportedLocale = 'zh-CN' | 'zh-TW' | 'en-US'
|
||||
const { t } = useI18n()
|
||||
|
||||
interface LanguageOption {
|
||||
key: SupportedLocale
|
||||
@@ -76,35 +75,37 @@ interface LanguageOption {
|
||||
locale: SupportedLocale
|
||||
}
|
||||
|
||||
const availableLanguages: LanguageOption[] = [
|
||||
const availableLanguages = computed<LanguageOption[]>(() => [
|
||||
{
|
||||
key: 'zh-CN',
|
||||
label: '简体中文',
|
||||
label: t('settings.languageSwitcher.languages.zh-CN'),
|
||||
locale: 'zh-CN'
|
||||
},
|
||||
{
|
||||
key: 'zh-TW',
|
||||
label: '繁體中文',
|
||||
label: t('settings.languageSwitcher.languages.zh-TW'),
|
||||
locale: 'zh-TW'
|
||||
},
|
||||
{
|
||||
key: 'en-US',
|
||||
label: 'English',
|
||||
label: t('settings.languageSwitcher.languages.en-US'),
|
||||
locale: 'en-US'
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
// 当前语言计算属性
|
||||
const currentLocale = computed(() => i18n.global.locale.value)
|
||||
const currentLocale = computed(() => i18n.global.locale.value as SupportedLocale)
|
||||
|
||||
const currentLanguageLabel = computed(() => {
|
||||
const current = availableLanguages.find(lang => lang.locale === currentLocale.value)
|
||||
return current ? `切换语言 / Switch Language (${current.label})` : '切换语言 / Switch Language'
|
||||
const current = availableLanguages.value.find(lang => lang.locale === currentLocale.value)
|
||||
return current
|
||||
? t('settings.languageSwitcher.ariaLabel', { language: current.label })
|
||||
: t('settings.languageSwitcher.label')
|
||||
})
|
||||
|
||||
// 为Naive UI Dropdown创建选项
|
||||
const dropdownOptions = computed<DropdownOption[]>(() => {
|
||||
return availableLanguages.map(language => ({
|
||||
return availableLanguages.value.map(language => ({
|
||||
key: language.key,
|
||||
label: language.label
|
||||
}))
|
||||
@@ -116,7 +117,7 @@ const isSupportedLocale = (value: unknown): value is SupportedLocale =>
|
||||
// 处理语言选择
|
||||
const handleLanguageSelect = async (key: string) => {
|
||||
if (!isSupportedLocale(key)) return
|
||||
const selectedLanguage = availableLanguages.find(lang => lang.key === key)
|
||||
const selectedLanguage = availableLanguages.value.find(lang => lang.key === key)
|
||||
if (!selectedLanguage) return
|
||||
|
||||
// 切换语言
|
||||
|
||||
@@ -201,7 +201,7 @@ const handleAddOption = (value: string | number) => {
|
||||
|
||||
const definition = props.parameterDefinitions.find((def) => def.name === value)
|
||||
if (!definition) {
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.unknownParam', '参数定义不存在'))
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.unknownParam', 'Parameter definition not found'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -216,20 +216,20 @@ const handleAddOption = (value: string | number) => {
|
||||
const handleConfirmCustom = () => {
|
||||
const trimmedKey = customForm.key.trim()
|
||||
if (!trimmedKey) {
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.customKeyRequired', '参数名称不能为空'))
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.customKeyRequired', 'Parameter name is required'))
|
||||
return false
|
||||
}
|
||||
if (!isSafeCustomKey(trimmedKey)) {
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.dangerousParam', '存在危险的参数名称'))
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.dangerousParam', 'This parameter name is not allowed'))
|
||||
return false
|
||||
}
|
||||
if (Object.prototype.hasOwnProperty.call(props.paramOverrides, trimmedKey)) {
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.duplicateParam', '参数已存在'))
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.duplicateParam', 'Parameter already exists'))
|
||||
return false
|
||||
}
|
||||
const trimmedValue = customForm.value.trim()
|
||||
if (!trimmedValue) {
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.customValueRequired', '参数值不能为空'))
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.customValueRequired', 'Parameter value is required'))
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -338,12 +338,12 @@ const customEntries = computed(() => {
|
||||
const handleAddDefinition = (name: string) => {
|
||||
const definition = schemaMap.value.get(name)
|
||||
if (!definition) {
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.unknownParam', '参数定义不存在'))
|
||||
message.error(withFallback('modelManager.advancedParameters.validation.unknownParam', 'Parameter definition not found'))
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(props.paramOverrides, name)) {
|
||||
message.warning(withFallback('modelManager.advancedParameters.validation.duplicateParam', '参数已存在'))
|
||||
message.warning(withFallback('modelManager.advancedParameters.validation.duplicateParam', 'Parameter already exists'))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -111,18 +111,18 @@ const testId = computed(() => props.testId || undefined)
|
||||
// 注入服务并获取 CompareService
|
||||
const services = inject<Ref<AppServices | null>>('services');
|
||||
if (!services) {
|
||||
throw new Error('[OutputDisplay] services未正确注入,请确保在App组件中正确provide了services');
|
||||
throw new Error('[OutputDisplay] Services were not injected correctly. Make sure App provides the services instance.');
|
||||
}
|
||||
|
||||
const compareService = computed(() => {
|
||||
const servicesValue = services.value;
|
||||
if (!servicesValue) {
|
||||
throw new Error('[OutputDisplay] services未初始化,请确保应用已正确启动');
|
||||
throw new Error('[OutputDisplay] Services are not initialized. Make sure the application has started correctly.');
|
||||
}
|
||||
|
||||
const service = servicesValue.compareService;
|
||||
if (!service) {
|
||||
throw new Error('[OutputDisplay] compareService未初始化,请确保服务已正确配置');
|
||||
throw new Error('[OutputDisplay] CompareService is not initialized. Make sure the service is configured correctly.');
|
||||
}
|
||||
|
||||
return service;
|
||||
|
||||
@@ -399,8 +399,8 @@ const handleCopy = (type: 'content' | 'reasoning' | 'all') => {
|
||||
break
|
||||
case 'all':
|
||||
textToCopy = [
|
||||
displayReasoning.value && `推理过程:\n${displayReasoning.value}`,
|
||||
`主要内容:\n${displayContent.value}`
|
||||
displayReasoning.value && `${t('common.reasoning')}:\n${displayReasoning.value}`,
|
||||
`${t('common.content')}:\n${displayContent.value}`
|
||||
].filter(Boolean).join('\n\n')
|
||||
break
|
||||
}
|
||||
@@ -535,7 +535,7 @@ const forceRefreshContent = () => {
|
||||
// 收藏相关方法 - 触发保存对话框而不是直接保存
|
||||
const handleFavorite = () => {
|
||||
if (!props.content) {
|
||||
message.warning('没有内容可以收藏');
|
||||
message.warning(t('toast.error.noContentToSave'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -79,18 +79,18 @@ const emit = defineEmits<{
|
||||
// 注入服务并获取 CompareService
|
||||
const services = inject<Ref<AppServices | null>>('services');
|
||||
if (!services) {
|
||||
throw new Error('[OutputDisplayFullscreen] services未正确注入,请确保在App组件中正确provide了services');
|
||||
throw new Error('[OutputDisplayFullscreen] Services were not injected correctly. Make sure App provides the services instance.');
|
||||
}
|
||||
|
||||
const compareService = computed(() => {
|
||||
const servicesValue = services.value;
|
||||
if (!servicesValue) {
|
||||
throw new Error('[OutputDisplayFullscreen] services未初始化,请确保应用已正确启动');
|
||||
throw new Error('[OutputDisplayFullscreen] Services are not initialized. Make sure the application has started correctly.');
|
||||
}
|
||||
|
||||
const service = servicesValue.compareService;
|
||||
if (!service) {
|
||||
throw new Error('[OutputDisplayFullscreen] compareService未初始化,请确保服务已正确配置');
|
||||
throw new Error('[OutputDisplayFullscreen] CompareService is not initialized. Make sure the service is configured correctly.');
|
||||
}
|
||||
|
||||
return service;
|
||||
|
||||
@@ -273,7 +273,7 @@ const updateGardenSnapshot = async (
|
||||
message.success(t('favorites.manager.preview.garden.saveSnapshotSuccess'))
|
||||
emit('favorite-updated', props.favorite.id)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误'
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
|
||||
message.error(`${t('favorites.manager.preview.garden.saveSnapshotFailed')}: ${errorMessage}`)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
|
||||
@@ -604,7 +604,7 @@ const switchToV0 = async () => {
|
||||
outputDisplayRef.value.forceRefreshContent();
|
||||
}
|
||||
|
||||
console.log("[PromptPanel] 已切换到 V0(原始内容)");
|
||||
console.log('[PromptPanel] Switched to V0 (original content).');
|
||||
};
|
||||
|
||||
// 处理评估按钮点击(触发评估)
|
||||
@@ -776,7 +776,7 @@ const switchVersion = async (version: PromptRecord) => {
|
||||
outputDisplayRef.value.forceRefreshContent();
|
||||
}
|
||||
|
||||
console.log("[PromptPanel] 版本切换完成,强制刷新内容:", {
|
||||
console.log('[PromptPanel] Version switch completed; forcing content refresh:', {
|
||||
versionId: version.id,
|
||||
version: version.version,
|
||||
});
|
||||
@@ -798,7 +798,7 @@ watch(
|
||||
if (outputDisplayRef.value) {
|
||||
outputDisplayRef.value.forceExitEditing();
|
||||
console.log(
|
||||
"[PromptPanel] 检测到开始优化/迭代,强制退出编辑状态",
|
||||
'[PromptPanel] Detected optimization/iteration start; forcing the editor to exit editing mode',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,7 +690,7 @@ const handleSave = async () => {
|
||||
} catch (error) {
|
||||
// 只忽略"标签已存在"错误,其他错误需要抛出
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code !== 'TAG_ALREADY_EXISTS') {
|
||||
console.error('添加标签到独立库失败:', error);
|
||||
console.error('Failed to add tag to the dedicated library:', error);
|
||||
throw error;
|
||||
}
|
||||
// 标签已存在,这是正常情况,继续处理
|
||||
@@ -775,7 +775,7 @@ const handleSave = async () => {
|
||||
emit('update:show', false);
|
||||
} catch (error) {
|
||||
const failedKey = props.mode === 'edit' ? 'favorites.dialog.messages.editFailed' : 'favorites.dialog.messages.saveFailed';
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
message.error(`${t(failedKey)}: ${errorMessage}`);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
|
||||
@@ -289,7 +289,7 @@ const loadTags = async () => {
|
||||
// 使用统一的类型转换器转换数据格式
|
||||
allTags.value = TagTypeConverter.toTagStatistics(tags);
|
||||
} catch (error: unknown) {
|
||||
message.error(t('favorites.manager.tagManager.messages.loadFailed') + `: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
message.error(t('favorites.manager.tagManager.messages.loadFailed') + `: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -330,7 +330,7 @@ const handleAddConfirm = async () => {
|
||||
showAddDialog.value = false;
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
message.error(t('favorites.manager.tagManager.messages.addFailed') + `: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
message.error(t('favorites.manager.tagManager.messages.addFailed') + `: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -367,7 +367,7 @@ const handleRenameConfirm = async () => {
|
||||
showRenameDialog.value = false;
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
message.error(t('favorites.manager.tagManager.messages.renameFailed') + `: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
message.error(t('favorites.manager.tagManager.messages.renameFailed') + `: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -394,7 +394,7 @@ const handleMergeConfirm = async () => {
|
||||
showMergeDialog.value = false;
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
message.error(t('favorites.manager.tagManager.messages.mergeFailed') + `: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
message.error(t('favorites.manager.tagManager.messages.mergeFailed') + `: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -412,7 +412,7 @@ const handleDelete = async (tag: TagStatistics) => {
|
||||
await loadTags();
|
||||
emit('updated');
|
||||
} catch (error: unknown) {
|
||||
message.error(t('favorites.manager.tagManager.messages.deleteFailed') + `: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
message.error(t('favorites.manager.tagManager.messages.deleteFailed') + `: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -915,10 +915,10 @@ const loadTemplates = async () => {
|
||||
// 统一使用异步方法
|
||||
const allTemplates = await getTemplateManager.value.listTemplates()
|
||||
templates.value = allTemplates
|
||||
console.log('加载到的提示词:', templates.value)
|
||||
console.log('Loaded templates:', templates.value)
|
||||
} catch (error) {
|
||||
console.error('加载提示词失败:', error)
|
||||
toast.error('加载提示词失败')
|
||||
console.error('Failed to load templates:', error)
|
||||
toast.error('Failed to load templates')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1180,7 +1180,7 @@ const handleSubmit = async () => {
|
||||
toast.success(editingTemplate.value ? t('template.success.updated') : t('template.success.added'))
|
||||
cancelEdit()
|
||||
} catch (error) {
|
||||
console.error('保存提示词失败:', error)
|
||||
console.error('Failed to save template:', error)
|
||||
toast.error(t('template.error.saveFailed'))
|
||||
}
|
||||
}
|
||||
@@ -1194,7 +1194,7 @@ const confirmDelete = async (templateId: string) => {
|
||||
|
||||
toast.success(t('template.success.deleted'))
|
||||
} catch (error) {
|
||||
console.error('删除提示词失败:', error)
|
||||
console.error('Failed to delete template:', error)
|
||||
toast.error(t('template.error.deleteFailed'))
|
||||
}
|
||||
}
|
||||
@@ -1235,7 +1235,7 @@ const copyTemplate = (template: Template) => {
|
||||
const isAdvanced = Array.isArray(template.content)
|
||||
|
||||
form.value = {
|
||||
name: `${template.name} - 副本`,
|
||||
name: `${template.name} - Copy`,
|
||||
content: isAdvanced ? '' : template.content as string,
|
||||
description: template.metadata.description || '',
|
||||
isAdvanced,
|
||||
|
||||
@@ -71,19 +71,19 @@ const isReady = ref(false)
|
||||
// 通过inject获取services,要求不能为null
|
||||
const services = inject<Ref<AppServices | null>>('services')
|
||||
if (!services) {
|
||||
throw new Error('[TemplateSelect] services未正确注入,请确保在App组件中正确provide了services')
|
||||
throw new Error('[TemplateSelect] Services were not injected correctly. Make sure App provides the services instance.')
|
||||
}
|
||||
|
||||
// 从services中获取templateManager
|
||||
const templateManager = computed(() => {
|
||||
const servicesValue = services.value
|
||||
if (!servicesValue) {
|
||||
throw new Error('[TemplateSelect] services未初始化,请确保应用已正确启动')
|
||||
throw new Error('[TemplateSelect] Services are not initialized. Make sure the application has started correctly.')
|
||||
}
|
||||
|
||||
const manager = servicesValue.templateManager
|
||||
if (!manager) {
|
||||
throw new Error('[TemplateSelect] templateManager未初始化,请确保服务已正确配置')
|
||||
throw new Error('[TemplateSelect] TemplateManager is not initialized. Make sure the service is configured correctly.')
|
||||
}
|
||||
|
||||
console.debug('[TemplateSelect] templateManager computed:', {
|
||||
@@ -147,7 +147,7 @@ const handleFocus = async () => {
|
||||
const ensureTemplateManagerReady = async () => {
|
||||
// templateManager的检查已经在computed中进行,这里直接使用
|
||||
isReady.value = true
|
||||
console.debug('[TemplateSelect] 模板管理器已就绪')
|
||||
console.debug('[TemplateSelect] Template manager is ready')
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ watch(
|
||||
() => services.value?.templateManager,
|
||||
async (newTemplateManager) => {
|
||||
if (newTemplateManager) {
|
||||
console.debug('[TemplateSelect] 检测到模板管理器变化,开始初始化...')
|
||||
console.debug('[TemplateSelect] Detected a template manager change; starting initialization...')
|
||||
await ensureTemplateManagerReady()
|
||||
await loadTemplatesByType()
|
||||
} else {
|
||||
|
||||
@@ -452,7 +452,7 @@ if (import.meta.env.DEV) {
|
||||
() => {
|
||||
const report = getPerformanceReport();
|
||||
if (report.grade.grade === "F") {
|
||||
console.warn("TestAreaPanel 性能较差:", report);
|
||||
console.warn('TestAreaPanel performance is poor:', report);
|
||||
}
|
||||
},
|
||||
5000,
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
<FocusAnalyzeButton
|
||||
v-else
|
||||
type="result"
|
||||
:label="t('evaluation.evaluate', '评估')"
|
||||
:label="t('evaluation.evaluate')"
|
||||
:loading="isEvaluatingSecondary"
|
||||
:button-props="{ size: 'small', type: 'tertiary' }"
|
||||
@evaluate="handleEvaluateSecondary"
|
||||
@@ -288,15 +288,15 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const primaryTitle = computed(() =>
|
||||
props.primaryTitle || t('test.compareResultA', '结果 A')
|
||||
props.primaryTitle || t('test.compareResultA')
|
||||
)
|
||||
|
||||
const secondaryTitle = computed(() =>
|
||||
props.secondaryTitle || t('test.compareResultB', '结果 B')
|
||||
props.secondaryTitle || t('test.compareResultB')
|
||||
)
|
||||
|
||||
const singleResultTitle = computed(() =>
|
||||
props.singleResultTitle || t('test.testResult', '测试结果')
|
||||
props.singleResultTitle || t('test.testResult')
|
||||
)
|
||||
|
||||
// 事件处理
|
||||
|
||||
@@ -313,7 +313,7 @@ const handleSubmit = async () => {
|
||||
emit('saved', id || undefined)
|
||||
handleUpdateShow(false)
|
||||
} catch (error) {
|
||||
console.error('保存模型失败:', error)
|
||||
console.error('Failed to save model:', error)
|
||||
|
||||
const rawError = error instanceof Error ? error.message : String(error)
|
||||
const fallback = isEditing.value
|
||||
|
||||
@@ -402,7 +402,7 @@ watch(
|
||||
}
|
||||
|
||||
hasShownStartupRepairToast.value = true
|
||||
toast.warning(`启动时已自动修复 ${report.actions.length} 项本地存储异常。`)
|
||||
toast.warning(`Automatically repaired ${report.actions.length} local storage issue(s) during startup.`)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
@@ -1043,7 +1043,7 @@ const handleContextEditorSaveSafe = (context?: {
|
||||
|
||||
// Best-effort persist the pro-multi session after an explicit save.
|
||||
void proMultiMessageSession.saveSession()
|
||||
toast.success('上下文已更新')
|
||||
toast.success('Context updated')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1160,7 +1160,7 @@ const restoreProVariableSessionToUserWorkspace = async () => {
|
||||
contextUserOptimization.currentVersions = chain.versions;
|
||||
// currentVersionId 已通过 binding 绑定,无需手动赋值
|
||||
} catch (error) {
|
||||
console.warn('[PromptOptimizerApp] Pro-user 恢复链失败,使用 session 快照继续:', error);
|
||||
console.warn('[PromptOptimizerApp] Failed to restore the Pro-user chain; continuing with the session snapshot:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1300,7 +1300,7 @@ watch(
|
||||
await restoreSessionToUI();
|
||||
} catch (error) {
|
||||
// 🔧 错误处理:避免未处理的 Promise rejection 传播到 Vue
|
||||
console.error('[PromptOptimizerApp] 模式切换后恢复会话失败:', error);
|
||||
console.error('[PromptOptimizerApp] Failed to restore the session after switching modes:', error);
|
||||
}
|
||||
},
|
||||
{ immediate: false } // 🔧 改为 false,不在 watch 创建时立即执行
|
||||
@@ -2135,7 +2135,7 @@ watch(
|
||||
// ⚠️ 切换后恢复状态到 UI
|
||||
await restoreSessionToUI();
|
||||
} catch (error) {
|
||||
console.error(`[PromptOptimizerApp] 路由切换失败: ${fromKey} → ${toKey}`, error);
|
||||
console.error(`[PromptOptimizerApp] Route switch failed: ${fromKey} -> ${toKey}`, error);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -2194,7 +2194,7 @@ let initTimeoutId: number | null = null
|
||||
const handlePagehide = () => {
|
||||
// 注意:这里不能用 await,因为浏览器不会等异步完成
|
||||
sessionManager.saveAllSessions().catch(err => {
|
||||
console.error('[PromptOptimizerApp] pagehide 异步保存失败:', err)
|
||||
console.error('[PromptOptimizerApp] Async save failed during pagehide:', err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2202,7 +2202,7 @@ const handlePagehide = () => {
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
sessionManager.saveAllSessions().catch(err => {
|
||||
console.error('[PromptOptimizerApp] visibilitychange 保存失败:', err)
|
||||
console.error('[PromptOptimizerApp] Save failed during visibilitychange:', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2226,7 +2226,7 @@ onMounted(() => {
|
||||
|
||||
// 设置超时定时器
|
||||
initTimeoutId = window.setTimeout(() => {
|
||||
console.error('[PromptOptimizerApp] Services 初始化超时')
|
||||
console.error('[PromptOptimizerApp] Timed out while initializing services')
|
||||
stopWatch?.()
|
||||
}, TIMEOUT)
|
||||
|
||||
@@ -2240,8 +2240,8 @@ onMounted(() => {
|
||||
// 理论上 watch(services) 会先执行 setPiniaServices(),但这里添加二次确认
|
||||
const $services = getPiniaServices()
|
||||
if (!$services) {
|
||||
console.warn('[PromptOptimizerApp] Pinia services 尚未注入,但 services.value 已存在')
|
||||
console.warn('[PromptOptimizerApp] 这可能是时序问题,继续等待下一轮')
|
||||
console.warn('[PromptOptimizerApp] Pinia services are not injected yet, but services.value already exists')
|
||||
console.warn('[PromptOptimizerApp] This may be a timing issue; waiting for the next cycle')
|
||||
// 不调用 stopWatch(),继续等待下一轮
|
||||
return
|
||||
}
|
||||
@@ -2251,7 +2251,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
// Services 和 Pinia 均已就绪,清除超时定时器并停止监听
|
||||
console.log('[PromptOptimizerApp] Services 和 Pinia 均已就绪,开始恢复会话')
|
||||
console.log('[PromptOptimizerApp] Services and Pinia are ready; starting session restore')
|
||||
if (initTimeoutId !== null) {
|
||||
window.clearTimeout(initTimeoutId)
|
||||
initTimeoutId = null
|
||||
@@ -2289,7 +2289,7 @@ onMounted(() => {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[PromptOptimizerApp] 初始化过程中发生错误:', error)
|
||||
console.error('[PromptOptimizerApp] An error occurred during initialization:', error)
|
||||
} finally {
|
||||
// Ensure the app can render even if session restore fails.
|
||||
hasRestoredInitialState.value = true
|
||||
|
||||
@@ -1192,15 +1192,15 @@ const formatToolsAsText = (tools: ToolDefinition[]): string => {
|
||||
return tools
|
||||
.map((tool) => {
|
||||
const func = tool.function
|
||||
let text = `工具名称: ${func.name}`
|
||||
if (func.description) text += `\n描述: ${func.description}`
|
||||
if (func.parameters) text += `\n参数结构: ${JSON.stringify(func.parameters, null, 2)}`
|
||||
let text = `Tool name: ${func.name}`
|
||||
if (func.description) text += `\nDescription: ${func.description}`
|
||||
if (func.parameters) text += `\nParameter schema: ${JSON.stringify(func.parameters, null, 2)}`
|
||||
return text
|
||||
})
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
const EXECUTION_PROMPT_MARKER = '【当前执行提示词见下方快照】'
|
||||
const EXECUTION_PROMPT_MARKER = '[See the current execution prompt in the snapshot below]'
|
||||
|
||||
const buildMessagesForSelection = (
|
||||
selection: TestPanelVersionValue,
|
||||
@@ -1568,7 +1568,7 @@ const buildConversationTestCaseDraft = (
|
||||
input: {
|
||||
kind: 'conversation' as const,
|
||||
label: 'Conversation Snapshot',
|
||||
summary: `目标消息已用“${EXECUTION_PROMPT_MARKER}”标记,实际内容见下方执行提示词。`,
|
||||
summary: `The target message is marked with "${EXECUTION_PROMPT_MARKER}". See the execution prompt below for the full content.`,
|
||||
content: buildConversationEvidenceContent(selection),
|
||||
},
|
||||
})
|
||||
@@ -1992,7 +1992,7 @@ const restoreFromHistory = async (payload: unknown) => {
|
||||
});
|
||||
if (mappingCount > 0) {
|
||||
console.log(
|
||||
`[ContextSystemWorkspace] 已重建 ${mappingCount} 个消息的优化链映射关系`,
|
||||
`[ContextSystemWorkspace] Rebuilt optimization chain mappings for ${mappingCount} message(s)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2007,7 +2007,7 @@ const restoreFromHistory = async (payload: unknown) => {
|
||||
conversationOptimization.currentRecordId.value = record.id;
|
||||
conversationOptimization.optimizedPrompt.value = record.optimizedPrompt;
|
||||
} catch (error) {
|
||||
console.error('[ContextSystemWorkspace] 历史记录恢复失败:', error);
|
||||
console.error('[ContextSystemWorkspace] Failed to restore history:', error);
|
||||
// 错误会向上传播到 App.vue 的 handleHistoryReuse 中统一处理
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ if (import.meta.env.DEV) {
|
||||
() => {
|
||||
const report = getPerformanceReport();
|
||||
if (report.grade.grade === "F") {
|
||||
console.warn("ContextUserTestPanel 性能较差:", report);
|
||||
console.warn('ContextUserTestPanel performance is poor:', report);
|
||||
}
|
||||
},
|
||||
5000,
|
||||
|
||||
@@ -387,7 +387,7 @@ if (import.meta.env.DEV) {
|
||||
() => {
|
||||
const report = getPerformanceReport();
|
||||
if (report.grade.grade === "F") {
|
||||
console.warn("ConversationTestPanel 性能较差:", report);
|
||||
console.warn('ConversationTestPanel performance is poor:', report);
|
||||
}
|
||||
},
|
||||
5000,
|
||||
|
||||
@@ -368,21 +368,21 @@ export const buildCompareRoleSuggestionReason = (
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.target.uniqueWorkspace',
|
||||
'这是当前唯一的工作区,所以建议作为优化目标。'
|
||||
'This is the only workspace column, so it is suggested as the optimization target.'
|
||||
)
|
||||
}
|
||||
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.target.workspace',
|
||||
'这是当前工作区列,所以建议作为优化目标。'
|
||||
'This is the current workspace column, so it is suggested as the optimization target.'
|
||||
)
|
||||
case 'baseline':
|
||||
if (candidate.promptRef.dynamicAlias === 'previous') {
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.baseline.dynamicPrevious',
|
||||
'这是当前工作区的上一版,所以建议作为上一版。'
|
||||
'This is the previous version of the current workspace, so it is suggested as the baseline.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ export const buildCompareRoleSuggestionReason = (
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.baseline.sameModelDifferentPrompt',
|
||||
'它与优化目标模型相同、提示词不同,所以建议作为上一版。'
|
||||
'It uses the same model as the optimization target but a different prompt, so it is suggested as the previous version.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -408,7 +408,7 @@ export const buildCompareRoleSuggestionReason = (
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.reference.samePromptDifferentModel',
|
||||
'这是不同模型的工作区结果,所以建议作为教师。'
|
||||
'This is a workspace result from a different model, so it is suggested as the teacher.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -416,7 +416,7 @@ export const buildCompareRoleSuggestionReason = (
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.reference.differentModel',
|
||||
'它与优化目标使用不同模型,所以建议作为教师。'
|
||||
'It uses a different model from the optimization target, so it is suggested as the teacher.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -430,7 +430,7 @@ export const buildCompareRoleSuggestionReason = (
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.referenceBaseline.sameModelDifferentPrompt',
|
||||
'它与教师模型相同、提示词不同,所以建议作为教师的上一版。'
|
||||
"It uses the same model as the teacher but a different prompt, so it is suggested as the teacher's previous version."
|
||||
)
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ export const buildCompareRoleSuggestionReason = (
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.replica.previousMatchesWorkspace',
|
||||
'它和工作区当前内容相同,所以建议按复测处理。'
|
||||
'It matches the current workspace content, so it is suggested as a retest.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -455,7 +455,7 @@ export const buildCompareRoleSuggestionReason = (
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.replica.samePromptAsTarget',
|
||||
'它与优化目标使用相同提示词,所以建议作为复测。'
|
||||
'It uses the same prompt as the optimization target, so it is suggested as a retest.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -464,13 +464,13 @@ export const buildCompareRoleSuggestionReason = (
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.auxiliary.default',
|
||||
'这列不会进入核心逐组比较,所以保留为其他测试。'
|
||||
'This column will not enter the core pairwise smart compare, so it stays as another test.'
|
||||
)
|
||||
}
|
||||
|
||||
return tOr(
|
||||
t,
|
||||
'evaluation.compareConfig.suggestionReasons.default',
|
||||
'系统根据当前版本、模型和提示词关系给出这个建议。'
|
||||
'The system inferred this suggestion from the current versions, models, and prompt relationships.'
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,7 @@
|
||||
data-testid="image-multiimage-input"
|
||||
:model-value="originalPrompt"
|
||||
:readonly="optimizing || isIterating"
|
||||
placeholder="请使用图1 / 图2 / 图3 来描述图片关系和生成目标"
|
||||
:placeholder="t('imageWorkspace.input.multiImagePromptPlaceholder')"
|
||||
:autosize="{ minRows: 4, maxRows: 12 }"
|
||||
clearable
|
||||
show-count
|
||||
@@ -107,7 +107,7 @@
|
||||
:value="originalPrompt"
|
||||
type="textarea"
|
||||
data-testid="image-multiimage-input"
|
||||
placeholder="请使用图1 / 图2 / 图3 来描述图片关系和生成目标"
|
||||
:placeholder="t('imageWorkspace.input.multiImagePromptPlaceholder')"
|
||||
:autosize="{ minRows: 4, maxRows: 12 }"
|
||||
clearable
|
||||
show-count
|
||||
@@ -150,8 +150,8 @@
|
||||
circle
|
||||
size="small"
|
||||
type="error"
|
||||
:aria-label="`删除图${index + 1}`"
|
||||
:title="`删除图${index + 1}`"
|
||||
:aria-label="t('imageWorkspace.input.removeImageAriaLabel', { index: index + 1 })"
|
||||
:title="t('imageWorkspace.input.removeImageAriaLabel', { index: index + 1 })"
|
||||
@mousedown.stop
|
||||
@click.stop="removeImage(item.id)"
|
||||
>
|
||||
@@ -166,21 +166,21 @@
|
||||
<AppPreviewImage
|
||||
class="image-card__preview"
|
||||
:src="toDataUrl(item)"
|
||||
:alt="`图${index + 1}`"
|
||||
:alt="t('imageWorkspace.input.imageAlt', { index: index + 1 })"
|
||||
object-fit="cover"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="image-card__footer"
|
||||
draggable="true"
|
||||
:title="`拖动调整图${index + 1}顺序`"
|
||||
:aria-label="`拖动调整图${index + 1}顺序`"
|
||||
:title="t('imageWorkspace.input.reorderImageAriaLabel', { index: index + 1 })"
|
||||
:aria-label="t('imageWorkspace.input.reorderImageAriaLabel', { index: index + 1 })"
|
||||
@click.stop
|
||||
@mousedown.stop
|
||||
@dragstart="handleImageDragStart(index, $event)"
|
||||
@dragend="handleImageDragEnd"
|
||||
>
|
||||
<NText strong class="image-card__label">{{ `图${index + 1}` }}</NText>
|
||||
<NText strong class="image-card__label">{{ t('imageWorkspace.input.imageLabel', { index: index + 1 }) }}</NText>
|
||||
<span class="image-card__drag-handle" aria-hidden="true">⋮⋮</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -504,7 +504,7 @@
|
||||
<NInput
|
||||
v-model:value="fullscreenValue"
|
||||
type="textarea"
|
||||
placeholder="请使用图1 / 图2 / 图3 来描述图片关系和生成目标"
|
||||
:placeholder="t('imageWorkspace.input.multiImagePromptPlaceholder')"
|
||||
:autosize="false"
|
||||
style="height: 100%; min-height: 0;"
|
||||
clearable
|
||||
@@ -789,12 +789,12 @@ const canOptimize = computed(() =>
|
||||
)
|
||||
const imageInputHint = computed(() => {
|
||||
if (session.inputImages.length >= 2) {
|
||||
return '拖动卡片可调整图1 / 图2 / 图3 的顺序语义'
|
||||
return t('imageWorkspace.input.multiImageHint')
|
||||
}
|
||||
if (session.inputImages.length === 1) {
|
||||
return '再添加一张图片后即可开始多图生图'
|
||||
return t('imageWorkspace.input.multiImageMinHint')
|
||||
}
|
||||
return '请至少上传两张图片,再开始多图生图'
|
||||
return t('imageWorkspace.input.multiImageReadyHint')
|
||||
})
|
||||
const testColumnCountModel = computed<TestColumnCount>({
|
||||
get: () => (session.layout.testColumnCount === 3 || session.layout.testColumnCount === 4 ? session.layout.testColumnCount : 2),
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
:bordered="false"
|
||||
type="default"
|
||||
>
|
||||
图{{ index + 1 }}{{ item.imageType ? ` (${item.imageType})` : '' }}:
|
||||
Image {{ index + 1 }}{{ item.imageType ? ` (${item.imageType})` : '' }}:
|
||||
{{ item.width }}x{{ item.height }}px,
|
||||
Aspect: {{ formatAspectRatio(item) }},
|
||||
Pricing Tier: {{ sizeMultiplier(item) }}
|
||||
|
||||
@@ -37,8 +37,8 @@ export interface ImageText2ImageEvaluationContext {
|
||||
}
|
||||
|
||||
const TEST_CASE_ID = 'image-text2image-intent'
|
||||
const TEST_CASE_LABEL = '生成意图'
|
||||
const OUTPUT_BLOCK_LABEL = '生成结果'
|
||||
const TEST_CASE_LABEL = 'Generation Intent'
|
||||
const OUTPUT_BLOCK_LABEL = 'Generated Output'
|
||||
|
||||
const normalizeInlineText = (value: string | undefined): string =>
|
||||
(value || '').replace(/\s+/gu, ' ').trim()
|
||||
@@ -116,7 +116,7 @@ const buildOutputBlock = (
|
||||
return {
|
||||
kind: 'image',
|
||||
label: OUTPUT_BLOCK_LABEL,
|
||||
content: `展示 ${variant.label} 列执行 prompt 生成的结果图。`,
|
||||
content: `Show the generated image result for column ${variant.label}.`,
|
||||
media,
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ const buildSnapshot = (
|
||||
testCaseId: TEST_CASE_ID,
|
||||
promptRef: buildTestPanelVersionPromptRef(variant.resolvedPrompt, versionLabels),
|
||||
promptText,
|
||||
output: `展示 ${variant.label} 列执行 prompt 生成的结果图。`,
|
||||
output: `Show the generated image result for column ${variant.label}.`,
|
||||
outputBlock,
|
||||
modelKey: variant.modelKey?.trim() || undefined,
|
||||
versionLabel: formatTestPanelVersionSelectionLabel(
|
||||
|
||||
@@ -96,6 +96,7 @@ import { useI18n } from "vue-i18n";
|
||||
import { useToast } from "../../composables/ui/useToast";
|
||||
import { useVariableDetection } from "./useVariableDetection";
|
||||
import VariableExtractionDialog from "./VariableExtractionDialog.vue";
|
||||
import { TEXT_SELECTION_ERRORS } from "./useTextSelection";
|
||||
import {
|
||||
variableHighlighter,
|
||||
variableAutocompletion,
|
||||
@@ -322,7 +323,7 @@ const validateSelection = (
|
||||
): { isValid: boolean; reason?: string } => {
|
||||
// 是否有有效选择
|
||||
if (start === end || !selectedText.trim()) {
|
||||
return { isValid: false, reason: "未选中任何文本" };
|
||||
return { isValid: false, reason: TEXT_SELECTION_ERRORS.emptySelection };
|
||||
}
|
||||
|
||||
// 检查是否跨越变量边界
|
||||
@@ -332,19 +333,19 @@ const validateSelection = (
|
||||
const openBracesBefore = (beforeSelection.match(/\{\{/g) || []).length;
|
||||
const closeBracesBefore = (beforeSelection.match(/\}\}/g) || []).length;
|
||||
if (openBracesBefore > closeBracesBefore) {
|
||||
return { isValid: false, reason: "不能跨越变量边界" };
|
||||
return { isValid: false, reason: TEXT_SELECTION_ERRORS.crossesVariableBoundary };
|
||||
}
|
||||
|
||||
const openBracesAfter = (afterSelection.match(/\{\{/g) || []).length;
|
||||
const closeBracesAfter = (afterSelection.match(/\}\}/g) || []).length;
|
||||
if (closeBracesAfter > openBracesAfter) {
|
||||
return { isValid: false, reason: "不能跨越变量边界" };
|
||||
return { isValid: false, reason: TEXT_SELECTION_ERRORS.crossesVariableBoundary };
|
||||
}
|
||||
|
||||
const openBracesInSelection = (selectedText.match(/\{\{/g) || []).length;
|
||||
const closeBracesInSelection = (selectedText.match(/\}\}/g) || []).length;
|
||||
if (openBracesInSelection !== closeBracesInSelection) {
|
||||
return { isValid: false, reason: "不能跨越变量边界" };
|
||||
return { isValid: false, reason: TEXT_SELECTION_ERRORS.crossesVariableBoundary };
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
@@ -477,7 +478,7 @@ const checkSelection = () => {
|
||||
|
||||
if (
|
||||
validation.reason &&
|
||||
validation.reason !== "未选中任何文本"
|
||||
validation.reason !== TEXT_SELECTION_ERRORS.emptySelection
|
||||
) {
|
||||
message.warning(validation.reason);
|
||||
}
|
||||
|
||||
@@ -19,9 +19,18 @@ export interface TextSelection {
|
||||
/** 是否有效选择 */
|
||||
isValid: boolean
|
||||
/** 无效原因 */
|
||||
invalidReason?: string
|
||||
invalidReason?: TextSelectionInvalidReason
|
||||
}
|
||||
|
||||
export const TEXT_SELECTION_ERRORS = {
|
||||
inputNotReady: 'Input is not ready',
|
||||
emptySelection: 'No text selected',
|
||||
crossesVariableBoundary: 'Selection cannot cross variable boundaries',
|
||||
} as const
|
||||
|
||||
export type TextSelectionInvalidReason =
|
||||
(typeof TEXT_SELECTION_ERRORS)[keyof typeof TEXT_SELECTION_ERRORS]
|
||||
|
||||
export function useTextSelection(inputRef: Ref<HTMLInputElement | HTMLTextAreaElement | null>) {
|
||||
const selection = ref<TextSelection>({
|
||||
text: '',
|
||||
@@ -41,7 +50,7 @@ export function useTextSelection(inputRef: Ref<HTMLInputElement | HTMLTextAreaEl
|
||||
start: 0,
|
||||
end: 0,
|
||||
isValid: false,
|
||||
invalidReason: '输入框未就绪'
|
||||
invalidReason: TEXT_SELECTION_ERRORS.inputNotReady
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,10 +80,10 @@ export function useTextSelection(inputRef: Ref<HTMLInputElement | HTMLTextAreaEl
|
||||
start: number,
|
||||
end: number,
|
||||
selectedText: string
|
||||
): { isValid: boolean; reason?: string } => {
|
||||
): { isValid: boolean; reason?: TextSelectionInvalidReason } => {
|
||||
// 检查是否有选中文本
|
||||
if (start === end || !selectedText.trim()) {
|
||||
return { isValid: false, reason: '未选中任何文本' }
|
||||
return { isValid: false, reason: TEXT_SELECTION_ERRORS.emptySelection }
|
||||
}
|
||||
|
||||
// 检查是否跨越变量边界
|
||||
@@ -85,14 +94,14 @@ export function useTextSelection(inputRef: Ref<HTMLInputElement | HTMLTextAreaEl
|
||||
const openBracesBeforeCount = (beforeSelection.match(/\{\{/g) || []).length
|
||||
const closeBracesBeforeCount = (beforeSelection.match(/\}\}/g) || []).length
|
||||
if (openBracesBeforeCount > closeBracesBeforeCount) {
|
||||
return { isValid: false, reason: '不能跨越变量边界' }
|
||||
return { isValid: false, reason: TEXT_SELECTION_ERRORS.crossesVariableBoundary }
|
||||
}
|
||||
|
||||
// 检查选中文本后是否有未闭合的 }}
|
||||
const openBracesAfterCount = (afterSelection.match(/\{\{/g) || []).length
|
||||
const closeBracesAfterCount = (afterSelection.match(/\}\}/g) || []).length
|
||||
if (closeBracesAfterCount > openBracesAfterCount) {
|
||||
return { isValid: false, reason: '不能跨越变量边界' }
|
||||
return { isValid: false, reason: TEXT_SELECTION_ERRORS.crossesVariableBoundary }
|
||||
}
|
||||
|
||||
// 检查选中文本内部是否包含完整的变量占位符
|
||||
@@ -101,7 +110,7 @@ export function useTextSelection(inputRef: Ref<HTMLInputElement | HTMLTextAreaEl
|
||||
if (openBracesInSelection > 0 || closeBracesInSelection > 0) {
|
||||
// 如果选中文本包含 {{ 或 }},检查是否是完整的变量
|
||||
if (openBracesInSelection !== closeBracesInSelection) {
|
||||
return { isValid: false, reason: '不能跨越变量边界' }
|
||||
return { isValid: false, reason: TEXT_SELECTION_ERRORS.crossesVariableBoundary }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
</svg>
|
||||
</NIcon>
|
||||
</template>
|
||||
已选择文件:{{ selectedFile.name }} ({{ (selectedFile.size / 1024).toFixed(1) }} KB)
|
||||
{{ t('variables.importer.selectedFile') }}: {{ selectedFile.name }} ({{ (selectedFile.size / 1024).toFixed(1) }} KB)
|
||||
</NAlert>
|
||||
|
||||
<!-- 预览区域 -->
|
||||
|
||||
@@ -57,15 +57,15 @@ export function useAccessibilityTesting() {
|
||||
const testRules = {
|
||||
// 图片替代文本
|
||||
'img-alt': {
|
||||
name: '图片替代文本',
|
||||
name: 'Image Alternative Text',
|
||||
wcagLevel: 'A' as const,
|
||||
severity: 'critical' as const,
|
||||
test: (element: HTMLImageElement) => {
|
||||
if (!element.alt && !element.getAttribute('aria-label') && !element.getAttribute('aria-labelledby')) {
|
||||
return {
|
||||
passed: false,
|
||||
message: '图片缺少替代文本',
|
||||
suggestion: '为图片添加 alt 属性或 aria-label 属性'
|
||||
message: 'Image is missing alternative text.',
|
||||
suggestion: 'Add an alt attribute or aria-label to the image.'
|
||||
}
|
||||
}
|
||||
return { passed: true }
|
||||
@@ -74,7 +74,7 @@ export function useAccessibilityTesting() {
|
||||
|
||||
// 表单标签
|
||||
'form-label': {
|
||||
name: '表单标签',
|
||||
name: 'Form Labels',
|
||||
wcagLevel: 'A' as const,
|
||||
severity: 'critical' as const,
|
||||
test: (element: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement) => {
|
||||
@@ -85,8 +85,8 @@ export function useAccessibilityTesting() {
|
||||
if (!hasLabel && !hasAriaLabel && !hasAriaLabelledby) {
|
||||
return {
|
||||
passed: false,
|
||||
message: '表单控件缺少标签',
|
||||
suggestion: '为表单控件添加 <label> 元素或 aria-label 属性'
|
||||
message: 'Form control is missing a label.',
|
||||
suggestion: 'Add a <label> element or aria-label to the form control.'
|
||||
}
|
||||
}
|
||||
return { passed: true }
|
||||
@@ -95,7 +95,7 @@ export function useAccessibilityTesting() {
|
||||
|
||||
// 链接文本
|
||||
'link-text': {
|
||||
name: '链接文本',
|
||||
name: 'Link Text',
|
||||
wcagLevel: 'A' as const,
|
||||
severity: 'serious' as const,
|
||||
test: (element: HTMLAnchorElement) => {
|
||||
@@ -106,18 +106,18 @@ export function useAccessibilityTesting() {
|
||||
if (!text && !ariaLabel && !title) {
|
||||
return {
|
||||
passed: false,
|
||||
message: '链接缺少描述文本',
|
||||
suggestion: '为链接添加描述性文本或 aria-label 属性'
|
||||
message: 'Link is missing descriptive text.',
|
||||
suggestion: 'Add descriptive text or an aria-label to the link.'
|
||||
}
|
||||
}
|
||||
|
||||
// 检查无意义的链接文本
|
||||
const meaninglessText = ['click here', 'read more', 'more', 'link', '点击这里', '更多', '链接']
|
||||
const meaninglessText = ['click here', 'read more', 'more', 'link']
|
||||
if (text && meaninglessText.includes(text.toLowerCase())) {
|
||||
return {
|
||||
passed: false,
|
||||
message: '链接文本不够描述性',
|
||||
suggestion: '使用更具描述性的链接文本,说明链接的目的或目标'
|
||||
message: 'Link text is not descriptive enough.',
|
||||
suggestion: 'Use more descriptive link text that explains the destination or purpose.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export function useAccessibilityTesting() {
|
||||
|
||||
// 按钮文本
|
||||
'button-text': {
|
||||
name: '按钮文本',
|
||||
name: 'Button Text',
|
||||
wcagLevel: 'A' as const,
|
||||
severity: 'critical' as const,
|
||||
test: (element: HTMLButtonElement) => {
|
||||
@@ -138,8 +138,8 @@ export function useAccessibilityTesting() {
|
||||
if (!text && !ariaLabel && !ariaLabelledby) {
|
||||
return {
|
||||
passed: false,
|
||||
message: '按钮缺少文本标签',
|
||||
suggestion: '为按钮添加文本内容或 aria-label 属性'
|
||||
message: 'Button is missing a text label.',
|
||||
suggestion: 'Add visible text or an aria-label to the button.'
|
||||
}
|
||||
}
|
||||
return { passed: true }
|
||||
@@ -148,7 +148,7 @@ export function useAccessibilityTesting() {
|
||||
|
||||
// 颜色对比度
|
||||
'color-contrast': {
|
||||
name: '颜色对比度',
|
||||
name: 'Color Contrast',
|
||||
wcagLevel: 'AA' as const,
|
||||
severity: 'serious' as const,
|
||||
test: (element: HTMLElement) => {
|
||||
@@ -174,7 +174,7 @@ export function useAccessibilityTesting() {
|
||||
|
||||
// 焦点指示器
|
||||
'focus-indicator': {
|
||||
name: '焦点指示器',
|
||||
name: 'Focus Indicator',
|
||||
wcagLevel: 'AA' as const,
|
||||
severity: 'serious' as const,
|
||||
test: (element: HTMLElement) => {
|
||||
@@ -187,8 +187,8 @@ export function useAccessibilityTesting() {
|
||||
if (outline === 'none' && !boxShadow.includes('0 0 0')) {
|
||||
return {
|
||||
passed: false,
|
||||
message: '可焦点元素缺少焦点指示器',
|
||||
suggestion: '为可焦点元素添加 :focus-visible 样式'
|
||||
message: 'Focusable element is missing a focus indicator.',
|
||||
suggestion: 'Add a :focus-visible style for the focusable element.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ export function useAccessibilityTesting() {
|
||||
|
||||
// 标题层级
|
||||
'heading-hierarchy': {
|
||||
name: '标题层级',
|
||||
name: 'Heading Hierarchy',
|
||||
wcagLevel: 'A' as const,
|
||||
severity: 'moderate' as const,
|
||||
test: (element: HTMLHeadingElement, context: { lastHeadingLevel?: number }) => {
|
||||
@@ -207,8 +207,8 @@ export function useAccessibilityTesting() {
|
||||
if (context.lastHeadingLevel && level > context.lastHeadingLevel + 1) {
|
||||
return {
|
||||
passed: false,
|
||||
message: '标题层级跳跃过大',
|
||||
suggestion: '确保标题层级是递进的,不要跳过级别'
|
||||
message: 'Heading hierarchy skips too many levels.',
|
||||
suggestion: 'Keep heading levels progressive and avoid skipping levels.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ export function useAccessibilityTesting() {
|
||||
|
||||
// 语言属性
|
||||
'lang-attribute': {
|
||||
name: '语言属性',
|
||||
name: 'Language Attribute',
|
||||
wcagLevel: 'A' as const,
|
||||
severity: 'moderate' as const,
|
||||
test: (element: HTMLHtmlElement) => {
|
||||
@@ -228,8 +228,8 @@ export function useAccessibilityTesting() {
|
||||
if (!lang) {
|
||||
return {
|
||||
passed: false,
|
||||
message: 'HTML 元素缺少 lang 属性',
|
||||
suggestion: '为 <html> 元素添加 lang 属性,如 lang="zh-CN"'
|
||||
message: 'The HTML element is missing a lang attribute.',
|
||||
suggestion: 'Add a lang attribute to <html>, for example lang="en-US".'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ export function useAccessibilityTesting() {
|
||||
|
||||
// ARIA 使用
|
||||
'aria-usage': {
|
||||
name: 'ARIA 使用',
|
||||
name: 'ARIA Usage',
|
||||
wcagLevel: 'A' as const,
|
||||
severity: 'serious' as const,
|
||||
test: (element: HTMLElement) => {
|
||||
@@ -257,8 +257,8 @@ export function useAccessibilityTesting() {
|
||||
if (!labelElement) {
|
||||
return {
|
||||
passed: false,
|
||||
message: 'aria-labelledby 引用的元素不存在',
|
||||
suggestion: '确保 aria-labelledby 引用的 ID 对应的元素存在'
|
||||
message: 'The element referenced by aria-labelledby does not exist.',
|
||||
suggestion: 'Make sure the ID referenced by aria-labelledby points to an existing element.'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,8 +303,8 @@ export function useAccessibilityTesting() {
|
||||
type: rule.severity === 'critical' ? 'error' : rule.severity === 'serious' ? 'warning' : 'info',
|
||||
rule: rule.name,
|
||||
element,
|
||||
message: ruleResult.message || '无障碍测试失败',
|
||||
suggestion: ruleResult.suggestion || '请检查元素的无障碍属性',
|
||||
message: ruleResult.message || 'Accessibility test failed.',
|
||||
suggestion: ruleResult.suggestion || 'Check the accessibility attributes for this element.',
|
||||
severity: rule.severity,
|
||||
wcagLevel: rule.wcagLevel,
|
||||
xpath: getElementXPath(element)
|
||||
@@ -499,10 +499,10 @@ export function useAccessibilityTesting() {
|
||||
case 'html': {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>可访问性测试报告</title>
|
||||
<title>Accessibility Test Report</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; }
|
||||
.score { font-size: 24px; font-weight: bold; margin: 20px 0; }
|
||||
@@ -517,22 +517,22 @@ export function useAccessibilityTesting() {
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>可访问性测试报告</h1>
|
||||
<p>测试时间: ${new Date(report.timestamp).toLocaleString('zh-CN')}</p>
|
||||
<h1>Accessibility Test Report</h1>
|
||||
<p>Test Time: ${new Date(report.timestamp).toLocaleString('en-US')}</p>
|
||||
<div class="score ${report.summary.passed ? 'passed' : 'failed'}">
|
||||
测试分数: ${report.summary.score}/100 ${report.summary.passed ? '(通过)' : '(未通过)'}
|
||||
Score: ${report.summary.score}/100 ${report.summary.passed ? '(Passed)' : '(Failed)'}
|
||||
</div>
|
||||
<h2>问题汇总</h2>
|
||||
<p>总计: ${report.summary.issues.total} | 错误: ${report.summary.issues.errors} | 警告: ${report.summary.issues.warnings} | 信息: ${report.summary.issues.info}</p>
|
||||
<h2>问题详情</h2>
|
||||
<h2>Issue Summary</h2>
|
||||
<p>Total: ${report.summary.issues.total} | Errors: ${report.summary.issues.errors} | Warnings: ${report.summary.issues.warnings} | Info: ${report.summary.issues.info}</p>
|
||||
<h2>Issue Details</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>规则</th>
|
||||
<th>消息</th>
|
||||
<th>建议</th>
|
||||
<th>严重程度</th>
|
||||
<th>WCAG级别</th>
|
||||
<th>Type</th>
|
||||
<th>Rule</th>
|
||||
<th>Message</th>
|
||||
<th>Suggestion</th>
|
||||
<th>Severity</th>
|
||||
<th>WCAG Level</th>
|
||||
</tr>
|
||||
${report.details.map(issue => `
|
||||
<tr>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ref, nextTick, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useAccessibility } from './useAccessibility'
|
||||
|
||||
@@ -26,6 +27,7 @@ export interface FocusState {
|
||||
}
|
||||
|
||||
export function useFocusManager(options: FocusManagerOptions = {}) {
|
||||
const { t } = useI18n()
|
||||
const { announce } = useAccessibility('FocusManager')
|
||||
|
||||
// 状态管理
|
||||
@@ -143,7 +145,7 @@ export function useFocusManager(options: FocusManagerOptions = {}) {
|
||||
element.getAttribute('title') ||
|
||||
`${role} ${currentFocusIndex.value + 1}`
|
||||
|
||||
announce(`已聚焦到 ${label}`, 'polite')
|
||||
announce(t('accessibility.liveRegion.focusMoved', { label }), 'polite')
|
||||
}
|
||||
|
||||
options.onFocusChange?.(element, currentFocusIndex.value)
|
||||
@@ -231,7 +233,7 @@ export function useFocusManager(options: FocusManagerOptions = {}) {
|
||||
const container = getContainer()
|
||||
container.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
announce('焦点已限制在当前区域内', 'assertive')
|
||||
announce(t('accessibility.liveRegion.focusTrapped'), 'assertive')
|
||||
}
|
||||
|
||||
// 释放焦点陷阱
|
||||
@@ -251,7 +253,7 @@ export function useFocusManager(options: FocusManagerOptions = {}) {
|
||||
}
|
||||
|
||||
previousActiveElement.value = null
|
||||
announce('焦点限制已解除', 'polite')
|
||||
announce(t('accessibility.liveRegion.focusReleased'), 'polite')
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
@@ -411,4 +413,4 @@ export function useFocusManager(options: FocusManagerOptions = {}) {
|
||||
hideFocusIndicator,
|
||||
destroy
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -237,7 +237,7 @@ export function useAppFavorite(options: AppFavoriteOptions): AppFavoriteReturn {
|
||||
await handleUseFavoriteImpl(favorite)
|
||||
} catch (error) {
|
||||
// 捕获收藏加载过程中的所有错误
|
||||
console.error('[App] 收藏加载失败:', error)
|
||||
console.error('[App] Failed to load favorite:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
toast.error(t('toast.error.favoriteLoadFailed', { error: errorMessage }))
|
||||
} finally {
|
||||
|
||||
@@ -233,7 +233,7 @@ export function useAppHistoryRestore(options: AppHistoryRestoreOptions): AppHist
|
||||
conversationSnapshot =
|
||||
conversationSnapshotRaw as ConversationSnapshotMessage[]
|
||||
console.log(
|
||||
'[App] 从历史记录恢复会话快照,消息数:',
|
||||
'[App] Restoring conversation snapshot from history. Message count:',
|
||||
conversationSnapshot.length,
|
||||
)
|
||||
|
||||
@@ -283,16 +283,16 @@ export function useAppHistoryRestore(options: AppHistoryRestoreOptions): AppHist
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
`[App] 消息 ${snapshotMsg.id} 版本 v${snapshotMsg.appliedVersion} 不存在,使用快照内容`,
|
||||
`[App] Message ${snapshotMsg.id} version v${snapshotMsg.appliedVersion} was not found. Falling back to snapshot content.`,
|
||||
)
|
||||
console.warn(
|
||||
`[App] 可用版本:`,
|
||||
'[App] Available versions:',
|
||||
msgChain.versions.map((v) => v.version),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[App] 消息 ${snapshotMsg.id} 版本加载失败,使用快照内容:`,
|
||||
`[App] Failed to load version for message ${snapshotMsg.id}. Falling back to snapshot content:`,
|
||||
error,
|
||||
)
|
||||
}
|
||||
@@ -340,17 +340,17 @@ export function useAppHistoryRestore(options: AppHistoryRestoreOptions): AppHist
|
||||
if (targetMessage) {
|
||||
toast.success(t('toast.success.conversationRestored'))
|
||||
} else if (messageId) {
|
||||
console.warn('[App] 会话快照中未找到被优化的消息 ID:', messageId)
|
||||
console.warn('[App] Optimized message ID was not found in the conversation snapshot:', messageId)
|
||||
toast.warning(t('toast.warning.messageNotFoundInSnapshot'))
|
||||
}
|
||||
} else if (messageId) {
|
||||
if (targetMessage) {
|
||||
console.log(
|
||||
'[App] 历史记录无会话快照,尝试在当前会话中查找消息(旧版本数据)',
|
||||
'[App] No conversation snapshot found in history. Trying to locate the message in the current session (legacy data).',
|
||||
)
|
||||
toast.warning(t('toast.warning.restoredFromLegacyHistory'))
|
||||
} else {
|
||||
console.warn('[App] 旧版本历史记录中未找到消息 ID:', messageId)
|
||||
console.warn('[App] Message ID was not found in legacy history data:', messageId)
|
||||
toast.warning(t('toast.warning.messageNotFoundInSnapshot'))
|
||||
}
|
||||
}
|
||||
@@ -369,7 +369,7 @@ export function useAppHistoryRestore(options: AppHistoryRestoreOptions): AppHist
|
||||
await handleHistoryReuseImpl(context)
|
||||
} catch (error) {
|
||||
// 捕获历史记录恢复过程中的所有错误
|
||||
console.error('[App] 历史记录恢复失败:', error)
|
||||
console.error('[App] Failed to restore history:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
toast.error(t('toast.error.historyRestoreFailed', { error: errorMessage }))
|
||||
} finally {
|
||||
|
||||
@@ -1370,7 +1370,7 @@ export function useAppPromptGardenImport(options: AppPromptGardenImportOptions)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[PromptGardenImport] Failed to load example input image:', e)
|
||||
toast.warning('示例输入图加载失败(请检查 Prompt Garden /prompt-assets 的 CORS 配置)')
|
||||
toast.warning('Failed to load the example input image. Check the Prompt Garden /prompt-assets CORS configuration.')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1404,7 +1404,7 @@ export function useAppPromptGardenImport(options: AppPromptGardenImportOptions)
|
||||
|
||||
if (loadedImages.length !== inputUrls.length) {
|
||||
console.warn('[PromptGardenImport] Failed to load one or more multi-image example inputs')
|
||||
toast.warning('部分示例输入图加载失败(请检查 Prompt Garden /prompt-assets 的 CORS 配置)')
|
||||
toast.warning('Some example input images could not be loaded. Check the Prompt Garden /prompt-assets CORS configuration.')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1423,7 +1423,7 @@ export function useAppPromptGardenImport(options: AppPromptGardenImportOptions)
|
||||
targetKey,
|
||||
})
|
||||
} catch (error) {
|
||||
toast.warning('Prompt Garden 导入成功,但收藏未保存。')
|
||||
toast.warning('Prompt Garden import succeeded, but saving the favorite failed.')
|
||||
}
|
||||
} else {
|
||||
console.warn('[PromptGardenImport] Favorite manager unavailable, skip auto-save')
|
||||
|
||||
@@ -22,6 +22,21 @@ import { useToast } from '../ui/useToast'
|
||||
|
||||
export function useContextEditor() {
|
||||
const toast = useToast()
|
||||
const unknownErrorFallback = 'Unknown error'
|
||||
const formatErrorSummary = (summary: string, error: unknown, fallback = unknownErrorFallback) => {
|
||||
const detail =
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: typeof error === 'string'
|
||||
? error
|
||||
: fallback
|
||||
|
||||
if (!detail || detail === fallback || detail === summary || /^\[object .+\]$/.test(detail)) {
|
||||
return summary
|
||||
}
|
||||
|
||||
return `${summary}: ${detail}`
|
||||
}
|
||||
|
||||
const isOpenAIRequest = (value: unknown): value is OpenAIRequest => {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
||||
@@ -74,17 +89,17 @@ export function useContextEditor() {
|
||||
const result = converter.fromLangFuse(langfuseData)
|
||||
if (result.success && result.data) {
|
||||
currentData.value = result.data
|
||||
toast.success('LangFuse数据转换成功')
|
||||
toast.success('LangFuse data converted successfully')
|
||||
} else {
|
||||
error.value = result.error || '转换失败'
|
||||
error.value = result.error || 'Conversion failed'
|
||||
toast.error(error.value)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '未知错误'
|
||||
const errorMsg = formatErrorSummary('Conversion failed', err, unknownErrorFallback)
|
||||
error.value = errorMsg
|
||||
toast.error(`转换失败: ${errorMsg}`)
|
||||
toast.error(errorMsg)
|
||||
return { success: false, error: errorMsg }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
@@ -103,17 +118,17 @@ export function useContextEditor() {
|
||||
const result = converter.fromOpenAI(openaiData)
|
||||
if (result.success && result.data) {
|
||||
currentData.value = result.data
|
||||
toast.success('OpenAI数据转换成功')
|
||||
toast.success('OpenAI data converted successfully')
|
||||
} else {
|
||||
error.value = result.error || '转换失败'
|
||||
error.value = result.error || 'Conversion failed'
|
||||
toast.error(error.value)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '未知错误'
|
||||
const errorMsg = formatErrorSummary('Conversion failed', err, unknownErrorFallback)
|
||||
error.value = errorMsg
|
||||
toast.error(`转换失败: ${errorMsg}`)
|
||||
toast.error(errorMsg)
|
||||
return { success: false, error: errorMsg }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
@@ -144,22 +159,22 @@ export function useContextEditor() {
|
||||
result = converter.fromConversationMessages(data as Array<Partial<ConversationMessage>>)
|
||||
break
|
||||
default:
|
||||
result = { success: false, error: `不支持的数据格式: ${format}` }
|
||||
result = { success: false, error: `Unsupported data format: ${format}` }
|
||||
}
|
||||
|
||||
if (result.success && result.data) {
|
||||
currentData.value = result.data
|
||||
toast.success(`${format.toUpperCase()}格式数据导入成功`)
|
||||
toast.success(`${format.toUpperCase()} data imported successfully`)
|
||||
} else {
|
||||
error.value = result.error || '导入失败'
|
||||
error.value = result.error || 'Import failed'
|
||||
toast.error(error.value)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '未知错误'
|
||||
const errorMsg = formatErrorSummary('Import failed', err, unknownErrorFallback)
|
||||
error.value = errorMsg
|
||||
toast.error(`导入失败: ${errorMsg}`)
|
||||
toast.error(errorMsg)
|
||||
return { success: false, error: errorMsg }
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
@@ -175,7 +190,7 @@ export function useContextEditor() {
|
||||
endIndex: number
|
||||
) => {
|
||||
if (!currentData.value) {
|
||||
toast.error('没有可编辑的数据')
|
||||
toast.error('No editable data available')
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -202,10 +217,10 @@ export function useContextEditor() {
|
||||
}
|
||||
(metadataRecord.variables as Record<string, string>)[variableName] = result.extractedVariable.value
|
||||
|
||||
toast.success(`变量 ${variableName} 提取成功`)
|
||||
toast.success(`Variable ${variableName} extracted successfully`)
|
||||
return true
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '变量提取失败'
|
||||
const errorMsg = formatErrorSummary('Variable extraction failed', err, unknownErrorFallback)
|
||||
toast.error(errorMsg)
|
||||
return false
|
||||
}
|
||||
@@ -226,7 +241,7 @@ export function useContextEditor() {
|
||||
description: suggestion.reason
|
||||
}))
|
||||
} catch (err) {
|
||||
console.error('变量建议生成失败:', err)
|
||||
console.error('Failed to generate variable suggestions:', err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -234,16 +249,16 @@ export function useContextEditor() {
|
||||
// 模板化处理
|
||||
const convertToTemplate = () => {
|
||||
if (!currentData.value) {
|
||||
toast.error('没有可处理的数据')
|
||||
toast.error('No data available to process')
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const result = templateProcessor.toTemplate(currentData.value)
|
||||
toast.success('模板转换成功')
|
||||
toast.success('Template converted successfully')
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '模板转换失败'
|
||||
const errorMsg = formatErrorSummary('Template conversion failed', err, unknownErrorFallback)
|
||||
toast.error(errorMsg)
|
||||
return null
|
||||
}
|
||||
@@ -256,10 +271,10 @@ export function useContextEditor() {
|
||||
try {
|
||||
const result = templateProcessor.fromTemplate(template, variables)
|
||||
currentData.value = result
|
||||
toast.success('变量应用成功')
|
||||
toast.success('Variables applied successfully')
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '变量应用失败'
|
||||
const errorMsg = formatErrorSummary('Variable application failed', err, unknownErrorFallback)
|
||||
toast.error(errorMsg)
|
||||
return null
|
||||
}
|
||||
@@ -272,7 +287,7 @@ export function useContextEditor() {
|
||||
try {
|
||||
return templateProcessor.validateVariables(template, variables)
|
||||
} catch (err) {
|
||||
console.error('变量验证失败:', err)
|
||||
console.error('Failed to validate variables:', err)
|
||||
return {
|
||||
isValid: false,
|
||||
missingVariables: [],
|
||||
@@ -289,15 +304,15 @@ export function useContextEditor() {
|
||||
|
||||
if (result.success && result.data) {
|
||||
currentData.value = result.data
|
||||
toast.success('文件导入成功')
|
||||
toast.success('File imported successfully')
|
||||
return true
|
||||
} else {
|
||||
error.value = result.error || '导入失败'
|
||||
error.value = result.error || 'Import failed'
|
||||
toast.error(error.value)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '文件导入失败'
|
||||
const errorMsg = formatErrorSummary('File import failed', err, unknownErrorFallback)
|
||||
error.value = errorMsg
|
||||
toast.error(errorMsg)
|
||||
return false
|
||||
@@ -312,15 +327,15 @@ export function useContextEditor() {
|
||||
|
||||
if (result.success && result.data) {
|
||||
currentData.value = result.data
|
||||
toast.success('剪贴板数据导入成功')
|
||||
toast.success('Clipboard data imported successfully')
|
||||
return true
|
||||
} else {
|
||||
error.value = result.error || '导入失败'
|
||||
error.value = result.error || 'Import failed'
|
||||
toast.error(error.value)
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '剪贴板导入失败'
|
||||
const errorMsg = formatErrorSummary('Clipboard import failed', err, unknownErrorFallback)
|
||||
error.value = errorMsg
|
||||
toast.error(errorMsg)
|
||||
return false
|
||||
@@ -329,16 +344,16 @@ export function useContextEditor() {
|
||||
|
||||
const exportToFile = (format: 'standard' | 'openai' | 'template', filename?: string) => {
|
||||
if (!currentData.value) {
|
||||
toast.error('没有可导出的数据')
|
||||
toast.error('No data available to export')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
importExportManager.exportToFile(currentData.value, format, filename)
|
||||
toast.success('数据已导出到文件')
|
||||
toast.success('Data exported to file')
|
||||
return true
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '导出失败'
|
||||
const errorMsg = formatErrorSummary('Export failed', err, unknownErrorFallback)
|
||||
toast.error(errorMsg)
|
||||
return false
|
||||
}
|
||||
@@ -346,20 +361,20 @@ export function useContextEditor() {
|
||||
|
||||
const exportToClipboard = async (format: 'standard' | 'openai' | 'template') => {
|
||||
if (!currentData.value) {
|
||||
toast.error('没有可导出的数据')
|
||||
toast.error('No data available to export')
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const success = await importExportManager.exportToClipboard(currentData.value, format)
|
||||
if (success) {
|
||||
toast.success('数据已复制到剪贴板')
|
||||
toast.success('Data copied to clipboard')
|
||||
} else {
|
||||
toast.error('复制失败')
|
||||
toast.error('Copy failed')
|
||||
}
|
||||
return success
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : '导出失败'
|
||||
const errorMsg = formatErrorSummary('Export failed', err, unknownErrorFallback)
|
||||
toast.error(errorMsg)
|
||||
return false
|
||||
}
|
||||
@@ -372,7 +387,7 @@ export function useContextEditor() {
|
||||
try {
|
||||
return templateProcessor.suggestOptimizations(currentData.value)
|
||||
} catch (err) {
|
||||
console.error('优化建议生成失败:', err)
|
||||
console.error('Failed to generate optimization suggestions:', err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ref, computed, watch, type Ref, type ComputedRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useToast } from "../ui/useToast";
|
||||
import type {
|
||||
@@ -26,6 +27,7 @@ export interface ContextManagementOptions {
|
||||
}
|
||||
|
||||
export function useContextManagement(options: ContextManagementOptions) {
|
||||
const { t } = useI18n()
|
||||
const {
|
||||
services,
|
||||
advancedModeEnabled,
|
||||
@@ -229,7 +231,7 @@ export function useContextManagement(options: ContextManagementOptions) {
|
||||
showContextEditor.value = false;
|
||||
|
||||
// 显示成功提示
|
||||
useToast().success("上下文已更新");
|
||||
useToast().success(t('contextEditor.saveSuccess'));
|
||||
};
|
||||
|
||||
// 处理上下文编辑器实时状态更新
|
||||
@@ -291,7 +293,7 @@ export function useContextManagement(options: ContextManagementOptions) {
|
||||
"[useContextManagement] Failed to change context mode:",
|
||||
error,
|
||||
);
|
||||
useToast().error("切换上下文模式失败");
|
||||
useToast().error(t('contextEditor.switchModeFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -52,15 +52,15 @@ export function useBasicSubMode(services: Ref<AppServices | null>): UseBasicSubM
|
||||
? saved
|
||||
: 'system'
|
||||
|
||||
console.log(`[useBasicSubMode] 初始化完成,当前值: ${singleton!.mode.value}`)
|
||||
console.log(`[useBasicSubMode] Initialization completed. Current value: ${singleton!.mode.value}`)
|
||||
|
||||
// 持久化默认值(如果未设置过)
|
||||
if (saved !== 'system' && saved !== 'user') {
|
||||
await setPreference(UI_SETTINGS_KEYS.BASIC_SUB_MODE, 'system')
|
||||
console.log('[useBasicSubMode] 首次初始化,已持久化默认值: system')
|
||||
console.log('[useBasicSubMode] First initialization complete. Persisted the default value: system')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[useBasicSubMode] 初始化失败,使用默认值 system:', e)
|
||||
console.error('[useBasicSubMode] Failed to initialize. Falling back to the default value system:', e)
|
||||
// 读取失败则保持默认 'system',并尝试持久化
|
||||
try {
|
||||
await setPreference(UI_SETTINGS_KEYS.BASIC_SUB_MODE, 'system')
|
||||
@@ -80,7 +80,7 @@ export function useBasicSubMode(services: Ref<AppServices | null>): UseBasicSubM
|
||||
await ensureInitialized()
|
||||
singleton!.mode.value = mode
|
||||
await setPreference(UI_SETTINGS_KEYS.BASIC_SUB_MODE, mode)
|
||||
console.log(`[useBasicSubMode] 子模式已切换并持久化: ${mode}`)
|
||||
console.log(`[useBasicSubMode] Sub-mode switched and persisted: ${mode}`)
|
||||
}
|
||||
|
||||
const switchToSystem = () => setBasicSubMode('system')
|
||||
|
||||
@@ -39,7 +39,7 @@ export function useImageSubMode(services: Ref<AppServices | null>): UseImageSubM
|
||||
|
||||
const ensureInitialized = async () => {
|
||||
// 路由已初始化,无需额外操作
|
||||
console.log(`[useImageSubMode] 当前子模式(来自路由): ${imageSubMode.value}`)
|
||||
console.log(`[useImageSubMode] Current sub-mode (from route): ${imageSubMode.value}`)
|
||||
}
|
||||
|
||||
const setImageSubMode = async (mode: ImageSubMode) => {
|
||||
@@ -47,7 +47,7 @@ export function useImageSubMode(services: Ref<AppServices | null>): UseImageSubM
|
||||
const targetPath = `/image/${mode}`
|
||||
if (route.path !== targetPath) {
|
||||
await router.push(targetPath)
|
||||
console.log(`[useImageSubMode] 子模式已切换(路由导航): ${mode}`)
|
||||
console.log(`[useImageSubMode] Sub-mode switched via route navigation: ${mode}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ export function useProSubMode(services: Ref<AppServices | null>): UseProSubModeA
|
||||
await setPreference(UI_SETTINGS_KEYS.PRO_SUB_MODE, normalized)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[useProSubMode] 初始化失败,使用默认值 ${DEFAULT_PRO_SUB_MODE}:`, e)
|
||||
console.error(`[useProSubMode] Failed to initialize. Falling back to the default value ${DEFAULT_PRO_SUB_MODE}:`, e)
|
||||
// 读取失败则保持默认值,并尝试持久化
|
||||
try {
|
||||
await setPreference(UI_SETTINGS_KEYS.PRO_SUB_MODE, DEFAULT_PRO_SUB_MODE)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ref, computed, inject } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from '../ui/useToast'
|
||||
import { getI18nErrorMessage } from '../../utils/error'
|
||||
import { formatErrorSummary, getI18nErrorMessage } from '../../utils/error'
|
||||
import type {
|
||||
ImageProvider,
|
||||
ImageModel,
|
||||
@@ -19,8 +19,9 @@ type EditableImageModelConfig = Omit<ImageModelConfig, 'provider' | 'model'> & {
|
||||
model?: ImageModel
|
||||
}
|
||||
|
||||
const toErrorMessage = (error: unknown): string => {
|
||||
return getI18nErrorMessage(error, 'Unknown error')
|
||||
const toOptionalErrorDetail = (error: unknown, fallback = 'Unknown error'): string | undefined => {
|
||||
const detail = getI18nErrorMessage(error, fallback)
|
||||
return detail === fallback ? undefined : detail
|
||||
}
|
||||
|
||||
export function useImageModelManager() {
|
||||
@@ -392,7 +393,7 @@ export function useImageModelManager() {
|
||||
return 'text2image' // 两种都支持,优先文生图
|
||||
}
|
||||
|
||||
throw new Error('模型不支持任何图像生成功能')
|
||||
throw new Error('This model does not support text-to-image or image-to-image generation')
|
||||
}
|
||||
|
||||
const testConnection = async () => {
|
||||
@@ -418,8 +419,8 @@ export function useImageModelManager() {
|
||||
if (detail.typeErrors.length) {
|
||||
parts.push(detail.typeErrors.map(e => t('image.connection.validation.invalidType', e)).join('; '))
|
||||
}
|
||||
connectionStatus.value = { type: 'error', messageKey: 'image.connection.testFailed', detail: parts.join(';') }
|
||||
toast.error(parts.join(';'))
|
||||
connectionStatus.value = { type: 'error', messageKey: 'image.connection.testFailed', detail: parts.join('; ') }
|
||||
toast.error(parts.join('; '))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -432,7 +433,7 @@ export function useImageModelManager() {
|
||||
selectedModel = adapter.buildDefaultModel(configForm.value.modelId)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`无法构建模型 ${configForm.value.modelId}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
`Unable to build model ${configForm.value.modelId}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error }
|
||||
)
|
||||
}
|
||||
@@ -476,14 +477,13 @@ export function useImageModelManager() {
|
||||
toast.success(t('image.connection.testSuccess'))
|
||||
|
||||
} catch (error) {
|
||||
const message = toErrorMessage(error)
|
||||
console.error('Connection test failed:', error)
|
||||
connectionStatus.value = {
|
||||
type: 'error',
|
||||
messageKey: 'image.connection.testError',
|
||||
detail: message
|
||||
detail: toOptionalErrorDetail(error)
|
||||
}
|
||||
toast.error(`${t('image.connection.testError')}: ${message}`)
|
||||
toast.error(formatErrorSummary(t('image.connection.testError'), error))
|
||||
} finally {
|
||||
isTestingConnection.value = false
|
||||
}
|
||||
@@ -501,7 +501,7 @@ export function useImageModelManager() {
|
||||
const parts: string[] = []
|
||||
if (detail.missing.length) parts.push(t('image.connection.validation.missing', { fields: detail.missing.join(', ') }))
|
||||
if (detail.typeErrors.length) parts.push(detail.typeErrors.map(e => t('image.connection.validation.invalidType', e)).join('; '))
|
||||
modelLoadingStatus.value = { type: 'warning', messageKey: 'image.model.connectionRequired', detail: parts.join(';') }
|
||||
modelLoadingStatus.value = { type: 'warning', messageKey: 'image.model.connectionRequired', detail: parts.join('; ') }
|
||||
return
|
||||
}
|
||||
|
||||
@@ -538,7 +538,7 @@ export function useImageModelManager() {
|
||||
type: 'warning',
|
||||
messageKey: 'image.model.dynamicFailed',
|
||||
count: staticModels.length,
|
||||
detail: toErrorMessage(error)
|
||||
detail: toOptionalErrorDetail(error)
|
||||
}
|
||||
} finally {
|
||||
isLoadingDynamicModels.value = false
|
||||
@@ -607,7 +607,7 @@ export function useImageModelManager() {
|
||||
const cachedProvider = providers.value.find(p => p.id === selectedProviderId.value)
|
||||
|
||||
if (!cachedProvider) {
|
||||
throw new Error(`提供商不存在: ${selectedProviderId.value}`)
|
||||
throw new Error(`Provider not found: ${selectedProviderId.value}`)
|
||||
}
|
||||
|
||||
// 获取模型信息:优先使用缓存,不存在时通过registry构建
|
||||
@@ -619,7 +619,7 @@ export function useImageModelManager() {
|
||||
cachedModel = adapter.buildDefaultModel(selectedModelId.value)
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`无法构建模型 ${selectedModelId.value}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
`Unable to build model ${selectedModelId.value}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error }
|
||||
)
|
||||
}
|
||||
@@ -650,7 +650,7 @@ export function useImageModelManager() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to save config:', error)
|
||||
toast.error(`${t('image.config.saveFailed')}: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
toast.error(formatErrorSummary(t('image.config.saveFailed'), error))
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
type TextProvider,
|
||||
getBuiltinModelIds
|
||||
} from '@prompt-optimizer/core'
|
||||
import { getI18nErrorMessage } from '../../utils/error'
|
||||
import { formatErrorSummary, getI18nErrorMessage } from '../../utils/error'
|
||||
import { useModelAdvancedParameters } from './useModelAdvancedParameters'
|
||||
import { computeConnectionConfig } from './useConnectionConfig'
|
||||
import type { AppServices } from '../../types/services'
|
||||
@@ -51,6 +51,22 @@ export function useTextModelManager() {
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const getErrorDetail = (error: unknown, fallback = 'Unknown error') => {
|
||||
return getI18nErrorMessage(error, fallback)
|
||||
}
|
||||
|
||||
const withLocalizedError = (
|
||||
key: string,
|
||||
error: unknown,
|
||||
params: Record<string, unknown> = {},
|
||||
fallback = 'Unknown error'
|
||||
) => {
|
||||
return t(key, {
|
||||
...params,
|
||||
error: getErrorDetail(error, fallback),
|
||||
})
|
||||
}
|
||||
|
||||
const services = inject<Ref<AppServices | null>>('services', ref(null))
|
||||
if (!services.value) {
|
||||
throw new Error('Services not provided!')
|
||||
@@ -211,7 +227,7 @@ export function useTextModelManager() {
|
||||
providers.value = textAdapterRegistry?.getAllProviders?.() || []
|
||||
providersLoaded.value = true
|
||||
} catch (error) {
|
||||
console.error('加载文本模型提供商失败:', error)
|
||||
console.error('Failed to load text model providers:', error)
|
||||
toast.error(t('modelManager.loadFailed'))
|
||||
providers.value = []
|
||||
} finally {
|
||||
@@ -231,7 +247,7 @@ export function useTextModelManager() {
|
||||
label: model.name || model.id
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('加载 Provider 模型失败:', error)
|
||||
console.error('Failed to load provider models:', error)
|
||||
modelOptions.value = []
|
||||
}
|
||||
}
|
||||
@@ -254,7 +270,7 @@ export function useTextModelManager() {
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('加载模型失败:', error)
|
||||
console.error('Failed to load models:', error)
|
||||
toast.error(t('modelManager.loadFailed'))
|
||||
} finally {
|
||||
loadingModels.value = false
|
||||
@@ -272,13 +288,12 @@ export function useTextModelManager() {
|
||||
await llmService.testConnection(id)
|
||||
toast.success(t('modelManager.testSuccess', { provider: model.name }))
|
||||
} catch (error) {
|
||||
console.error('连接测试失败:', error)
|
||||
console.error('Connection test failed:', error)
|
||||
const model = await modelManager.getModel(id)
|
||||
const modelName = model?.name || id
|
||||
const errorMessage = getI18nErrorMessage(error, 'Unknown error')
|
||||
toast.error(t('modelManager.testFailed', {
|
||||
provider: modelName,
|
||||
error: errorMessage
|
||||
error: getErrorDetail(error)
|
||||
}))
|
||||
} finally {
|
||||
delete testingConnections.value[id]
|
||||
@@ -293,9 +308,8 @@ export function useTextModelManager() {
|
||||
await loadModels()
|
||||
toast.success(t('modelManager.enableSuccess'))
|
||||
} catch (error: unknown) {
|
||||
console.error('启用模型失败:', error)
|
||||
const message = getI18nErrorMessage(error, 'Unknown error')
|
||||
toast.error(t('modelManager.enableFailed', { error: message }))
|
||||
console.error('Failed to enable model:', error)
|
||||
toast.error(withLocalizedError('modelManager.enableFailed', error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -307,9 +321,8 @@ export function useTextModelManager() {
|
||||
await loadModels()
|
||||
toast.success(t('modelManager.disableSuccess'))
|
||||
} catch (error: unknown) {
|
||||
console.error('禁用模型失败:', error)
|
||||
const message = getI18nErrorMessage(error, 'Unknown error')
|
||||
toast.error(t('modelManager.disableFailed', { error: message }))
|
||||
console.error('Failed to disable model:', error)
|
||||
toast.error(withLocalizedError('modelManager.disableFailed', error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,9 +373,8 @@ export function useTextModelManager() {
|
||||
await loadModels()
|
||||
toast.success(t('modelManager.deleteSuccess'))
|
||||
} catch (error: unknown) {
|
||||
console.error('删除模型失败:', error)
|
||||
const message = getI18nErrorMessage(error, 'Unknown error')
|
||||
toast.error(t('modelManager.deleteFailed', { error: message }))
|
||||
console.error('Failed to delete model:', error)
|
||||
toast.error(withLocalizedError('modelManager.deleteFailed', error))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -485,7 +497,7 @@ export function useTextModelManager() {
|
||||
// 编辑时不自动刷新模型列表,避免不必要的网络请求和延迟
|
||||
// 用户可以通过手动点击刷新按钮来获取最新模型列表
|
||||
} catch (error) {
|
||||
console.error('加载模型失败:', error)
|
||||
console.error('Failed to load models:', error)
|
||||
toast.error(t('modelManager.loadFailed'))
|
||||
} finally {
|
||||
formReady.value = true
|
||||
@@ -556,11 +568,11 @@ export function useTextModelManager() {
|
||||
form.value.modelId = fetchedModels[0].value
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('获取模型列表失败:', error)
|
||||
console.error('Failed to fetch model list:', error)
|
||||
|
||||
// Keep UX consistent: if dynamic fetch fails, fall back to static models
|
||||
// but surface the failure to avoid a misleading "success" toast.
|
||||
const errorMessage = getI18nErrorMessage(error, t('modelManager.loadFailed'))
|
||||
const errorMessage = getErrorDetail(error, t('modelManager.loadFailed'))
|
||||
|
||||
let staticCount: number
|
||||
try {
|
||||
@@ -601,12 +613,12 @@ export function useTextModelManager() {
|
||||
|
||||
const updateExistingModel = async () => {
|
||||
if (!form.value.originalId) {
|
||||
throw new Error('编辑会话无效')
|
||||
throw new Error('Invalid edit session')
|
||||
}
|
||||
|
||||
const existingConfig = await modelManager.getModel(form.value.originalId)
|
||||
if (!existingConfig) {
|
||||
throw new Error('模型不存在')
|
||||
throw new Error('Model not found')
|
||||
}
|
||||
|
||||
const connectionConfig: TextConnectionConfig = {
|
||||
@@ -706,7 +718,7 @@ export function useTextModelManager() {
|
||||
|
||||
try {
|
||||
if (!form.value.providerId || !form.value.modelId) {
|
||||
throw new Error('模型未选择')
|
||||
throw new Error('No model selected')
|
||||
}
|
||||
|
||||
// 编辑模式下获取现有配置,新增模式下为 undefined
|
||||
@@ -751,18 +763,19 @@ export function useTextModelManager() {
|
||||
try {
|
||||
await modelManager.deleteModel(tempConfig.id)
|
||||
} catch (cleanupError) {
|
||||
console.warn('清理临时测试模型失败:', cleanupError)
|
||||
console.warn('Failed to clean up temporary test model:', cleanupError)
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('连接测试失败:', error)
|
||||
console.error('Connection test failed:', error)
|
||||
const displayName = form.value.name || form.value.modelId
|
||||
const summary = t('modelManager.testConnection')
|
||||
formConnectionStatus.value = {
|
||||
type: 'error',
|
||||
message: t('modelManager.testFailed', { provider: displayName, error: error instanceof Error ? error.message : 'Unknown error' })
|
||||
message: formatErrorSummary(`${summary} (${displayName})`, error)
|
||||
}
|
||||
toast.error(t('modelManager.testFailed', { provider: displayName, error: error instanceof Error ? error.message : 'Unknown error' }))
|
||||
toast.error(withLocalizedError('modelManager.testFailed', error, { provider: displayName }))
|
||||
} finally {
|
||||
isTestingFormConnection.value = false
|
||||
}
|
||||
|
||||
@@ -88,19 +88,19 @@ export function usePerformanceMonitor(componentName: string = 'Unknown') {
|
||||
const suggestions: string[] = []
|
||||
|
||||
if (updateCount.value > 50) {
|
||||
suggestions.push('组件更新过于频繁,考虑使用 debounce 或 throttle')
|
||||
suggestions.push('Component updates are too frequent. Consider using debounce or throttle.')
|
||||
}
|
||||
|
||||
if (metrics.value.renderTime > 16) {
|
||||
suggestions.push('渲染时间超过 16ms,可能影响 60fps 体验')
|
||||
suggestions.push('Render time exceeds 16ms and may impact a 60fps experience.')
|
||||
}
|
||||
|
||||
if (memoryUsage.value > 50 * 1024 * 1024) { // 50MB
|
||||
suggestions.push('内存使用量较高,检查是否存在内存泄漏')
|
||||
suggestions.push('Memory usage is high. Check for possible memory leaks.')
|
||||
}
|
||||
|
||||
if (renderCount.value > 0 && updateCount.value / renderCount.value > 10) {
|
||||
suggestions.push('更新渲染比例过高,考虑优化响应式数据')
|
||||
suggestions.push('The update-to-render ratio is high. Consider optimizing reactive data flow.')
|
||||
}
|
||||
|
||||
return suggestions
|
||||
@@ -126,11 +126,11 @@ export function usePerformanceMonitor(componentName: string = 'Unknown') {
|
||||
else if (memoryMB > 50) score -= 10
|
||||
else if (memoryMB > 25) score -= 5
|
||||
|
||||
if (score >= 90) return { grade: 'A', color: 'success', text: '优秀' }
|
||||
if (score >= 80) return { grade: 'B', color: 'info', text: '良好' }
|
||||
if (score >= 70) return { grade: 'C', color: 'warning', text: '一般' }
|
||||
if (score >= 60) return { grade: 'D', color: 'warning', text: '较差' }
|
||||
return { grade: 'F', color: 'error', text: '需要优化' }
|
||||
if (score >= 90) return { grade: 'A', color: 'success', text: 'Excellent' }
|
||||
if (score >= 80) return { grade: 'B', color: 'info', text: 'Good' }
|
||||
if (score >= 70) return { grade: 'C', color: 'warning', text: 'Acceptable' }
|
||||
if (score >= 60) return { grade: 'D', color: 'warning', text: 'Poor' }
|
||||
return { grade: 'F', color: 'error', text: 'Needs Improvement' }
|
||||
})
|
||||
|
||||
// 开始性能监控
|
||||
|
||||
@@ -177,8 +177,8 @@ export function useContextUserOptimization(
|
||||
|
||||
toast.success(t('toast.success.optimizeSuccess'))
|
||||
} catch (error: unknown) {
|
||||
console.error('创建历史记录失败:', error)
|
||||
toast.error('创建历史记录失败: ' + getI18nErrorMessage(error, t('toast.error.optimizeFailed')))
|
||||
console.error('Failed to create history record:', error)
|
||||
toast.warning(t('toast.warning.saveHistoryFailed'))
|
||||
} finally {
|
||||
state.isOptimizing = false
|
||||
}
|
||||
@@ -288,14 +288,14 @@ export function useContextUserOptimization(
|
||||
|
||||
toast.success(t('toast.success.iterateComplete'))
|
||||
} catch (error: unknown) {
|
||||
console.error('[History] 迭代记录失败:', error)
|
||||
console.error('[History] Failed to save the iteration record:', error)
|
||||
toast.warning(t('toast.warning.saveHistoryFailed'))
|
||||
} finally {
|
||||
state.isIterating = false
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error('[Iterate] 迭代失败:', error)
|
||||
console.error('[Iterate] Iteration failed:', error)
|
||||
toast.error(t('toast.error.iterateFailed'))
|
||||
state.isIterating = false
|
||||
}
|
||||
@@ -303,7 +303,7 @@ export function useContextUserOptimization(
|
||||
selectedIterateTemplate.value.id,
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
console.error('[Iterate] 迭代失败:', error)
|
||||
console.error('[Iterate] Iteration failed:', error)
|
||||
toast.error(t('toast.error.iterateFailed'))
|
||||
state.isIterating = false
|
||||
}
|
||||
@@ -436,7 +436,7 @@ export function useContextUserOptimization(
|
||||
state.currentVersions = updatedChain.versions
|
||||
state.currentVersionId = updatedChain.currentRecord.id
|
||||
} catch (error: unknown) {
|
||||
console.error('[useContextUserOptimization] 保存本地修改失败:', error)
|
||||
console.error('[useContextUserOptimization] Failed to save local edits:', error)
|
||||
toast.warning(t('toast.warning.saveHistoryFailed'))
|
||||
}
|
||||
},
|
||||
|
||||
@@ -252,7 +252,7 @@ export function useConversationOptimization(
|
||||
if (messageId) {
|
||||
restoredMap.set(messageId, value)
|
||||
hasMigrated = true
|
||||
console.log(`[ConversationOptimization] 迁移旧格式 key: ${key} → ${messageId}`)
|
||||
console.log(`[ConversationOptimization] Migrated legacy key format: ${key} -> ${messageId}`)
|
||||
}
|
||||
} else {
|
||||
// 新格式 key,直接使用
|
||||
@@ -264,7 +264,7 @@ export function useConversationOptimization(
|
||||
|
||||
// 🔧 如果发生了迁移,立即同步到 session store 以保存新格式
|
||||
if (hasMigrated) {
|
||||
console.log('[ConversationOptimization] 检测到旧格式 key,已自动迁移并保存')
|
||||
console.log('[ConversationOptimization] Detected legacy key format; migrated and saved automatically')
|
||||
syncMessageChainMapToSession()
|
||||
}
|
||||
}
|
||||
@@ -319,7 +319,7 @@ export function useConversationOptimization(
|
||||
const latest = chain.versions[chain.versions.length - 1]
|
||||
return latest ? latest.version : 0
|
||||
} catch (error) {
|
||||
console.warn(`[ConversationOptimization] 获取消息 ${messageId} 版本号失败:`, error)
|
||||
console.warn(`[ConversationOptimization] Failed to get the applied version for message ${messageId}:`, error)
|
||||
return 0 // 失败时默认 v0
|
||||
}
|
||||
}
|
||||
@@ -363,7 +363,7 @@ export function useConversationOptimization(
|
||||
optimizedPrompt.value = chain.currentRecord.optimizedPrompt
|
||||
currentRecordId.value = chain.currentRecord.id
|
||||
} catch (error) {
|
||||
console.error('[ConversationOptimization] 加载工作链失败:', error)
|
||||
console.error('[ConversationOptimization] Failed to load the working chain:', error)
|
||||
toast.error(t('toast.error.loadChainFailed'))
|
||||
// 重置为首次优化状态
|
||||
currentChainId.value = ''
|
||||
@@ -519,7 +519,7 @@ export function useConversationOptimization(
|
||||
// 显示成功提示
|
||||
toast.success(t('toast.success.optimizeAndApply', { version: 'v1' }))
|
||||
} catch (error) {
|
||||
console.error('[ConversationOptimization] 保存历史记录失败:', error)
|
||||
console.error('[ConversationOptimization] Failed to save history:', error)
|
||||
toast.warning(t('toast.warning.saveHistoryFailed'))
|
||||
// 优化结果仍然可用,但未保存历史
|
||||
} finally {
|
||||
@@ -527,14 +527,14 @@ export function useConversationOptimization(
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error('[ConversationOptimization] 优化失败:', error)
|
||||
console.error('[ConversationOptimization] Optimization failed:', error)
|
||||
toast.error(getI18nErrorMessage(error, t('toast.error.optimizeFailed')))
|
||||
isOptimizing.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[ConversationOptimization] 优化失败:', error)
|
||||
console.error('[ConversationOptimization] Optimization failed:', error)
|
||||
toast.error(getI18nErrorMessage(error, t('toast.error.optimizeFailed')))
|
||||
isOptimizing.value = false
|
||||
}
|
||||
@@ -665,14 +665,14 @@ export function useConversationOptimization(
|
||||
toast.success(t('toast.success.optimizeAndApply', { version: `v${versionNumber}` }))
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ConversationOptimization] 保存迭代历史失败:', error)
|
||||
console.error('[ConversationOptimization] Failed to save iteration history:', error)
|
||||
toast.warning(t('toast.warning.saveHistoryFailed'))
|
||||
} finally {
|
||||
isOptimizing.value = false
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error('[ConversationOptimization] 迭代失败:', error)
|
||||
console.error('[ConversationOptimization] Iteration failed:', error)
|
||||
toast.error(getI18nErrorMessage(error, t('toast.error.iterateFailed')))
|
||||
isOptimizing.value = false
|
||||
}
|
||||
@@ -686,7 +686,7 @@ export function useConversationOptimization(
|
||||
},
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[ConversationOptimization] 迭代失败:', error)
|
||||
console.error('[ConversationOptimization] Iteration failed:', error)
|
||||
toast.error(getI18nErrorMessage(error, t('toast.error.iterateFailed')))
|
||||
isOptimizing.value = false
|
||||
}
|
||||
@@ -767,7 +767,7 @@ export function useConversationOptimization(
|
||||
|
||||
const removed = removeMessageMapping(messageId)
|
||||
if (removed) {
|
||||
console.log('[ConversationOptimization] 已清理消息映射:', messageId)
|
||||
console.log('[ConversationOptimization] Cleaned up the message mapping:', messageId)
|
||||
}
|
||||
|
||||
if (selectedMessageId.value === messageId) {
|
||||
@@ -784,7 +784,7 @@ export function useConversationOptimization(
|
||||
optimizedPrompt.value = ''
|
||||
optimizedReasoning.value = ''
|
||||
currentRecordId.value = ''
|
||||
console.log('[ConversationOptimization] 已清空当前选中状态')
|
||||
console.log('[ConversationOptimization] Cleared the current selection state')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -870,7 +870,7 @@ export function useConversationOptimization(
|
||||
currentVersions.value = updatedChain.versions
|
||||
currentRecordId.value = updatedChain.currentRecord.id
|
||||
} catch (error: unknown) {
|
||||
console.error('[useConversationOptimization] 保存本地修改失败:', error)
|
||||
console.error('[useConversationOptimization] Failed to save local edits:', error)
|
||||
toast.warning(t('toast.warning.saveHistoryFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ export function useEvaluationContext(): UseEvaluationReturn {
|
||||
const evaluation = inject(EvaluationKey)
|
||||
if (!evaluation) {
|
||||
throw new Error(
|
||||
'[useEvaluationContext] 必须在提供了评估上下文的组件树中使用。' +
|
||||
'请确保父组件调用了 provideEvaluation()。'
|
||||
'[useEvaluationContext] This composable must be used inside a component tree that provides evaluation context. ' +
|
||||
'Make sure the parent component calls provideEvaluation().'
|
||||
)
|
||||
}
|
||||
return evaluation
|
||||
|
||||
@@ -131,7 +131,7 @@ const summarizeText = (content: string | undefined, maxLength = 80): string => {
|
||||
: normalized
|
||||
}
|
||||
|
||||
const WORKSPACE_PROMPT_MARKER = '【当前工作区要优化的提示词】'
|
||||
const WORKSPACE_PROMPT_MARKER = '[Current workspace prompt under optimization]'
|
||||
const ANALYSIS_CONVERSATION_CONTEXT_MAX_LINES = 6
|
||||
|
||||
const isProUserEvaluationContext = (
|
||||
@@ -162,8 +162,8 @@ const toVariableDesignContextBlock = (
|
||||
return {
|
||||
kind: 'variables',
|
||||
label: 'Variable Structure',
|
||||
summary: '这里只说明模板变量结构,不包含任何测试值。',
|
||||
content: `变量: ${variableNames.join(', ')}`,
|
||||
summary: 'This block describes the template variable structure only. It does not include test values.',
|
||||
content: `Variables: ${variableNames.join(', ')}`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,8 +214,8 @@ const toConversationDesignContextBlock = (
|
||||
})()
|
||||
|
||||
const contentLines = [
|
||||
`目标消息角色: ${targetRole}`,
|
||||
'会话上下文:',
|
||||
`Target message role: ${targetRole}`,
|
||||
'Conversation context:',
|
||||
]
|
||||
|
||||
if (visibleConversationLines.length) {
|
||||
@@ -225,7 +225,7 @@ const toConversationDesignContextBlock = (
|
||||
return {
|
||||
kind: 'conversation',
|
||||
label: 'Conversation Design Context',
|
||||
summary: `当前分析目标是 ${targetRole} 消息;会话中的该位置已用“${WORKSPACE_PROMPT_MARKER}”标记。`,
|
||||
summary: `The current analysis target is the ${targetRole} message. This position is marked as "${WORKSPACE_PROMPT_MARKER}" in the conversation.`,
|
||||
content: contentLines.join('\n'),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ export function usePromptHistory(
|
||||
|
||||
toast.success(t('toast.success.historyLoaded'))
|
||||
} catch (error) {
|
||||
console.error('[History] 加载历史记录失败:', error)
|
||||
console.error('[History] Failed to load history:', error)
|
||||
toast.error(t('toast.error.loadHistoryFailed'))
|
||||
}
|
||||
},
|
||||
|
||||
@@ -187,8 +187,8 @@ export function usePromptOptimizer(
|
||||
|
||||
toast.success(t('toast.success.optimizeSuccess'))
|
||||
} catch (error: unknown) {
|
||||
console.error('创建历史记录失败:', error)
|
||||
toast.error('创建历史记录失败: ' + getI18nErrorMessage(error, t('toast.error.optimizeFailed')))
|
||||
console.error('Failed to create history record:', error)
|
||||
toast.warning(t('toast.warning.saveHistoryFailed'))
|
||||
} finally {
|
||||
state.isOptimizing = false
|
||||
}
|
||||
@@ -326,8 +326,8 @@ export function usePromptOptimizer(
|
||||
|
||||
toast.success(t('toast.success.optimizeSuccess'))
|
||||
} catch (error: unknown) {
|
||||
console.error('创建历史记录失败:', error)
|
||||
toast.error('创建历史记录失败: ' + getI18nErrorMessage(error, t('toast.error.optimizeFailed')))
|
||||
console.error('Failed to create the history record:', error)
|
||||
toast.error('Failed to create the history record: ' + getI18nErrorMessage(error, t('toast.error.optimizeFailed')))
|
||||
} finally {
|
||||
state.isOptimizing = false
|
||||
}
|
||||
@@ -403,14 +403,14 @@ export function usePromptOptimizer(
|
||||
|
||||
toast.success(t('toast.success.iterateComplete'))
|
||||
} catch (error: unknown) {
|
||||
console.error('[History] 迭代记录失败:', error)
|
||||
console.error('[History] Failed to save the iteration record:', error)
|
||||
toast.warning(t('toast.warning.historyFailed'))
|
||||
} finally {
|
||||
state.isIterating = false
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
console.error('[Iterate] 迭代失败:', error)
|
||||
console.error('[Iterate] Iteration failed:', error)
|
||||
toast.error(t('toast.error.iterateFailed'))
|
||||
state.isIterating = false
|
||||
}
|
||||
@@ -418,7 +418,7 @@ export function usePromptOptimizer(
|
||||
state.selectedIterateTemplate.id
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
console.error('[Iterate] 迭代失败:', error)
|
||||
console.error('[Iterate] Iteration failed:', error)
|
||||
toast.error(t('toast.error.iterateFailed'))
|
||||
state.isIterating = false
|
||||
}
|
||||
@@ -484,7 +484,7 @@ export function usePromptOptimizer(
|
||||
state.currentVersions = updatedChain.versions
|
||||
state.currentVersionId = updatedChain.currentRecord.id
|
||||
} catch (error: unknown) {
|
||||
console.error('[usePromptOptimizer] 保存本地修改失败:', error)
|
||||
console.error('[usePromptOptimizer] Failed to save local edits:', error)
|
||||
toast.warning(t('toast.warning.saveHistoryFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,8 @@ export function usePromptTester(
|
||||
} else {
|
||||
// 系统提示词模式:提示词作为系统消息
|
||||
systemPrompt = selectedPrompt
|
||||
userPrompt = testContent || '请按照你的角色设定,展示你的能力并与我互动。'
|
||||
userPrompt =
|
||||
testContent || 'Please follow your role instructions and show your capabilities in the conversation.'
|
||||
}
|
||||
|
||||
// 变量:合并全局变量 + 测试变量
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { useToast } from '../ui/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getI18nErrorMessage } from '../../utils/error'
|
||||
import { formatErrorSummary } from '../../utils/error'
|
||||
import type { AppServices } from '../../types/services'
|
||||
import { VARIABLE_VALIDATION, isValidVariableName } from '../../types/variable'
|
||||
import type {
|
||||
@@ -96,8 +96,7 @@ export function useVariableExtraction(
|
||||
toast.info(t('evaluation.variableExtraction.noVariables'))
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = getI18nErrorMessage(error, 'Unknown error')
|
||||
toast.error(`${t('evaluation.variableExtraction.extractFailed')}: ${errorMsg}`)
|
||||
toast.error(formatErrorSummary(t('evaluation.variableExtraction.extractFailed'), error))
|
||||
console.error('[useVariableExtraction] Extract failed:', error)
|
||||
} finally {
|
||||
isExtracting.value = false
|
||||
|
||||
@@ -35,7 +35,7 @@ export function useSessionRestoreCoordinator(restoreFn: () => Promise<void> | vo
|
||||
const executeRestore = async () => {
|
||||
// 🔧 互斥检查:如果正在恢复中,设置 pending 标志后返回
|
||||
if (isRestoring.value) {
|
||||
console.warn('[SessionRestoreCoordinator] executeRestore 已在执行中,设置 pendingRestore 标志')
|
||||
console.warn('[SessionRestoreCoordinator] executeRestore is already running; setting the pendingRestore flag')
|
||||
pendingRestore.value = true
|
||||
return
|
||||
}
|
||||
@@ -55,11 +55,11 @@ export function useSessionRestoreCoordinator(restoreFn: () => Promise<void> | vo
|
||||
// 🔧 Codex 建议:使用 queueMicrotask 异步排队,避免递归压力(而非 await 递归)
|
||||
if (pendingRestore.value) {
|
||||
pendingRestore.value = false
|
||||
console.log('[SessionRestoreCoordinator] 检测到 pendingRestore,异步排队补跑恢复')
|
||||
console.log('[SessionRestoreCoordinator] Detected pendingRestore; queueing another restore asynchronously')
|
||||
queueMicrotask(() => {
|
||||
// 🔧 Codex 修复:组件卸载后跳过恢复,避免无意义工作/日志噪声
|
||||
if (isUnmounted.value) {
|
||||
console.log('[SessionRestoreCoordinator] 组件已卸载,跳过 pending restore')
|
||||
console.log('[SessionRestoreCoordinator] Component is unmounted; skipping the pending restore')
|
||||
return
|
||||
}
|
||||
// 🔧 Codex 修复:添加错误处理,避免未处理的 Promise rejection
|
||||
|
||||
@@ -18,7 +18,7 @@ export async function getPreference<T>(
|
||||
if (services.value?.preferenceService) {
|
||||
return services.value.preferenceService.get(key, defaultValue);
|
||||
}
|
||||
throw new Error(`[getPreference] preferenceService不可用,无法获取键: ${key}`);
|
||||
throw new Error(`[getPreference] preferenceService is unavailable. Cannot read key: ${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +36,7 @@ export async function setPreference<T>(
|
||||
if (services.value?.preferenceService) {
|
||||
return services.value.preferenceService.set(key, value);
|
||||
}
|
||||
throw new Error(`[setPreference] preferenceService不可用,无法设置键: ${key}`);
|
||||
throw new Error(`[setPreference] preferenceService is unavailable. Cannot write key: ${key}`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,4 +62,4 @@ export function usePreferences(services: Ref<AppServices | null>) {
|
||||
};
|
||||
|
||||
return { getPreference: get, setPreference: set };
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ export function useAppInitializer(): {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
console.log('[AppInitializer] 开始应用初始化...');
|
||||
console.log('[AppInitializer] Starting application initialization...');
|
||||
|
||||
|
||||
let modelManager: IModelManager;
|
||||
@@ -126,15 +126,15 @@ export function useAppInitializer(): {
|
||||
let textAdapterRegistryInstance: ITextAdapterRegistry | undefined;
|
||||
|
||||
if (isRunningInElectron()) {
|
||||
console.log('[AppInitializer] 检测到Electron环境,等待API就绪...');
|
||||
console.log('[AppInitializer] Electron environment detected; waiting for API readiness...');
|
||||
|
||||
// 等待 Electron API 完全就绪
|
||||
const apiReady = await waitForElectronApi();
|
||||
if (!apiReady) {
|
||||
throw new Error('Electron API 初始化超时,请检查preload脚本是否正确加载');
|
||||
throw new Error('Electron API initialization timed out. Please verify that the preload script loaded correctly.')
|
||||
}
|
||||
|
||||
console.log('[AppInitializer] Electron API 就绪,初始化代理服务...');
|
||||
console.log('[AppInitializer] Electron API is ready; initializing proxy services...');
|
||||
|
||||
// 在Electron环境中,不需要storageProvider
|
||||
// 所有存储操作都通过各个manager的代理完成
|
||||
@@ -158,7 +158,7 @@ export function useAppInitializer(): {
|
||||
imageService = new ElectronImageServiceProxy();
|
||||
|
||||
// 🆕 图像存储服务:Electron 渲染进程同样使用 IndexedDB(与 Web 行为一致)
|
||||
console.log('[AppInitializer] 初始化图像存储服务(Electron)...');
|
||||
console.log('[AppInitializer] Initializing image storage service (Electron)...');
|
||||
imageStorageService = createImageStorageService({
|
||||
maxCacheSize: 50 * 1024 * 1024, // 50 MB
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
|
||||
@@ -223,15 +223,15 @@ export function useAppInitializer(): {
|
||||
variableValueGenerationService = createVariableValueGenerationService(llmService, modelManager, templateManager);
|
||||
|
||||
// 🆕 读取当前上下文的模式
|
||||
console.log('[AppInitializer] 读取当前上下文模式...');
|
||||
console.log('[AppInitializer] Reading current context mode...');
|
||||
const contextMode = ref<ContextMode>(DEFAULT_CONTEXT_MODE);
|
||||
try {
|
||||
const currentId = await contextRepo.getCurrentId();
|
||||
const currentContext = await contextRepo.get(currentId);
|
||||
contextMode.value = currentContext.mode || DEFAULT_CONTEXT_MODE;
|
||||
console.log('[AppInitializer] 当前上下文模式:', contextMode.value);
|
||||
console.log('[AppInitializer] Current context mode:', contextMode.value);
|
||||
} catch (err) {
|
||||
console.warn('[AppInitializer] 读取上下文模式失败,使用默认值:', err);
|
||||
console.warn('[AppInitializer] Failed to read context mode; using default value:', err);
|
||||
}
|
||||
|
||||
services.value = {
|
||||
@@ -257,7 +257,7 @@ export function useAppInitializer(): {
|
||||
variableExtractionService, // 🆕 变量提取服务
|
||||
variableValueGenerationService, // 🆕 变量值生成服务
|
||||
};
|
||||
console.log('[AppInitializer] Electron代理服务初始化完成');
|
||||
console.log('[AppInitializer] Electron proxy services initialized');
|
||||
|
||||
// 只保留 session 引用的图片:启动后做一次 best-effort GC
|
||||
if (imageStorageService) {
|
||||
@@ -267,7 +267,7 @@ export function useAppInitializer(): {
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('[AppInitializer] 检测到Web环境,初始化完整服务...');
|
||||
console.log('[AppInitializer] Web environment detected; initializing full service set...');
|
||||
// 在Web环境中,我们创建一套完整的、真实的服务
|
||||
const storageProvider = StorageFactory.create('dexie');
|
||||
const stage1StartupRepairReport = await runStorageStartupSafetyCheck(storageProvider)
|
||||
@@ -291,7 +291,7 @@ export function useAppInitializer(): {
|
||||
const imageModelManagerInstance = createImageModelManager(storageProvider, imageAdapterRegistry);
|
||||
|
||||
// 🆕 创建图像存储服务(独立 IndexedDB 数据库)
|
||||
console.log('[AppInitializer] 初始化图像存储服务...');
|
||||
console.log('[AppInitializer] Initializing image storage service...');
|
||||
imageStorageService = createImageStorageService({
|
||||
maxCacheSize: 50 * 1024 * 1024, // 50 MB
|
||||
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 天
|
||||
@@ -313,7 +313,7 @@ export function useAppInitializer(): {
|
||||
// 如果将来需要迁移,可以使用 migrateLegacySessions() 函数
|
||||
|
||||
// Initialize language service first, as template manager depends on it
|
||||
console.log('[AppInitializer] 初始化语言服务...');
|
||||
console.log('[AppInitializer] Initializing language service...');
|
||||
await languageService.initialize();
|
||||
|
||||
const templateManagerInstance = createTemplateManager(storageProvider, languageService);
|
||||
@@ -324,7 +324,7 @@ export function useAppInitializer(): {
|
||||
const historyManagerInstance = createHistoryManager(storageProvider, modelManagerInstance);
|
||||
|
||||
// Now ensure model manager with async init is ready (template manager no longer needs async init)
|
||||
console.log('[AppInitializer] 确保模型管理器初始化完成...');
|
||||
console.log('[AppInitializer] Ensuring model manager initialization is complete...');
|
||||
await modelManagerInstance.ensureInitialized();
|
||||
|
||||
// Assign instances after they are fully initialized
|
||||
@@ -389,7 +389,7 @@ export function useAppInitializer(): {
|
||||
};
|
||||
|
||||
// Services that depend on initialized managers
|
||||
console.log('[AppInitializer] 创建依赖其他管理器的服务...');
|
||||
console.log('[AppInitializer] Creating services that depend on initialized managers...');
|
||||
llmService = createLLMService(modelManagerInstance);
|
||||
const imageUnderstandingService = createImageUnderstandingService({
|
||||
registry: textAdapterRegistryInstance,
|
||||
@@ -455,15 +455,15 @@ export function useAppInitializer(): {
|
||||
variableValueGenerationService = createVariableValueGenerationService(llmService, modelManagerAdapter, templateManagerAdapter);
|
||||
|
||||
// 🆕 读取当前上下文的模式
|
||||
console.log('[AppInitializer] 读取当前上下文模式...');
|
||||
console.log('[AppInitializer] Reading current context mode...');
|
||||
const contextMode = ref<ContextMode>(DEFAULT_CONTEXT_MODE);
|
||||
try {
|
||||
const currentId = await contextRepo.getCurrentId();
|
||||
const currentContext = await contextRepo.get(currentId);
|
||||
contextMode.value = currentContext.mode || DEFAULT_CONTEXT_MODE;
|
||||
console.log('[AppInitializer] 当前上下文模式:', contextMode.value);
|
||||
console.log('[AppInitializer] Current context mode:', contextMode.value);
|
||||
} catch (err) {
|
||||
console.warn('[AppInitializer] 读取上下文模式失败,使用默认值:', err);
|
||||
console.warn('[AppInitializer] Failed to read context mode; using default value:', err);
|
||||
}
|
||||
|
||||
// 将所有服务实例赋值给 services.value
|
||||
@@ -491,7 +491,7 @@ export function useAppInitializer(): {
|
||||
variableValueGenerationService, // 🆕 变量值生成服务
|
||||
};
|
||||
|
||||
console.log('[AppInitializer] 所有服务初始化完成');
|
||||
console.log('[AppInitializer] All services initialized');
|
||||
|
||||
// 只保留 session 引用的图片:启动后做一次 best-effort GC
|
||||
if (imageStorageService) {
|
||||
@@ -503,12 +503,12 @@ export function useAppInitializer(): {
|
||||
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
console.error("[AppInitializer] 关键服务初始化失败:", errorMessage);
|
||||
console.error("[AppInitializer] 错误详情:", err);
|
||||
console.error("[AppInitializer] Critical service initialization failed:", errorMessage);
|
||||
console.error("[AppInitializer] Error details:", err);
|
||||
error.value = err instanceof Error ? err : new Error(String(err));
|
||||
} finally {
|
||||
isInitializing.value = false;
|
||||
console.log('[AppInitializer] 应用初始化完成');
|
||||
console.log('[AppInitializer] Application initialization complete');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ export function useNaiveTheme() {
|
||||
const isCurrentThemeDark = computed(() => isDarkTheme.value)
|
||||
|
||||
// 当前主题名称
|
||||
const currentThemeName = computed(() => themeConfig.value.name)
|
||||
const currentThemeLabelKey = computed(() => themeConfig.value.labelKey)
|
||||
|
||||
// 主题切换函数
|
||||
const changeTheme = (newThemeId: string): boolean => {
|
||||
@@ -74,7 +74,7 @@ export function useNaiveTheme() {
|
||||
themeConfig,
|
||||
naiveTheme,
|
||||
themeOverrides,
|
||||
currentThemeName,
|
||||
currentThemeLabelKey,
|
||||
availableThemes,
|
||||
isCurrentThemeDark,
|
||||
|
||||
@@ -103,4 +103,4 @@ export function useNaiveTheme() {
|
||||
}
|
||||
|
||||
// 默认导出,方便使用
|
||||
export default useNaiveTheme
|
||||
export default useNaiveTheme
|
||||
@@ -191,7 +191,7 @@ export function useResponsiveTestLayout(options: ResponsiveTestLayoutOptions = {
|
||||
showPrimary: !isMobile.value
|
||||
},
|
||||
singleMode: {
|
||||
title: '测试结果',
|
||||
title: 'test.testResult',
|
||||
showToolbar: isDesktop.value
|
||||
},
|
||||
display: {
|
||||
|
||||
@@ -180,22 +180,38 @@ export function useTestModeConfig(
|
||||
switch (optimizationMode.value) {
|
||||
case 'system':
|
||||
return {
|
||||
title: '系统提示词测试模式',
|
||||
description: '在此模式下,原始/优化提示词作为系统消息,您需要提供用户问题进行测试。',
|
||||
requirements: ['需要提供测试内容作为用户问题', '支持对比测试原始和优化版本'],
|
||||
features: ['智能输入框', '对比模式', '全屏编辑', '高级对话管理']
|
||||
title: 'test.modeHelp.system.title',
|
||||
description: 'test.modeHelp.system.description',
|
||||
requirements: [
|
||||
'test.modeHelp.system.requirements.contentRequired',
|
||||
'test.modeHelp.system.requirements.compareSupported',
|
||||
],
|
||||
features: [
|
||||
'test.modeHelp.common.smartInput',
|
||||
'test.modeHelp.common.compareMode',
|
||||
'test.modeHelp.common.fullscreenEdit',
|
||||
'test.modeHelp.common.advancedConversation',
|
||||
]
|
||||
}
|
||||
case 'user':
|
||||
return {
|
||||
title: '用户提示词测试模式',
|
||||
description: '在此模式下,原始/优化提示词直接作为用户消息进行测试。',
|
||||
requirements: ['无需额外测试内容', '直接测试提示词效果'],
|
||||
features: ['简化界面', '对比模式', '全屏编辑', '高级对话管理']
|
||||
title: 'test.modeHelp.user.title',
|
||||
description: 'test.modeHelp.user.description',
|
||||
requirements: [
|
||||
'test.modeHelp.user.requirements.noExtraContent',
|
||||
'test.modeHelp.user.requirements.directPromptTest',
|
||||
],
|
||||
features: [
|
||||
'test.modeHelp.user.features.simpleLayout',
|
||||
'test.modeHelp.common.compareMode',
|
||||
'test.modeHelp.common.fullscreenEdit',
|
||||
'test.modeHelp.common.advancedConversation',
|
||||
]
|
||||
}
|
||||
default:
|
||||
return {
|
||||
title: '未知模式',
|
||||
description: '当前模式配置不正确',
|
||||
title: 'test.modeHelp.unknown.title',
|
||||
description: 'test.modeHelp.unknown.description',
|
||||
requirements: [],
|
||||
features: []
|
||||
}
|
||||
@@ -218,11 +234,11 @@ export function useTestModeConfig(
|
||||
const errors: string[] = []
|
||||
|
||||
if (!hasPrompt) {
|
||||
errors.push('需要提供提示词')
|
||||
errors.push('test.validation.promptRequired')
|
||||
}
|
||||
|
||||
if (requiresTestContent.value && !testContent.trim()) {
|
||||
errors.push('需要提供测试内容')
|
||||
errors.push('test.validation.contentRequired')
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useToast } from '../ui/useToast'
|
||||
import { useFunctionModelManager } from '../model/useFunctionModelManager'
|
||||
import { useVariableValueGeneration } from './useVariableValueGeneration'
|
||||
import type { AppServices } from '../../types/services'
|
||||
import { getI18nErrorMessage } from '../../utils/error'
|
||||
import { formatErrorSummary } from '../../utils/error'
|
||||
|
||||
type VariableSource = 'predefined' | 'test' | 'global' | 'empty'
|
||||
|
||||
@@ -95,8 +95,7 @@ export function useSmartVariableValueGeneration(
|
||||
try {
|
||||
await functionModelManager.initialize()
|
||||
} catch (error) {
|
||||
const errorMsg = getI18nErrorMessage(error, 'Unknown error')
|
||||
toast.error(`${t('test.variableValueGeneration.generateFailed')}: ${errorMsg}`)
|
||||
toast.error(formatErrorSummary(t('test.variableValueGeneration.generateFailed'), error))
|
||||
console.error('[useSmartVariableValueGeneration] initialize failed:', error)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { ref, type Ref } from 'vue'
|
||||
import { useToast } from '../ui/useToast'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getI18nErrorMessage } from '../../utils/error'
|
||||
import { formatErrorSummary } from '../../utils/error'
|
||||
import type { AppServices } from '../../types/services'
|
||||
import type {
|
||||
VariableValueGenerationResponse,
|
||||
@@ -89,8 +89,7 @@ export function useVariableValueGeneration(
|
||||
toast.info(t('test.variableValueGeneration.noValues'))
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = getI18nErrorMessage(error, 'Unknown error')
|
||||
toast.error(`${t('test.variableValueGeneration.generateFailed')}: ${errorMsg}`)
|
||||
toast.error(formatErrorSummary(t('test.variableValueGeneration.generateFailed'), error))
|
||||
console.error('[useVariableValueGeneration] Generate failed:', error)
|
||||
} finally {
|
||||
isGenerating.value = false
|
||||
|
||||
@@ -209,7 +209,7 @@ export function useBasicWorkspaceLogic(options: UseBasicWorkspaceLogicOptions) {
|
||||
onOptimizeComplete?.(chain)
|
||||
toast.success(t('toast.success.optimizeSuccess'))
|
||||
} catch (error) {
|
||||
console.error('[useBasicWorkspaceLogic] 创建历史记录失败:', error)
|
||||
console.error('[useBasicWorkspaceLogic] Failed to create the history record:', error)
|
||||
currentVersions.value = []
|
||||
currentChainId.value = ''
|
||||
currentVersionId.value = ''
|
||||
@@ -342,7 +342,7 @@ export function useBasicWorkspaceLogic(options: UseBasicWorkspaceLogicOptions) {
|
||||
onIterateComplete?.(chain)
|
||||
toast.success(t('toast.success.iterateComplete'))
|
||||
} catch (error) {
|
||||
console.error('[useBasicWorkspaceLogic] 保存迭代记录失败:', error)
|
||||
console.error('[useBasicWorkspaceLogic] Failed to save the iteration record:', error)
|
||||
currentVersions.value = []
|
||||
currentChainId.value = ''
|
||||
currentVersionId.value = ''
|
||||
@@ -459,7 +459,7 @@ export function useBasicWorkspaceLogic(options: UseBasicWorkspaceLogicOptions) {
|
||||
onLocalEditComplete?.(chain)
|
||||
toast.success(t('toast.success.localEditSaved'))
|
||||
} catch (error) {
|
||||
console.error('[useBasicWorkspaceLogic] 保存本地编辑失败:', error)
|
||||
console.error('[useBasicWorkspaceLogic] Failed to save local edits:', error)
|
||||
toast.warning(t('toast.warning.saveHistoryFailed'))
|
||||
}
|
||||
}
|
||||
@@ -531,7 +531,7 @@ export function useBasicWorkspaceLogic(options: UseBasicWorkspaceLogicOptions) {
|
||||
currentChainId.value = chain.chainId
|
||||
currentVersionId.value = sessionStore.versionId || chain.currentRecord.id
|
||||
} catch (error) {
|
||||
console.error('[useBasicWorkspaceLogic] 加载版本失败:', error)
|
||||
console.error('[useBasicWorkspaceLogic] Failed to load versions:', error)
|
||||
currentVersions.value = []
|
||||
currentChainId.value = ''
|
||||
currentVersionId.value = ''
|
||||
|
||||
@@ -11,7 +11,7 @@ export const currentThemeId = ref<string>('light')
|
||||
// 主题类型定义
|
||||
export interface ThemeConfig {
|
||||
id: string
|
||||
name: string
|
||||
labelKey: string
|
||||
naiveTheme: GlobalTheme | null
|
||||
themeOverrides: GlobalThemeOverrides
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export interface ThemeConfig {
|
||||
export const naiveThemeConfigs: Record<string, ThemeConfig> = {
|
||||
light: {
|
||||
id: 'light',
|
||||
name: '日间模式',
|
||||
labelKey: 'theme.light',
|
||||
naiveTheme: lightTheme,
|
||||
themeOverrides: {
|
||||
common: {
|
||||
@@ -155,7 +155,7 @@ export const naiveThemeConfigs: Record<string, ThemeConfig> = {
|
||||
|
||||
dark: {
|
||||
id: 'dark',
|
||||
name: '夜间模式',
|
||||
labelKey: 'theme.dark',
|
||||
naiveTheme: darkTheme,
|
||||
themeOverrides: {
|
||||
common: {
|
||||
@@ -177,7 +177,7 @@ export const naiveThemeConfigs: Record<string, ThemeConfig> = {
|
||||
|
||||
blue: {
|
||||
id: 'blue',
|
||||
name: '蓝色模式',
|
||||
labelKey: 'theme.blue',
|
||||
naiveTheme: lightTheme,
|
||||
themeOverrides: {
|
||||
common: {
|
||||
@@ -306,7 +306,7 @@ export const naiveThemeConfigs: Record<string, ThemeConfig> = {
|
||||
|
||||
classic: {
|
||||
id: 'classic',
|
||||
name: '米杏模式',
|
||||
labelKey: 'theme.classic',
|
||||
naiveTheme: lightTheme,
|
||||
themeOverrides: {
|
||||
common: {
|
||||
@@ -444,7 +444,7 @@ export const naiveThemeConfigs: Record<string, ThemeConfig> = {
|
||||
|
||||
green: {
|
||||
id: 'green',
|
||||
name: '绿色模式',
|
||||
labelKey: 'theme.green',
|
||||
naiveTheme: darkTheme,
|
||||
themeOverrides: {
|
||||
common: {
|
||||
@@ -589,7 +589,7 @@ export const naiveThemeConfigs: Record<string, ThemeConfig> = {
|
||||
|
||||
purple: {
|
||||
id: 'purple',
|
||||
name: '暗紫模式',
|
||||
labelKey: 'theme.purple',
|
||||
naiveTheme: darkTheme,
|
||||
themeOverrides: {
|
||||
common: {
|
||||
|
||||
File renamed without changes.
File renamed without changes.
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user