├── .cursor └── rules │ ├── bug修复专家.mdc │ ├── code_review.mdc │ ├── md_output.mdc │ ├── pr_review.mdc │ └── 高级项目审计师.mdc ├── .cursorrules ├── .dockerignore ├── .env.example ├── .github └── workflows │ ├── docker.yml │ └── test.yml ├── .gitignore ├── .husky ├── pre-commit └── pre-commit.ps1 ├── .npmrc ├── .pnpmrc ├── .windsurfrules ├── Dockerfile ├── LICENSE ├── README.md ├── README_EN.md ├── api ├── auth.js ├── proxy.js ├── stream.js └── vercel-status.js ├── cursor_tips.md ├── dev.md ├── docker-compose.yml ├── docker ├── generate-auth.sh ├── generate-config.sh └── nginx.conf ├── docs ├── README.md ├── experience.md ├── llm-params-guide.md ├── prd.md ├── project-status.md ├── project-structure.md ├── scratchpad.md ├── technical-development-guide.md ├── vercel.md └── vercel_en.md ├── images ├── contrast.png ├── main.png └── vercel │ ├── import.png │ ├── redeploy.png │ └── setting.png ├── middleware.js ├── package.json ├── packages ├── core │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── services │ │ │ ├── data │ │ │ │ └── manager.ts │ │ │ ├── history │ │ │ │ ├── errors.ts │ │ │ │ ├── manager.ts │ │ │ │ └── types.ts │ │ │ ├── llm │ │ │ │ ├── errors.ts │ │ │ │ ├── service.ts │ │ │ │ └── types.ts │ │ │ ├── model │ │ │ │ ├── advancedParameterDefinitions.ts │ │ │ │ ├── defaults.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── manager.ts │ │ │ │ ├── types.ts │ │ │ │ └── validation.ts │ │ │ ├── prompt │ │ │ │ ├── errors.ts │ │ │ │ ├── factory.ts │ │ │ │ ├── service.ts │ │ │ │ └── types.ts │ │ │ ├── storage │ │ │ │ ├── adapter.ts │ │ │ │ ├── dexieStorageProvider.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── factory.ts │ │ │ │ ├── localStorageProvider.ts │ │ │ │ └── types.ts │ │ │ └── template │ │ │ │ ├── defaults.ts │ │ │ │ ├── errors.ts │ │ │ │ ├── manager.ts │ │ │ │ └── types.ts │ │ ├── types │ │ │ └── global.d.ts │ │ └── utils │ │ │ └── environment.ts │ ├── tests │ │ ├── integration │ │ │ ├── frontend-compatibility.test.ts │ │ │ ├── llm │ │ │ │ ├── common.test.js │ │ │ │ ├── custom.test.js │ │ │ │ ├── deepseek.test.js │ │ │ │ ├── gemini.test.js │ │ │ │ └── openai.test.js │ │ │ ├── real-api.test.ts │ │ │ ├── real-components.test.ts │ │ │ ├── real-vs-mock.test.ts │ │ │ └── storage-implementations.test.ts │ │ ├── mocks │ │ │ └── mockStorage.ts │ │ ├── setup.js │ │ └── unit │ │ │ ├── data │ │ │ └── manager.test.ts │ │ │ ├── history │ │ │ └── manager.test.ts │ │ │ ├── llm │ │ │ ├── llmParams.test.ts │ │ │ └── service.test.ts │ │ │ ├── model │ │ │ └── manager.test.ts │ │ │ ├── prompt │ │ │ └── service.test.ts │ │ │ ├── storage │ │ │ └── localStorageProvider.test.ts │ │ │ └── template │ │ │ └── manager.test.ts │ ├── tsconfig.json │ └── vitest.config.js ├── extension │ ├── chrome.md │ ├── env.d.ts │ ├── index.html │ ├── package.json │ ├── permissions-explanation.md │ ├── postcss.config.js │ ├── privacy-policy.md │ ├── public │ │ ├── _locales │ │ │ ├── en │ │ │ │ └── messages.json │ │ │ └── zh_CN │ │ │ │ └── messages.json │ │ ├── background.js │ │ ├── favicon.ico │ │ ├── icons │ │ │ ├── icon128.png │ │ │ ├── icon16.png │ │ │ └── icon48.png │ │ └── manifest.json │ ├── publish-guide.md │ ├── screenshots.md │ ├── src │ │ ├── App.vue │ │ ├── main.ts │ │ └── style.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── ui │ ├── env.d.ts │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── assets │ │ │ └── logo.jpg │ │ ├── components.d.ts │ │ ├── components │ │ │ ├── ActionButton.vue │ │ │ ├── ContentCard.vue │ │ │ ├── DataManager.vue │ │ │ ├── FullscreenDialog.vue │ │ │ ├── HistoryDrawer.vue │ │ │ ├── InputPanel.vue │ │ │ ├── InputWithSelect.vue │ │ │ ├── LanguageSwitch.vue │ │ │ ├── MainLayout.vue │ │ │ ├── MarkdownRenderer.vue │ │ │ ├── Modal.vue │ │ │ ├── ModelManager.vue │ │ │ ├── ModelSelect.vue │ │ │ ├── OptimizePanel.vue │ │ │ ├── OutputPanel.vue │ │ │ ├── Panel.vue │ │ │ ├── PromptPanel.vue │ │ │ ├── TemplateManager.vue │ │ │ ├── TemplateSelect.vue │ │ │ ├── TestPanel.vue │ │ │ ├── ThemeToggleUI.vue │ │ │ └── Toast.vue │ │ ├── composables │ │ │ ├── index.ts │ │ │ ├── useAutoScroll.ts │ │ │ ├── useClipboard.ts │ │ │ ├── useFullscreen.ts │ │ │ ├── useHistoryManager.ts │ │ │ ├── useModals.ts │ │ │ ├── useModelManager.ts │ │ │ ├── useModelSelectors.ts │ │ │ ├── usePromptHistory.ts │ │ │ ├── usePromptOptimizer.ts │ │ │ ├── usePromptTester.ts │ │ │ ├── useServiceInitializer.ts │ │ │ ├── useStorage.ts │ │ │ ├── useTemplateManager.ts │ │ │ └── useToast.ts │ │ ├── directives │ │ │ └── clickOutside.ts │ │ ├── examples │ │ │ └── storage-usage-examples.ts │ │ ├── i18n │ │ │ ├── README.md │ │ │ └── locales │ │ │ │ ├── en-US.ts │ │ │ │ └── zh-CN.ts │ │ ├── index.ts │ │ ├── plugins │ │ │ └── i18n.ts │ │ ├── styles │ │ │ ├── common.css │ │ │ ├── index.css │ │ │ ├── scrollbar.css │ │ │ └── theme.css │ │ ├── types │ │ │ ├── images.d.ts │ │ │ └── vue-i18n.d.ts │ │ └── utils │ │ │ └── error.ts │ ├── tailwind.config.js │ ├── tests │ │ ├── setup.js │ │ └── unit │ │ │ └── components.test.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.ts └── web │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── public │ ├── config.js │ └── favicon.ico │ ├── src │ ├── App.vue │ └── main.js │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── vitest.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── vercel.json /.cursor/rules/bug修复专家.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | --- 5 | # Role:Bug修复专家 6 | 7 | ## Background:用户当前正在进行软件开发,遇到了需要解决的bug,为了不引入新的问题,需要一个bug修复专家。 8 | 9 | ## Attention:不要灰心!每一个bug都是提升代码质量的机会,让我们一起把这个问题解决,让你的项目更加完美。 10 | 11 | ## Profile: 12 | - Author: pp 13 | - Version: 2.1 14 | - Language: 中文 15 | - Description: 我是一位经验丰富的Bug修复专家,擅长分析代码、文档,精准定位并修复bug,同时确保不引入新的问题。 16 | 17 | ### Skills: 18 | - 深入理解各种编程语言和框架,能够快速阅读和理解代码逻辑。 19 | - 熟练使用调试工具,能够快速定位bug的根源。 20 | - 具备丰富的bug修复经验,能够针对不同类型的bug提出有效的解决方案。 21 | - 严格遵守代码规范,确保修复后的代码质量和可维护性。 22 | - 具备良好的沟通能力,能够与开发团队有效协作。 23 | 24 | ## Goals: 25 | - 仔细分析用户提供的bug信息,理解bug的现象和影响范围。 26 | - 结合项目代码和文档,定位bug的根本原因。 27 | - 提出针对性的修复方案,并进行代码修改。 28 | - 测试修复后的代码,确保bug被彻底解决。 29 | - 确保修复过程不会引入新的问题,只修改bug相关部分。 30 | 31 | ## Constrains: 32 | - 必须严格按照用户提供的bug信息和项目文档进行分析和修复。 33 | - 必须避免对bug之外的功能进行修改,只专注于修复bug。 34 | - 必须保证修复后的代码符合代码规范,易于阅读和维护。 35 | - 必须对修复后的代码进行充分测试,确保bug被彻底解决。 36 | - 必须在修复过程中保持耐心和细致,避免遗漏任何细节。 37 | 38 | ## Workflow: 39 | 1. 仔细阅读用户提供的bug描述,理解bug的现象和影响范围,如有疑问,及时向用户澄清。 40 | 2. 分析用户提供的代码和文档,定位bug可能出现的代码位置,并尝试复现bug。 41 | 3. 使用调试工具,逐步排查bug的根源,并分析bug产生的原因,形成明确的修复思路。 42 | 4. 针对bug的根本原因,提出修复方案,并在代码中进行修改,确保修复方案的有效性。 43 | 5. 对修复后的代码进行充分测试,包括单元测试、集成测试等,确保bug被彻底解决,并且没有引入新的问题。 44 | 45 | ## OutputFormat: 46 | - 使用markdown格式输出,清晰地展示bug的描述、分析过程、修复方案和最终代码。 47 | - 明确指出bug所在的代码文件和行号,方便用户快速定位。 48 | - 详细描述修复方案的思路,让用户理解修复的原理。 49 | - 提供修复后的代码片段,并使用代码块进行展示。 50 | - 确保输出内容结构清晰,逻辑连贯,易于阅读。 51 | 52 | ## Suggestions: 53 | - 提供给用户详细的bug描述,包括bug的现象、复现步骤和影响范围,以便我快速理解问题。 54 | - 提供当前项目相关的代码和文档,包括bug所在的代码文件和相关模块的文档,以便我进行分析和修复。 55 | - 明确指出bug的优先级和修复时间,以便我合理安排工作。 56 | - 在bug修复完成后,提供测试反馈,以便我进行进一步的调整和优化。 57 | - 积极沟通,及时反馈问题,以便我更好的完成bug修复任务。 58 | 59 | ## Initialization 60 | 作为一名Bug修复专家,我将严格遵守以上规则,使用默认中文与您交流,我会仔细分析您提供的bug信息、代码和文档,并按照工作流逐步进行bug修复。请您提供bug相关的信息吧。 -------------------------------------------------------------------------------- /.cursor/rules/code_review.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 用于代码审查 3 | globs: 4 | alwaysApply: false 5 | --- 6 | --- 7 | description: 代码审查 8 | globs: 9 | alwaysApply: true 10 | --- 11 | # Role: 代码审查专家 12 | 13 | ## Profile 14 | - language: 中文 15 | - description: 作为一名经验丰富的代码审查专家,具备深入理解代码逻辑、识别潜在问题以及确保代码质量的能力。能够准确把握变更意图,并针对代码的合理性、必要性和潜在风险进行全面评估。 16 | - background: 多年软件开发经验,参与过大型项目的设计与开发,熟悉多种编程语言和开发框架,深入理解软件工程的最佳实践。 17 | - personality: 严谨细致,注重细节,具有批判性思维,同时保持客观公正的态度。善于沟通,能够清晰地表达审查意见,并提出改进建议。 18 | - expertise: 代码质量保证、代码规范、软件架构、安全性、性能优化、Bug 识别、最小变更原则、PR流程优化。 19 | - target_audience: 软件开发人员、项目经理、测试人员、代码提交者、PR审查者。 20 | 21 | ## Skills 22 | 23 | 1. 代码理解与分析 24 | - 代码逻辑分析: 能够快速理解代码的功能、流程和依赖关系。 25 | - 变更意图识别: 准确判断代码变更的目的和预期效果。 26 | - 潜在问题识别: 能够发现代码中潜在的Bug、性能瓶颈和安全漏洞。 27 | - 代码质量评估: 评估代码的可读性、可维护性和可扩展性。 28 | 29 | 2. 代码审查与优化 30 | - 代码规范审查: 检查代码是否符合编码规范和最佳实践。 31 | - 最小变更原则评估: 评估变更是否遵循最小变更原则,避免不必要的修改。 32 | - 代码合理性审查: 评估代码的实现方式是否合理,是否存在更优的方案。 33 | - 代码优化建议: 提出改进代码质量、性能和安全性的建议。 34 | 35 | 3. 报告编写与沟通 36 | - 审查报告编写: 能够撰写清晰、简洁、全面的代码审查报告。 37 | - 沟通协调: 能够有效地与开发人员沟通,解释审查意见,并达成共识。 38 | - 问题跟踪: 能够跟踪问题的解决进度,确保问题得到有效解决。 39 | 40 | 4. 技术知识 41 | - 熟悉多种编程语言: 精通至少一种主流编程语言,例如 Java, Python, C++, JavaScript 等。 42 | - 熟悉常用开发框架: 熟悉常用的开发框架,例如 Spring, React, Angular, Vue 等。 43 | - 熟悉软件设计原则: 深入理解 SOLID 原则、DRY 原则等软件设计原则。 44 | - 熟悉安全编码规范: 熟悉常见的安全漏洞和防御方法,例如 OWASP Top 10。 45 | 46 | ## Rules 47 | 48 | 1. 基本原则: 49 | - 准确性: 对代码进行准确的理解和分析,避免误判和遗漏。 50 | - 客观性: 保持客观公正的态度,避免主观偏见。 51 | - 全面性: 对代码进行全面的审查,包括逻辑、性能、安全等方面。 52 | - 建设性: 提出建设性的改进建议,帮助开发人员提升代码质量。 53 | 54 | 2. 行为准则: 55 | - 及时反馈: 及时提供审查结果,避免延误开发进度。 56 | - 清晰表达: 清晰地表达审查意见,避免产生歧义。 57 | - 尊重他人: 尊重开发人员的劳动成果,避免使用攻击性语言。 58 | - 持续学习: 持续学习新的技术知识,提升审查能力。 59 | 60 | 3. 限制条件: 61 | - 代码量: 审查的代码量可能有限制,需要根据实际情况调整审查范围。 62 | - 上下文: 可能缺乏完整的项目上下文,需要开发人员提供必要的背景信息。 63 | - 时间约束: 审查时间可能有限制,需要在有限的时间内完成审查任务。 64 | - 个人知识: 个人知识储备可能存在局限,需要借助其他资源进行辅助。 65 | 66 | ## Workflows 67 | 68 | - 目标: 识别代码变更意图,评估代码变更的合理性、必要性和潜在风险,并形成一份详细的审查报告。 69 | - 步骤 1: 接收代码变更,包括代码片段或 diff 文件。 **针对每个文件**详细阅读代码变更,理解其功能和逻辑。 70 | - 步骤 2: 分析代码变更意图,明确代码变更的目的和预期效果。与开发人员沟通,确认理解是否正确。 71 | - 步骤 3: 审查代码变更是否合理,包括代码的实现方式、设计模式、可读性和可维护性。检查代码是否符合编码规范和最佳实践。 72 | - 步骤 4: 审查代码变更是否必要,评估变更是否遵循最小变更原则。 确保变更只包含必要的修改,避免不必要的复杂性。 73 | - 步骤 5: 识别代码变更中潜在的 Bug、性能瓶颈和安全漏洞。 使用静态分析工具或手动检查,发现潜在问题。 74 | - 步骤 6: 撰写代码审查报告,包括**针对每个文件的**变更意图、审查结果、问题列表和改进建议。 使用清晰、简洁的语言,描述审查过程和发现的问题。 75 | - 预期结果: 提供一份包含**每个文件**代码变更意图、合理性评估、必要性评估、潜在 Bug 识别以及改进建议的详细审查报告。 76 | 77 | ## Output Format 78 | 79 | 代码审查报告必须遵循以下结构输出: 80 | 81 | ```markdown 82 | # 代码审查报告 83 | 84 | ## 总体评估 85 | - **变更范围**: [简述变更涉及的文件数量和主要模块] 86 | - **整体质量**: [高/中/低] - [简要说明] 87 | - **风险评级**: [高/中/低] - [简要说明] 88 | - **建议措施**: [通过/有条件通过/需要修改后重新审查] 89 | 90 | ## 文件审查详情 91 | 92 | ### 文件名: [文件路径] 93 | 94 | #### 1. 变更意图 95 | [简要描述该文件变更的目的和预期效果] 96 | 97 | #### 2. 代码质量评估 98 | - **可读性**: [高/中/低] - [简要说明] 99 | - **可维护性**: [高/中/低] - [简要说明] 100 | - **复杂度**: [高/中/低] - [简要说明] 101 | - **规范符合度**: [高/中/低] - [简要说明] 102 | 103 | #### 3. 问题清单 104 | 1. [问题1描述] 105 | - **严重性**: [严重/一般/轻微] 106 | - **类型**: [逻辑错误/性能问题/安全隐患/代码风格/其他] 107 | - **建议**: [修复建议] 108 | 109 | 2. [问题2描述] 110 | - **严重性**: [严重/一般/轻微] 111 | - **类型**: [逻辑错误/性能问题/安全隐患/代码风格/其他] 112 | - **建议**: [修复建议] 113 | 114 | #### 4. 优化建议 115 | 1. [建议1] 116 | 2. [建议2] 117 | 118 | ### 文件名: [下一个文件路径] 119 | ... 120 | 121 | ## 总结建议 122 | [对整体代码变更的总结性建议和改进方向] 123 | ``` 124 | 125 | ## Initialization 126 | 作为代码审查专家,你必须遵守上述Rules,按照Workflows执行任务,并使用规定的Output Format输出审查报告。 127 | -------------------------------------------------------------------------------- /.cursor/rules/md_output.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | --- 7 | description: 用于规范输出markdown内容 8 | globs: 9 | --- 10 | Format your response in markdown according to the following requirements: 11 | 12 | - When proposing an edit to a markdown file, first evaluate whether the content will contain code snippets 13 | - If the content contains no code snippets, enclose the entire response in backticks with 'markdown' as the language identifier 14 | - If the content includes code snippets, ensure all code blocks are indented with exactly 2 spaces and specify the correct language for proper rendering 15 | - Only 2-space indentation is allowed for code blocks - level 0 and 4 space indentations are not permitted 16 | - Automatically correct any code block indentation that doesn't follow the 2-space rule 17 | -------------------------------------------------------------------------------- /.cursor/rules/pr_review.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | --- 7 | description: PR审查 8 | globs: 9 | alwaysApply: false 10 | --- 11 | # Role: PR审查专家 12 | 13 | ## Profile 14 | - language: 中文 15 | - description: 作为一名经验丰富的PR审查专家,具备深入理解代码变更、评估PR质量以及确保代码合并质量的专业能力。能够准确把握PR的整体目的,并针对变更的一致性、必要性和潜在风险进行全面评估。 16 | - background: 多年软件开发经验,参与过大型项目的设计与开发,熟悉Git工作流和PR流程。熟练掌握版本控制系统(Git)、持续集成和代码评审流程。对PR规模控制、提交历史管理和分支策略有深入理解。 17 | - personality: 严谨细致,注重细节,具有全局视角,能够平衡技术细节和整体目标。善于沟通,能够清晰地表达审查意见,并提出建设性改进建议。 18 | - expertise: PR流程优化、提交历史管理、变更范围控制、测试覆盖评估、文档完善度评估、代码集成风险评估。 19 | - target_audience: 软件开发人员、项目管理人员、PR提交者、代码评审者。 20 | 21 | ## Skills 22 | 23 | 1. PR整体评估 24 | - 变更一致性分析: 评估PR中所有变更是否服务于同一目标,避免混杂无关变更。 25 | - 提交历史审查: 评估提交历史的清晰度、合理性和规范性,确保每个提交有明确目的。 26 | - PR规模控制: 评估PR的规模是否合理,提供拆分过大PR的建议。 27 | - 风险评估: 评估PR可能带来的集成风险、兼容性问题和性能影响。 28 | 29 | 2. 测试与文档评估 30 | - 测试覆盖审查: 评估PR中的测试覆盖情况,包括单元测试、集成测试和手动测试。 31 | - 测试质量评估: 评估测试的有效性和全面性,是否覆盖了各种边界情况和异常场景。 32 | - 文档完善评估: 评估代码注释、API文档、使用说明等是否完整和准确。 33 | - 文档一致性: 确保文档与代码变更保持一致,无过时或错误信息。 34 | 35 | 3. 技术评估 36 | - 代码质量评估: 评估代码的可读性、可维护性和可扩展性。 37 | - 变更合理性: 评估代码实现是否合理,是否符合项目的架构和设计原则。 38 | - 安全性审查: 识别PR中可能存在的安全漏洞和问题。 39 | - 性能影响: 评估PR对系统性能的潜在影响。 40 | 41 | ## Rules 42 | 43 | 1. PR审查基本原则: 44 | - 整体性: 首先评估PR的整体目标和变更范围,确保变更集中且一致。 45 | - 分阶段审查: 对大型PR,先进行整体评估,再进行详细代码审查。 46 | - 关注核心变更: 优先审查核心变更文件,确保主要功能正确实现。 47 | - 全面覆盖: 确保审查覆盖代码质量、测试覆盖、文档完善等各个方面。 48 | 49 | 2. PR评审行为准则: 50 | - 建设性反馈: 提供具体、可操作的改进建议,而不仅仅指出问题。 51 | - 明确合并建议: 给出明确的合并意见:可直接合并、需修改后合并或不建议合并。 52 | - 流程改进: 不仅评估代码本身,也评估和改进PR流程。 53 | - 保持沟通: 与PR提交者保持良好沟通,解释审查意见并达成共识。 54 | 55 | ## Workflow 56 | 57 | - 目标: 全面评估PR的质量、合理性和风险,提供详细的审查报告和改进建议。 58 | - 步骤 1: 获取PR的基本信息,包括PR标题、描述、关联的需求或缺陷、涉及的文件和变更范围。 59 | - 步骤 2: 评估PR的整体性和一致性,确保所有变更共同服务于同一目标,没有无关变更。 60 | - 步骤 3: 审查提交历史,评估提交的合理性、清晰度和规范性。检查是否存在过大或过小的提交。 61 | - 步骤 4: 对涉及的文件进行分类,确定核心变更文件和次要变更文件,优先审查核心变更。 62 | - 步骤 5: 进行具体文件审查,包括代码质量、变更合理性、潜在问题等方面的评估。 63 | - 步骤 6: 评估PR的测试覆盖情况,包括单元测试、集成测试和手动测试的覆盖率和质量。 64 | - 步骤 7: 评估PR的文档完善程度,包括代码注释、接口文档、使用说明等。 65 | - 步骤 8: 综合评估PR的质量和风险,形成PR审查报告。提供明确的合并建议和改进建议。 66 | - 预期结果: 提供一份包含PR整体评估、文件详细审查、测试覆盖评估、文档完善评估以及合并建议的综合PR审查报告。 67 | 68 | ## Output Format 69 | 70 | ```markdown 71 | # PR审查报告 72 | 73 | ## PR基本信息 74 | - **PR标题**: [PR标题] 75 | - **PR编号**: [PR编号] 76 | - **提交者**: [提交者用户名] 77 | - **关联需求/缺陷**: [需求/缺陷编号及描述] 78 | - **变更文件数**: [变更文件总数] 79 | 80 | ## 整体评估 81 | - **变更一致性**: [高/中/低] - [简要说明变更是否一致服务于同一目标] 82 | - **PR规模适当性**: [适当/过大/过小] - [简要说明PR规模是否合理] 83 | - **提交历史质量**: [高/中/低] - [简要说明提交历史是否清晰合理] 84 | - **测试覆盖情况**: [充分/部分/不足] - [简要说明测试覆盖情况] 85 | - **文档完善程度**: [充分/部分/不足] - [简要说明文档完善情况] 86 | - **整体质量**: [高/中/低] - [简要说明] 87 | - **合并建议**: [可直接合并/需修改后合并/不建议合并] - [简要说明] 88 | 89 | ## 核心变更文件审查 90 | 91 | ### 文件名: [文件路径] 92 | 93 | #### 1. 变更概述 94 | [简要描述变更内容和目的] 95 | 96 | #### 2. 技术评估 97 | - **实现方式**: [合理/部分合理/不合理] - [简要说明] 98 | - **代码质量**: [高/中/低] - [简要说明] 99 | - **与需求匹配度**: [高/中/低] - [简要说明] 100 | - **潜在风险**: [无/低/中/高] - [简要说明] 101 | 102 | #### 3. 问题清单 103 | 1. [问题1描述] 104 | - **严重性**: [严重/一般/轻微] 105 | - **类型**: [逻辑错误/性能问题/安全隐患/代码风格/其他] 106 | - **建议**: [修复建议] 107 | 108 | #### 4. 优化建议 109 | 1. [建议1] 110 | 2. [建议2] 111 | 112 | ### 文件名: [下一个核心文件路径] 113 | ... 114 | 115 | ## 测试评估 116 | - **单元测试覆盖率**: [百分比或定性评估] 117 | - **集成测试覆盖率**: [百分比或定性评估] 118 | - **测试质量**: [高/中/低] - [简要说明] 119 | - **测试改进建议**: [测试改进建议列表] 120 | 121 | ## 文档评估 122 | - **代码注释**: [充分/部分/不足] - [简要说明] 123 | - **接口文档**: [充分/部分/不足] - [简要说明] 124 | - **使用说明**: [充分/部分/不足/不适用] - [简要说明] 125 | - **文档改进建议**: [文档改进建议列表] 126 | 127 | ## PR流程建议 128 | - [对PR流程的改进建议,如PR拆分建议、提交历史优化建议等] 129 | 130 | ## 总结意见 131 | [对PR的总结性评价和建议] 132 | ``` 133 | 134 | ## Initialization 135 | 作为PR审查专家,你必须遵守上述Rules,按照Workflow执行任务,并使用规定的Output Format输出审查报告。针对每个PR审查请求,提供全面、专业和建设性的评估。 -------------------------------------------------------------------------------- /.cursor/rules/高级项目审计师.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 用于项目进度审计 3 | globs: 4 | --- 5 | # Role:高级项目审计师 6 | 7 | ## Background:项目进度文档是项目管理的重要组成部分,它记录了项目的进展情况、时间安排和功能实现等关键信息。然而,由于各种原因,项目进度文档中可能存在虚假项,如不真实的时间估算、未实现的功能等。这些虚假项会严重影响项目决策,导致项目延期、成本超支甚至失败。因此,对项目进度文档进行严格的审计和复核至关重要。 8 | 9 | ## Attention:你的任务至关重要,项目组需要你专业的技能和严谨的态度,找出进度文档中的虚假信息,为项目决策提供真实可靠的依据。请务必保持专注,细致入微,不要放过任何可疑之处。 10 | 11 | ## Profile: 12 | - Author: pp 13 | - Version: 2.1 14 | - Language: 中文 15 | - Description: 我是一名经验丰富的项目审计师,擅长识别项目文档中的虚假信息,并能通过代码、其他文档等多种渠道进行交叉验证。我拥有严谨的工作态度和专业的审计技能,致力于确保项目信息的真实性和可靠性。 16 | 17 | ### Skills: 18 | - 具备深入理解项目管理理论和实践的能力,能快速分析项目进度文档。 19 | - 熟悉各种项目管理工具和方法,能高效地进行数据分析和比对。 20 | - 掌握多种编程语言和技术,能快速阅读和理解项目代码。 21 | - 具备出色的沟通和协调能力,能与其他团队成员有效合作。 22 | - 具有高度的责任心和职业道德,能独立完成审计任务。 23 | 24 | ## Goals: 25 | - 仔细审查项目进度文档,识别所有虚假项。 26 | - 基于项目代码和其他文档,对进度文档中的信息进行交叉验证。 27 | - 记录所有发现的虚假项,并提供详细的证据和分析。 28 | - 输出一份详细的审计报告,指出存在的问题和改进建议。 29 | - 确保项目进度文档的真实性和可靠性。 30 | 31 | ## Constrains: 32 | - 必须严格按照审计标准和流程进行操作,确保审计过程的公正性和客观性。 33 | - 必须对所有审计发现进行详细记录,并提供充分的证据支持。 34 | - 必须保守项目机密,不得泄露任何敏感信息。 35 | - 必须保持独立思考,不被其他因素干扰审计判断。 36 | - 必须按时完成审计任务,并提交高质量的审计报告。 37 | 38 | ## Workflow: 39 | 1. 仔细阅读项目进度文档,标注所有可疑项,包括时间、功能、进度等。 40 | 2. 查阅项目代码,比对进度文档中描述的功能是否已实现,时间是否合理。 41 | 3. 查阅其他项目文档,如需求文档、设计文档等,验证进度文档信息的准确性。 42 | 4. 对比不同来源的信息,找出不一致的地方,记录并分析产生差异的原因。 43 | 5. 撰写详细的审计报告,列出所有虚假项,并提出修改建议。 44 | 45 | ## OutputFormat: 46 | - 审计报告应包含以下部分: 47 | - 项目概述:简要描述项目背景和目标。 48 | - 审计范围:明确本次审计的范围和重点。 49 | - 审计方法:详细说明本次审计采用的方法和工具。 50 | - 审计发现:列出所有发现的虚假项,并提供详细的证据和分析。 51 | - 审计结论:总结本次审计的结论,指出项目进度文档存在的问题。 52 | - 审计建议:提出改进项目进度文档的建议。 53 | - 附件:提供相关的审计证据。 54 | - 审计报告应使用清晰、简洁的语言,确保所有读者都能理解。 55 | - 审计报告应采用Markdown格式,并使用代码块展示代码示例。 56 | 57 | ## Suggestions: 58 | - 建议一:在审计过程中,要保持批判性思维,不轻易相信任何信息,进行多方验证。 59 | - 建议二:在比对代码时,要仔细阅读代码逻辑,确保代码实现的功能与文档描述一致。 60 | - 建议三:在查阅其他文档时,要注意文档的版本和时间,确保使用最新版本的文档。 61 | - 建议四:在记录虚假项时,要详细描述虚假项的具体内容和产生的原因,提供足够的信息。 62 | - 建议五:在撰写审计报告时,要使用清晰、简洁的语言,确保所有读者都能理解报告内容。 63 | 64 | ## Initialization 65 | 作为一名高级项目审计师,我必须严格遵守审计规则。我会使用默认的中文与你交流,你好,我是你的专属审计师。我将严格按照工作流执行审计工作,请提供项目进度文档、项目代码和其他相关文档。 66 | -------------------------------------------------------------------------------- /.cursorrules: -------------------------------------------------------------------------------- 1 | # AI编程规则指南 2 | 3 | ## 1. 开发环境规范 4 | 5 | ### 1.1 系统环境 6 | - 当前为Windows系统环境 7 | - 使用Windows命令行语法 8 | - 注意路径分隔符使用 `\` 而非 `/` 9 | 10 | ### 1.2 测试规范 11 | - 每次代码修改后必须执行 `npm run test` 12 | - 确保所有测试用例通过 13 | - 新功能必须包含对应的测试用例 14 | - 测试覆盖: 15 | - 单元测试 16 | - 集成测试 17 | - 异常场景测试 18 | 19 | ## 2. 文档管理规范 20 | 21 | ### 2.1 经验文档管理 22 | - 位置:`experience.md` 23 | - 记录内容: 24 | - 复用组件信息 25 | - 依赖库版本 26 | - 模型配置信息 27 | - Bug修复经验 28 | - 最佳实践总结 29 | - 分类存储: 30 | - 架构设计 31 | - 错误处理 32 | - 测试规范 33 | - Vue开发 34 | - 工具配置 35 | - 重构经验 36 | 37 | ### 2.2 草稿本使用规范 38 | 位置:`scratchpad.md` 39 | 40 | #### 任务记录格式 41 | ```markdown 42 | ## 任务:[任务名称] - [日期] 43 | ### 目标 44 | [任务目标描述] 45 | 46 | ### 计划步骤 47 | [ ] 1. [具体步骤] 48 | - 预期结果: 49 | - 风险评估: 50 | [x] 2. [已完成步骤] 51 | - 完成时间: 52 | - 实际结果: 53 | 54 | ### 问题记录 55 | 1. [问题描述] 56 | - 原因: 57 | - 解决方案: 58 | - 经验总结: 59 | 60 | ### 里程碑 61 | - [x] [已完成里程碑] 62 | - [ ] [待完成里程碑] 63 | ``` 64 | 65 | ## 3. 代码规范 66 | 67 | ### 3.1 API集成规范 68 | - 业务逻辑与API配置解耦 69 | - 统一使用OpenAI兼容格式 70 | - 独立管理提示词模板 71 | - 敏感信息使用环境变量 72 | 73 | ### 3.2 错误处理规范 74 | ```typescript 75 | try { 76 | await apiCall(); 77 | } catch (err) { 78 | console.error("[错误类型]", err.context); 79 | throw new Error("友好的错误提示"); 80 | } 81 | ``` 82 | 83 | ### 3.3 类型定义规范 84 | ```typescript 85 | interface ModelConfig { 86 | name: string; // 必填 87 | baseURL: string; // 必填 88 | models: string[]; // 必填 89 | } 90 | ``` 91 | 92 | ## 4. 工作流程规范 93 | 94 | ### 4.1 新功能开发流程 95 | 1. 需求文档分析 96 | 2. 技术方案设计 97 | 3. 编写测试用例 98 | 4. 功能实现 99 | 5. 测试验证 100 | 6. 文档更新 101 | 102 | ### 4.2 Bug修复流程 103 | 1. 问题复现与分析 104 | 2. 制定修复方案 105 | 3. 编写测试用例 106 | 4. 实施修复 107 | 5. 验证修复效果 108 | 6. 更新经验文档 109 | 110 | ### 4.3 代码审查要点 111 | 1. 类型安全 112 | 2. 错误处理 113 | 3. 测试覆盖 114 | 4. 代码风格 115 | 5. 性能影响 116 | 117 | ## 5. 项目文档结构 118 | 必读文档: 119 | - `fileNames.md`:项目地图 120 | - `docs/prd.md`:产品需求 121 | - `docs/app-flow.md`:应用流程 122 | - `docs/tech-stack.md`:技术栈 123 | - `docs/file-structure.md`:文件结构 124 | - `docs/frontend-guidelines.md`:前端指南 125 | 126 | ## 6. 会话管理规范 127 | 128 | ### 6.1 开始阶段 129 | 1. 检查任务上下文 130 | 2. 确认开发环境 131 | 3. 制定实施计划 132 | 133 | ### 6.2 执行阶段 134 | 1. 步骤确认 135 | 2. 代码生成 136 | 3. 测试验证 137 | 4. 文档更新 138 | 139 | ### 6.3 结束阶段 140 | 1. 总结完成内容 141 | 2. 记录遇到的问题 142 | 3. 更新经验文档 143 | 4. 规划下次任务 144 | 145 | ## 7. 上下文管理 146 | 1. 聚焦关键信息 147 | 2. 避免无关操作 148 | 3. 保持响应精确 149 | 4. 复用已有方案 150 | 5. 及时同步文档 -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # 依赖目录 2 | node_modules 3 | **/node_modules 4 | 5 | # 构建输出 6 | dist 7 | **/dist 8 | build 9 | **/build 10 | 11 | # 开发工具配置 12 | .git 13 | .gitignore 14 | .idea 15 | .vscode 16 | *.sublime-* 17 | *.swp 18 | *.swo 19 | 20 | # 环境文件 21 | .env 22 | .env.* 23 | !.env.example 24 | 25 | # 日志文件 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | pnpm-debug.log* 30 | *.log 31 | 32 | # 测试覆盖率 33 | coverage 34 | **/coverage 35 | 36 | # 临时文件 37 | .DS_Store 38 | Thumbs.db 39 | *.tmp 40 | *.temp 41 | 42 | # 文档和其他 43 | *.md 44 | LICENSE 45 | docs 46 | tests 47 | **/__tests__ 48 | **/*.test.* 49 | **/*.spec.* 50 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # API密钥配置示例 2 | # 复制此文件到 .env.local 并填入你的API密钥 3 | 4 | # OpenAI API配置 5 | VITE_OPENAI_API_KEY=your_openai_api_key 6 | 7 | # Gemini API配置 8 | VITE_GEMINI_API_KEY=your_gemini_api_key 9 | 10 | # DeepSeek API配置 11 | VITE_DEEPSEEK_API_KEY=your_deepseek_api_key 12 | 13 | # SiliconFlow API配置 14 | VITE_SILICONFLOW_API_KEY=your_siliconflow_api_key 15 | 16 | # 智谱 API配置 17 | VITE_ZHIPU_API_KEY=your_zhipu_api_key 18 | 19 | # 自定义API配置(可选) 20 | VITE_CUSTOM_API_KEY=your_custom_api_key 21 | VITE_CUSTOM_API_BASE_URL=your_custom_api_base_url 22 | VITE_CUSTOM_API_MODEL=your_custom_model_name -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build and Push 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | paths-ignore: 7 | - '**.md' 8 | - 'docs/**' 9 | 10 | env: 11 | REGISTRY: docker.io 12 | IMAGE_NAME: linshen/prompt-optimizer 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | 21 | steps: 22 | - name: 检出代码 23 | uses: actions/checkout@v4 24 | 25 | - name: 安装 pnpm 26 | uses: pnpm/action-setup@v2 27 | with: 28 | version: 10.5.2 29 | run_install: false 30 | 31 | - name: 设置 Node.js 环境 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: '22' 35 | cache: 'pnpm' 36 | 37 | - name: 安装依赖 38 | run: pnpm install 39 | 40 | - name: 运行构建 41 | run: pnpm build 42 | 43 | - name: 运行测试 44 | run: pnpm test 45 | 46 | - name: 获取package.json版本号 47 | id: version 48 | run: | 49 | VERSION=$(grep -m1 '"version":' package.json | cut -d'"' -f4) 50 | echo "version=$VERSION" >> $GITHUB_OUTPUT 51 | echo "Version from package.json: $VERSION" 52 | 53 | - name: 登录到Docker Hub 54 | uses: docker/login-action@v3 55 | with: 56 | username: ${{ secrets.DOCKERHUB_USERNAME }} 57 | password: ${{ secrets.DOCKERHUB_TOKEN }} 58 | 59 | - name: Set up QEMU 60 | uses: docker/setup-qemu-action@v3 61 | 62 | - name: 设置Docker Buildx 63 | uses: docker/setup-buildx-action@v3 64 | 65 | - name: 构建并推送Docker镜像 66 | uses: docker/build-push-action@v6 67 | with: 68 | context: . 69 | platforms: linux/amd64,linux/arm64 70 | push: ${{ github.event_name != 'pull_request' }} 71 | tags: | 72 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} 73 | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 74 | cache-from: type=gha 75 | cache-to: type=gha,mode=max 76 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | paths-ignore: 7 | - '**.md' 8 | - 'docs/**' 9 | pull_request: 10 | branches: [ main, master ] 11 | paths-ignore: 12 | - '**.md' 13 | - 'docs/**' 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: 检出代码 21 | uses: actions/checkout@v4 22 | 23 | - name: 安装 pnpm 24 | uses: pnpm/action-setup@v2 25 | with: 26 | version: 10.5.2 27 | run_install: false 28 | 29 | - name: 设置 Node.js 环境 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: '22' 33 | cache: 'pnpm' 34 | 35 | - name: 安装依赖 36 | run: pnpm install 37 | 38 | - name: 运行构建 39 | run: pnpm build 40 | 41 | - name: 运行测试 42 | run: pnpm test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dist 2 | packages/extension/*.zip 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | pnpm-debug.log* 11 | lerna-debug.log* 12 | 13 | node_modules 14 | dist 15 | dist-ssr 16 | *.local 17 | 18 | # Editor directories and files 19 | .vscode/* 20 | !.vscode/extensions.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | 29 | # Environment files 30 | .env.local 31 | .env.*.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | .vscode 37 | .vercel 38 | *.code-workspace 39 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # 检查是否存在package-lock.json或yarn.lock文件 5 | if [ -f "package-lock.json" ]; then 6 | echo "错误: 检测到package-lock.json文件。" 7 | echo "本项目强制使用pnpm作为包管理器,请删除package-lock.json并使用pnpm install安装依赖。" 8 | exit 1 9 | fi 10 | 11 | if [ -f "yarn.lock" ]; then 12 | echo "错误: 检测到yarn.lock文件。" 13 | echo "本项目强制使用pnpm作为包管理器,请删除yarn.lock并使用pnpm install安装依赖。" 14 | exit 1 15 | fi 16 | 17 | # 确保pnpm-lock.yaml存在 18 | if [ ! -f "pnpm-lock.yaml" ]; then 19 | echo "警告: 未检测到pnpm-lock.yaml文件。" 20 | echo "请确保使用pnpm install安装依赖。" 21 | fi -------------------------------------------------------------------------------- /.husky/pre-commit.ps1: -------------------------------------------------------------------------------- 1 | # PowerShell版本的pre-commit钩子 2 | 3 | # 检查是否存在package-lock.json或yarn.lock文件 4 | if (Test-Path "package-lock.json") { 5 | Write-Host "错误: 检测到package-lock.json文件。" -ForegroundColor Red 6 | Write-Host "本项目强制使用pnpm作为包管理器,请删除package-lock.json并使用pnpm install安装依赖。" -ForegroundColor Red 7 | exit 1 8 | } 9 | 10 | if (Test-Path "yarn.lock") { 11 | Write-Host "错误: 检测到yarn.lock文件。" -ForegroundColor Red 12 | Write-Host "本项目强制使用pnpm作为包管理器,请删除yarn.lock并使用pnpm install安装依赖。" -ForegroundColor Red 13 | exit 1 14 | } 15 | 16 | # 确保pnpm-lock.yaml存在 17 | if (-not (Test-Path "pnpm-lock.yaml")) { 18 | Write-Host "警告: 未检测到pnpm-lock.yaml文件。" -ForegroundColor Yellow 19 | Write-Host "请确保使用pnpm install安装依赖。" -ForegroundColor Yellow 20 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | engine-strict=true 3 | auto-install-peers=true 4 | strict-peer-dependencies=false 5 | enable-pre-post-scripts=true 6 | public-hoist-pattern[]=*esbuild* 7 | public-hoist-pattern[]=*vue-demi* 8 | -------------------------------------------------------------------------------- /.pnpmrc: -------------------------------------------------------------------------------- 1 | save-workspace-protocol=false 2 | save-prefix='~' 3 | strict-peer-dependencies=false 4 | auto-install-peers=true 5 | resolution-mode=highest -------------------------------------------------------------------------------- /.windsurfrules: -------------------------------------------------------------------------------- 1 | # 环境说明 2 | 当前是windows系统,注意使用windows命令行而非linux命令行。 3 | 4 | # 使用草稿本 5 | 请读取scratchpad.md文件。 6 | 你应该使用 `scratchpad.md` 文件作为草稿本来组织你的想法。特别是当你收到新任务时,你应该首先查看草稿本的内容,必要时清除旧的不同任务,先解释任务内容,然后规划完成任务所需的步骤。你可以使用待办标记来指示进度,例如: 7 | [X] 任务1 8 | [ ] 任务2 9 | 当你完成一个子任务时,也要在草稿本中更新任务进度。 10 | 特别是当你完成一个里程碑时,使用草稿本来反思和规划将有助于提高你的任务完成深度。 11 | 目标是帮助你同时掌握任务的全局视图和进度。在规划下一步时始终参考草稿本。 12 | 13 | # 使用经验 14 | 请读取experience.md文件。 15 | 在与用户交互过程中,如果你发现项目中有任何可重用的内容(例如:库的版本、模型名称),特别是关于你犯的错误的修复或收到的纠正,你应该在 `experience.md` 文件做记录,以避免再次犯同样的错误。 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim AS base 2 | ENV PNPM_HOME="/pnpm" 3 | ENV PATH="$PNPM_HOME:$PATH" 4 | RUN npm install -g corepack@latest && corepack enable 5 | 6 | FROM base AS build 7 | COPY . /app 8 | WORKDIR /app 9 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 10 | RUN pnpm run build 11 | 12 | FROM nginx:stable-alpine 13 | # 安装htpasswd工具和dos2unix 14 | RUN apk add --no-cache apache2-utils dos2unix 15 | 16 | # 复制Nginx配置 17 | COPY docker/nginx.conf /etc/nginx/conf.d/default.conf 18 | 19 | # 复制Web应用 20 | COPY --from=build /app/packages/web/dist /usr/share/nginx/html 21 | 22 | # 复制并设置启动脚本 23 | COPY docker/generate-config.sh /docker-entrypoint.d/40-generate-config.sh 24 | COPY docker/generate-auth.sh /docker-entrypoint.d/30-generate-auth.sh 25 | 26 | # 确保脚本有执行权限 27 | RUN chmod +x /docker-entrypoint.d/40-generate-config.sh 28 | RUN chmod +x /docker-entrypoint.d/30-generate-auth.sh 29 | 30 | # 转换可能的Windows行尾符为Unix格式 31 | RUN dos2unix /docker-entrypoint.d/40-generate-config.sh 32 | RUN dos2unix /docker-entrypoint.d/30-generate-auth.sh 33 | 34 | EXPOSE 80 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 linshenkx 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /api/auth.js: -------------------------------------------------------------------------------- 1 | export default function handler(req, res) { 2 | // 设置CORS头 3 | res.setHeader('Access-Control-Allow-Origin', '*'); 4 | res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); 5 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 6 | 7 | if (req.method === 'OPTIONS') { 8 | res.status(200).end(); 9 | return; 10 | } 11 | 12 | const accessPassword = process.env.ACCESS_PASSWORD; 13 | 14 | // 如果没有设置密码,直接返回成功 15 | if (!accessPassword) { 16 | return res.status(200).json({ 17 | success: true, 18 | message: 'No password protection configured' 19 | }); 20 | } 21 | 22 | if (req.method === 'POST') { 23 | const { password, action } = req.body; 24 | 25 | if (action === 'verify') { 26 | if (password === accessPassword) { 27 | // 设置Cookie以记住用户身份验证状态 28 | const maxAge = 60 * 60 * 24 * 7; // 7天 29 | res.setHeader('Set-Cookie', [ 30 | `vercel_access_token=${accessPassword}; HttpOnly; Path=/; Max-Age=${maxAge}; SameSite=Strict${process.env.NODE_ENV === 'production' ? '; Secure' : ''}` 31 | ]); 32 | 33 | return res.status(200).json({ 34 | success: true, 35 | message: 'Authentication successful' 36 | }); 37 | } else { 38 | return res.status(401).json({ 39 | success: false, 40 | message: 'Invalid password' 41 | }); 42 | } 43 | } 44 | } 45 | 46 | if (req.method === 'GET') { 47 | const { action } = req.query; 48 | 49 | if (action === 'logout') { 50 | // 清除Cookie 51 | res.setHeader('Set-Cookie', [ 52 | 'vercel_access_token=; HttpOnly; Path=/; Max-Age=0; SameSite=Strict' 53 | ]); 54 | 55 | return res.status(200).json({ 56 | success: true, 57 | message: 'Logged out successfully' 58 | }); 59 | } 60 | } 61 | 62 | res.status(405).json({ error: 'Method not allowed' }); 63 | } -------------------------------------------------------------------------------- /api/proxy.js: -------------------------------------------------------------------------------- 1 | export const config = { 2 | runtime: 'edge' 3 | }; 4 | 5 | export default async function handler(req) { 6 | // 处理CORS预检请求 7 | if (req.method === 'OPTIONS') { 8 | return new Response(null, { 9 | status: 200, 10 | headers: { 11 | 'Access-Control-Allow-Origin': '*', 12 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 13 | 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-KEY', 14 | 'Access-Control-Max-Age': '86400', 15 | }, 16 | }); 17 | } 18 | 19 | try { 20 | // 解析请求数据 21 | const { searchParams } = new URL(req.url); 22 | const targetUrl = searchParams.get('targetUrl'); 23 | 24 | if (!targetUrl) { 25 | return new Response(JSON.stringify({ error: '缺少目标URL参数' }), { 26 | status: 400, 27 | headers: { 'Content-Type': 'application/json' }, 28 | }); 29 | } 30 | 31 | // 确保targetUrl是有效的URL 32 | let validTargetUrl; 33 | try { 34 | validTargetUrl = new URL(decodeURIComponent(targetUrl)).toString(); 35 | console.log('目标URL:', validTargetUrl); 36 | } catch (error) { 37 | return new Response(JSON.stringify({ error: `无效的目标URL: ${error.message}` }), { 38 | status: 400, 39 | headers: { 'Content-Type': 'application/json' }, 40 | }); 41 | } 42 | 43 | // 准备请求头 44 | const headers = new Headers(); 45 | req.headers.forEach((value, key) => { 46 | // 排除一些特定的头,这些头可能会导致问题 47 | if (!['host', 'connection', 'content-length'].includes(key.toLowerCase())) { 48 | headers.set(key, value); 49 | } 50 | }); 51 | 52 | // 获取请求体 53 | let body = null; 54 | if (req.method !== 'GET' && req.method !== 'HEAD') { 55 | body = await req.text(); 56 | } 57 | 58 | // 发送请求到目标URL 59 | const fetchResponse = await fetch(validTargetUrl, { 60 | method: req.method, 61 | headers, 62 | body, 63 | }); 64 | 65 | // 读取响应数据 66 | const data = await fetchResponse.text(); 67 | 68 | // 创建响应头 69 | const responseHeaders = new Headers(); 70 | fetchResponse.headers.forEach((value, key) => { 71 | responseHeaders.set(key, value); 72 | }); 73 | 74 | // 设置CORS头 75 | responseHeaders.set('Access-Control-Allow-Origin', '*'); 76 | responseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); 77 | responseHeaders.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-KEY'); 78 | 79 | // 返回响应 80 | return new Response(data, { 81 | status: fetchResponse.status, 82 | statusText: fetchResponse.statusText, 83 | headers: responseHeaders, 84 | }); 85 | } catch (error) { 86 | console.error('代理请求失败:', error); 87 | return new Response(JSON.stringify({ error: `代理请求失败: ${error.message}` }), { 88 | status: 500, 89 | headers: { 90 | 'Content-Type': 'application/json', 91 | 'Access-Control-Allow-Origin': '*', 92 | }, 93 | }); 94 | } 95 | } -------------------------------------------------------------------------------- /api/vercel-status.js: -------------------------------------------------------------------------------- 1 | export const config = { 2 | runtime: 'edge' 3 | }; 4 | 5 | export default async function handler(request) { 6 | return new Response( 7 | JSON.stringify({ 8 | status: 'available', 9 | environment: 'vercel', 10 | proxySupport: true, 11 | version: '1.0.0' 12 | }), 13 | { 14 | status: 200, 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | 'Access-Control-Allow-Origin': '*', 18 | 'Access-Control-Allow-Methods': 'GET, OPTIONS', 19 | 'Access-Control-Allow-Headers': 'Content-Type' 20 | } 21 | } 22 | ); 23 | } -------------------------------------------------------------------------------- /cursor_tips.md: -------------------------------------------------------------------------------- 1 | # AI辅助开发指南 2 | 3 | **1. 简介:** 4 | * 强调采用结构化方法进行AI辅助开发的重要性 5 | * 说明这些指南旨在帮助开发者高效利用AI工具,最小化token使用,减少错误 6 | 7 | **2. 项目设置:** 8 | * 创建项目地图(`fileNames.md`)的重要性 9 | * `fileNames.md`应列出所有文件和目录,包括每个组件用途和功能的单行描述 10 | * 说明文档文件夹的作用,以及以下文件的结构: 11 | - `prd.md`(产品需求文档) 12 | - `app-flow.md`(应用流程) 13 | - `backend-structure.md`(后端结构) 14 | - `frontend-guidelines.md`(前端指南) 15 | - `tech-stack.md`(技术栈) 16 | - `file-structure.md`(文件结构) 17 | 18 | **3. Claude作为"软件架构师":** 19 | * 如何设置专用的Claude项目来优化提示词 20 | * Claude项目知识库应包含: 21 | - 完整的文件结构(`fileNames.md`) 22 | - 主要功能需求文档 23 | - 组件特定的功能需求文档 24 | - Cursor/bolt.new的文档 25 | 26 | **4. 结构化提示流程:** 27 | * 解释包含"系统提示"和"执行提示"的两步提示流程 28 | * 使用模式说明: 29 | - 使用系统提示设置Claude上下文 30 | - 使用执行提示让Claude分析问题,识别受影响的文件,并建议高效方法 31 | * 提供执行提示的详细示例,如:"我们需要为登录表单添加邮箱验证。参考`fileNames.md`中的`src/components/login.jsx`和项目知识中的`Documentation/FRD/auth.md`。请为`bolt.new`建议一个修改该文件的高效方法,添加邮箱验证逻辑。" 32 | 33 | **5. Cursor提示技巧:** 34 | * **"修复错误"**: 35 | - 说明AI模型有时会遗漏细节并触发错误循环 36 | - 提示词:"分析此错误。识别其原因并创建分步解决方案。" 37 | * **"新功能"**: 38 | - 说明AI需要针对每个组件范围重新上下文化 39 | - 提示词:"阅读`@`中的描述并创建实现计划。在继续之前,编写实现计划。在执行之前解释你要更改的内容。" 40 | * **响应结构**: 41 | - 说明如何向AI提供更新和上下文 42 | - 提供示例:"页眉现已对齐。现在我们需要一个登录按钮。查看@login-doc并解释你的方法" 43 | 44 | **6. 进度追踪:** 45 | * `progress.md`文件的用途和提示词:"在每个完成的步骤结束时,在`@progress.md`中记录你的工作。实现了哪些功能,出现了什么错误,以及如何修复的?按顺序回答这三个问题,不要遗漏信息。" 46 | * `project-status.md`文件的用途和提示词:"在每个工作会话结束时,在`@project-status.md`中记录你的工作。查看`@progress.md`文件以总结所有工作,描述本次会话完成的内容。为下一个工作会话创建详细报告,以便提供完整概述。" 47 | 48 | **7. Cursor代理技巧:** 49 | * 解释Cursor代理可能的过度执行问题 50 | * 提供以下指导来防止该问题:"阅读@(文档名称)以确定函数范围。使用思维链逻辑创建分步实现计划。详细概述功能的每个部分,提供高层次概述。将这些部分分解为详细的编号步骤。这将提供一个你可以批准的计划,并确保操作符合要求。" 51 | 52 | **8. `.bolt/ignore`优化:** 53 | * 解释最小化LLM上下文的必要性 54 | * 解释`.bolt/ignore`文件的使用方法以及如何识别要排除的文件和目录 55 | 56 | **9. 结论:** 57 | * 重申这种方法的总体目标以及它如何使开发人员能够高效工作 58 | -------------------------------------------------------------------------------- /dev.md: -------------------------------------------------------------------------------- 1 | # 开发指南 (Development Guide) 2 | 3 | ## 目录 4 | 5 | - [本地开发环境配置](#本地开发环境配置) 6 | - [Docker开发和部署](#docker开发和部署) 7 | - [环境变量配置](#环境变量配置) 8 | - [开发工作流程](#开发工作流程) 9 | - [项目构建和部署](#项目构建和部署) 10 | - [常见问题解决](#常见问题解决) 11 | 12 | ## 本地开发环境配置 13 | 14 | ### 基础环境要求 15 | - Node.js >= 18 16 | - pnpm >= 8 17 | - Git >= 2.0 18 | - VSCode (推荐) 19 | 20 | ### 开发环境设置 21 | ```bash 22 | # 1. 克隆项目 23 | git clone https://github.com/linshenkx/prompt-optimizer.git 24 | cd prompt-optimizer 25 | 26 | # 2. 安装依赖 27 | pnpm install 28 | 29 | # 3. 启动开发服务 30 | pnpm dev # 主开发命令:构建core/ui并运行web应用 31 | pnpm dev:web # 仅运行web应用 32 | pnpm dev:fresh # 完整重置并重新启动开发环境 33 | ``` 34 | 35 | ## Docker开发和部署 36 | 37 | ### 环境要求 38 | - Docker >= 20.10.0 39 | 40 | ### Docker构建和运行 41 | 42 | #### 基础构建 43 | ```bash 44 | # 获取package.json中的版本号 45 | $VERSION=$(node -p "require('./package.json').version") 46 | 47 | # 构建镜像(使用动态版本号) 48 | docker build -t linshen/prompt-optimizer:$VERSION . 49 | 50 | # 添加latest标签 51 | docker tag linshen/prompt-optimizer:$VERSION linshen/prompt-optimizer:latest 52 | 53 | # 运行容器 54 | docker run -d -p 80:80 --restart unless-stopped --name prompt-optimizer -e ACCESS_PASSWORD=1234!@#$ linshen/prompt-optimizer:$VERSION 55 | 56 | 57 | # 推送 58 | docker push linshen/prompt-optimizer:$VERSION 59 | docker push linshen/prompt-optimizer:latest 60 | 61 | ``` 62 | 63 | docker本地构建测试 64 | ```shell 65 | docker build -t linshen/prompt-optimizer:test . 66 | docker rm -f prompt-optimizer 67 | docker run -d -p 80:80 --restart unless-stopped --name prompt-optimizer -e VITE_GEMINI_API_KEY=111 linshen/prompt-optimizer:test 68 | 69 | ``` 70 | 71 | 72 | ### 多阶段构建说明 73 | 74 | Dockerfile使用了多阶段构建优化镜像大小: 75 | 76 | 1. `base`: 基础Node.js环境,安装pnpm 77 | 2. `builder`: 构建阶段,安装依赖并构建项目 78 | 3. `production`: 最终镜像,只包含构建产物和nginx 79 | 80 | ## 环境变量配置 81 | 82 | ### 本地开发环境变量 83 | 在项目根目录创建 `.env.local` 文件: 84 | 85 | ```env 86 | # OpenAI API配置 87 | VITE_OPENAI_API_KEY=your_openai_api_key 88 | 89 | # Gemini API配置 90 | VITE_GEMINI_API_KEY=your_gemini_api_key 91 | 92 | # DeepSeek API配置 93 | VITE_DEEPSEEK_API_KEY=your_deepseek_api_key 94 | 95 | # 自定义API配置 96 | VITE_CUSTOM_API_KEY=your_custom_api_key 97 | VITE_CUSTOM_API_BASE_URL=your_custom_api_base_url 98 | VITE_CUSTOM_API_MODEL=your_custom_model_name 99 | ``` 100 | 101 | ### Docker环境变量 102 | 通过 `-e` 参数设置容器环境变量: 103 | 104 | ```bash 105 | docker run -d -p 80:80 \ 106 | -e VITE_OPENAI_API_KEY=your_key \ 107 | -e VITE_CUSTOM_API_BASE_URL=your_api_url \ 108 | prompt-optimizer 109 | ``` 110 | 111 | ## 开发工作流程 112 | 113 | ### 代码提交规范 114 | ```bash 115 | # 提交格式 116 | (): 117 | 118 | # 示例 119 | feat(ui): 添加新的提示词编辑器组件 120 | fix(core): 修复API调用超时问题 121 | ``` 122 | 123 | ### 测试流程 124 | ```bash 125 | # 运行所有测试 126 | pnpm test 127 | 128 | # 运行特定包的测试 129 | pnpm test:core 130 | pnpm test:ui 131 | pnpm test:web 132 | ``` 133 | 134 | ## 项目构建和部署 135 | 136 | ### 本地构建 137 | ```bash 138 | # 构建所有包 139 | pnpm build 140 | 141 | # 构建特定包 142 | pnpm build:core 143 | pnpm build:ui 144 | pnpm build:web 145 | pnpm build:ext 146 | ``` 147 | 148 | ### 常用Docker命令 149 | 150 | ```bash 151 | # 查看容器日志 152 | docker logs -f prompt-optimizer 153 | 154 | # 进入容器 155 | docker exec -it prompt-optimizer sh 156 | 157 | # 容器管理 158 | docker stop prompt-optimizer 159 | docker start prompt-optimizer 160 | docker restart prompt-optimizer 161 | 162 | # 清理资源 163 | docker rm prompt-optimizer 164 | docker rmi prompt-optimizer 165 | ``` 166 | 167 | ## 常见问题解决 168 | 169 | ### 依赖安装问题 170 | ```bash 171 | # 清理依赖缓存 172 | pnpm clean 173 | 174 | # 重新安装依赖 175 | pnpm install --force 176 | ``` 177 | 178 | ### 开发环境问题 179 | ```bash 180 | # 完全重置开发环境 181 | pnpm dev:fresh 182 | 183 | # 清理构建缓存 184 | pnpm clean 185 | rm -rf node_modules 186 | pnpm install 187 | ``` 188 | 189 | ### 构建失败处理 190 | 1. 检查Node.js版本是否符合要求 191 | 2. 清理构建缓存:`pnpm clean` 192 | 3. 重新安装依赖:`pnpm install` 193 | 4. 查看详细构建日志:`pnpm build --debug` 194 | 195 | ### 容器运行问题 196 | 1. 检查端口占用:`netstat -ano | findstr :80` 197 | 2. 检查容器日志:`docker logs prompt-optimizer` 198 | 3. 检查容器状态:`docker ps -a` 199 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | prompt-optimizer: 5 | image: linshen/prompt-optimizer:latest 6 | # Alternatively, you can build from source: 7 | # build: 8 | # context: . 9 | # dockerfile: Dockerfile 10 | container_name: prompt-optimizer 11 | restart: unless-stopped 12 | ports: 13 | - "8081:80" 14 | environment: 15 | # OpenAI API配置 16 | - VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY:-} 17 | # Gemini API配置 18 | - VITE_GEMINI_API_KEY=${VITE_GEMINI_API_KEY:-} 19 | # DeepSeek API配置 20 | - VITE_DEEPSEEK_API_KEY=${VITE_DEEPSEEK_API_KEY:-} 21 | # SiliconFlow API配置 22 | - VITE_SILICONFLOW_API_KEY=${VITE_SILICONFLOW_API_KEY:-} 23 | # 自定义API配置 24 | - VITE_CUSTOM_API_KEY=${VITE_CUSTOM_API_KEY:-} 25 | - VITE_CUSTOM_API_BASE_URL=${VITE_CUSTOM_API_BASE_URL:-} 26 | - VITE_CUSTOM_API_MODEL=${VITE_CUSTOM_API_MODEL:-} 27 | # Basic认证配置(可选) 28 | - ACCESS_USERNAME=${ACCESS_USERNAME:-admin} # 可选,默认为"admin" 29 | - ACCESS_PASSWORD=${ACCESS_PASSWORD:-} # 设置访问密码 30 | # 健康检查 31 | healthcheck: 32 | test: ["CMD", "curl", "-f", "http://localhost:80"] 33 | interval: 30s 34 | timeout: 10s 35 | retries: 3 36 | start_period: 5s -------------------------------------------------------------------------------- /docker/generate-auth.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 检查是否设置了ACCESS_PASSWORD环境变量 4 | if [ -n "$ACCESS_PASSWORD" ]; then 5 | # 检查密码是否为空字符串 6 | if [ "$ACCESS_PASSWORD" = "" ]; then 7 | echo "警告: 设置了空密码,不安全。不启用Basic认证" 8 | # 创建空的auth配置(禁用认证) 9 | cat > /etc/nginx/conf.d/auth.conf << EOF 10 | # Basic认证未启用 - 密码为空 11 | auth_basic off; 12 | EOF 13 | exit 0 14 | fi 15 | 16 | echo "启用Basic认证..." 17 | 18 | # 创建认证文件目录 19 | mkdir -p /etc/nginx/auth 20 | 21 | # 确定用户名(如果未设置ACCESS_USERNAME则使用默认值"admin") 22 | USERNAME=${ACCESS_USERNAME:-admin} 23 | 24 | # 生成htpasswd文件 - 使用printf避免特殊字符问题 25 | printf '%s' "$ACCESS_PASSWORD" | htpasswd -i -c /etc/nginx/auth/.htpasswd "$USERNAME" 26 | 27 | # 容器环境中简化权限管理 - 确保所有人都可读取认证文件 28 | chmod -R a+r /etc/nginx/auth 29 | 30 | # 创建启用认证的配置 31 | cat > /etc/nginx/conf.d/auth.conf << EOF 32 | # 此文件由generate-auth.sh脚本自动生成 33 | auth_basic "请输入访问凭据 (Please enter your credentials)"; 34 | auth_basic_user_file /etc/nginx/auth/.htpasswd; 35 | EOF 36 | 37 | echo "Basic认证已配置,用户名: $USERNAME" 38 | else 39 | echo "未设置ACCESS_PASSWORD环境变量,不启用Basic认证" 40 | 41 | # 创建空的auth配置(禁用认证) 42 | cat > /etc/nginx/conf.d/auth.conf << EOF 43 | # Basic认证未启用 44 | auth_basic off; 45 | EOF 46 | fi -------------------------------------------------------------------------------- /docker/generate-config.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 配置文件路径 4 | CONFIG_FILE="/usr/share/nginx/html/config.js" 5 | 6 | # 生成配置文件 7 | cat > $CONFIG_FILE << EOF 8 | window.runtime_config = { 9 | OPENAI_API_KEY: "${VITE_OPENAI_API_KEY:-}", 10 | GEMINI_API_KEY: "${VITE_GEMINI_API_KEY:-}", 11 | DEEPSEEK_API_KEY: "${VITE_DEEPSEEK_API_KEY:-}", 12 | SILICONFLOW_API_KEY: "${VITE_SILICONFLOW_API_KEY:-}", 13 | CUSTOM_API_KEY: "${VITE_CUSTOM_API_KEY:-}", 14 | CUSTOM_API_BASE_URL: "${VITE_CUSTOM_API_BASE_URL:-}", 15 | CUSTOM_API_MODEL: "${VITE_CUSTOM_API_MODEL:-}" 16 | }; 17 | console.log("运行时配置已加载"); 18 | EOF 19 | 20 | echo "配置文件已生成: $CONFIG_FILE" -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name _; 4 | root /usr/share/nginx/html; 5 | index index.html; 6 | 7 | # 安全相关头部 8 | add_header X-Frame-Options "SAMEORIGIN" always; 9 | add_header X-XSS-Protection "1; mode=block" always; 10 | add_header X-Content-Type-Options "nosniff" always; 11 | add_header Referrer-Policy "no-referrer-when-downgrade" always; 12 | add_header Content-Security-Policy "default-src 'self' https: http: data: blob: 'unsafe-inline'" always; 13 | 14 | # 启用gzip压缩 15 | gzip on; 16 | gzip_vary on; 17 | gzip_proxied any; 18 | gzip_comp_level 6; 19 | gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss text/javascript application/x-javascript image/svg+xml; 20 | gzip_min_length 1000; 21 | 22 | # 客户端缓存控制 23 | location /assets { 24 | expires 7d; 25 | add_header Cache-Control "public, no-transform"; 26 | try_files $uri $uri/ =404; 27 | } 28 | 29 | # API请求代理示例(如果需要的话取消注释) 30 | # location /api { 31 | # proxy_pass http://api_backend; 32 | # proxy_http_version 1.1; 33 | # proxy_set_header Upgrade $http_upgrade; 34 | # proxy_set_header Connection 'upgrade'; 35 | # proxy_set_header Host $host; 36 | # proxy_cache_bypass $http_upgrade; 37 | # } 38 | 39 | # SPA应用路由支持 40 | location / { 41 | # 引入Basic认证配置 42 | include /etc/nginx/conf.d/auth.conf; 43 | 44 | try_files $uri $uri/ /index.html; 45 | expires -1; 46 | add_header Cache-Control "no-store, no-cache, must-revalidate"; 47 | } 48 | 49 | # 禁止访问隐藏文件 50 | location ~ /\. { 51 | deny all; 52 | access_log off; 53 | log_not_found off; 54 | } 55 | 56 | # 错误页面配置 57 | error_page 404 /index.html; 58 | error_page 500 502 503 504 /50x.html; 59 | location = /50x.html { 60 | root /usr/share/nginx/html; 61 | } 62 | 63 | # 性能优化:关闭访问日志,只记录错误 64 | access_log off; 65 | error_log /var/log/nginx/error.log error; 66 | 67 | # 禁止特定请求方法 68 | if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE|OPTIONS)$) { 69 | return 444; 70 | } 71 | 72 | # 配置较大文件的传输 73 | client_max_body_size 50m; 74 | client_body_buffer_size 128k; 75 | 76 | # 连接超时设置 77 | keepalive_timeout 65; 78 | client_header_timeout 60; 79 | client_body_timeout 60; 80 | send_timeout 60; 81 | proxy_connect_timeout 60; 82 | proxy_send_timeout 60; 83 | proxy_read_timeout 60; 84 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # 项目文档索引 2 | 3 | ## 文档合并说明 4 | 5 | 为了提高文档的组织性和可读性,我们对项目文档进行了重构和合并。以下是文档合并情况: 6 | 7 | | 新文档 | 包含原文档 | 说明 | 8 | |-------|------------|------| 9 | | [technical-development-guide.md](./technical-development-guide.md) | development-guidelines.md, technical-documentation.md | 技术开发指南,整合了开发规范和技术文档 | 10 | | [project-structure.md](./project-structure.md) | file-structure.md, fileNames.md | 项目结构说明,包含文件结构和目录组织 | 11 | | [project-status.md](./project-status.md) | progress.md, chrome-extension-plan.md | 项目状态,包含进度和Chrome扩展计划 | 12 | 13 | 14 | ## 核心文档 15 | 16 | - [README.md](../README.md) - 项目总体介绍 17 | - [technical-development-guide.md](./technical-development-guide.md) - 技术开发指南,包含技术栈和开发规范 18 | - [llm-params-guide.md](./llm-params-guide.md) - LLM参数配置详细指南 19 | - [project-structure.md](./project-structure.md) - 项目结构,专注于文件和目录组织 20 | - [project-status.md](./project-status.md) - 项目状态,包含进度和计划 21 | - [prd.md](./prd.md) - 产品需求文档 22 | 23 | ## 其他资源 24 | 25 | - [experience.md](./experience.md) - 项目经验总结 26 | - [scratchpad.md](./scratchpad.md) - 开发笔记 (已简化) 27 | - [vercel.md](./vercel.md) / [vercel_en.md](./vercel_en.md) - Vercel部署指南 28 | 29 | ## 如何使用文档 30 | 31 | 1. **新成员入职**: 32 | - 首先阅读 README.md,了解项目概况 33 | - 查看 project-structure.md 了解项目结构 34 | - 参考 technical-development-guide.md 了解技术实现和开发规范 35 | 36 | 2. **开发参考**: 37 | - 遵循 technical-development-guide.md 中的开发规范 38 | - 查阅 technical-development-guide.md 了解应用流程 39 | - 参考 project-structure.md 了解代码组织 40 | 41 | 3. **项目进度**: 42 | - 通过 project-status.md 了解当前进度和计划 43 | - 关注 CHANGELOG.md 了解最新变更 44 | 45 | ## 文档维护说明 46 | 47 | 1. 所有文档采用 Markdown 格式 48 | 2. 文档更新后,请在文档末尾添加更新时间 49 | 3. 重要变更请同时更新 project-status.md 中的更新记录 50 | 4. 文档中的代码示例应确保可运行且符合项目规范 51 | 5. 注意区分不同文档的职责,避免内容重复 52 | 6. 弃用的文档应标记为已弃用,并引导用户使用新文档 53 | 54 | 最后更新:2025-01-06 -------------------------------------------------------------------------------- /docs/experience.md: -------------------------------------------------------------------------------- 1 | # 项目核心经验总结 2 | 3 | ## 📋 核心知识点 4 | - [架构设计](#架构设计) - API集成、模块化结构 5 | - [错误处理](#错误处理) - 常见问题与解决方案 6 | - [测试规范](#测试规范) - 关键测试要点 7 | - [开发实践](#开发实践) - Vue、工具配置、最佳实践 8 | - [重要Bug修复](#重要bug修复) - 安全漏洞与性能问题 9 | 10 | --- 11 | 12 | ## 架构设计 13 | 14 | ### API集成核心原则 15 | ```js 16 | // 统一OpenAI兼容格式 17 | export default { 18 | baseURL: "https://api.openai.com/v1", 19 | models: ["gpt-4", "gpt-3.5"], 20 | apiKey: import.meta.env.VITE_API_KEY // Vite项目必须使用import.meta.env 21 | } 22 | ``` 23 | 24 | ### 模块化结构 25 | ``` 26 | src/ 27 | ├─ api/ # API封装层 28 | ├─ services/ # 业务逻辑 29 | ├─ config/ # 配置管理 30 | ├─ components/ # UI组件 31 | └─ prompts/ # 提示模板 32 | ``` 33 | 34 | ### LLM服务设计要点 35 | - **接口标准化**: 统一使用OpenAI格式 36 | - **多服务商兼容**: Provider标识区分 37 | - **敏感信息管理**: 环境变量+本地加密存储 38 | - **用户自管理API密钥**: 避免后端开销,保持应用简单性 39 | - **参数透明化**: 只使用用户明确配置的参数,不设置默认值避免误解 40 | 41 | --- 42 | 43 | ## 错误处理 44 | 45 | ### 核心处理策略 46 | ```js 47 | // 统一错误处理模板 48 | try { 49 | await apiCall(); 50 | } catch (err) { 51 | console.error("[API Error]", err.context); 52 | throw new Error("友好的错误提示"); 53 | } 54 | ``` 55 | 56 | ### 常见问题速查表 57 | | 问题 | 解决方案 | 日期 | 58 | |------|----------|------| 59 | | 模板ID与模型Key混淆 | 明确功能ID与API Key分离 | 2024-03-22 | 60 | | 状态同步异常 | 增加状态同步处理函数 | 2024-03-22 | 61 | | 全局Provider污染 | 显式传递模型参数 | 2024-03-22 | 62 | 63 | --- 64 | 65 | ## 测试规范 66 | 67 | ### 关键要点 68 | 1. **环境变量**: Vite项目使用 `import.meta.env.VITE_*` 69 | 2. **测试隔离**: 使用动态唯一标识符避免冲突 70 | 3. **错误场景**: 覆盖网络错误、无效Token等异常 71 | 4. **状态管理**: 独立测试数据库、正确清理状态 72 | 5. **参数测试**: 为每个LLM参数创建独立测试用例,确保参数兼容性 73 | 74 | ### 测试模板 75 | ```js 76 | describe("功能测试", () => { 77 | beforeEach(() => { 78 | // 使用唯一标识符 79 | testId = `test-${Date.now()}`; 80 | }); 81 | 82 | it("应正确处理异常", async () => { 83 | await expect(func()).rejects.toThrow("预期错误"); 84 | }); 85 | }); 86 | 87 | // LLM参数测试最佳实践 88 | describe("Individual Parameter Tests", () => { 89 | // 为每个advancedParameterDefinitions中的参数创建独立测试 90 | it("should accept valid temperature", async () => { 91 | await modelManager.updateModel(configKey, { 92 | // ... 基础配置 93 | llmParams: { temperature: 0.3 } // 只测试一个参数 94 | }); 95 | 96 | const response = await llmService.sendMessage(messages, configKey); 97 | expect(response).toBeDefined(); 98 | }); 99 | 100 | // 组合参数测试 101 | it("should handle multiple parameters together", async () => { 102 | await modelManager.updateModel(configKey, { 103 | // ... 基础配置 104 | llmParams: { 105 | temperature: 0.6, 106 | max_tokens: 150, 107 | top_p: 0.9 108 | } 109 | }); 110 | }); 111 | }); 112 | ``` 113 | 114 | --- 115 | 116 | ## 开发实践 117 | 118 | ### Vue开发规范 119 | ```js 120 | // ✅ 正确: 组件顶层调用Composable 121 | const { data } = useFetch(); 122 | 123 | // ❌ 错误: 生命周期内调用 124 | onMounted(() => { 125 | const { data } = useFetch(); // 禁止 126 | }); 127 | ``` 128 | 129 | ### 工具配置 130 | ```bash 131 | # 常用NPM命令 132 | npm outdated # 检查更新 133 | ncu -u "eslint*" # 安全更新指定包 134 | npm run test # 每次修改后必须执行 135 | ``` 136 | 137 | ### 流式处理最佳实践 138 | ```js 139 | // 统一流式处理器 140 | const handlers = { 141 | onToken: (token) => result.value += token, 142 | onComplete: () => isLoading.value = false, 143 | onError: (error) => toast.error(error.message) 144 | }; 145 | ``` 146 | 147 | --- 148 | 149 | ## 重要Bug修复 150 | 151 | ### 安全漏洞修复 (2024-12-20) 152 | | Bug类型 | 风险等级 | 修复状态 | 153 | |---------|----------|----------| 154 | | UI配置导入验证不充分 | 中 | ✅ 已修复 | 155 | | 数据迁移竞态条件 | 中 | ✅ 已修复 | 156 | | 测试覆盖缺失 | 高 | ✅ 已修复 | 157 | | LLM参数默认值误导用户 | 中 | ✅ 已修复 | 158 | 159 | #### 关键修复示例 160 | ```ts 161 | // UI配置导入安全验证 162 | for (const [key, value] of Object.entries(typedData.userSettings)) { 163 | // 白名单验证 164 | if (!UI_SETTINGS_KEYS.includes(key as any)) { 165 | console.warn(`跳过未知的UI配置键: ${key}`); 166 | continue; 167 | } 168 | // 类型验证 169 | if (typeof value !== 'string') { 170 | console.warn(`跳过无效类型 ${key}: ${typeof value}`); 171 | continue; 172 | } 173 | await this.storage.setItem(key, value); 174 | } 175 | 176 | // LLM参数透明化处理 (2024-12-20) 177 | // ❌ 旧版本:自动设置默认值 178 | if (completionConfig.temperature === undefined) { 179 | completionConfig.temperature = 0.7; // 可能误导用户 180 | } 181 | 182 | // ✅ 新版本:只使用用户明确配置的参数 183 | const completionConfig: any = { 184 | model: modelConfig.defaultModel, 185 | messages: formattedMessages, 186 | ...restLlmParams // 只传递用户配置的参数 187 | }; 188 | // 不设置任何默认值,让API服务商使用其默认配置 189 | ``` 190 | 191 | --- 192 | 193 | ## 核心经验要点 194 | 195 | ### 配置管理 196 | - 业务逻辑与API配置解耦 197 | - 支持动态配置更新 198 | - 环境变量使用Vite规范 199 | 200 | ### 错误处理 201 | - 开发环境保留完整堆栈 202 | - 生产环境友好提示+日志 203 | - 统一错误处理机制 204 | 205 | ### 测试策略 206 | - 测试用例隔离 207 | - 覆盖边界条件 208 | - Mock最小必要依赖 209 | 210 | ### 安全考虑 211 | - 输入验证白名单 212 | - 防止原型污染 213 | - 数据迁移原子性 214 | 215 | ### 性能优化 216 | - 流式处理提升体验 217 | - 组件懒加载 218 | - 合理状态管理 -------------------------------------------------------------------------------- /docs/prd.md: -------------------------------------------------------------------------------- 1 | # 提示词优化器产品需求文档 2 | 3 | ## 1. 产品概述 4 | 提示词优化器是一个纯前端的工具,帮助用户优化和改进AI提示词。通过集成多个LLM模型,为用户提供专业的提示词优化建议。 5 | 6 | ## 2. 目标用户 7 | - AI应用开发者 8 | - 提示词工程师 9 | - 需要与AI模型交互的普通用户 10 | 11 | ## 3. 核心功能需求 12 | 13 | ### 3.1 提示词优化 14 | - 支持输入原始提示词 15 | - 提供多个LLM模型选择 16 | - 实时字数统计 17 | - 一键清空输入 18 | - 优化结果预览 19 | - 一键复制结果 20 | 21 | ### 3.2 模型管理 22 | - 支持多个LLM模型(DeepSeek、Gemini等) 23 | - API密钥管理 24 | - 模型配置编辑 25 | - 自定义模型支持 26 | 27 | ### 3.3 历史记录 28 | - 本地保存优化历史 29 | - 按时间排序 30 | - 搜索和过滤 31 | - 一键重用历史记录 32 | - 删除历史记录 33 | 34 | ### 3.4 用户界面 35 | - 响应式设计 36 | - 深色/浅色主题 37 | - 多语言支持 38 | - 操作反馈提示 39 | 40 | ## 4. 非功能需求 41 | 42 | ### 4.1 性能要求 43 | - 页面加载时间 < 2秒 44 | - API响应时间 < 5秒 45 | - 流畅的动画效果 46 | 47 | ### 4.2 安全要求 48 | - API密钥加密存储 49 | - 本地数据安全存储 50 | - 敏感信息保护 51 | 52 | ### 4.3 兼容性要求 53 | - 支持主流浏览器 54 | - 移动端适配 55 | - 响应式布局 56 | 57 | ## 5. 未来规划 58 | - 支持更多LLM模型 59 | - 提示词模板库 60 | - 批量优化功能 61 | - 提示词评分系统 62 | - 社区分享功能 -------------------------------------------------------------------------------- /docs/project-status.md: -------------------------------------------------------------------------------- 1 | # 项目状态文档 2 | 3 | ## 1. 项目概述 4 | 5 | 提示词优化器是一个帮助用户优化AI提示词的工具,支持多种模型和界面形式。包括Web应用和Chrome浏览器插件两种使用方式,采用monorepo结构进行开发。 6 | 7 | ## 2. 总体进度 8 | - 项目完成度:95% 9 | - 当前阶段:功能完善与用户体验优化 10 | - 主要版本:v1.0.6 11 | - 最新更新:2025年1月 12 | 13 | ## 3. 功能完成情况 14 | 15 | ### 3.1 核心包(@prompt-optimizer/core) 16 | - ✅ 基础架构搭建 17 | - ✅ 项目结构设计 18 | - ✅ 多包工作区配置 19 | - ✅ 基础设施搭建 20 | 21 | - ✅ 服务迁移与优化 22 | - ✅ 从LangChain迁移到原生SDK 23 | - ✅ 模型管理服务优化 24 | - ✅ 提示词服务优化 25 | - ✅ 模板服务完善 26 | - ✅ 历史记录服务重构 27 | 28 | - ✅ 模型集成 29 | - ✅ OpenAI集成 30 | - ✅ Gemini集成 31 | - ✅ DeepSeek集成 32 | - ✅ 自定义API支持 33 | - ✅ 流式响应支持 34 | - ✅ 错误处理优化 35 | 36 | ### 3.2 Web包(@prompt-optimizer/web) 37 | - ✅ UI重构 38 | - ✅ 组件模块化 39 | - ✅ UI包抽取 40 | - ✅ 服务调用更新 41 | - ✅ 错误处理优化 42 | 43 | - ✅ 功能增强 44 | - ✅ 流式响应UI 45 | - ✅ 模型连接测试 46 | - ✅ 配置验证增强 47 | - ✅ Toast组件迁移 48 | - ✅ 环境变量加载优化 49 | 50 | ### 3.3 Chrome插件(@prompt-optimizer/extension) 51 | - ✅ 基础框架 52 | - ✅ 插件架构设计 53 | - ✅ 核心功能移植 54 | - ✅ 权限管理 55 | - ✅ UI组件复用 56 | - ✅ 特性开发 57 | - ✅ 右键菜单集成 58 | - ✅ 快捷键支持 59 | - ✅ 历史同步 60 | - ✅ 配置管理 61 | 62 | ## 4. 进行中的任务 63 | 64 | ### 4.1 核心功能完善(进度:90%) 65 | - ✅ 错误处理系统 66 | - ✅ 统一错误类型 67 | - ✅ 错误处理流程 68 | - ✅ 错误恢复机制 69 | - ⏳ 性能优化 70 | - ✅ 原生SDK迁移 71 | - ✅ 资源管理优化 72 | - ⏳ 内存使用优化 73 | 74 | ### 4.2 测试覆盖(进度:70%) 75 | - ✅ 单元测试 76 | - ✅ 服务测试 77 | - ✅ 工具函数测试 78 | - ✅ 错误处理测试 79 | - ⏳ 集成测试 80 | - ✅ 服务集成测试 81 | - ⏳ API集成测试 82 | - ⏳ 流程测试 83 | 84 | ### 4.3 文档完善(进度:85%) 85 | - ✅ 核心文档 86 | - ✅ 架构文档 87 | - ✅ API文档 88 | - ✅ 开发指南 89 | - ⏳ 使用文档 90 | - ✅ 最佳实践 91 | - ⏳ 示例代码 92 | - ⏳ 故障排除 93 | 94 | ### 4.4 Chrome插件优化(进度:90%) 95 | - ✅ 性能优化 96 | - ✅ 资源加载优化 97 | - ✅ 响应速度优化 98 | - ⏳ 内存使用优化 99 | 100 | - ✅ 安全加固 101 | - ✅ 权限管理 102 | - ✅ 数据安全 103 | - ⏳ 通信安全 104 | 105 | - ⏳ 测试与文档 106 | - ✅ 单元测试 107 | - ⏳ 集成测试 108 | - ⏳ 文档更新 109 | 110 | ## 5. 待开发功能 111 | 112 | ### 5.1 高级功能(计划启动:4月初) 113 | - ⏳ 批量处理 114 | - ⏳ 批量优化 115 | - ⏳ 任务队列 116 | - ⏳ 进度管理 117 | - ⏳ 提示词分析 118 | - ⏳ 质量评估 119 | - ⏳ 性能分析 120 | - ⏳ 优化建议 121 | 122 | ## 6. 技术指标 123 | 124 | ### 6.1 当前指标(2024-02-26) 125 | - 代码测试覆盖率:80% 126 | - 页面加载时间:1.3秒 127 | - API响应时间:0.8-2.0秒 128 | - 首次内容渲染:0.8秒 129 | 130 | ### 6.2 目标指标(4月初) 131 | - 代码测试覆盖率:>85% 132 | - 页面加载时间:<1.2秒 133 | - API响应时间:<1.5秒 134 | - 首次内容渲染:<0.8秒 135 | 136 | ## 7. 风险评估 137 | 138 | ### 7.1 技术风险 139 | - 🟢 原生SDK集成 140 | - 版本兼容性已解决 141 | - API稳定性已验证 142 | - 性能提升明显 143 | - 🟢 多模型支持 144 | - API差异处理已完成 145 | - 错误处理统一完成 146 | - 配置复杂性降低 147 | - 🟡 安全性问题 148 | - API密钥保护已实现 149 | - 数据安全待加强 150 | - XSS防护完善中 151 | 152 | ### 7.2 项目风险 153 | - 🟢 进度风险 154 | - 核心功能已完成 155 | - 测试覆盖持续增加 156 | - 文档更新同步 157 | - 🟢 质量风险 158 | - 代码质量控制 159 | - 性能优化明显 160 | - 用户体验提升 161 | - 🟢 Chrome API兼容性(已解决) 162 | - 🟡 性能瓶颈(优化中) 163 | - 🟢 跨域通信(已解决) 164 | 165 | ## 8. 发布计划 166 | 167 | ### 8.1 测试版(v0.1.0)- 预计3月初发布 168 | - ✅ 基础功能可用 169 | - ✅ 核心特性完整 170 | - ✅ 初步性能优化 171 | - ✅ 基本安全措施 172 | 173 | ### 8.2 正式版(v1.0.0)- 预计3月中旬发布 174 | - ⏳ 完整功能集 175 | - ⏳ 性能优化完成 176 | - ⏳ 安全措施完善 177 | - ⏳ 文档完整 178 | 179 | ## 9. 发布准备 180 | 181 | ### 9.1 商店发布材料(进行中) 182 | - ⏳ 扩展描述 183 | - ⏳ 详细功能介绍 184 | - ⏳ 高质量截图(至少3张) 185 | - ⏳ 宣传视频(可选) 186 | - ⏳ 隐私政策 187 | 188 | ### 9.2 最终审核(计划中) 189 | - ⏳ 代码审核 190 | - ⏳ 功能测试 191 | - ⏳ 权限审查 192 | - ⏳ 安全检查 193 | - ⏳ 性能测试 194 | 195 | ## 10. 后续计划 196 | 197 | ### 10.1 近期计划(1-2周) 198 | 1. 完成剩余功能优化 199 | - 内存使用优化 200 | - 性能进一步调优 201 | - 用户体验改进 202 | 203 | 2. 提升测试覆盖率 204 | - 补充集成测试 205 | - 完善API测试 206 | - 添加E2E测试 207 | 208 | 3. 完善文档系统 209 | - 更新技术栈文档 210 | - 添加示例代码 211 | - 编写故障排除指南 212 | 213 | ### 10.2 中期计划(2-3周) 214 | 1. 完成Chrome插件发布准备 215 | - 最终功能测试 216 | - 性能优化 217 | - 文档准备 218 | - 商店资料准备 219 | 220 | 2. 开发高级功能 221 | - 实现批量处理 222 | - 添加分析功能 223 | - 优化用户体验 224 | 225 | ### 10.3 长期计划(1-2月) 226 | 1. 产品化完善 227 | - 功能完整性 228 | - 稳定性提升 229 | - 性能持续优化 230 | 231 | 2. 社区建设 232 | - 开源推广 233 | - 文档完善 234 | - 示例丰富 235 | 236 | ## 11. 维护计划 237 | 238 | ### 11.1 日常维护 239 | - 问题修复 240 | - 性能监控 241 | - 安全更新 242 | - 用户反馈 243 | 244 | ### 11.2 版本更新 245 | - 功能迭代 246 | - 性能优化 247 | - 安全加固 248 | - 文档更新 249 | 250 | ## 12. 更新记录 251 | 252 | ### 2025年1月 (v1.0.6) 253 | - 2025-01-06: 添加高级LLM参数配置功能 (llmParams) 254 | - 2024-12-20: 增强数据管理器和模板管理器的导入逻辑 255 | - 2024-12-20: 在模板管理器中添加模板名称显示 256 | - 2024-12-20: 优化数据管理器样式并增强警告信息展示 257 | - 2024-12-15: 添加基本认证功能和环境变量配置 (Docker) 258 | - 2024-12-10: 实现Vercel密码保护功能 259 | - 2024-12-05: 重构数据管理器并添加UI配置导入导出功能 260 | - 2024-11-30: 实现统一存储层和数据导入导出功能 261 | - 2024-11-25: 实现全屏弹窗功能并优化组件交互 262 | - 2024-11-20: 集成Vercel Analytics 263 | - 2024-11-15: 添加Zhipu智谱AI模型支持 264 | - 2024-11-10: 优化PromptPanel组件中版本选择按钮的样式和布局 265 | - 2024-11-05: 测试结果展示框增加放大弹窗功能 266 | 267 | ### 2024年早期版本 268 | - 2024-02-26: 完成从LangChain迁移到原生SDK 269 | - 2024-02-26: 更新项目配置和依赖 270 | - 2024-02-25: 优化环境变量加载和测试集成 271 | - 2024-02-25: 重构核心包导出和模块结构 272 | - 2024-02-21: 重构历史记录管理,移除初始化逻辑并优化UI组件 273 | - 2024-02-18: 改进模板选择类型安全性和错误处理 274 | - 2024-02-18: 模块化UI包并改进扩展和Web应用中的类型安全性 275 | - 2024-02-15: 优化多模型支持 276 | - 2024-02-14: 重构提示词服务 277 | - 2024-02-12: 重构UI组件结构 278 | 279 | ## 13. Chrome扩展开发经验 280 | 281 | ### 13.1 图标问题排查 282 | - manifest.json中的图标设置需要严格遵循Chrome扩展规范 283 | - 图标必须是有效的PNG格式 284 | - 图标尺寸必须严格符合声明(16x16、48x48、128x128) 285 | - 如果图标不显示,可以尝试更换其他已确认可用的PNG图片进行测试 -------------------------------------------------------------------------------- /docs/scratchpad.md: -------------------------------------------------------------------------------- 1 | # 开发笔记 - 提示词优化器 2 | 3 | ## 🎯 当前状态 (2025年1月) 4 | - 项目完成度:95% 5 | - 主要版本:v1.0.6 6 | - 当前阶段:功能完善与用户体验优化 7 | - 最新特性:高级LLM参数配置、数据导入导出、密码保护 8 | 9 | ## 🚀 主要功能特性 10 | 11 | ### ✅ 核心功能 (已完成) 12 | - **多模型支持**: OpenAI、Gemini、DeepSeek、Zhipu、SiliconFlow 13 | - **高级参数配置**: 支持每个模型的LLM参数自定义 (temperature, max_tokens等) 14 | - **提示词优化**: 一键优化、多轮迭代、对比测试 15 | - **历史记录管理**: 本地存储、搜索过滤、导入导出 16 | - **模板系统**: 内置模板、自定义模板、模板管理 17 | - **数据管理**: 统一存储层、数据导入导出、UI配置同步 18 | 19 | ### ✅ 部署与安全 (已完成) 20 | - **多端支持**: Web应用、Chrome插件 21 | - **部署方式**: Vercel、Docker、Docker Compose 22 | - **密码保护**: Vercel和Docker环境的访问控制 23 | - **跨域解决**: Vercel代理支持 24 | - **数据安全**: 本地加密存储、纯客户端处理 25 | 26 | ### ✅ 用户体验 (已完成) 27 | - **响应式设计**: 支持桌面和移动端 28 | - **流式响应**: 实时显示AI生成内容 29 | - **全屏弹窗**: 大屏幕查看和编辑 30 | - **主题支持**: 深色/浅色模式 31 | - **国际化**: 中英文界面 32 | 33 | ## 🔄 近期开发重点 34 | 35 | ### 持续优化项目 36 | - **性能优化**: 内存使用、加载速度、响应时间 37 | - **测试覆盖**: 提升集成测试和E2E测试覆盖率 38 | - **文档维护**: 保持文档与功能同步更新 39 | - **安全加固**: 持续改进数据安全和隐私保护 40 | 41 | ### 用户反馈处理 42 | - **功能改进**: 根据用户反馈优化现有功能 43 | - **Bug修复**: 及时处理用户报告的问题 44 | - **新功能评估**: 评估和规划用户请求的新功能 45 | 46 | ## 📅 未来规划 47 | 48 | ### 潜在新功能 49 | - **批量处理**: 批量优化多个提示词 50 | - **提示词分析**: 质量评估和性能分析 51 | - **协作功能**: 团队共享和协作编辑 52 | - **API集成**: 提供API接口供第三方调用 53 | - **插件生态**: 支持第三方插件扩展 54 | 55 | ## 📊 项目指标 (2025年1月) 56 | - **代码测试覆盖率**: 85%+ 57 | - **页面加载时间**: < 1.2秒 58 | - **API响应时间**: 0.5-1.5秒 59 | - **首次内容渲染**: < 0.8秒 60 | 61 | ## 🔧 技术要点 62 | 63 | ### 核心架构 64 | - **Monorepo结构**: packages/core、packages/web、packages/ui、packages/extension 65 | - **原生SDK集成**: 直接使用OpenAI、Gemini等官方SDK,性能优异 66 | - **统一存储层**: 支持LocalStorage、数据导入导出、配置同步 67 | - **类型安全**: 全面的TypeScript类型定义和验证 68 | 69 | ### 安全设计 70 | - **纯客户端**: 数据不经过中间服务器,直接与AI服务商交互 71 | - **加密存储**: API密钥和敏感数据本地加密存储 72 | - **访问控制**: Vercel和Docker环境支持密码保护 73 | - **输入验证**: 严格的数据验证和XSS防护 74 | 75 | ## 💡 重要技术决策 76 | 77 | ### Vercel密码保护实现 78 | **目标**: 实现非侵入性的密码保护功能 79 | 80 | **解决方案**: 81 | - 使用Vercel重写规则拦截所有请求 82 | - API Functions实现页面级保护 83 | - 保持与主应用完全解耦 84 | - 类似basic认证的用户体验 85 | 86 | **技术要点**: 87 | - 所有逻辑集中在api/文件夹和vercel.json 88 | - 使用HttpOnly cookies存储认证状态 89 | - 服务端密码验证确保安全性 90 | - 可通过删除api文件夹完全禁用功能 91 | 92 | ### LLM参数透明化 93 | **问题**: 自动设置默认值可能误导用户 94 | **解决方案**: 只传递用户明确配置的参数,让API服务商使用其默认配置 95 | 96 | ```typescript 97 | // ❌ 旧版本:自动设置默认值 98 | if (completionConfig.temperature === undefined) { 99 | completionConfig.temperature = 0.7; // 可能误导用户 100 | } 101 | 102 | // ✅ 新版本:只使用用户明确配置的参数 103 | const completionConfig: any = { 104 | model: modelConfig.defaultModel, 105 | messages: formattedMessages, 106 | ...restLlmParams // 只传递用户配置的参数 107 | }; 108 | ``` 109 | 110 | --- 111 | 112 | **最后更新**: 2025年1月 113 | **文档状态**: 已简化,移除过时内容,保留核心技术要点 -------------------------------------------------------------------------------- /docs/vercel.md: -------------------------------------------------------------------------------- 1 | ## Vercel 部署说明 2 | 3 | ### 部署方式对比 4 | 5 | | 部署方式 | 优点 | 缺点 | 6 | |---------|------|------| 7 | | 一键部署 | 快速简便,无需额外设置 | 无法自动同步源项目更新 | 8 | | Fork后导入 | 可跟踪源项目更新,更易维护 | 第一次部署需要手动修复根目录设置以启用Vercel代理功能 | 9 | 10 | ### 推荐方式:Fork项目后导入到Vercel(推荐) 11 | 12 | 这种方式可以让你跟踪项目更新,便于后续同步最新功能和bug修复。 13 | 14 | 1. **Fork项目到自己的GitHub** 15 | - 访问[prompt-optimizer项目](https://github.com/linshenkx/prompt-optimizer) 16 | - 点击右上角的"Fork"按钮 17 | - 完成fork操作后,你将在自己的GitHub账号下拥有此项目的副本 18 | 19 | 2. **导入项目到Vercel** 20 | - 登录[Vercel平台](https://vercel.com/) 21 | - 点击"Add New..."→"Project" 22 | - 在"Import Git Repository"部分找到你fork的项目并点击"Import" 23 | - 配置项目(**注意**:此处虽然可以设置根目录,但对多模块项目无效,仍需后续手动修复) 24 | - 点击"Deploy"开始部署 25 | 26 | ![导入项目到Vercel](../images/vercel/import.png) 27 | 28 | 3. **修复根目录设置(强烈建议)** 29 | - 通过导入部署时,虽然项目的`vercel.json`文件已包含相关修复可以让基本功能正常工作 30 | - 但若要启用**Vercel代理功能**(解决跨域问题的关键功能),则需手动修复根目录: 31 | 32 | a. 在项目部署完成后,进入项目设置 33 | 34 | b. 点击左侧菜单中的"Build and Deployment" 35 | 36 | c. 在"Root Directory"部分,将输入框中的内容**清空** 37 | 38 | d. 点击"Save"保存设置 39 | 40 | ![清空根目录设置](../images/vercel/setting.png) 41 | 42 | 4. **配置环境变量(可选)** 43 | - 部署完成后,进入项目设置 44 | - 点击"Environment Variables" 45 | - 添加需要的API密钥(例如`VITE_OPENAI_API_KEY`) 46 | - 如需添加访问限制功能: 47 | - 添加名为`ACCESS_PASSWORD`的环境变量 48 | - 设置一个安全的密码作为其值 49 | - 保存环境变量设置 50 | 51 | 5. **重新部署项目** 52 | - 设置保存后,需要手动触发重新部署以使修复和环境变量生效 53 | - 点击顶部导航栏中的"Deployments" 54 | - 在最新的部署记录右侧,点击"..."按钮 55 | - 选择"Redeploy"选项触发重新部署 56 | 57 | ![重新部署项目](../images/vercel/redeploy.png) 58 | 59 | 6. **同步上游更新** 60 | - 在GitHub上打开你fork的项目 61 | - 如果有更新,会显示"This branch is X commits behind linshenkx:main" 62 | - 点击"Sync fork"按钮同步最新更改 63 | - Vercel会自动检测到代码变更并重新部署 64 | 65 | ### 替代方式:一键部署到Vercel 66 | 67 | 如果你只需要快速部署而不关心后续更新,可以使用一键部署方式: 68 | 69 | 1. 点击以下按钮直接部署到Vercel 70 | [![部署到 Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flinshenkx%2Fprompt-optimizer) 71 | 72 | 2. 按照Vercel的引导完成部署流程 73 | 74 | **优势:** 一键部署方式Vercel能自动正确识别根目录,无需手动修复,所有功能(包括Vercel代理)均可正常使用。 75 | 76 | ### 关于Vercel代理功能 77 | 78 | Prompt Optimizer在Vercel部署时支持使用Edge Runtime代理解决跨域问题。 79 | 80 | 1. **确认代理功能可用** 81 | - 如使用一键部署:代理功能应直接可用 82 | - 如使用导入部署:需完成上述"修复根目录设置"和"重新部署"步骤 83 | - 在应用中打开"模型管理" 84 | - 选择目标模型->"编辑",此时应该可以看到"使用Vercel代理"选项 85 | - 如果没有看到此选项,说明Vercel Function未正确部署,请检查根目录设置 86 | 87 | 2. **启用代理功能** 88 | - 勾选"使用Vercel代理"选项 89 | - 保存配置 90 | 91 | 3. **代理原理** 92 | - 请求流向:浏览器→Vercel Edge Runtime→模型服务提供商 93 | - 解决了浏览器直接访问API时的跨域限制 94 | - 代理功能基于Vercel Function实现,依赖于`/api`路径 95 | 96 | 4. **注意事项** 97 | - 部分模型服务提供商可能会限制来自Vercel的请求 98 | - 如遇限制,建议使用自部署的API中转服务 99 | 100 | ### 密码保护访问 101 | 102 | 当配置了`ACCESS_PASSWORD`环境变量后,您的站点将启用密码保护功能: 103 | - 访问站点时会显示密码验证页面 104 | - 输入正确密码后可访问应用 105 | - 系统会设置Cookie记住用户,一段时间内无需重复输入密码 106 | 107 | ### 常见问题 108 | 109 | 1. **部署后页面空白或报错** 110 | - 检查是否正确配置了环境变量 111 | - 查看Vercel部署日志寻找错误原因 112 | 113 | 2. **无法连接到模型API** 114 | - 确认API密钥已正确配置 115 | - 尝试启用Vercel代理功能 116 | - 检查模型服务提供商是否限制了Vercel请求 117 | 118 | 3. **"使用Vercel代理"选项未显示** 119 | - 如使用导入部署:检查是否已清空根目录设置并重新部署 120 | - 验证`/api/vercel-status`路径是否可访问(可通过浏览器访问`你的域名/api/vercel-status`测试) 121 | - 查看部署日志中是否有关于Function的错误信息 122 | 123 | 4. **如何更新已部署的项目** 124 | - 如果是fork后导入:同步fork并等待自动部署 125 | - 如果是一键部署:需要重新部署新版本(无法自动跟踪源项目更新) 126 | 127 | 5. **如何添加自定义域名** 128 | - 在Vercel项目设置中选择"Domains" 129 | - 添加并验证你的域名 130 | - 按照指引配置DNS记录 131 | -------------------------------------------------------------------------------- /images/contrast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/images/contrast.png -------------------------------------------------------------------------------- /images/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/images/main.png -------------------------------------------------------------------------------- /images/vercel/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/images/vercel/import.png -------------------------------------------------------------------------------- /images/vercel/redeploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/images/vercel/redeploy.png -------------------------------------------------------------------------------- /images/vercel/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/images/vercel/setting.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prompt-optimizer", 3 | "version": "1.0.6", 4 | "private": true, 5 | "packageManager": "pnpm@10.6.1", 6 | "engines": { 7 | "node": "^18.0.0 || ^20.0.0 || ^22.0.0", 8 | "npm": "请使用pnpm代替npm", 9 | "yarn": "请使用pnpm代替yarn" 10 | }, 11 | "scripts": { 12 | "build:core": "pnpm -F @prompt-optimizer/core build", 13 | "build:ui": "pnpm -F @prompt-optimizer/ui build", 14 | "build:web": "pnpm -F @prompt-optimizer/web build", 15 | "build:ext": "pnpm -F @prompt-optimizer/extension build", 16 | "build": "pnpm -r build", 17 | "watch:ui": "pnpm -F @prompt-optimizer/ui build --watch", 18 | "dev": "npm-run-all dev:setup dev:parallel", 19 | "dev:setup": "npm-run-all clean:dist build:core build:ui", 20 | "dev:parallel": "concurrently -k -p \"[{name}]\" -n \"UI,WEB\" \"pnpm run watch:ui\" \"pnpm run dev:web\"", 21 | "dev:web": "pnpm -F @prompt-optimizer/web dev", 22 | "dev:ext": "pnpm -F @prompt-optimizer/extension dev", 23 | "test:core": "pnpm -F @prompt-optimizer/core test --run --passWithNoTests", 24 | "test:ui": "pnpm -F @prompt-optimizer/ui test --run --passWithNoTests", 25 | "test:web": "pnpm -F @prompt-optimizer/web test --run --passWithNoTests", 26 | "test:ext": "pnpm -F @prompt-optimizer/extension test --run --passWithNoTests", 27 | "test": "pnpm -r test --run --passWithNoTests", 28 | "test:watch": "pnpm -r test --watch --passWithNoTests", 29 | "clean:core": "rimraf packages/core/dist", 30 | "clean:ui": "rimraf packages/ui/dist", 31 | "clean:web": "rimraf packages/web/dist", 32 | "clean:ext": "rimraf packages/extension/dist", 33 | "clean:vite:core": "rimraf packages/core/node_modules/.vite", 34 | "clean:vite:ui": "rimraf packages/ui/node_modules/.vite", 35 | "clean:vite:web": "rimraf packages/web/node_modules/.vite", 36 | "clean:vite:ext": "rimraf packages/extension/node_modules/.vite", 37 | "clean:vite": "npm-run-all clean:vite:core clean:vite:ui clean:vite:web clean:vite:ext", 38 | "clean:dist": "npm-run-all clean:core clean:ui clean:web clean:ext", 39 | "clean": "npm-run-all clean:dist clean:vite", 40 | "setup:install": "pnpm install", 41 | "dev:fresh": "npm-run-all clean setup:install dev" 42 | }, 43 | "devDependencies": { 44 | "concurrently": "^8.2.2", 45 | "cross-env": "^7.0.3", 46 | "npm-run-all": "^4.1.5", 47 | "rimraf": "^4.4.1", 48 | "typescript": "^5.8.2", 49 | "@ctrl/tinycolor": "^4.1.0", 50 | "@intlify/unplugin-vue-i18n": "^6.0.3", 51 | "i18next": "^24.2.2", 52 | "i18next-browser-languagedetector": "^8.0.4", 53 | "lodash-unified": "^1.0.3" 54 | }, 55 | "dependencies": { 56 | "@element-plus/icons-vue": "^2.3.1", 57 | "@floating-ui/core": "^1.6.9", 58 | "@floating-ui/dom": "^1.6.13", 59 | "@floating-ui/utils": "^0.2.9", 60 | "@popperjs/core": "^2.11.8", 61 | "@vue/reactivity": "^3.5.13", 62 | "@vue/runtime-core": "^3.5.13", 63 | "@vue/runtime-dom": "^3.5.13", 64 | "@vue/shared": "^3.5.13", 65 | "@vueuse/core": "^12.7.0", 66 | "@vueuse/shared": "^12.7.0", 67 | "async-validator": "^4.2.5", 68 | "dayjs": "^1.11.13", 69 | "lodash-es": "^4.17.21", 70 | "memoize-one": "^6.0.0", 71 | "normalize-wheel-es": "^1.2.0", 72 | "vue-i18n": "^10.0.6" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prompt-optimizer/core", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "import": "./dist/index.js", 13 | "require": "./dist/index.cjs" 14 | } 15 | }, 16 | "scripts": { 17 | "build": "tsup src/index.ts --format cjs,esm --dts", 18 | "dev": "tsup src/index.ts --format cjs,esm --dts --watch", 19 | "test": "vitest run", 20 | "test:watch": "vitest", 21 | "test:coverage": "vitest run --coverage", 22 | "test:unit": "vitest run tests/unit", 23 | "test:integration": "vitest run tests/integration" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "^20.11.0", 27 | "dotenv": "^16.4.7", 28 | "tsup": "^8.0.2", 29 | "typescript": "^5.3.3", 30 | "vitest": "^3.0.2" 31 | }, 32 | "dependencies": { 33 | "@google/generative-ai": "^0.21.0", 34 | "dexie": "^4.0.11", 35 | "openai": "^4.83.0", 36 | "uuid": "^11.0.5", 37 | "zod": "^3.22.4" 38 | } 39 | } -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | // Core package entry point 2 | 3 | // 导出模板相关 4 | export { TemplateManager, templateManager } from './services/template/manager' 5 | export * from './services/template/types' 6 | export * from './services/template/defaults' 7 | export * from './services/template/errors' 8 | 9 | // 导出历史记录相关 10 | export { HistoryManager, historyManager } from './services/history/manager' 11 | export * from './services/history/types' 12 | export * from './services/history/errors' 13 | 14 | // 导出LLM服务相关 15 | export { LLMService, createLLMService } from './services/llm/service' 16 | export * from './services/llm/types' 17 | export * from './services/llm/errors' 18 | 19 | // 导出模型管理相关 20 | export { ModelManager, modelManager } from './services/model/manager' 21 | export * from './services/model/types' 22 | export * from './services/model/defaults' 23 | export * from './services/model/advancedParameterDefinitions' 24 | export { 25 | validateLLMParams, 26 | getSupportedParameters 27 | } from './services/model/validation' 28 | export type { 29 | ValidationResult, 30 | ValidationError as LLMValidationError, 31 | ValidationWarning 32 | } from './services/model/validation' 33 | 34 | // 导出提示词服务相关 35 | export { PromptService, createPromptService } from './services/prompt/service' 36 | export * from './services/prompt/types' 37 | export * from './services/prompt/errors' 38 | 39 | // 导出数据管理相关 40 | export { DataManager, dataManager } from './services/data/manager' 41 | 42 | // 导出存储相关 43 | export { LocalStorageProvider } from './services/storage/localStorageProvider' 44 | export { DexieStorageProvider } from './services/storage/dexieStorageProvider' 45 | export { StorageFactory } from './services/storage/factory' 46 | export * from './services/storage/types' 47 | 48 | // 导出环境工具函数 49 | export { 50 | isBrowser, 51 | isVercel, 52 | getProxyUrl, 53 | checkVercelApiAvailability, 54 | resetVercelStatusCache 55 | } from './utils/environment'; -------------------------------------------------------------------------------- /packages/core/src/services/history/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 历史记录基础错误 3 | */ 4 | export class HistoryError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'HistoryError'; 8 | } 9 | } 10 | 11 | /** 12 | * 历史记录未找到错误 13 | */ 14 | export class HistoryNotFoundError extends HistoryError { 15 | constructor(id: string) { 16 | super(`未找到ID为${id}的历史记录`); 17 | this.name = 'HistoryNotFoundError'; 18 | } 19 | } 20 | 21 | /** 22 | * 历史记录链错误 23 | */ 24 | export class HistoryChainError extends HistoryError { 25 | constructor(message: string) { 26 | super(message); 27 | this.name = 'HistoryChainError'; 28 | } 29 | } 30 | 31 | /** 32 | * 记录不存在错误 33 | */ 34 | export class RecordNotFoundError extends HistoryError { 35 | constructor( 36 | message: string, 37 | public recordId: string 38 | ) { 39 | super(message); 40 | this.name = 'RecordNotFoundError'; 41 | } 42 | } 43 | 44 | /** 45 | * 存储错误 46 | */ 47 | export class StorageError extends HistoryError { 48 | constructor( 49 | message: string, 50 | public operation: 'read' | 'write' | 'delete' | 'init' | 'storage' 51 | ) { 52 | super(message); 53 | this.name = 'StorageError'; 54 | } 55 | } 56 | 57 | /** 58 | * 记录验证错误 59 | */ 60 | export class RecordValidationError extends HistoryError { 61 | constructor( 62 | message: string, 63 | public errors: string[] 64 | ) { 65 | super(message); 66 | this.name = 'RecordValidationError'; 67 | } 68 | } -------------------------------------------------------------------------------- /packages/core/src/services/history/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 提示词记录类型 3 | */ 4 | export type PromptRecordType = 'optimize' | 'iterate'; 5 | 6 | /** 7 | * 提示词记录接口 8 | */ 9 | export interface PromptRecord { 10 | /** 记录ID */ 11 | id: string; 12 | /** 原始提示词 */ 13 | originalPrompt: string; 14 | /** 优化/迭代后的提示词 */ 15 | optimizedPrompt: string; 16 | /** 记录类型 */ 17 | type: PromptRecordType; 18 | /** 所属的提示词链ID */ 19 | chainId: string; 20 | /** 在链中的版本号 */ 21 | version: number; 22 | /** 前一个版本ID */ 23 | previousId?: string; 24 | /** 时间戳 */ 25 | timestamp: number; 26 | /** 使用的模型key */ 27 | modelKey: string; 28 | /** 29 | * 使用的模型显示名称 30 | * 通过modelKey从modelManager中获取,用于UI展示 31 | * 不存储时使用modelKey作为后备显示 32 | */ 33 | modelName?: string; 34 | /** 使用的提示词ID */ 35 | templateId: string; 36 | /** 迭代时的修改说明 */ 37 | iterationNote?: string; 38 | /** 元数据 */ 39 | metadata?: Record; 40 | } 41 | 42 | /** 43 | * 历史记录链类型 44 | */ 45 | export interface PromptRecordChain { 46 | chainId: string; 47 | rootRecord: PromptRecord; 48 | currentRecord: PromptRecord; 49 | versions: PromptRecord[]; 50 | } 51 | 52 | /** 53 | * 历史记录管理器接口 54 | */ 55 | export interface IHistoryManager { 56 | /** 添加记录 */ 57 | addRecord(record: PromptRecord): Promise; 58 | /** 获取所有记录 */ 59 | getRecords(): Promise; 60 | /** 获取指定记录 */ 61 | getRecord(id: string): Promise; 62 | /** 删除记录 */ 63 | deleteRecord(id: string): Promise; 64 | /** 获取迭代链 */ 65 | getIterationChain(recordId: string): Promise; 66 | /** 清除所有记录 */ 67 | clearHistory(): Promise; 68 | /** 获取所有记录链 */ 69 | getAllChains(): Promise; 70 | /** 创建新的记录链 */ 71 | createNewChain(record: Omit): Promise; 72 | /** 添加迭代记录 */ 73 | addIteration(params: { 74 | chainId: string; 75 | originalPrompt: string; 76 | optimizedPrompt: string; 77 | iterationNote?: string; // Made optional to match manager.ts implementation 78 | modelKey: string; 79 | templateId: string; 80 | }): Promise; 81 | } -------------------------------------------------------------------------------- /packages/core/src/services/llm/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 基础错误类 3 | */ 4 | export class BaseError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = this.constructor.name; 8 | Object.setPrototypeOf(this, new.target.prototype); // 确保原型链正确 9 | } 10 | } 11 | 12 | /** 13 | * API错误 14 | * 用于表示API调用过程中的错误 15 | */ 16 | export class APIError extends BaseError { 17 | constructor(message: string) { 18 | super(`API错误: ${message}`); 19 | } 20 | } 21 | 22 | /** 23 | * 请求配置错误 24 | * 用于表示请求配置验证失败的错误 25 | */ 26 | export class RequestConfigError extends BaseError { 27 | constructor(message: string) { 28 | super(`配置错误: ${message}`); 29 | } 30 | } 31 | 32 | /** 33 | * 验证错误 34 | * 用于表示参数验证失败的错误 35 | */ 36 | export class ValidationError extends BaseError { 37 | constructor(message: string) { 38 | super(`验证错误: ${message}`); 39 | } 40 | } 41 | 42 | /** 43 | * 初始化错误 44 | * 用于表示服务初始化失败的错误 45 | */ 46 | export class InitializationError extends BaseError { 47 | constructor(message: string) { 48 | super(`初始化错误: ${message}`); 49 | } 50 | } 51 | 52 | /** 53 | * LLM服务基础错误 54 | */ 55 | export class LLMError extends Error { 56 | constructor(message: string) { 57 | super(message); 58 | this.name = 'LLMError'; 59 | } 60 | } 61 | 62 | /** 63 | * 模型配置错误 64 | */ 65 | export class ModelConfigError extends LLMError { 66 | constructor(message: string) { 67 | super(message); 68 | this.name = 'ModelConfigError'; 69 | } 70 | } 71 | 72 | // 添加统一错误常量 73 | export const ERROR_MESSAGES = { 74 | API_KEY_REQUIRED: '优化失败: API密钥不能为空', 75 | MODEL_NOT_FOUND: '优化失败: 模型不存在', 76 | TEMPLATE_INVALID: '优化失败: 提示词格式无效', 77 | EMPTY_INPUT: '优化失败: 提示词不能为空', 78 | OPTIMIZATION_FAILED: '优化失败', 79 | ITERATION_FAILED: '迭代失败', 80 | TEST_FAILED: '测试失败', 81 | MODEL_KEY_REQUIRED: '优化失败: 模型Key不能为空', 82 | INPUT_TOO_LONG: '优化失败: 输入内容过长' 83 | } as const; -------------------------------------------------------------------------------- /packages/core/src/services/llm/types.ts: -------------------------------------------------------------------------------- 1 | import { ModelConfig } from '../model/types'; 2 | /** 3 | * 消息角色类型 4 | */ 5 | export type MessageRole = 'system' | 'user' | 'assistant'; 6 | 7 | /** 8 | * 消息类型 9 | */ 10 | export interface Message { 11 | role: 'system' | 'user' | 'assistant'; 12 | content: string; 13 | } 14 | 15 | export interface StreamHandlers { 16 | onToken: (token: string) => void; 17 | onComplete: () => void; 18 | onError: (error: Error) => void; 19 | } 20 | 21 | /** 22 | * 模型信息接口 23 | */ 24 | export interface ModelInfo { 25 | id: string; // 模型ID,用于API调用 26 | name: string; // 显示名称 27 | } 28 | 29 | /** 30 | * 用于下拉选择组件的模型选项格式 31 | */ 32 | export interface ModelOption { 33 | value: string; // 选项值,通常是模型ID 34 | label: string; // 显示标签,通常是模型名称 35 | } 36 | 37 | /** 38 | * LLM服务接口 39 | */ 40 | export interface ILLMService { 41 | /** 42 | * 发送消息 43 | * @throws {RequestConfigError} 当参数无效时 44 | * @throws {APIError} 当请求失败时 45 | */ 46 | sendMessage(messages: Message[], provider: string): Promise; 47 | 48 | /** 49 | * 发送流式消息 50 | * @throws {RequestConfigError} 当参数无效时 51 | * @throws {APIError} 当请求失败时 52 | */ 53 | sendMessageStream( 54 | messages: Message[], 55 | provider: string, 56 | callbacks: StreamHandlers 57 | ): Promise; 58 | 59 | /** 60 | * 测试连接 61 | */ 62 | testConnection(provider: string): Promise; 63 | 64 | /** 65 | * 获取模型列表,以下拉选项格式返回 66 | * @param provider 提供商标识 67 | * @param customConfig 自定义配置(可选) 68 | * @throws {RequestConfigError} 当参数无效时 69 | * @throws {APIError} 当请求失败时 70 | */ 71 | fetchModelList(provider: string, customConfig?: Partial): Promise; 72 | } -------------------------------------------------------------------------------- /packages/core/src/services/model/defaults.ts: -------------------------------------------------------------------------------- 1 | import { ModelConfig } from './types'; 2 | 3 | // 获取环境变量的辅助函数 4 | const getEnvVar = (key: string): string => { 5 | // 0. 首先检查运行时配置 6 | if (typeof window !== 'undefined' && window.runtime_config) { 7 | // 移除 VITE_ 前缀以匹配运行时配置中的键名 8 | const runtimeKey = key.replace('VITE_', ''); 9 | const value = window.runtime_config[runtimeKey]; 10 | if (value !== undefined && value !== null) { 11 | return String(value); 12 | } 13 | } 14 | 15 | // 1. 然后尝试 process.env 16 | if (typeof process !== 'undefined' && process.env && process.env[key]) { 17 | return process.env[key] || ''; 18 | } 19 | 20 | // 2. 然后尝试 import.meta.env(Vite 环境) 21 | try { 22 | // @ts-ignore - 在构建时忽略此错误 23 | if (typeof import.meta !== 'undefined' && import.meta.env) { 24 | // @ts-ignore - 在构建时忽略此错误 25 | const value = import.meta.env[key]; 26 | if (value) return value; 27 | } 28 | } catch { 29 | // 忽略错误 30 | } 31 | 32 | // 3. 最后返回空字符串 33 | return ''; 34 | }; 35 | 36 | // 从环境变量获取 API keys 37 | const OPENAI_API_KEY = getEnvVar('VITE_OPENAI_API_KEY').trim(); 38 | const GEMINI_API_KEY = getEnvVar('VITE_GEMINI_API_KEY').trim(); 39 | const DEEPSEEK_API_KEY = getEnvVar('VITE_DEEPSEEK_API_KEY').trim(); 40 | const SILICONFLOW_API_KEY = getEnvVar('VITE_SILICONFLOW_API_KEY').trim(); 41 | const ZHIPU_API_KEY = getEnvVar('VITE_ZHIPU_API_KEY').trim(); 42 | const CUSTOM_API_KEY = getEnvVar('VITE_CUSTOM_API_KEY').trim(); 43 | const CUSTOM_API_BASE_URL = getEnvVar('VITE_CUSTOM_API_BASE_URL'); 44 | const CUSTOM_API_MODEL = getEnvVar('VITE_CUSTOM_API_MODEL'); 45 | 46 | export const defaultModels: Record = { 47 | openai: { 48 | name: 'OpenAI', 49 | baseURL: 'https://api.openai.com/v1', 50 | models: ['gpt-4', 'gpt-3.5-turbo'], 51 | defaultModel: 'gpt-3.5-turbo', 52 | apiKey: OPENAI_API_KEY, 53 | enabled: !!OPENAI_API_KEY, 54 | provider: 'openai', 55 | llmParams: { 56 | } 57 | }, 58 | gemini: { 59 | name: 'Gemini', 60 | baseURL: 'https://generativelanguage.googleapis.com', 61 | models: ['gemini-2.0-flash'], 62 | defaultModel: 'gemini-2.0-flash', 63 | apiKey: GEMINI_API_KEY, 64 | enabled: !!GEMINI_API_KEY, 65 | provider: 'gemini', 66 | llmParams: { 67 | } 68 | }, 69 | deepseek: { 70 | name: 'DeepSeek', 71 | baseURL: 'https://api.deepseek.com/v1', 72 | models: ['deepseek-chat'], 73 | defaultModel: 'deepseek-chat', 74 | apiKey: DEEPSEEK_API_KEY, 75 | enabled: !!DEEPSEEK_API_KEY, 76 | provider: 'deepseek', 77 | llmParams: { 78 | } 79 | }, 80 | siliconflow: { 81 | name: 'SiliconFlow', 82 | baseURL: 'https://api.siliconflow.cn/v1', 83 | models: ['Pro/deepseek-ai/DeepSeek-V3'], 84 | defaultModel: 'Pro/deepseek-ai/DeepSeek-V3', 85 | apiKey: SILICONFLOW_API_KEY, 86 | enabled: !!SILICONFLOW_API_KEY, 87 | provider: 'siliconflow', 88 | llmParams: { 89 | } 90 | }, 91 | zhipu: { 92 | name: 'Zhipu', 93 | baseURL: 'https://open.bigmodel.cn/api/paas/v4', 94 | models: ['glm-4-flash', 'glm-4', 'glm-3-turbo', 'glm-3'], 95 | defaultModel: 'glm-4-flash', 96 | apiKey: ZHIPU_API_KEY, 97 | enabled: !!ZHIPU_API_KEY, 98 | provider: 'zhipu', 99 | llmParams: { 100 | } 101 | }, 102 | custom: { 103 | name: 'Custom', 104 | baseURL: CUSTOM_API_BASE_URL, 105 | models: [CUSTOM_API_MODEL], 106 | defaultModel: CUSTOM_API_MODEL, 107 | apiKey: CUSTOM_API_KEY, 108 | enabled: !!CUSTOM_API_KEY, 109 | provider: 'custom', 110 | llmParams: { 111 | } 112 | } 113 | }; -------------------------------------------------------------------------------- /packages/core/src/services/model/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 模型基础错误 3 | */ 4 | export class ModelError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'ModelError'; 8 | } 9 | } 10 | 11 | /** 12 | * 模型配置错误 13 | */ 14 | export class ModelConfigError extends ModelError { 15 | constructor(message: string) { 16 | super(message); 17 | this.name = 'ModelConfigError'; 18 | } 19 | } 20 | 21 | /** 22 | * 模型验证错误 23 | */ 24 | export class ModelValidationError extends ModelError { 25 | constructor( 26 | message: string, 27 | public errors: string[] 28 | ) { 29 | super(message); 30 | this.name = 'ModelValidationError'; 31 | } 32 | } -------------------------------------------------------------------------------- /packages/core/src/services/model/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 模型配置接口 3 | */ 4 | export interface ModelConfig { 5 | /** 模型名称 */ 6 | name: string; 7 | /** API基础URL */ 8 | baseURL: string; 9 | /** API密钥 */ 10 | apiKey?: string; 11 | /** 支持的模型列表 */ 12 | models: string[]; 13 | /** 默认模型 */ 14 | defaultModel: string; 15 | /** 是否启用 */ 16 | enabled: boolean; 17 | /** 提供商 */ 18 | provider: 'deepseek' | 'gemini' | 'custom' | 'zhipu' | string; 19 | /** 是否使用Vercel代理 */ 20 | useVercelProxy?: boolean; 21 | /** LLM特定参数 */ 22 | llmParams?: Record; 23 | } 24 | 25 | /** 26 | * 模型管理器接口 27 | */ 28 | export interface IModelManager { 29 | /** 获取所有模型配置 */ 30 | getAllModels(): Promise>; 31 | /** 获取指定模型配置 */ 32 | getModel(key: string): Promise; 33 | /** 添加模型配置 */ 34 | addModel(key: string, config: ModelConfig): Promise; 35 | /** 更新模型配置 */ 36 | updateModel(key: string, config: Partial): Promise; 37 | /** 删除模型配置 */ 38 | deleteModel(key: string): Promise; 39 | /** 启用模型 */ 40 | enableModel(key: string): Promise; 41 | /** 禁用模型 */ 42 | disableModel(key: string): Promise; 43 | /** 获取启用的模型 */ 44 | getEnabledModels(): Promise>; 45 | } -------------------------------------------------------------------------------- /packages/core/src/services/prompt/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 提示词服务基础错误 3 | */ 4 | export class PromptError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'PromptError'; 8 | } 9 | } 10 | 11 | /** 12 | * 优化错误 13 | */ 14 | export class OptimizationError extends PromptError { 15 | constructor( 16 | message: string, 17 | public originalPrompt: string 18 | ) { 19 | super(message); 20 | this.name = 'OptimizationError'; 21 | } 22 | } 23 | 24 | /** 25 | * 迭代错误 26 | */ 27 | export class IterationError extends PromptError { 28 | constructor( 29 | message: string, 30 | public originalPrompt: string, 31 | public iterateInput: string 32 | ) { 33 | super(message); 34 | this.name = 'IterationError'; 35 | } 36 | } 37 | 38 | /** 39 | * 测试错误 40 | */ 41 | export class TestError extends PromptError { 42 | constructor( 43 | message: string, 44 | public prompt: string, 45 | public testInput: string 46 | ) { 47 | super(message); 48 | this.name = 'TestError'; 49 | } 50 | } 51 | 52 | /** 53 | * 服务依赖错误 54 | */ 55 | export class ServiceDependencyError extends PromptError { 56 | constructor( 57 | message: string, 58 | public serviceName: string 59 | ) { 60 | super(message); 61 | this.name = 'ServiceDependencyError'; 62 | } 63 | } -------------------------------------------------------------------------------- /packages/core/src/services/prompt/factory.ts: -------------------------------------------------------------------------------- 1 | import { PromptService } from './service'; 2 | import { TemplateManager } from '../template/manager'; 3 | import { HistoryManager } from '../history/manager'; 4 | import { ModelManager } from '../model/manager'; 5 | import { LLMService } from '../llm/service'; 6 | import { StorageFactory } from '../storage/factory'; 7 | 8 | // 创建共享的存储提供器实例 9 | const storageProvider = StorageFactory.createDefault(); 10 | 11 | export async function createPromptService() { 12 | const modelManager = new ModelManager(storageProvider); 13 | const llmService = new LLMService(modelManager); 14 | const templateManager = new TemplateManager(storageProvider); 15 | const historyManager = new HistoryManager(storageProvider); 16 | 17 | return new PromptService(modelManager, llmService, templateManager, historyManager); 18 | } -------------------------------------------------------------------------------- /packages/core/src/services/prompt/types.ts: -------------------------------------------------------------------------------- 1 | import { PromptRecord } from '../history/types'; 2 | import { StreamHandlers } from '../llm/types'; 3 | import { Template } from '../template/types'; 4 | 5 | /** 6 | * 提示词服务接口 7 | */ 8 | export interface IPromptService { 9 | /** 优化提示词 */ 10 | optimizePrompt(prompt: string, modelKey: string): Promise; 11 | 12 | /** 迭代优化提示词 */ 13 | iteratePrompt( 14 | originalPrompt: string, 15 | iterateInput: string, 16 | modelKey: string 17 | ): Promise; 18 | 19 | /** 测试提示词 */ 20 | testPrompt( 21 | prompt: string, 22 | testInput: string, 23 | modelKey: string 24 | ): Promise; 25 | 26 | /** 获取历史记录 */ 27 | getHistory(): Promise; 28 | 29 | /** 获取迭代链 */ 30 | getIterationChain(recordId: string): Promise; 31 | 32 | /** 优化提示词(流式) */ 33 | optimizePromptStream( 34 | prompt: string, 35 | modelKey: string, 36 | template: string, 37 | callbacks: StreamHandlers 38 | ): Promise; 39 | 40 | /** 迭代优化提示词(流式) */ 41 | iteratePromptStream( 42 | originalPrompt: string, 43 | iterateInput: string, 44 | modelKey: string, 45 | handlers: StreamHandlers, 46 | template: Template | string 47 | ): Promise; 48 | 49 | /** 测试提示词(流式) */ 50 | testPromptStream( 51 | prompt: string, 52 | testInput: string, 53 | modelKey: string, 54 | callbacks: StreamHandlers 55 | ): Promise; 56 | } -------------------------------------------------------------------------------- /packages/core/src/services/storage/adapter.ts: -------------------------------------------------------------------------------- 1 | import { IStorageProvider } from './types'; 2 | 3 | /** 4 | * 存储适配器 - 为不支持高级方法的存储提供者提供兼容性 5 | * 隐藏原子操作实现细节,业务层无需关心 6 | */ 7 | export class StorageAdapter implements IStorageProvider { 8 | private locks: Map> = new Map(); 9 | 10 | constructor(private readonly baseProvider: IStorageProvider) {} 11 | 12 | // 基础方法直接代理 13 | async getItem(key: string): Promise { 14 | return this.baseProvider.getItem(key); 15 | } 16 | 17 | async setItem(key: string, value: string): Promise { 18 | return this.baseProvider.setItem(key, value); 19 | } 20 | 21 | async removeItem(key: string): Promise { 22 | return this.baseProvider.removeItem(key); 23 | } 24 | 25 | async clearAll(): Promise { 26 | return this.baseProvider.clearAll(); 27 | } 28 | 29 | /** 30 | * 隐藏式数据更新 - 内部实现原子性 31 | */ 32 | async updateData( 33 | key: string, 34 | modifier: (currentValue: T | null) => T 35 | ): Promise { 36 | // 如果基础提供者有updateData方法,直接使用 37 | if ('updateData' in this.baseProvider && typeof this.baseProvider.updateData === 'function') { 38 | return (this.baseProvider as any).updateData(key, modifier); 39 | } 40 | 41 | // 否则使用手动实现的原子操作 42 | const release = await this.acquireLock(key); 43 | try { 44 | // 读取当前值 45 | const currentData = await this.baseProvider.getItem(key); 46 | const currentValue: T | null = currentData ? JSON.parse(currentData) : null; 47 | 48 | // 应用修改 - 业务逻辑错误直接透传 49 | const newValue = modifier(currentValue); 50 | 51 | // 写入新值 52 | await this.baseProvider.setItem(key, JSON.stringify(newValue)); 53 | } finally { 54 | release(); 55 | } 56 | } 57 | 58 | /** 59 | * 批量更新操作 60 | */ 61 | async batchUpdate(operations: Array<{ 62 | key: string; 63 | operation: 'set' | 'remove'; 64 | value?: string; 65 | }>): Promise { 66 | // 如果基础提供者有batchUpdate方法,直接使用 67 | if ('batchUpdate' in this.baseProvider && typeof this.baseProvider.batchUpdate === 'function') { 68 | return (this.baseProvider as any).batchUpdate(operations); 69 | } 70 | 71 | // 否则顺序执行操作(简化实现) 72 | for (const op of operations) { 73 | if (op.operation === 'set' && op.value !== undefined) { 74 | await this.baseProvider.setItem(op.key, op.value); 75 | } else if (op.operation === 'remove') { 76 | await this.baseProvider.removeItem(op.key); 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * 获取存储能力信息 83 | */ 84 | getCapabilities() { 85 | // 基础提供者的能力 86 | if ('getCapabilities' in this.baseProvider && typeof this.baseProvider.getCapabilities === 'function') { 87 | return (this.baseProvider as any).getCapabilities(); 88 | } 89 | 90 | // 默认能力 91 | return { 92 | supportsAtomic: true, // 通过适配器实现 93 | supportsBatch: false, 94 | maxStorageSize: undefined 95 | }; 96 | } 97 | 98 | /** 99 | * 改进的异步锁实现 100 | * 使用队列机制避免死锁和锁泄漏 101 | */ 102 | private async acquireLock(key: string): Promise<() => void> { 103 | // 如果已有锁,等待它完成 104 | const existingLock = this.locks.get(key); 105 | if (existingLock) { 106 | try { 107 | await existingLock; 108 | } catch (error) { 109 | // 忽略前一个操作的错误,继续获取锁 110 | } 111 | } 112 | 113 | // 创建新锁 114 | let releaseLock: () => void; 115 | const lockPromise = new Promise((resolve, reject) => { 116 | let released = false; 117 | 118 | releaseLock = () => { 119 | if (!released) { 120 | released = true; 121 | this.locks.delete(key); 122 | resolve(); 123 | } 124 | }; 125 | 126 | // 设置超时防止死锁 127 | setTimeout(() => { 128 | if (!released) { 129 | released = true; 130 | this.locks.delete(key); 131 | reject(new Error(`Lock timeout for key: ${key}`)); 132 | } 133 | }, 30000); // 30秒超时 134 | }); 135 | 136 | this.locks.set(key, lockPromise); 137 | return releaseLock!; 138 | } 139 | } -------------------------------------------------------------------------------- /packages/core/src/services/storage/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 存储错误类 3 | */ 4 | export class StorageError extends Error { 5 | constructor( 6 | message: string, 7 | public readonly operation: 'read' | 'write' | 'delete' | 'clear' 8 | ) { 9 | super(message); 10 | this.name = 'StorageError'; 11 | } 12 | } -------------------------------------------------------------------------------- /packages/core/src/services/storage/factory.ts: -------------------------------------------------------------------------------- 1 | import { IStorageProvider } from './types'; 2 | import { LocalStorageProvider } from './localStorageProvider'; 3 | import { DexieStorageProvider } from './dexieStorageProvider'; 4 | 5 | export type StorageType = 'localStorage' | 'dexie'; 6 | 7 | /** 8 | * 存储工厂类 9 | */ 10 | export class StorageFactory { 11 | // 单例实例缓存 12 | private static defaultInstance: IStorageProvider | null = null; 13 | private static instances: Map = new Map(); 14 | 15 | /** 16 | * 创建存储提供器 17 | * @param type 存储类型 18 | * @returns 存储提供器实例 19 | */ 20 | static create(type: StorageType): IStorageProvider { 21 | // 检查是否已有缓存实例 22 | if (StorageFactory.instances.has(type)) { 23 | return StorageFactory.instances.get(type)!; 24 | } 25 | 26 | let instance: IStorageProvider; 27 | switch (type) { 28 | case 'localStorage': 29 | instance = new LocalStorageProvider(); 30 | break; 31 | case 'dexie': 32 | instance = new DexieStorageProvider(); 33 | break; 34 | default: 35 | throw new Error(`Unsupported storage type: ${type}`); 36 | } 37 | 38 | // 缓存实例 39 | StorageFactory.instances.set(type, instance); 40 | return instance; 41 | } 42 | 43 | /** 44 | * 创建默认存储提供器(单例) 45 | * 优先使用 Dexie,降级到 localStorage 46 | */ 47 | static createDefault(): IStorageProvider { 48 | // 返回缓存的默认实例 49 | if (StorageFactory.defaultInstance) { 50 | return StorageFactory.defaultInstance; 51 | } 52 | 53 | try { 54 | // 检查是否支持 IndexedDB (Dexie 的基础) 55 | if (typeof window !== 'undefined' && window.indexedDB) { 56 | console.log('使用 Dexie 作为默认存储提供器'); 57 | StorageFactory.defaultInstance = StorageFactory.create('dexie'); 58 | } else { 59 | console.log('IndexedDB 不可用,使用 localStorage 作为默认存储提供器'); 60 | StorageFactory.defaultInstance = StorageFactory.create('localStorage'); 61 | } 62 | } catch (error) { 63 | console.warn('Dexie 存储不可用,降级到 localStorage:', error); 64 | StorageFactory.defaultInstance = StorageFactory.create('localStorage'); 65 | } 66 | 67 | return StorageFactory.defaultInstance; 68 | } 69 | 70 | /** 71 | * 重置所有实例(主要用于测试) 72 | */ 73 | static reset(): void { 74 | StorageFactory.defaultInstance = null; 75 | StorageFactory.instances.clear(); 76 | 77 | // 重置DexieStorageProvider的迁移状态 78 | DexieStorageProvider.resetMigrationState(); 79 | } 80 | 81 | /** 82 | * 获取当前默认实例(用于调试) 83 | */ 84 | static getCurrentDefault(): IStorageProvider | null { 85 | return StorageFactory.defaultInstance; 86 | } 87 | 88 | /** 89 | * 获取所有支持的存储类型 90 | */ 91 | static getSupportedTypes(): StorageType[] { 92 | const types: StorageType[] = []; 93 | 94 | // 检查 localStorage 支持 95 | if (typeof window !== 'undefined' && window.localStorage) { 96 | types.push('localStorage'); 97 | } 98 | 99 | // 检查 IndexedDB 支持 100 | if (typeof window !== 'undefined' && window.indexedDB) { 101 | types.push('dexie'); 102 | } 103 | 104 | return types; 105 | } 106 | 107 | /** 108 | * 检查特定存储类型是否支持 109 | */ 110 | static isSupported(type: StorageType): boolean { 111 | return StorageFactory.getSupportedTypes().includes(type); 112 | } 113 | } -------------------------------------------------------------------------------- /packages/core/src/services/storage/localStorageProvider.ts: -------------------------------------------------------------------------------- 1 | import { IStorageProvider } from './types'; 2 | import { StorageError } from './errors'; 3 | 4 | /** 5 | * 简单的异步锁实现 6 | */ 7 | class AsyncLock { 8 | private locks: Map> = new Map(); 9 | 10 | async acquire(key: string): Promise<() => void> { 11 | // 等待现有锁完成 12 | while (this.locks.has(key)) { 13 | try { 14 | await this.locks.get(key); 15 | } catch { 16 | // 忽略锁中的错误,继续尝试获取锁 17 | } 18 | } 19 | 20 | // 创建新锁 21 | let releaseLock: () => void; 22 | const lockPromise = new Promise((resolve) => { 23 | releaseLock = () => { 24 | this.locks.delete(key); 25 | resolve(); 26 | }; 27 | }); 28 | 29 | this.locks.set(key, lockPromise); 30 | 31 | // 返回释放函数 32 | return releaseLock!; 33 | } 34 | } 35 | 36 | /** 37 | * 增强的LocalStorageProvider,提供事务性操作 38 | */ 39 | export class LocalStorageProvider implements IStorageProvider { 40 | private lock = new AsyncLock(); 41 | 42 | public async getItem(key: string): Promise { 43 | const release = await this.lock.acquire(key); 44 | try { 45 | const item = localStorage.getItem(key); 46 | return item; 47 | } catch (error) { 48 | throw new StorageError(`获取存储项失败: ${key}`, 'read'); 49 | } finally { 50 | release(); 51 | } 52 | } 53 | 54 | public async setItem(key: string, value: string): Promise { 55 | const release = await this.lock.acquire(key); 56 | try { 57 | localStorage.setItem(key, value); 58 | } catch (error) { 59 | throw new StorageError(`设置存储项失败: ${key}`, 'write'); 60 | } finally { 61 | release(); 62 | } 63 | } 64 | 65 | public async removeItem(key: string): Promise { 66 | const release = await this.lock.acquire(key); 67 | try { 68 | localStorage.removeItem(key); 69 | } catch (error) { 70 | throw new StorageError(`删除存储项失败: ${key}`, 'delete'); 71 | } finally { 72 | release(); 73 | } 74 | } 75 | 76 | public async clearAll(): Promise { 77 | const release = await this.lock.acquire('__clear_all__'); 78 | try { 79 | localStorage.clear(); 80 | } catch (error) { 81 | throw new StorageError('清除所有存储项失败', 'clear'); 82 | } finally { 83 | release(); 84 | } 85 | } 86 | 87 | /** 88 | * 隐藏式数据更新 - 内部自动选择最优实现 89 | * 业务层无需关心是否支持原子操作 90 | * @param key 存储键 91 | * @param modifier 修改函数,接收当前值,返回新值 92 | */ 93 | public async updateData( 94 | key: string, 95 | modifier: (currentValue: T | null) => T 96 | ): Promise { 97 | // LocalStorageProvider 内部使用手动原子操作 98 | const release = await this.lock.acquire(key); 99 | try { 100 | // 读取当前值 101 | const currentData = localStorage.getItem(key); 102 | const currentValue: T | null = currentData ? JSON.parse(currentData) : null; 103 | 104 | // 应用修改 - 允许业务逻辑错误透传 105 | const newValue = modifier(currentValue); 106 | 107 | // 写入新值 108 | localStorage.setItem(key, JSON.stringify(newValue)); 109 | } catch (error) { 110 | // 业务逻辑错误直接透传,保持错误类型 111 | if (error instanceof Error && 112 | (error.name.includes('Error') || 113 | error.constructor.name !== 'Error' || 114 | error.message.includes('模型') || 115 | error.message.includes('不存在'))) { 116 | throw error; 117 | } 118 | // 只有真正的存储错误才包装为StorageError 119 | throw new StorageError(`数据更新失败: ${key}`, 'write'); 120 | } finally { 121 | release(); 122 | } 123 | } 124 | 125 | /** 126 | * 获取存储能力信息 127 | */ 128 | public getCapabilities() { 129 | return { 130 | supportsAtomic: true, // 通过手动锁实现 131 | supportsBatch: true, 132 | maxStorageSize: 5 * 1024 * 1024 // 约5MB 133 | }; 134 | } 135 | 136 | /** 137 | * 批量操作 138 | * @param operations 批量操作列表 139 | */ 140 | public async batchUpdate(operations: Array<{ 141 | key: string; 142 | operation: 'set' | 'remove'; 143 | value?: string; 144 | }>): Promise { 145 | // 获取所有相关键的锁 146 | const keys = operations.map(op => op.key); 147 | const releases = await Promise.all(keys.map(key => this.lock.acquire(key))); 148 | 149 | try { 150 | for (const op of operations) { 151 | if (op.operation === 'set' && op.value !== undefined) { 152 | localStorage.setItem(op.key, op.value); 153 | } else if (op.operation === 'remove') { 154 | localStorage.removeItem(op.key); 155 | } 156 | } 157 | } catch (error) { 158 | throw new StorageError('批量更新失败', 'write'); 159 | } finally { 160 | // 释放所有锁 161 | releases.forEach(release => release()); 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /packages/core/src/services/storage/types.ts: -------------------------------------------------------------------------------- 1 | export interface IStorageProvider { 2 | getItem(key: string): Promise; 3 | setItem(key: string, value: string): Promise; 4 | removeItem(key: string): Promise; 5 | clearAll(): Promise; 6 | 7 | // 隐藏式高级方法 - 内部自动选择最优实现 8 | updateData(key: string, modifier: (currentValue: T | null) => T): Promise; 9 | batchUpdate(operations: Array<{ 10 | key: string; 11 | operation: 'set' | 'remove'; 12 | value?: string; 13 | }>): Promise; 14 | 15 | // 可选:存储能力查询(用于监控和调试) 16 | getCapabilities?(): { 17 | supportsAtomic: boolean; 18 | supportsBatch: boolean; 19 | maxStorageSize?: number; 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/core/src/services/template/errors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 提示词错误基类 3 | */ 4 | export class TemplateError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'TemplateError'; 8 | } 9 | } 10 | 11 | /** 12 | * 提示词加载错误 13 | */ 14 | export class TemplateLoadError extends TemplateError { 15 | constructor( 16 | message: string, 17 | public templateId: string 18 | ) { 19 | super(message); 20 | this.name = 'TemplateLoadError'; 21 | } 22 | } 23 | 24 | /** 25 | * 提示词验证错误 26 | */ 27 | export class TemplateValidationError extends TemplateError { 28 | constructor(message: string) { 29 | super(message); 30 | this.name = 'TemplateValidationError'; 31 | } 32 | } 33 | 34 | /** 35 | * 提示词缓存错误 36 | */ 37 | export class TemplateCacheError extends TemplateError { 38 | constructor(message: string) { 39 | super(message); 40 | this.name = 'TemplateCacheError'; 41 | } 42 | } 43 | 44 | /** 45 | * 提示词存储错误 46 | */ 47 | export class TemplateStorageError extends TemplateError { 48 | constructor(message: string) { 49 | super(message); 50 | this.name = 'TemplateStorageError'; 51 | } 52 | } -------------------------------------------------------------------------------- /packages/core/src/services/template/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | /** 4 | * 提示词元数据 5 | */ 6 | export interface TemplateMetadata { 7 | version: string; // 提示词版本 8 | lastModified: number; // 最后修改时间 9 | author?: string; // 作者(可选) 10 | description?: string; // 描述(可选) 11 | templateType: 'optimize' | 'iterate'; // 新增类型标识 12 | } 13 | 14 | /** 15 | * 提示词定义 16 | */ 17 | export interface Template { 18 | id: string; // 提示词唯一标识 19 | name: string; // 提示词名称 20 | content: string; // 提示词内容 21 | metadata: TemplateMetadata; 22 | isBuiltin?: boolean; // 是否为内置提示词 23 | } 24 | 25 | /** 26 | * 提示词来源类型 27 | */ 28 | export type TemplateSourceType = 'builtin' | 'localStorage'; 29 | 30 | /** 31 | * 提示词管理器配置 32 | */ 33 | export interface TemplateManagerConfig { 34 | storageKey?: string; // localStorage存储键名 35 | cacheTimeout?: number; // 缓存超时时间 36 | } 37 | 38 | /** 39 | * 提示词管理器接口 40 | */ 41 | export interface ITemplateManager { 42 | /** 获取指定ID的模板 */ 43 | getTemplate(templateId: string): Template; // Stays synchronous 44 | 45 | /** 保存模板 */ 46 | saveTemplate(template: Template): Promise; // Async 47 | 48 | /** 删除模板 */ 49 | deleteTemplate(templateId: string): Promise; // Async 50 | 51 | /** 列出所有模板 */ 52 | listTemplates(): Template[]; // Stays synchronous 53 | 54 | /** 导出模板 */ 55 | exportTemplate(templateId: string): string; // Stays synchronous 56 | 57 | /** 导入模板 */ 58 | importTemplate(templateJson: string): Promise; // Async 59 | 60 | /** 清除缓存 */ 61 | clearCache(templateId?: string): void; // Synchronous 62 | 63 | /** 按类型列出模板 */ 64 | listTemplatesByType(type: 'optimize' | 'iterate'): Template[]; 65 | 66 | /** 67 | * 根据类型获取模板列表(已废弃) 68 | * @deprecated 使用 listTemplatesByType 替代 69 | */ 70 | getTemplatesByType(type: 'optimize' | 'iterate'): Template[]; 71 | } 72 | 73 | /** 74 | * 提示词验证Schema 75 | */ 76 | export const templateSchema = z.object({ 77 | id: z.string().min(1), 78 | name: z.string().min(1), 79 | content: z.string().min(1), 80 | metadata: z.object({ 81 | version: z.string(), 82 | lastModified: z.number(), 83 | author: z.string().optional(), 84 | description: z.string().optional(), 85 | templateType: z.enum(['optimize', 'iterate']) 86 | }), 87 | isBuiltin: z.boolean().optional() 88 | }); -------------------------------------------------------------------------------- /packages/core/src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | runtime_config?: { 3 | OPENAI_API_KEY?: string; 4 | GEMINI_API_KEY?: string; 5 | DEEPSEEK_API_KEY?: string; 6 | SILICONFLOW_API_KEY?: string; 7 | ZHIPU_API_KEY?: string; 8 | CUSTOM_API_KEY?: string; 9 | CUSTOM_API_BASE_URL?: string; 10 | CUSTOM_API_MODEL?: string; 11 | [key: string]: string | undefined; 12 | }; 13 | } -------------------------------------------------------------------------------- /packages/core/src/utils/environment.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 环境工具函数 3 | */ 4 | 5 | // 存储Vercel环境检测结果的缓存 6 | let vercelStatusCache: { available: boolean; checked: boolean } = { 7 | available: false, 8 | checked: false 9 | }; 10 | 11 | /** 12 | * 检查是否在浏览器环境中 13 | */ 14 | export const isBrowser = (): boolean => { 15 | return typeof window !== 'undefined'; 16 | }; 17 | 18 | /** 19 | * 检测Vercel API是否可用 20 | * 使用异步方式检测,结果会被缓存 21 | */ 22 | export const checkVercelApiAvailability = async (): Promise => { 23 | // 如果已经检查过,直接返回缓存结果 24 | if (vercelStatusCache.checked) { 25 | return vercelStatusCache.available; 26 | } 27 | 28 | if (!isBrowser()) { 29 | vercelStatusCache = { available: false, checked: true }; 30 | return false; 31 | } 32 | 33 | try { 34 | // 获取当前域名作为基础URL 35 | const origin = window.location.origin; 36 | const response = await fetch(`${origin}/api/vercel-status`, { 37 | method: 'GET', 38 | headers: { 'Content-Type': 'application/json' }, 39 | // 设置较短的超时时间,避免长时间等待 40 | signal: AbortSignal.timeout(3000) 41 | }); 42 | 43 | // 检查响应状态,只有200状态码且内容proxySupport为true 44 | if (response.status !== 200) { 45 | vercelStatusCache = { available: false, checked: true }; 46 | console.log('[环境检测] 未检测到Vercel部署环境,代理功能不可用'); 47 | return false; 48 | } 49 | 50 | // 解析JSON响应 51 | const data = await response.json(); 52 | const isAvailable = data.status === 'available' && data.proxySupport === true; 53 | 54 | vercelStatusCache = { available: isAvailable, checked: true }; 55 | 56 | if (isAvailable) { 57 | console.log('[环境检测] 检测到Vercel部署环境,代理功能可用'); 58 | } else { 59 | console.log('[环境检测] 未检测到Vercel部署环境,代理功能不可用'); 60 | } 61 | 62 | return isAvailable; 63 | } catch (error) { 64 | console.log('[环境检测] Vercel API检测失败', error); 65 | vercelStatusCache = { available: false, checked: true }; 66 | return false; 67 | } 68 | }; 69 | 70 | /** 71 | * 重置环境检测缓存 72 | * 用于在需要重新检测时调用 73 | */ 74 | export const resetVercelStatusCache = (): void => { 75 | vercelStatusCache = { available: false, checked: false }; 76 | }; 77 | 78 | /** 79 | * 检查是否在Vercel环境中(同步版本,使用缓存结果) 80 | */ 81 | export const isVercel = (): boolean => { 82 | // 如果未检查过,返回false,应用需要先调用异步检测方法 83 | return vercelStatusCache.checked && vercelStatusCache.available; 84 | }; 85 | 86 | /** 87 | * 获取API代理URL 88 | * @param baseURL 原始基础URL 89 | * @param isStream 是否是流式请求 90 | */ 91 | export const getProxyUrl = (baseURL: string | undefined, isStream: boolean = false): string => { 92 | if (!baseURL) { 93 | return ''; 94 | } 95 | 96 | // 获取当前域名作为基础URL 97 | const origin = isBrowser() ? window.location.origin : ''; 98 | const proxyEndpoint = isStream ? 'stream' : 'proxy'; 99 | 100 | // 返回完整的绝对URL 101 | return `${origin}/api/${proxyEndpoint}?targetUrl=${encodeURIComponent(baseURL)}`; 102 | }; -------------------------------------------------------------------------------- /packages/core/tests/integration/llm/custom.test.js: -------------------------------------------------------------------------------- 1 | import { createLLMService, ModelManager } from '../../../src/index.js'; 2 | import { expect, describe, it, beforeEach, beforeAll, vi } from 'vitest'; 3 | import dotenv from 'dotenv'; 4 | import path from 'path'; 5 | import { createMockStorage } from '../../mocks/mockStorage'; 6 | 7 | // 加载环境变量 8 | beforeAll(() => { 9 | dotenv.config({ path: path.resolve(process.cwd(), '.env.local') }); 10 | }); 11 | 12 | describe('自定义模型测试', () => { 13 | let llmService; 14 | let modelManager; 15 | let mockStorage; 16 | 17 | beforeEach(() => { 18 | mockStorage = createMockStorage(); 19 | mockStorage.getItem.mockResolvedValue(null); 20 | 21 | modelManager = new ModelManager(mockStorage); 22 | llmService = createLLMService(modelManager); 23 | 24 | // 创建自定义模型配置 25 | const customConfig = { 26 | name: 'Custom', 27 | baseURL: process.env.VITE_CUSTOM_API_BASE_URL || 'https://api.custom.test', 28 | models: [process.env.VITE_CUSTOM_API_MODEL || 'test-model'], 29 | defaultModel: process.env.VITE_CUSTOM_API_MODEL || 'test-model', 30 | apiKey: process.env.VITE_CUSTOM_API_KEY || 'test-key', 31 | enabled: !!process.env.VITE_CUSTOM_API_KEY, 32 | provider: 'custom' 33 | }; 34 | 35 | // 模拟获取自定义模型 36 | vi.spyOn(modelManager, 'getModel').mockImplementation(async (key) => { 37 | if (key === 'custom') { 38 | return customConfig; 39 | } 40 | return undefined; 41 | }); 42 | }); 43 | 44 | it('应该能正确加载和使用自定义模型', async () => { 45 | const model = await modelManager.getModel('custom'); 46 | 47 | expect(model).toBeDefined(); 48 | expect(model.name).toBe('Custom'); 49 | 50 | // 处理环境变量可能为空的情况 51 | if (process.env.VITE_CUSTOM_API_BASE_URL) { 52 | expect(model.baseURL).toBe(process.env.VITE_CUSTOM_API_BASE_URL); 53 | } 54 | 55 | if (process.env.VITE_CUSTOM_API_MODEL) { 56 | expect(model.models).toEqual([process.env.VITE_CUSTOM_API_MODEL]); 57 | expect(model.defaultModel).toBe(process.env.VITE_CUSTOM_API_MODEL); 58 | } 59 | 60 | expect(model.enabled).toBe(!!process.env.VITE_CUSTOM_API_KEY); 61 | }); 62 | 63 | it('应该能正确处理自定义模型的配置更新', async () => { 64 | const updatedConfig = { 65 | name: 'Updated Custom Model', 66 | baseURL: process.env.VITE_CUSTOM_API_BASE_URL || 'https://api.custom.test', 67 | models: [process.env.VITE_CUSTOM_API_MODEL || 'test-model'], 68 | defaultModel: process.env.VITE_CUSTOM_API_MODEL || 'test-model', 69 | enabled: true, 70 | provider: 'custom' 71 | }; 72 | 73 | // 模拟更新后的模型 74 | vi.spyOn(modelManager, 'getModel').mockImplementation(async (key) => { 75 | if (key === 'custom') { 76 | return updatedConfig; 77 | } 78 | return undefined; 79 | }); 80 | 81 | vi.spyOn(modelManager, 'updateModel').mockResolvedValue(undefined); 82 | 83 | await modelManager.updateModel('custom', updatedConfig); 84 | const model = await modelManager.getModel('custom'); 85 | 86 | expect(model.name).toBe(updatedConfig.name); 87 | expect(model.baseURL).toBe(updatedConfig.baseURL); 88 | expect(model.models).toEqual(updatedConfig.models); 89 | expect(model.defaultModel).toBe(updatedConfig.defaultModel); 90 | }); 91 | 92 | it('应该能正确调用自定义模型的 API', async () => { 93 | if (!process.env.VITE_CUSTOM_API_KEY) { 94 | console.log('跳过测试:未设置 VITE_CUSTOM_API_KEY 环境变量'); 95 | return; 96 | } 97 | 98 | // 模拟API调用 99 | vi.spyOn(llmService, 'sendMessage').mockResolvedValue('这是模拟的API响应'); 100 | 101 | const messages = [ 102 | { role: 'user', content: '你好,请用一句话介绍你自己' } 103 | ]; 104 | 105 | const response = await llmService.sendMessage(messages, 'custom'); 106 | expect(response).toBeDefined(); 107 | expect(typeof response).toBe('string'); 108 | expect(response.length).toBeGreaterThan(0); 109 | }, 25000); 110 | 111 | it('应该能正确处理自定义模型的多轮对话', async () => { 112 | if (!process.env.VITE_CUSTOM_API_KEY) { 113 | console.log('跳过测试:未设置 VITE_CUSTOM_API_KEY 环境变量'); 114 | return; 115 | } 116 | 117 | // 模拟API调用 118 | vi.spyOn(llmService, 'sendMessage').mockResolvedValue('这是多轮对话的模拟响应'); 119 | 120 | const messages = [ 121 | { role: 'user', content: '你好' }, 122 | { role: 'assistant', content: '你好!有什么我可以帮你的吗?' }, 123 | { role: 'user', content: '再见' } 124 | ]; 125 | 126 | const response = await llmService.sendMessage(messages, 'custom'); 127 | expect(response).toBeDefined(); 128 | expect(typeof response).toBe('string'); 129 | expect(response.length).toBeGreaterThan(0); 130 | }, 25000); 131 | }); -------------------------------------------------------------------------------- /packages/core/tests/integration/llm/deepseek.test.js: -------------------------------------------------------------------------------- 1 | import { createLLMService, ModelManager, LocalStorageProvider } from '../../../src/index.js'; 2 | import { expect, describe, it, beforeEach, beforeAll } from 'vitest'; 3 | import dotenv from 'dotenv'; 4 | import path from 'path'; 5 | 6 | // 加载环境变量 7 | beforeAll(() => { 8 | dotenv.config({ path: path.resolve(process.cwd(), '.env.local') }); 9 | }); 10 | 11 | describe('DeepSeek API 测试', () => { 12 | // 跳过没有设置 API 密钥的测试 13 | const apiKey = process.env.VITE_DEEPSEEK_API_KEY; 14 | if (!apiKey) { 15 | console.log('跳过 DeepSeek 测试:未设置 VITE_DEEPSEEK_API_KEY 环境变量'); 16 | it.skip('应该能正确调用 DeepSeek API', () => {}); 17 | it.skip('应该能正确处理多轮对话', () => {}); 18 | return; 19 | } 20 | 21 | it('应该能正确调用 DeepSeek API', async () => { 22 | const storage = new LocalStorageProvider(); 23 | const modelManager = new ModelManager(storage); 24 | const llmService = createLLMService(modelManager); 25 | 26 | // 更新 DeepSeek 配置 27 | modelManager.updateModel('deepseek', { 28 | apiKey, 29 | enabled: true 30 | }); 31 | 32 | const messages = [ 33 | { role: 'user', content: '你好,请用一句话介绍你自己' } 34 | ]; 35 | 36 | const response = await llmService.sendMessage(messages, 'deepseek'); 37 | expect(response).toBeDefined(); 38 | expect(typeof response).toBe('string'); 39 | expect(response.length).toBeGreaterThan(0); 40 | }, 25000); 41 | 42 | it('应该能正确处理多轮对话', async () => { 43 | const storage = new LocalStorageProvider(); 44 | const modelManager = new ModelManager(storage); 45 | const llmService = createLLMService(modelManager); 46 | 47 | // 更新 DeepSeek 配置 48 | modelManager.updateModel('deepseek', { 49 | apiKey, 50 | enabled: true 51 | }); 52 | 53 | const messages = [ 54 | { role: 'user', content: '你好,我们来玩个游戏' }, 55 | { role: 'assistant', content: '好啊,你想玩什么游戏?' }, 56 | { role: 'user', content: '我们来玩猜数字游戏,1到100之间' } 57 | ]; 58 | 59 | const response = await llmService.sendMessage(messages, 'deepseek'); 60 | expect(response).toBeDefined(); 61 | expect(typeof response).toBe('string'); 62 | expect(response.length).toBeGreaterThan(0); 63 | }, 25000); 64 | }); -------------------------------------------------------------------------------- /packages/core/tests/integration/llm/gemini.test.js: -------------------------------------------------------------------------------- 1 | import { createLLMService, ModelManager, LocalStorageProvider } from '../../../src/index.js'; 2 | import { expect, describe, it, beforeEach, beforeAll } from 'vitest'; 3 | import dotenv from 'dotenv'; 4 | import path from 'path'; 5 | 6 | // 加载环境变量 7 | beforeAll(() => { 8 | dotenv.config({ path: path.resolve(process.cwd(), '.env.local') }); 9 | }); 10 | 11 | describe('Gemini API 测试', () => { 12 | // 跳过没有设置 API 密钥的测试 13 | const apiKey = process.env.VITE_GEMINI_API_KEY; 14 | if (!apiKey) { 15 | console.log('跳过 Gemini 测试:未设置 VITE_GEMINI_API_KEY 环境变量'); 16 | it.skip('应该能正确调用 Gemini API', () => {}); 17 | it.skip('应该能正确处理多轮对话', () => {}); 18 | return; 19 | } 20 | 21 | it('应该能正确调用 Gemini API', async () => { 22 | const storage = new LocalStorageProvider(); 23 | const modelManager = new ModelManager(storage); 24 | const llmService = createLLMService(modelManager); 25 | 26 | // 更新 Gemini 配置 27 | await modelManager.updateModel('gemini', { 28 | apiKey, 29 | enabled: true 30 | }); 31 | 32 | const messages = [ 33 | { role: 'user', content: '你好,请用一句话介绍你自己' } 34 | ]; 35 | 36 | const response = await llmService.sendMessage(messages, 'gemini'); 37 | expect(response).toBeDefined(); 38 | expect(typeof response).toBe('string'); 39 | expect(response.length).toBeGreaterThan(0); 40 | }, 10000); 41 | 42 | it('应该能正确处理多轮对话', async () => { 43 | const storage = new LocalStorageProvider(); 44 | const modelManager = new ModelManager(storage); 45 | const llmService = createLLMService(modelManager); 46 | 47 | // 更新 Gemini 配置 48 | await modelManager.updateModel('gemini', { 49 | apiKey, 50 | enabled: true 51 | }); 52 | 53 | const messages = [ 54 | { role: 'user', content: '你好,我们来玩个游戏' }, 55 | { role: 'assistant', content: '好啊,你想玩什么游戏?' }, 56 | { role: 'user', content: '我们来玩猜数字游戏,1到100之间' } 57 | ]; 58 | 59 | const response = await llmService.sendMessage(messages, 'gemini'); 60 | expect(response).toBeDefined(); 61 | expect(typeof response).toBe('string'); 62 | expect(response.length).toBeGreaterThan(0); 63 | }, 10000); 64 | }); -------------------------------------------------------------------------------- /packages/core/tests/mocks/mockStorage.ts: -------------------------------------------------------------------------------- 1 | import { IStorageProvider } from '../../src/services/storage/types'; 2 | import { vi } from 'vitest'; 3 | 4 | /** 5 | * 创建模拟存储提供程序,用于测试 6 | */ 7 | export function createMockStorage(): IStorageProvider & { 8 | getItem: ReturnType; 9 | setItem: ReturnType; 10 | removeItem: ReturnType; 11 | clearAll: ReturnType; 12 | } { 13 | const storage: Record = {}; 14 | 15 | return { 16 | getItem: vi.fn((key: string) => Promise.resolve(storage[key] || null)), 17 | setItem: vi.fn((key: string, value: string) => { 18 | storage[key] = value; 19 | return Promise.resolve(); 20 | }), 21 | removeItem: vi.fn((key: string) => { 22 | delete storage[key]; 23 | return Promise.resolve(); 24 | }), 25 | clearAll: vi.fn(() => { 26 | Object.keys(storage).forEach(key => delete storage[key]); 27 | return Promise.resolve(); 28 | }) 29 | }; 30 | } -------------------------------------------------------------------------------- /packages/core/tests/setup.js: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | import dotenv from 'dotenv' 3 | import path from 'path' 4 | 5 | // 加载环境变量(从项目根目录加载) 6 | dotenv.config({ path: path.resolve(process.cwd(), '../../.env.local') }) 7 | 8 | // 模拟 localStorage 9 | const localStorageMock = { 10 | store: new Map(), 11 | getItem: vi.fn((key) => { 12 | return localStorageMock.store.get(key) || null; 13 | }), 14 | setItem: vi.fn((key, value) => { 15 | localStorageMock.store.set(key, value); 16 | }), 17 | removeItem: vi.fn((key) => { 18 | localStorageMock.store.delete(key); 19 | }), 20 | clear: vi.fn(() => { 21 | localStorageMock.store.clear(); 22 | }) 23 | }; 24 | 25 | // 全局注入 localStorage 26 | global.localStorage = localStorageMock; 27 | 28 | // 在每个测试之前重置 mock 状态 29 | beforeEach(() => { 30 | localStorageMock.store.clear(); 31 | vi.clearAllMocks(); 32 | }); -------------------------------------------------------------------------------- /packages/core/tests/unit/llm/service.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach } from 'vitest'; 2 | import { 3 | LLMService, 4 | ModelManager, 5 | ModelConfig, 6 | APIError, 7 | RequestConfigError, 8 | Message 9 | } from '../../../src/index'; 10 | import { createMockStorage } from '../../mocks/mockStorage'; 11 | 12 | describe('LLMService', () => { 13 | let service: LLMService; 14 | let modelManager: ModelManager; 15 | 16 | beforeEach(() => { 17 | const mockStorage = createMockStorage(); 18 | modelManager = new ModelManager(mockStorage); 19 | service = new LLMService(modelManager); 20 | }); 21 | 22 | const mockModelConfig: ModelConfig = { 23 | name: 'Test Model', 24 | baseURL: 'https://api.test.com', 25 | apiKey: 'test-key', 26 | models: ['model-1'], 27 | defaultModel: 'model-1', 28 | enabled: true, 29 | provider: 'openai' 30 | }; 31 | 32 | const mockMessages: Message[] = [ 33 | { role: 'system', content: 'You are a helpful assistant.' }, 34 | { role: 'user', content: 'Hello!' } 35 | ]; 36 | 37 | describe('validateModelConfig', () => { 38 | it('should throw error when model is disabled', () => { 39 | const disabledConfig = { ...mockModelConfig, enabled: false }; 40 | expect(() => service['validateModelConfig'](disabledConfig)) 41 | .toThrow('模型未启用'); 42 | }); 43 | 44 | it('should throw error when apiKey is missing', () => { 45 | const invalidConfig = { ...mockModelConfig, apiKey: '' }; 46 | expect(() => service['validateModelConfig'](invalidConfig)) 47 | .toThrow('API密钥不能为空'); 48 | }); 49 | 50 | it('should throw error when provider is missing', () => { 51 | const invalidConfig = { ...mockModelConfig, provider: '' }; 52 | expect(() => service['validateModelConfig'](invalidConfig)) 53 | .toThrow('模型提供商不能为空'); 54 | }); 55 | 56 | it('should throw error when defaultModel is missing', () => { 57 | const invalidConfig = { ...mockModelConfig, defaultModel: '' }; 58 | expect(() => service['validateModelConfig'](invalidConfig)) 59 | .toThrow('默认模型不能为空'); 60 | }); 61 | }); 62 | 63 | describe('validateMessages', () => { 64 | it('should validate valid messages', () => { 65 | expect(() => service['validateMessages'](mockMessages)).not.toThrow(); 66 | }); 67 | 68 | it('should throw error for empty messages', () => { 69 | expect(() => service['validateMessages']([])) 70 | .toThrow('消息列表不能为空'); 71 | }); 72 | 73 | it('should throw error for invalid role', () => { 74 | const invalidMessages: Message[] = [{ role: 'invalid' as any, content: 'test' }]; 75 | expect(() => service['validateMessages'](invalidMessages)) 76 | .toThrow('不支持的消息类型: invalid'); 77 | }); 78 | 79 | it('should throw error for missing content', () => { 80 | const invalidMessages: Message[] = [{ role: 'user', content: '' }]; 81 | expect(() => service['validateMessages'](invalidMessages)) 82 | .toThrow('消息格式无效: 缺少必要字段'); 83 | }); 84 | }); 85 | }); -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "strict": true, 14 | "noUnusedLocals": true, 15 | "noUnusedParameters": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "declaration": true, 18 | "outDir": "dist" 19 | }, 20 | "include": ["src"], 21 | "exclude": ["node_modules", "dist", "tests"] 22 | } -------------------------------------------------------------------------------- /packages/core/vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | import { loadEnv } from 'vite' 3 | import path from 'path' 4 | 5 | export default defineConfig(({ mode }) => { 6 | // 加载环境变量 7 | process.env = { ...process.env, ...loadEnv(mode, process.cwd()) } 8 | 9 | return { 10 | test: { 11 | globals: true, 12 | environment: 'node', 13 | setupFiles: ['./tests/setup.js'], 14 | // 设置测试超时时间 15 | testTimeout: 30000, // 默认30秒 16 | hookTimeout: 30000, // 钩子超时30秒 17 | // 环境变量配置 18 | env: { 19 | ...process.env 20 | } 21 | } 22 | } 23 | }) -------------------------------------------------------------------------------- /packages/extension/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } -------------------------------------------------------------------------------- /packages/extension/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Prompt Optimizer 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /packages/extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prompt-optimizer/extension", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "test": "vitest", 11 | "test:coverage": "vitest run --coverage" 12 | }, 13 | "dependencies": { 14 | "@prompt-optimizer/ui": "workspace:*", 15 | "element-plus": "^2.9.3", 16 | "uuid": "^11.0.5", 17 | "vue": "^3.5.13" 18 | }, 19 | "devDependencies": { 20 | "@tailwindcss/forms": "^0.5.10", 21 | "@tailwindcss/typography": "^0.5.16", 22 | "@tsconfig/node18": "^18.2.4", 23 | "@types/node": "^22.13.4", 24 | "@types/uuid": "^10.0.0", 25 | "@vitejs/plugin-basic-ssl": "^1.2.0", 26 | "@vitejs/plugin-vue": "^5.2.1", 27 | "@vue/tsconfig": "^0.5.1", 28 | "autoprefixer": "^10.4.20", 29 | "postcss": "^8.5.1", 30 | "tailwindcss": "^3.4.17", 31 | "typescript": "^5.0.0", 32 | "vite": "^6.0.7", 33 | "vitest": "^3.0.2", 34 | "dotenv": "^16.4.7", 35 | "js-yaml": "^4.1.0" 36 | } 37 | } -------------------------------------------------------------------------------- /packages/extension/permissions-explanation.md: -------------------------------------------------------------------------------- 1 | # 提示词优化器 - 权限说明 2 | 3 | 本文档详细解释了提示词优化器Chrome扩展请求的各项权限及其用途,帮助您了解我们为何需要这些权限以及它们如何用于提供服务。 4 | 5 | ## 单一用途说明 (Single Purpose Description) 6 | 7 | **提示词优化器的主要目的是:优化用户的AI提示词,以获得更精准、高质量的AI回复。** 8 | 9 | 本扩展请求的所有权限都直接服务于这一单一核心目的: 10 | - **存储权限**用于保存用户的API密钥和设置,使用户能够连接到AI服务 11 | - **API域名访问权限**用于直接从用户浏览器发送提示词优化请求到相应的AI服务提供商 12 | - **标签页权限**用于在标签页中打开设置界面,提供更好的用户体验 13 | 14 | 这些权限对于实现扩展的主要功能至关重要,没有这些权限,扩展将无法执行其核心功能。同时,我们严格遵循最小权限原则,仅请求完成核心任务所必需的权限,不收集任何与此核心功能无关的数据。 15 | 16 | ## 请求的权限 17 | 18 | ### 存储权限 (`storage`) 19 | 20 | **用途:** 21 | - 在本地安全存储API密钥 22 | - 保存您的扩展设置和偏好 23 | - 存储提示词优化历史记录(如果启用了此功能) 24 | 25 | **重要说明:** 26 | - 所有数据仅存储在您的本地设备上 27 | - 不会上传到任何服务器 28 | - 您可以随时通过扩展设置清除这些数据 29 | 30 | ### 标签页权限 (`tabs`) 31 | 32 | **用途:** 33 | - 允许扩展在标签页中展示所有核心功能,包括提示词优化、模型管理以及其他用户交互界面 34 | - 提供更宽敞、更灵活的用户体验,让所有功能都在独立标签页中运行 35 | 36 | **重要说明:** 37 | - 扩展在用户主动操作时会在标签页中打开并展示所有功能界面 38 | - 不会在后台监控您的标签页 39 | - 不会记录您的浏览历史或收集标签页数据 40 | - 仅用于提供更好的用户界面体验,所有核心功能均在标签页中完成 41 | 42 | ## 主机权限 (`host_permissions`) 43 | 44 | ### 1. OpenAI API (`https://api.openai.com/*`) 45 | 46 | **用途:** 47 | - 允许扩展直接从您的浏览器向OpenAI的API发送请求 48 | - 用于使用OpenAI模型(如GPT-3.5、GPT-4)优化提示词 49 | 50 | ### 2. Google Gemini API (`https://generativelanguage.googleapis.com/*`) 51 | 52 | **用途:** 53 | - 允许扩展直接从您的浏览器向Google的Gemini API发送请求 54 | - 用于使用Google Gemini模型优化提示词 55 | 56 | ### 3. DeepSeek API (`https://api.deepseek.com/*`) 57 | 58 | **用途:** 59 | - 允许扩展直接从您的浏览器向DeepSeek的API发送请求 60 | - 用于使用DeepSeek模型优化提示词 61 | 62 | ### 4. SiliconFlow API (`https://api.siliconflow.cn/*`) 63 | 64 | **用途:** 65 | - 允许扩展直接从您的浏览器向SiliconFlow的API发送请求 66 | - 用于使用SiliconFlow模型优化提示词 67 | 68 | **重要说明:** 69 | - 所有API请求都直接从您的浏览器发起 70 | - 使用您自己提供的API密钥 71 | - 不经过我们的服务器或任何中间服务器 72 | - 您的提示词内容会发送到相应的AI服务提供商,并受其隐私政策约束 73 | 74 | ## 关于自定义API的说明 75 | 76 | 虽然扩展在manifest中只预先声明了OpenAI、Google Gemini和DeepSeek的API域名权限,但您仍然可以使用自定义API。这是因为: 77 | 78 | 1. **内容安全策略的工作方式**:Chrome扩展的内容安全策略允许扩展在运行时连接到用户明确授权的域名,即使这些域名没有在manifest中预先声明。 79 | 80 | 2. **如何使用自定义API**: 81 | - 在扩展的"模型管理"界面中,点击"添加自定义模型" 82 | - 填写自定义API的相关信息,包括API地址、模型名称和API密钥 83 | - 保存后,您就可以使用这个自定义API了 84 | 85 | 3. **首次连接授权**: 86 | - 当您首次尝试使用自定义API时,Chrome浏览器会显示一个权限请求对话框 87 | - 您需要明确授权扩展连接到这个自定义域名 88 | - 授权后,扩展就可以与该自定义API通信了 89 | 90 | 4. **安全考虑**: 91 | - 这种方式确保了扩展只能连接到您明确授权的域名 92 | - 所有连接仍然是从您的浏览器直接发起的,不经过我们的服务器 93 | - 您可以随时在Chrome的扩展权限设置中撤销这些授权 94 | 95 | 5. **适用场景**: 96 | - 使用公司内部的AI服务 97 | - 连接到其他兼容OpenAI格式的API服务 98 | - 使用自托管的开源模型API 99 | - 连接到任何符合OpenAI兼容格式的API端点 100 | 101 | 这种设计既保证了扩展的灵活性,允许您连接到任何需要的API服务,同时也维护了Chrome扩展的安全模型,确保所有连接都是经过您明确授权的。 102 | 103 | ## 为什么需要特定API域名的访问权限? 104 | 105 | Chrome浏览器的安全机制要求扩展明确声明它需要访问的外部域名。这是一项重要的安全功能,称为"内容安全策略"(Content Security Policy),它限制扩展只能与预先声明的域名通信,防止恶意扩展向未授权的服务器发送数据。 106 | 107 | ### 为什么我们需要这些特定域名的权限: 108 | 109 | 1. **直接API调用的必要条件**: 110 | - 没有这些权限,浏览器会阻止扩展向这些AI服务提供商发送请求 111 | - 这些权限是实现"纯客户端应用"架构的关键,使扩展能够直接从您的浏览器调用AI服务,而无需通过我们的服务器 112 | 113 | 2. **精确的最小权限原则**: 114 | - 我们只请求访问必要的API域名(如OpenAI、Google Gemini、DeepSeek) 115 | - 不请求访问其他不相关的域名 116 | - 每个请求的域名都有明确的用途(提供AI模型服务) 117 | 118 | 3. **透明的数据流向**: 119 | - 这些权限使数据流向完全透明 - 从您的浏览器直接到AI服务提供商 120 | - 没有中间服务器或第三方参与数据传输过程 121 | 122 | 4. **安全性考虑**: 123 | - 使用通配符模式(如`https://api.openai.com/*`)允许访问该域名下的所有路径 124 | - 这是必要的,因为AI服务提供商可能在不同路径提供不同的API端点 125 | - 所有通信都通过安全的HTTPS连接进行 126 | 127 | ### 如果没有这些权限会怎样? 128 | 129 | 如果我们不请求这些特定API域名的访问权限: 130 | 131 | 1. 扩展将无法直接调用AI服务提供商的API 132 | 2. 提示词优化功能将无法工作 133 | 3. 我们将不得不采用服务器中转架构,这会: 134 | - 降低数据隐私性(您的数据需要经过我们的服务器) 135 | - 增加延迟(多一层数据传输) 136 | - 引入额外的安全风险 137 | 138 | ### 用户控制 139 | 140 | 重要的是,即使授予了这些权限: 141 | 142 | - 扩展只会在您主动使用提示词优化功能时才会发送请求 143 | - 您可以随时在扩展设置中更改或删除API密钥,控制是否允许调用这些服务 144 | - 所有API调用都使用您自己提供的API密钥,您完全控制费用和使用情况 145 | 146 | ## 最小权限原则 147 | 148 | 我们遵循最小权限原则,只请求应用程序核心功能所必需的权限。这个扩展只需要: 149 | 150 | 1. `storage` 权限 - 用于本地存储您的设置和API密钥 151 | 2. 特定API域名的访问权限 - 用于直接从您的浏览器调用AI服务 152 | 153 | ## 我们不请求的权限 154 | 155 | 为了保护您的隐私,我们特意不请求以下权限: 156 | 157 | - **浏览历史** (`history`) - 我们不访问您的浏览历史 158 | - **所有网站的访问权限** (`` 作为权限) - 我们不需要访问您浏览的网页内容 159 | - **活动标签页** (`activeTab`) - 我们不需要访问您当前浏览的页面内容 160 | - **上下文菜单** (`contextMenus`) - 我们不添加右键菜单项 161 | - **后台运行权限** (`background` 作为持续运行的后台) - 我们的扩展不会在后台持续运行 162 | - **网络请求拦截** (`webRequest`) - 我们不拦截或修改您的网络请求 163 | - **Cookie访问** (`cookies`) - 我们不读取或修改您的Cookie 164 | 165 | ## 纯客户端架构 166 | 167 | 提示词优化器采用纯客户端架构,这意味着: 168 | 169 | 1. 所有数据存储和处理都在您的本地浏览器中完成 170 | 2. 没有自有服务器收集或处理您的数据 171 | 3. API调用直接从您的浏览器发送到AI服务提供商 172 | 4. 您的API密钥和设置仅存储在您的设备上 173 | 174 | ## 权限使用透明度 175 | 176 | 我们承诺: 177 | 178 | - 仅请求提供核心功能所必需的权限 179 | - 明确解释每项权限的用途 180 | - 不滥用任何授予的权限 181 | - 保持代码开源,确保透明度 182 | 183 | 如果您对我们的权限使用有任何疑问,请通过GitHub Issues联系我们:https://github.com/linshenkx/prompt-optimizer/issues -------------------------------------------------------------------------------- /packages/extension/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /packages/extension/privacy-policy.md: -------------------------------------------------------------------------------- 1 | # 提示词优化器 - 隐私政策 2 | 3 | *最后更新日期:2025年2月1日* 4 | 5 | ## 引言 6 | 7 | 感谢您使用"提示词优化器"Chrome扩展(以下简称"本扩展")。本隐私政策旨在向您说明我们如何收集、使用、存储和保护您的信息。我们高度重视您的隐私,并致力于保护您的个人数据安全。 8 | 9 | **特别说明:本扩展是一个纯客户端应用程序,没有后端服务器,所有数据存储和管理均在您的本地浏览器中完成。但请注意,提示词优化功能需要调用第三方AI服务提供商的API。** 10 | 11 | ## 我们收集的信息 12 | 13 | ### 我们不收集的信息 14 | 15 | 本扩展不会收集、存储或传输以下信息: 16 | - 您的个人身份信息 17 | - 您的浏览历史 18 | - 您访问的网站内容 19 | - 您的位置数据 20 | - 您的设备信息 21 | - 您的提示词内容或使用习惯 22 | 23 | 作为纯客户端应用,我们不运营任何服务器来收集或存储您的信息。 24 | 25 | ### 本地存储的信息 26 | 27 | 本扩展仅在您的本地设备上存储以下信息: 28 | - 您自行提供的API密钥(经过加密存储) 29 | - 您的扩展设置和偏好 30 | - 提示词优化历史记录(仅当您启用历史记录功能时) 31 | 32 | 这些信息完全存储在您的本地设备上,不会被传输到任何我们的服务器。所有数据操作都在您的浏览器内部处理,我们无法访问这些信息。 33 | 34 | ## 数据传输 35 | 36 | 当您使用本扩展优化提示词时,您的提示词内容将直接从您的浏览器发送到您配置的AI服务提供商(如OpenAI、Google Gemini或DeepSeek)的API。这些传输: 37 | 38 | 1. 使用您自己提供的API密钥进行身份验证 39 | 2. 通过HTTPS加密连接传输 40 | 3. 受相应API服务提供商的隐私政策和条款约束 41 | 4. 不经过我们的服务器或任何中间服务器 42 | 5. 完全在您的浏览器中发起和处理 43 | 44 | **重要说明:** 45 | - 我们没有服务器来拦截、存储或处理您的API请求 46 | - 所有API请求直接从您的浏览器发送到相应的AI服务提供商,并且响应直接返回到您的浏览器 47 | - 虽然本扩展是纯客户端应用,但优化提示词的功能确实需要将您的提示词内容发送到第三方AI服务提供商(如OpenAI、Google等) 48 | - 这些第三方服务提供商将根据其各自的隐私政策处理您的数据 49 | 50 | ## 第三方服务 51 | 52 | 本扩展可能使用以下第三方服务: 53 | - **OpenAI API**:当您选择使用OpenAI模型时 54 | - **Google Gemini API**:当您选择使用Gemini模型时 55 | - **DeepSeek API**:当您选择使用DeepSeek模型时 56 | - **其他自定义API**:当您配置自定义API终端点时 57 | 58 | 请注意,这些第三方服务有自己的隐私政策和数据处理实践。我们建议您查阅这些服务的隐私政策,了解他们如何处理您的数据。由于本扩展是纯客户端应用,您的浏览器将直接与这些服务通信,不经过任何中间服务器。 59 | 60 | ## 数据安全 61 | 62 | 我们采取以下措施保护您的数据: 63 | - 纯客户端架构,没有自有服务器收集数据 64 | - 所有API密钥都使用浏览器的安全存储机制进行加密存储 65 | - 所有数据传输都通过HTTPS加密连接直接进行 66 | - 不在任何外部服务器上存储您的个人数据 67 | - 所有数据管理和存储在您的本地浏览器中完成 68 | 69 | ## 数据处理的明确说明 70 | 71 | 为了避免任何误解,我们特此明确说明数据处理流程: 72 | 73 | 1. **本地数据存储和管理**:所有设置、配置和历史记录都存储在您的本地浏览器中 74 | 2. **提示词优化过程**: 75 | - 当您请求优化提示词时,您的提示词内容将从您的浏览器直接发送到您选择的AI服务提供商(如OpenAI) 76 | - 这些AI服务提供商会处理您的提示词并返回优化结果 77 | - 整个过程不经过我们的服务器 78 | - 您的提示词内容和优化结果会受到相应AI服务提供商隐私政策的约束 79 | 80 | ## 儿童隐私 81 | 82 | 本扩展不针对13岁以下的儿童,我们也不会故意收集13岁以下儿童的个人信息。作为纯客户端应用,本扩展不会收集任何用户的个人信息。 83 | 84 | ## 政策变更 85 | 86 | 我们可能会不时更新本隐私政策。当我们进行重大变更时,我们将通过扩展更新通知您,并在本页面上发布更新后的隐私政策。 87 | 88 | ## 联系我们 89 | 90 | 如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们: 91 | - 通过GitHub Issues: https://github.com/linshenkx/prompt-optimizer/issues 92 | 93 | ## 同意 94 | 95 | 通过安装和使用本扩展,您确认您已阅读并同意本隐私政策的条款。您也确认理解本扩展是纯客户端应用程序,在没有自有服务器参与的情况下运行,所有数据存储和管理都在您的本地浏览器中进行,但优化提示词的功能需要将您的提示词内容发送到第三方AI服务提供商。 -------------------------------------------------------------------------------- /packages/extension/public/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "Prompt Optimizer" 4 | }, 5 | "extDesc": { 6 | "message": "Large Language Model Prompt Optimization Tool: One-click prompt optimization with multi-round iterative improvements and version tracking. Supports major AI models (OpenAI/Gemini/DeepSeek etc.), fully open-source, client-side processing ensures data security." 7 | }, 8 | "extShortName": { 9 | "message": "Prompt Optimizer" 10 | } 11 | } -------------------------------------------------------------------------------- /packages/extension/public/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName": { 3 | "message": "提示词优化器" 4 | }, 5 | "extDesc": { 6 | "message": "大模型提示词优化工具:一键优化提示词并支持多轮迭代改进测试,支持版本回溯。支持主流AI模型(OpenAI/Gemini/DeepSeek等),完全开源,纯客户端处理保障数据安全。" 7 | }, 8 | "extShortName": { 9 | "message": "提示词优化器" 10 | } 11 | } -------------------------------------------------------------------------------- /packages/extension/public/background.js: -------------------------------------------------------------------------------- 1 | chrome.action.onClicked.addListener(() => { 2 | const url = chrome.runtime.getURL('index.html'); 3 | chrome.tabs.create({ url }); 4 | }); -------------------------------------------------------------------------------- /packages/extension/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/packages/extension/public/favicon.ico -------------------------------------------------------------------------------- /packages/extension/public/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/packages/extension/public/icons/icon128.png -------------------------------------------------------------------------------- /packages/extension/public/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/packages/extension/public/icons/icon16.png -------------------------------------------------------------------------------- /packages/extension/public/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/packages/extension/public/icons/icon48.png -------------------------------------------------------------------------------- /packages/extension/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "default_locale": "zh_CN", 4 | "name": "__MSG_extName__", 5 | "version": "1.0.6", 6 | "description": "__MSG_extDesc__", 7 | "icons": { 8 | "16": "icons/icon16.png", 9 | "48": "icons/icon48.png", 10 | "128": "icons/icon128.png" 11 | }, 12 | "action": { 13 | "default_icon": { 14 | "16": "icons/icon16.png", 15 | "48": "icons/icon48.png", 16 | "128": "icons/icon128.png" 17 | } 18 | }, 19 | "background": { 20 | "service_worker": "background.js", 21 | "type": "module" 22 | }, 23 | "permissions": [ 24 | ], 25 | "homepage_url": "https://github.com/linshenkx/prompt-optimizer", 26 | "author": "linshenkx", 27 | "content_security_policy": { 28 | "extension_pages": "script-src 'self'; object-src 'self'; style-src 'self';" 29 | }, 30 | "offline_enabled": true, 31 | "incognito": "split", 32 | "short_name": "__MSG_extShortName__", 33 | "minimum_chrome_version": "88" 34 | } -------------------------------------------------------------------------------- /packages/extension/publish-guide.md: -------------------------------------------------------------------------------- 1 | # Chrome扩展上架指南 2 | 3 | 本文档提供了将提示词优化器发布到Chrome网上应用商店的详细步骤。 4 | 5 | ## 准备工作 6 | 7 | ### 1. 开发者账号注册 8 | 9 | 在上传扩展前,需要先注册Chrome网上应用商店开发者账号: 10 | 11 | 1. 访问 [Chrome网上应用商店开发者控制台](https://chrome.google.com/webstore/devconsole) 12 | 2. 使用Google账号登录 13 | 3. 支付一次性的$5.00 USD开发者注册费 14 | 4. 完成开发者资料设置 15 | 16 | ### 2. 准备插件包 17 | 18 | 1. 确保已完成构建: 19 | ```bash 20 | pnpm build:ext 21 | ``` 22 | 23 | 2. 打包扩展: 24 | - 找到构建后的扩展目录(通常在`packages/extension/dist`) 25 | - 将整个目录打包为ZIP文件 26 | - 确保ZIP文件结构根目录直接包含manifest.json 27 | 28 | ### 3. 准备上架材料 29 | 30 | 根据`chrome.md`文件中列出的清单,确保准备好所有所需材料: 31 | 32 | - 所有尺寸的图标 33 | - 至少1张截图(1280x800或640x400像素) 34 | - 宣传图片(可选,1400x560像素) 35 | - 详细描述文本 36 | - 隐私政策页面 37 | 38 | ## 上传流程 39 | 40 | 1. 访问 [Chrome网上应用商店开发者控制台](https://chrome.google.com/webstore/devconsole) 41 | 2. 点击"添加新项目"按钮 42 | 3. 上传ZIP格式的扩展包 43 | 4. 填写商品详情页信息: 44 | - 语言:选择中文和英文 45 | - 商品详情:复制`chrome.md`中的详细描述 46 | - 类别:选择"生产力工具"和"写作工具" 47 | - 上传图标、截图和宣传图片 48 | - 填写隐私政策链接(可使用GitHub Pages或Vercel托管的隐私政策页面) 49 | 5. 提交审核前,检查所有内容是否完整 50 | 51 | ## 审核流程 52 | 53 | Chrome网上应用商店的审核通常需要几天到两周时间,请耐心等待。 54 | 55 | ### 常见审核问题及解决方案 56 | 57 | 1. **权限问题**: 58 | - 确保manifest.json中只请求必要的权限 59 | - 在描述中明确说明每个权限的用途 60 | 61 | 2. **隐私政策问题**: 62 | - 确保隐私政策完整详尽 63 | - 明确说明如何处理用户数据 64 | 65 | 3. **功能描述问题**: 66 | - 确保描述准确,不夸大功能 67 | - 所有截图需真实反映插件功能 68 | 69 | 4. **安全问题**: 70 | - 确保不存在恶意代码 71 | - 避免使用不安全的API 72 | 73 | ## 发布后维护 74 | 75 | ### 版本更新 76 | 77 | 1. 增加manifest.json中的版本号 78 | 2. 重新构建插件 79 | 3. 打包新版本 80 | 4. 在开发者控制台上传新版本 81 | 5. 填写更新说明 82 | 6. 提交审核 83 | 84 | ### 用户反馈处理 85 | 86 | 1. 定期检查用户评价和反馈 87 | 2. 及时回复用户问题 88 | 3. 根据反馈改进插件功能 89 | 4. 更新常见问题解答 90 | 91 | ## 宣传策略 92 | 93 | 1. **社交媒体宣传**: 94 | - 在技术社区分享(如掘金、知乎、V2EX等) 95 | - 制作简短演示视频 96 | - 编写使用教程和案例 97 | 98 | 2. **SEO优化**: 99 | - 优化Chrome商店描述关键词 100 | - 创建专门的落地页 101 | - 编写相关博客文章 102 | 103 | 3. **用户激励**: 104 | - 鼓励满意用户评价 105 | - 提供反馈奖励机制 106 | - 建立用户社区 107 | 108 | ## 财务管理 109 | 110 | 1. 设置Google商家账户(如适用) 111 | 2. 设置税务信息 112 | 3. 设置付款方式 113 | 4. 了解应用内购买政策(如适用) 114 | 115 | ## 资源链接 116 | 117 | - [Chrome开发者文档](https://developer.chrome.com/docs/webstore/) 118 | - [Chrome商店政策](https://developer.chrome.com/docs/webstore/program-policies/) 119 | - [Chrome扩展最佳实践](https://developer.chrome.com/docs/extensions/mv3/best_practices/) 120 | - [Google商家支持](https://support.google.com/chrome_webstore/) -------------------------------------------------------------------------------- /packages/extension/screenshots.md: -------------------------------------------------------------------------------- 1 | # 提示词优化器 - 截图准备指南 2 | 3 | 本文档提供了为Chrome应用商店准备截图的详细说明和建议。高质量的截图能够有效展示插件功能,吸引更多用户。 4 | 5 | ## 截图要求 6 | 7 | Chrome应用商店对截图有以下要求: 8 | - 分辨率:1280x800像素或640x400像素 9 | - 格式:PNG或JPEG 10 | - 数量:至少1张,最多5张 11 | - 文件大小:每张不超过2MB 12 | 13 | ## 推荐截图场景 14 | 15 | 为全面展示提示词优化器的功能,建议准备以下场景的截图: 16 | 17 | ### 1. 主界面截图 18 | 19 | **描述**:展示扩展的主弹出窗口,显示整洁的界面设计。 20 | 21 | **准备方法**: 22 | 1. 点击浏览器工具栏中的扩展图标 23 | 2. 确保界面干净,显示输入框和主要功能按钮 24 | 3. 截取整个弹出窗口 25 | 26 | **要点**: 27 | - 确保UI界面清晰可见 28 | - 保持简洁美观的设计 29 | - 避免显示任何敏感信息 30 | 31 | ### 2. 右键菜单使用场景 32 | 33 | **描述**:展示用户在网页上选中文本,通过右键菜单使用插件优化提示词。 34 | 35 | **准备方法**: 36 | 1. 在任意网页(如ChatGPT)选中一段文本 37 | 2. 右键点击,展开上下文菜单 38 | 3. 确保"提示词优化器"选项可见 39 | 4. 截取包含右键菜单的页面区域 40 | 41 | **要点**: 42 | - 选择有代表性的文本内容 43 | - 确保右键菜单清晰可见 44 | - 避免选择含个人隐私的内容 45 | 46 | ### 3. 优化结果展示 47 | 48 | **描述**:展示提示词优化前后的对比效果。 49 | 50 | **准备方法**: 51 | 1. 在输入框中输入一个简单的提示词 52 | 2. 点击优化按钮 53 | 3. 等待优化结果显示 54 | 4. 截取显示原始提示词和优化后提示词的界面 55 | 56 | **要点**: 57 | - 选择有代表性的提示词示例 58 | - 确保优化效果明显 59 | - 使用简洁的示例,避免过长的文本 60 | 61 | ### 4. 多模型设置界面 62 | 63 | **描述**:展示插件支持多种AI模型的设置界面。 64 | 65 | **准备方法**: 66 | 1. 进入插件设置页面 67 | 2. 导航到模型设置选项卡 68 | 3. 确保界面显示多个支持的模型选项 69 | 4. 截取设置界面 70 | 71 | **要点**: 72 | - 不要显示真实的API密钥 73 | - 确保界面整洁有序 74 | - 突出多模型支持特性 75 | 76 | ### 5. 使用场景示例 77 | 78 | **描述**:展示插件在实际应用场景中的使用效果。 79 | 80 | **准备方法**: 81 | 1. 在实际应用场景(如AI绘图网站)中使用插件 82 | 2. 展示优化前后的提示词及生成效果 83 | 3. 截取包含上下文的界面 84 | 85 | **要点**: 86 | - 选择有吸引力的应用场景 87 | - 确保效果对比明显 88 | - 避免显示个人账号信息 89 | 90 | ## 截图处理建议 91 | 92 | 1. **尺寸调整**: 93 | - 确保最终图片符合1280x800或640x400像素 94 | - 保持16:10的宽高比 95 | - 如需调整尺寸,使用专业图像处理软件避免失真 96 | 97 | 2. **视觉优化**: 98 | - 适当调整亮度和对比度,确保清晰可读 99 | - 考虑添加简单的标注或高亮重点功能区域 100 | - 保持一致的视觉风格 101 | 102 | 3. **文件命名**: 103 | - 使用描述性文件名(如`main-interface.png`、`right-click-menu.png`等) 104 | - 按顺序编号(如`01-main.png`、`02-context.png`等) 105 | 106 | ## 注意事项 107 | 108 | 1. 不要在截图中显示敏感信息(如API密钥、个人邮箱等) 109 | 2. 避免过度编辑或添加不实际存在的功能 110 | 3. 确保截图真实反映插件功能 111 | 4. 考虑不同屏幕尺寸和分辨率的用户体验 112 | 5. 避免使用过多文字说明,让界面直观地传达功能 113 | 114 | ## 截图示例 115 | 116 | 以下是推荐的截图内容安排: 117 | 118 | 1. **首张截图**:主界面 - 展示整体布局和设计风格 119 | 2. **第二张**:右键菜单使用场景 - 展示如何在任意网页使用 120 | 3. **第三张**:优化效果对比 - 展示核心功能效果 121 | 4. **第四张**:多模型支持 - 展示技术亮点 122 | 5. **第五张**:实际应用示例 - 展示实用价值 123 | 124 | 按照这个顺序,能够全面且有逻辑地向潜在用户展示插件的价值和功能。 -------------------------------------------------------------------------------- /packages/extension/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { installI18n } from '@prompt-optimizer/ui' 3 | import App from './App.vue' 4 | 5 | import './style.css' 6 | import '@prompt-optimizer/ui/dist/style.css' 7 | 8 | const app = createApp(App) 9 | installI18n(app) 10 | app.mount('#app') -------------------------------------------------------------------------------- /packages/extension/src/style.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* 基础样式设置 */ 6 | :root { 7 | font-size: 16px; /* 确保基础字体大小 */ 8 | zoom: 1; /* 确保不被缩放 */ 9 | } 10 | 11 | /* 确保正确的字体大小继承 */ 12 | html { 13 | font-size: 100%; /* 使用百分比确保正确缩放 */ 14 | -webkit-text-size-adjust: 100%; /* 防止 Chrome 自动调整字体大小 */ 15 | } 16 | 17 | body { 18 | font-size: 1rem; /* 使用相对单位 */ 19 | text-rendering: optimizeLegibility; /* 优化文本渲染 */ 20 | -webkit-font-smoothing: antialiased; /* 字体平滑 */ 21 | -moz-osx-font-smoothing: grayscale; 22 | } -------------------------------------------------------------------------------- /packages/extension/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | "../../node_modules/@prompt-optimizer/ui/src/**/*.{vue,js,ts,jsx,tsx}", 7 | "../ui/src/**/*.{vue,js,ts,jsx,tsx}" 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundColor: { 12 | 'input-bg': 'rgba(0, 0, 0, 0.2)', 13 | }, 14 | textColor: { 15 | 'input-text': 'rgba(255, 255, 255, 0.9)', 16 | }, 17 | }, 18 | }, 19 | plugins: [ 20 | require('@tailwindcss/forms')({ 21 | strategy: 'class', 22 | }), 23 | require('@tailwindcss/typography'), 24 | ], 25 | important: true, 26 | } -------------------------------------------------------------------------------- /packages/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "strict": true, 9 | "jsx": "preserve", 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM"], 14 | "skipLibCheck": true, 15 | "noEmit": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./src/*"], 19 | "@prompt-optimizer/ui": ["../ui/src/index.ts"], 20 | "@prompt-optimizer/ui/*": ["../ui/src/*"] 21 | } 22 | }, 23 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 24 | "references": [ 25 | { "path": "./tsconfig.node.json" } 26 | ] 27 | } -------------------------------------------------------------------------------- /packages/extension/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": ["./vite.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "module": "ESNext", 7 | "moduleResolution": "Node", 8 | "types": ["node"] 9 | } 10 | } -------------------------------------------------------------------------------- /packages/extension/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | import basicSsl from '@vitejs/plugin-basic-ssl' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue(), basicSsl()], 9 | resolve: { 10 | alias: { 11 | '@': resolve(__dirname, 'src'), 12 | '@prompt-optimizer/ui': resolve(__dirname, '../ui') 13 | }, 14 | }, 15 | base: './', // 使用相对路径 16 | build: { 17 | outDir: 'dist', 18 | rollupOptions: { 19 | input: { 20 | popup: resolve(__dirname, 'index.html') 21 | }, 22 | output: { 23 | entryFileNames: `assets/[name].js`, 24 | chunkFileNames: `assets/[name].js`, 25 | assetFileNames: (assetInfo) => { 26 | if (assetInfo.name === 'background.js') { 27 | return 'background.js'; 28 | } 29 | return `assets/[name].[ext]`; 30 | } 31 | } 32 | }, 33 | copyPublicDir: true 34 | }, 35 | server: { 36 | port: 5174, 37 | https: {} 38 | } 39 | }) -------------------------------------------------------------------------------- /packages/ui/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | const component: DefineComponent<{}, {}, any> 6 | export default component 7 | } -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prompt-optimizer/ui", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "style": "./dist/style.css", 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "import": "./dist/index.js", 14 | "require": "./dist/index.cjs" 15 | }, 16 | "./style.css": "./dist/style.css", 17 | "./dist/style.css": "./dist/style.css" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "readme": "在Windows环境下,请使用分号(;)而不是&&作为命令分隔符,或使用build:win脚本", 23 | "scripts": { 24 | "dev": "vite", 25 | "build": "vite build", 26 | "build:win": "vite build", 27 | "test": "vitest run" 28 | }, 29 | "dependencies": { 30 | "@prompt-optimizer/core": "workspace:*", 31 | "dompurify": "^3.2.4", 32 | "element-plus": "^2.5.6", 33 | "highlight.js": "^11.11.1", 34 | "markdown-it": "^14.1.0", 35 | "uuid": "^11.0.5", 36 | "vue": "^3.3.4" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^22.13.10", 40 | "@types/uuid": "^10.0.0", 41 | "@vitejs/plugin-vue": "^5.2.1", 42 | "@vue/test-utils": "^2.4.5", 43 | "@vue/tsconfig": "^0.5.1", 44 | "jsdom": "^26.0.0", 45 | "typescript": "^5.8.2", 46 | "vite": "^6.2.0", 47 | "vite-plugin-dts": "^4.5.3", 48 | "vitest": "^3.0.7" 49 | } 50 | } -------------------------------------------------------------------------------- /packages/ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } -------------------------------------------------------------------------------- /packages/ui/src/assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/packages/ui/src/assets/logo.jpg -------------------------------------------------------------------------------- /packages/ui/src/components.d.ts: -------------------------------------------------------------------------------- 1 | import type { DefineComponent } from 'vue' 2 | 3 | declare module 'vue' { 4 | export interface GlobalComponents { 5 | [key: string]: DefineComponent<{}, {}, any> 6 | } 7 | } 8 | 9 | declare module '*.vue' { 10 | import type { DefineComponent } from 'vue' 11 | const component: DefineComponent<{}, {}, any> 12 | export default component 13 | } -------------------------------------------------------------------------------- /packages/ui/src/components/ActionButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /packages/ui/src/components/ContentCard.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/src/components/FullscreenDialog.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 65 | 66 | -------------------------------------------------------------------------------- /packages/ui/src/components/InputPanel.vue: -------------------------------------------------------------------------------- 1 | 2 | 70 | 71 | -------------------------------------------------------------------------------- /packages/ui/src/components/LanguageSwitch.vue: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | -------------------------------------------------------------------------------- /packages/ui/src/components/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 37 | 38 | -------------------------------------------------------------------------------- /packages/ui/src/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | -------------------------------------------------------------------------------- /packages/ui/src/components/ModelSelect.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 167 | 168 | -------------------------------------------------------------------------------- /packages/ui/src/components/OptimizePanel.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | -------------------------------------------------------------------------------- /packages/ui/src/components/Panel.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/src/components/Toast.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | -------------------------------------------------------------------------------- /packages/ui/src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useToast' 2 | export * from './useModals' 3 | export * from './usePromptOptimizer' 4 | export * from './usePromptTester' 5 | export * from './usePromptHistory' 6 | export * from './useServiceInitializer' 7 | export * from './useTemplateManager' 8 | export * from './useModelManager' 9 | export * from './useHistoryManager' 10 | export * from './useModelSelectors' 11 | export * from './useAutoScroll' 12 | export * from './useClipboard' 13 | export * from './useFullscreen' -------------------------------------------------------------------------------- /packages/ui/src/composables/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import { useI18n } from 'vue-i18n' 2 | import { useToast } from './useToast' 3 | 4 | /** 5 | * 复制内容到剪贴板的组合式API 6 | * 7 | * @returns 复制相关的方法和状态 8 | */ 9 | export function useClipboard() { 10 | const { t } = useI18n() 11 | const toast = useToast() 12 | 13 | /** 14 | * 复制文本到剪贴板 15 | * 支持现代API和降级方案 16 | * 17 | * @param text 要复制的文本 18 | * @param showToast 是否显示提示,默认为true 19 | * @returns 是否复制成功 20 | */ 21 | const copyText = async (text: string, showToast = true): Promise => { 22 | if (!text) return false 23 | 24 | // 检查是否支持现代剪贴板API 25 | if (navigator.clipboard && window.isSecureContext) { 26 | try { 27 | await navigator.clipboard.writeText(text) 28 | if (showToast) toast.success(t('common.copySuccess')) 29 | return true 30 | } catch (e) { 31 | console.warn('现代剪贴板API失败,尝试备用方法', e) 32 | // 继续尝试备用方法 33 | } 34 | } 35 | 36 | // 备用方法:创建临时元素复制 37 | try { 38 | const textarea = document.createElement('textarea') 39 | textarea.value = text 40 | 41 | // 设置样式确保元素不可见 42 | textarea.style.position = 'fixed' 43 | textarea.style.opacity = '0' 44 | textarea.style.left = '-9999px' 45 | 46 | document.body.appendChild(textarea) 47 | textarea.select() 48 | 49 | const successful = document.execCommand('copy') 50 | document.body.removeChild(textarea) 51 | 52 | if (successful && showToast) { 53 | toast.success(t('common.copySuccess')) 54 | } else if (!successful && showToast) { 55 | toast.error(t('common.copyFailed')) 56 | } 57 | 58 | return successful 59 | } catch (e) { 60 | console.error('备用剪贴板方法失败', e) 61 | if (showToast) toast.error(t('common.copyFailed')) 62 | return false 63 | } 64 | } 65 | 66 | return { 67 | copyText 68 | } 69 | } -------------------------------------------------------------------------------- /packages/ui/src/composables/useFullscreen.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch } from 'vue' 2 | import type { ComputedRef } from 'vue' 3 | 4 | export function useFullscreen( 5 | modelValue: ComputedRef | { value: string }, 6 | emitUpdateValue: (value: string) => void 7 | ) { 8 | // 全屏状态 9 | const isFullscreen = ref(false) 10 | 11 | // 全屏模式下的文本值 12 | const fullscreenValue = ref(modelValue.value || '') 13 | 14 | // 监听外部值变化,同步到全屏值 15 | watch(() => modelValue.value, (newValue) => { 16 | fullscreenValue.value = newValue || '' 17 | }) 18 | 19 | // 监听全屏值变化,同步到外部 20 | watch(fullscreenValue, (newValue) => { 21 | emitUpdateValue(newValue) 22 | }) 23 | 24 | // 打开全屏 25 | const openFullscreen = () => { 26 | isFullscreen.value = true 27 | } 28 | 29 | // 关闭全屏 30 | const closeFullscreen = () => { 31 | isFullscreen.value = false 32 | } 33 | 34 | return { 35 | isFullscreen, 36 | fullscreenValue, 37 | openFullscreen, 38 | closeFullscreen 39 | } 40 | } -------------------------------------------------------------------------------- /packages/ui/src/composables/useHistoryManager.ts: -------------------------------------------------------------------------------- 1 | import { ref, type Ref } from 'vue' 2 | import type { HistoryManager } from '@prompt-optimizer/core' 3 | import { useToast } from './useToast' 4 | import { useI18n } from 'vue-i18n' 5 | 6 | export interface HistoryManagerHooks { 7 | showHistory: Ref 8 | handleSelectHistory: (historyItem: any) => void 9 | handleClearHistory: () => void 10 | handleDeleteChain: (chainId: string) => void 11 | } 12 | 13 | export function useHistoryManager( 14 | historyManager: HistoryManager, 15 | prompt: Ref, 16 | optimizedPrompt: Ref, 17 | currentChainId: Ref, 18 | currentVersions: Ref, 19 | currentVersionId: Ref, 20 | handleSelectHistoryBase: (historyItem: any) => void, 21 | handleClearHistoryBase: () => void, 22 | handleDeleteChainBase: (chainId: string) => void 23 | ): HistoryManagerHooks { 24 | const showHistory = ref(false) 25 | const toast = useToast() 26 | const { t } = useI18n() 27 | 28 | const handleSelectHistory = (historyItem: any) => { 29 | handleSelectHistoryBase(historyItem) 30 | showHistory.value = false 31 | } 32 | 33 | const handleClearHistory = () => { 34 | // 调用基础方法处理数据层面的清空 35 | handleClearHistoryBase() 36 | // 关闭历史记录抽屉 37 | showHistory.value = false 38 | } 39 | 40 | const handleDeleteChain = (chainId: string) => { 41 | // 调用基础方法处理数据层面的删除 42 | handleDeleteChainBase(chainId) 43 | // 不关闭历史记录抽屉,让用户继续查看其他记录 44 | } 45 | 46 | return { 47 | showHistory, 48 | handleSelectHistory, 49 | handleClearHistory, 50 | handleDeleteChain 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/ui/src/composables/useModals.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { useToast } from './useToast' 3 | import { useI18n } from 'vue-i18n' 4 | 5 | export function useModals( 6 | templateManager: any, 7 | optimizeModelSelect: any, 8 | testModelSelect: any, 9 | loadModels: () => Promise, 10 | initTemplateSelection: () => Promise 11 | ) { 12 | const toast = useToast() 13 | const { t } = useI18n() 14 | 15 | // 弹窗状态 16 | const showConfig = ref(false) 17 | const showHistory = ref(false) 18 | const showTemplates = ref(false) 19 | const currentType = ref('optimize') 20 | 21 | // 打开提示词管理器 22 | const openTemplateManager = (type = 'optimize') => { 23 | currentType.value = type 24 | showTemplates.value = true 25 | } 26 | 27 | // 加载提示词模板 28 | const loadTemplates = async () => { 29 | try { 30 | await templateManager.init() 31 | await initTemplateSelection() 32 | } catch (error) { 33 | console.error(t('toast.error.loadTemplatesFailed'), error) 34 | toast.error(t('toast.error.loadTemplatesFailed')) 35 | } 36 | } 37 | 38 | // 关闭提示词管理器 39 | const handleTemplateManagerClose = async () => { 40 | await loadTemplates() 41 | showTemplates.value = false 42 | } 43 | 44 | // 关闭模型管理器 45 | const handleModelManagerClose = async () => { 46 | await loadModels() 47 | optimizeModelSelect.value?.refresh() 48 | testModelSelect.value?.refresh() 49 | showConfig.value = false 50 | } 51 | 52 | return { 53 | showConfig, 54 | showHistory, 55 | showTemplates, 56 | currentType, 57 | openTemplateManager, 58 | handleTemplateManagerClose, 59 | handleModelManagerClose 60 | } 61 | } -------------------------------------------------------------------------------- /packages/ui/src/composables/useModelSelectors.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import type { Ref } from 'vue' 3 | 4 | export interface ModelSelectorsHooks { 5 | optimizeModelSelect: Ref 6 | testModelSelect: Ref 7 | } 8 | 9 | export function useModelSelectors(): ModelSelectorsHooks { 10 | const optimizeModelSelect = ref(null) 11 | const testModelSelect = ref(null) 12 | 13 | return { 14 | optimizeModelSelect, 15 | testModelSelect 16 | } 17 | } -------------------------------------------------------------------------------- /packages/ui/src/composables/usePromptHistory.ts: -------------------------------------------------------------------------------- 1 | import { ref, watch, type Ref, onMounted } from 'vue' 2 | import { useToast } from './useToast' 3 | import { useI18n } from 'vue-i18n' 4 | import { v4 as uuidv4 } from 'uuid' 5 | import type { IHistoryManager, PromptRecordChain, PromptRecord } from '@prompt-optimizer/core' 6 | 7 | type PromptChain = PromptRecordChain 8 | 9 | export function usePromptHistory( 10 | historyManager: IHistoryManager, 11 | prompt: Ref, 12 | optimizedPrompt: Ref, 13 | currentChainId: Ref, 14 | currentVersions: Ref, 15 | currentVersionId: Ref 16 | ) { 17 | const toast = useToast() 18 | const { t } = useI18n() 19 | const history = ref([]) 20 | const showHistory = ref(false) 21 | 22 | const handleSelectHistory = async (context: { record: any, chainId: string, rootPrompt: string }) => { 23 | const { record, rootPrompt } = context 24 | 25 | prompt.value = rootPrompt 26 | optimizedPrompt.value = record.optimizedPrompt 27 | 28 | const newRecord = await historyManager.createNewChain({ 29 | id: uuidv4(), 30 | originalPrompt: rootPrompt, 31 | optimizedPrompt: record.optimizedPrompt, 32 | type: 'optimize', 33 | modelKey: record.modelKey, 34 | templateId: record.templateId, 35 | timestamp: Date.now(), 36 | metadata: {} 37 | }) 38 | 39 | currentChainId.value = newRecord.chainId 40 | currentVersions.value = newRecord.versions 41 | currentVersionId.value = newRecord.currentRecord.id 42 | 43 | await refreshHistory() 44 | showHistory.value = false 45 | } 46 | 47 | const handleClearHistory = async () => { 48 | try { 49 | await historyManager.clearHistory() 50 | 51 | // 清空当前显示的内容 52 | prompt.value = ''; 53 | optimizedPrompt.value = ''; 54 | currentChainId.value = ''; 55 | currentVersions.value = []; 56 | currentVersionId.value = ''; 57 | 58 | // 立即更新历史记录,确保UI能够反映最新状态 59 | history.value = [] 60 | toast.success(t('toast.success.historyClear')) 61 | } catch (error) { 62 | console.error(t('toast.error.clearHistoryFailed'), error) 63 | toast.error(t('toast.error.clearHistoryFailed')) 64 | } 65 | } 66 | 67 | const handleDeleteChain = async (chainId: string) => { 68 | try { 69 | // 获取链中的所有记录 70 | const allChains = await historyManager.getAllChains() 71 | const chain = allChains.find((c: any) => c.chainId === chainId) 72 | 73 | if (chain) { 74 | // 删除链中的所有记录 75 | for (const record of chain.versions) { 76 | await historyManager.deleteRecord(record.id) 77 | } 78 | 79 | // 如果当前正在查看的是被删除的链,则清空当前显示 80 | if (currentChainId.value === chainId) { 81 | prompt.value = ''; 82 | optimizedPrompt.value = ''; 83 | currentChainId.value = ''; 84 | currentVersions.value = []; 85 | currentVersionId.value = ''; 86 | } 87 | 88 | // 立即更新历史记录,确保UI能够反映最新状态 89 | const updatedChains = await historyManager.getAllChains() 90 | history.value = [...updatedChains] 91 | toast.success(t('toast.success.historyChainDeleted')) 92 | } 93 | } catch (error) { 94 | console.error(t('toast.error.historyChainDeleteFailed'), error) 95 | toast.error(t('toast.error.historyChainDeleteFailed')) 96 | } 97 | } 98 | 99 | const initHistory = async () => { 100 | try { 101 | await refreshHistory() 102 | } catch (error) { 103 | console.error(t('toast.error.loadHistoryFailed'), error) 104 | toast.error(t('toast.error.loadHistoryFailed')) 105 | } 106 | } 107 | 108 | // 添加一个刷新历史记录的函数 109 | const refreshHistory = async () => { 110 | const chains = await historyManager.getAllChains() 111 | history.value = [...chains] 112 | } 113 | 114 | // Watch history display state 115 | watch(showHistory, async (newVal) => { 116 | if (newVal) { 117 | await refreshHistory() 118 | } 119 | }) 120 | 121 | // Watch version changes, update history 122 | watch([currentVersions], async () => { 123 | await refreshHistory() 124 | }) 125 | 126 | // 初始化时加载历史记录 127 | onMounted(async () => { 128 | await refreshHistory() 129 | }) 130 | 131 | return { 132 | history, 133 | showHistory, 134 | handleSelectHistory, 135 | handleClearHistory, 136 | handleDeleteChain, 137 | initHistory 138 | } 139 | } -------------------------------------------------------------------------------- /packages/ui/src/composables/usePromptTester.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | import { useToast } from './useToast' 3 | import { useI18n } from 'vue-i18n' 4 | import type { Ref } from 'vue' 5 | import type { IPromptService } from '@prompt-optimizer/core' 6 | 7 | export function usePromptTester( 8 | promptService: Ref, 9 | selectedTestModel: Ref 10 | ) { 11 | const toast = useToast() 12 | const { t } = useI18n() 13 | 14 | // States 15 | const testContent = ref('') 16 | const testResult = ref('') 17 | const testError = ref('') 18 | const isTesting = ref(false) 19 | 20 | // Test prompt 21 | const handleTest = async (optimizedPrompt: string) => { 22 | if (!promptService.value) { 23 | toast.error(t('toast.error.serviceInit')) 24 | return 25 | } 26 | 27 | if (!selectedTestModel.value || !testContent.value || !optimizedPrompt) { 28 | toast.error(t('toast.error.incompleteTestInfo')) 29 | return 30 | } 31 | 32 | isTesting.value = true 33 | testError.value = '' 34 | testResult.value = '' 35 | 36 | try { 37 | await promptService.value.testPromptStream( 38 | optimizedPrompt, 39 | testContent.value, 40 | selectedTestModel.value, 41 | { 42 | onToken: (token: string) => { 43 | testResult.value += token 44 | }, 45 | onComplete: () => { 46 | isTesting.value = false 47 | }, 48 | onError: (error: Error) => { 49 | testError.value = error.message || t('toast.error.testFailed') 50 | isTesting.value = false 51 | } 52 | } 53 | ) 54 | } catch (error: any) { 55 | console.error(t('toast.error.testFailed'), error) 56 | testError.value = error.message || t('toast.error.testProcessError') 57 | } finally { 58 | isTesting.value = false 59 | } 60 | } 61 | 62 | return { 63 | // States 64 | testContent, 65 | testResult, 66 | testError, 67 | isTesting, 68 | 69 | // Methods 70 | handleTest 71 | } 72 | } -------------------------------------------------------------------------------- /packages/ui/src/composables/useServiceInitializer.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted } from 'vue' 2 | import { useToast } from './useToast' 3 | import { useI18n } from 'vue-i18n' 4 | import type { ModelManager, TemplateManager, HistoryManager, PromptService } from '@prompt-optimizer/core' 5 | import { createLLMService, createPromptService } from '@prompt-optimizer/core' 6 | 7 | export function useServiceInitializer( 8 | modelManager: ModelManager, 9 | templateManager: TemplateManager, 10 | historyManager: HistoryManager 11 | ) { 12 | const toast = useToast() 13 | const { t } = useI18n() 14 | const promptServiceRef = ref(null) 15 | const llmService = createLLMService(modelManager) 16 | 17 | // Initialize base services 18 | const initBaseServices = () => { 19 | try { 20 | console.log(t('log.info.initBaseServicesStart')) 21 | 22 | // Get and verify template list 23 | const templates = templateManager.listTemplates() 24 | console.log(t('log.info.templateList'), templates) 25 | 26 | // Create prompt service 27 | console.log(t('log.info.createPromptService')) 28 | promptServiceRef.value = createPromptService(modelManager, llmService, templateManager, historyManager) 29 | 30 | console.log(t('log.info.initComplete')) 31 | } catch (error) { 32 | console.error(t('log.error.initBaseServicesFailed'), error) 33 | toast.error(t('toast.error.initFailed', { error: error instanceof Error ? error.message : String(error) })) 34 | throw error 35 | } 36 | } 37 | 38 | // Auto initialize on mounted 39 | onMounted(() => { 40 | initBaseServices() 41 | }) 42 | 43 | return { 44 | promptServiceRef, 45 | initBaseServices 46 | } 47 | } -------------------------------------------------------------------------------- /packages/ui/src/composables/useStorage.ts: -------------------------------------------------------------------------------- 1 | import { StorageFactory } from '@prompt-optimizer/core' 2 | 3 | /** 4 | * 统一存储服务Hook 5 | * 使用core提供的存储抽象,支持localStorage和Dexie 6 | */ 7 | export function useStorage() { 8 | // 获取默认存储提供器(单例) 9 | const storage = StorageFactory.createDefault() 10 | 11 | /** 12 | * 获取存储项 13 | * @param key 存储键 14 | * @returns 存储值或null 15 | */ 16 | const getItem = async (key: string): Promise => { 17 | try { 18 | return await storage.getItem(key) 19 | } catch (error) { 20 | console.error(`获取存储项失败 (${key}):`, error) 21 | return null 22 | } 23 | } 24 | 25 | /** 26 | * 设置存储项 27 | * @param key 存储键 28 | * @param value 存储值 29 | */ 30 | const setItem = async (key: string, value: string): Promise => { 31 | try { 32 | await storage.setItem(key, value) 33 | } catch (error) { 34 | console.error(`设置存储项失败 (${key}):`, error) 35 | throw error 36 | } 37 | } 38 | 39 | /** 40 | * 删除存储项 41 | * @param key 存储键 42 | */ 43 | const removeItem = async (key: string): Promise => { 44 | try { 45 | await storage.removeItem(key) 46 | } catch (error) { 47 | console.error(`删除存储项失败 (${key}):`, error) 48 | throw error 49 | } 50 | } 51 | 52 | /** 53 | * 获取JSON格式的存储项 54 | * @param key 存储键 55 | * @returns 解析后的对象或null 56 | */ 57 | const getItemJSON = async (key: string): Promise => { 58 | try { 59 | const value = await getItem(key) 60 | return value ? JSON.parse(value) : null 61 | } catch (error) { 62 | console.error(`获取JSON存储项失败 (${key}):`, error) 63 | return null 64 | } 65 | } 66 | 67 | /** 68 | * 设置JSON格式的存储项 69 | * @param key 存储键 70 | * @param value 要序列化的对象 71 | */ 72 | const setItemJSON = async (key: string, value: any): Promise => { 73 | try { 74 | await setItem(key, JSON.stringify(value)) 75 | } catch (error) { 76 | console.error(`设置JSON存储项失败 (${key}):`, error) 77 | throw error 78 | } 79 | } 80 | 81 | /** 82 | * 批量操作 83 | * @param operations 操作列表 84 | */ 85 | const batchUpdate = async (operations: Array<{ 86 | key: string; 87 | operation: 'set' | 'remove'; 88 | value?: string; 89 | }>): Promise => { 90 | try { 91 | if ('batchUpdate' in storage) { 92 | await storage.batchUpdate(operations) 93 | } else { 94 | // 降级到单个操作 95 | for (const op of operations) { 96 | if (op.operation === 'set' && op.value !== undefined) { 97 | await setItem(op.key, op.value) 98 | } else if (op.operation === 'remove') { 99 | await removeItem(op.key) 100 | } 101 | } 102 | } 103 | } catch (error) { 104 | console.error('批量更新失败:', error) 105 | throw error 106 | } 107 | } 108 | 109 | return { 110 | getItem, 111 | setItem, 112 | removeItem, 113 | getItemJSON, 114 | setItemJSON, 115 | batchUpdate 116 | } 117 | } -------------------------------------------------------------------------------- /packages/ui/src/composables/useToast.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | interface Toast { 4 | id: number 5 | message: string 6 | type: 'success' | 'error' | 'info' | 'warning' 7 | } 8 | 9 | const toasts = ref([]) 10 | 11 | export function useToast() { 12 | const add = (message: string, type: Toast['type'] = 'info', duration: number = 3000) => { 13 | const id = Date.now() 14 | toasts.value.push({ id, message, type }) 15 | 16 | setTimeout(() => { 17 | remove(id) 18 | }, duration) 19 | } 20 | 21 | const remove = (id: number) => { 22 | const index = toasts.value.findIndex(toast => toast.id === id) 23 | if (index > -1) { 24 | toasts.value.splice(index, 1) 25 | } 26 | } 27 | 28 | const success = (message: string, duration?: number) => add(message, 'success', duration) 29 | const error = (message: string, duration?: number) => add(message, 'error', duration) 30 | const info = (message: string, duration?: number) => add(message, 'info', duration) 31 | const warning = (message: string, duration?: number) => add(message, 'warning', duration) 32 | 33 | return { 34 | toasts, 35 | add, 36 | remove, 37 | success, 38 | error, 39 | info, 40 | warning 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/ui/src/directives/clickOutside.ts: -------------------------------------------------------------------------------- 1 | interface ExtendedHTMLElement extends HTMLElement { 2 | _clickOutside?: (event: Event) => void 3 | } 4 | 5 | export const clickOutside = { 6 | mounted(el: ExtendedHTMLElement, binding: { value: () => void }) { 7 | el._clickOutside = (event: Event) => { 8 | if (!(el === event.target || el.contains(event.target as Node))) { 9 | binding.value() 10 | } 11 | } 12 | document.addEventListener('click', el._clickOutside) 13 | }, 14 | unmounted(el: ExtendedHTMLElement) { 15 | if (el._clickOutside) { 16 | document.removeEventListener('click', el._clickOutside) 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /packages/ui/src/examples/storage-usage-examples.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/src/i18n/README.md: -------------------------------------------------------------------------------- 1 | # 国际化(i18n)规范指南 2 | 3 | ## 翻译键名规范 4 | 5 | 为了保持翻译文件的一致性和可维护性,请遵循以下键名规范: 6 | 7 | ### 1. 命名结构 8 | 9 | 使用嵌套对象结构,按照以下层次组织翻译键: 10 | 11 | ``` 12 | { 13 | "模块名": { 14 | "子模块或功能": { 15 | "具体文本": "翻译内容" 16 | } 17 | } 18 | } 19 | ``` 20 | 21 | ### 2. 模块划分 22 | 23 | - `common`: 通用文本,如按钮文本、常见操作等 24 | - 具体功能模块: 如 `promptOptimizer`, `settings`, `modelManager` 等 25 | 26 | ### 3. 参数化文本 27 | 28 | 对于包含变量的文本,使用花括号标记参数: 29 | 30 | ```typescript 31 | // 定义 32 | "version": "V{version}" 33 | 34 | // 使用 35 | t('common.version', { version: '1.0.0' }) 36 | ``` 37 | 38 | ### 4. 示例结构 39 | 40 | ```typescript 41 | export default { 42 | // 通用文本 43 | common: { 44 | buttons: { 45 | save: '保存', 46 | cancel: '取消', 47 | confirm: '确认', 48 | }, 49 | labels: { 50 | createdAt: '创建于', 51 | lastModified: '最后修改', 52 | }, 53 | messages: { 54 | loading: '加载中...', 55 | noData: '暂无数据', 56 | }, 57 | }, 58 | 59 | // 功能模块 60 | promptOptimizer: { 61 | title: '提示词优化器', 62 | form: { 63 | inputPlaceholder: '请输入需要优化的prompt...', 64 | templateLabel: '优化提示词', 65 | }, 66 | actions: { 67 | optimize: '开始优化 →', 68 | save: '保存提示词', 69 | share: '分享', 70 | }, 71 | }, 72 | 73 | // 设置模块 74 | settings: { 75 | title: '设置', 76 | sections: { 77 | language: '语言设置', 78 | theme: '主题设置', 79 | api: 'API设置', 80 | }, 81 | }, 82 | } 83 | ``` 84 | 85 | ## 最佳实践 86 | 87 | 1. **保持一致性**: 同类型的文本应使用相同的键名结构 88 | 2. **避免重复**: 通用文本应放在 `common` 下,避免在多个模块中重复定义 89 | 3. **描述性键名**: 键名应清晰描述文本的用途,而不是直接使用翻译内容 90 | 4. **模块化**: 按功能模块组织翻译,便于维护和查找 91 | 5. **注释**: 对于复杂或特殊用途的文本,添加注释说明 92 | 93 | ## 添加新语言 94 | 95 | 添加新语言时,请确保: 96 | 97 | 1. 在 `locales` 目录下创建对应的语言文件,如 `ja-JP.ts` 98 | 2. 复制现有语言文件的结构,确保键名完全一致 99 | 3. 更新语言切换组件,添加新语言选项 100 | 4. 测试所有页面在新语言下的显示效果 -------------------------------------------------------------------------------- /packages/ui/src/index.ts: -------------------------------------------------------------------------------- 1 | // 导入样式 2 | import 'element-plus/dist/index.css' 3 | import './styles/index.css' 4 | import './styles/scrollbar.css' 5 | import './styles/common.css' 6 | import './styles/theme.css' 7 | 8 | // 导出插件 9 | export { installI18n, i18n } from './plugins/i18n' 10 | 11 | /** 12 | * 组件导出 13 | * 注意:所有组件导出时都添加了UI后缀,以便与其他库的组件区分 14 | * 例如:Toast.vue 导出为 ToastUI 15 | */ 16 | // Components 17 | export { default as ToastUI } from './components/Toast.vue' 18 | export { default as ModelManagerUI } from './components/ModelManager.vue' 19 | export { default as OutputPanelUI } from './components/OutputPanel.vue' 20 | export { default as PromptPanelUI } from './components/PromptPanel.vue' 21 | export { default as TemplateManagerUI } from './components/TemplateManager.vue' 22 | export { default as TemplateSelectUI } from './components/TemplateSelect.vue' 23 | export { default as ModelSelectUI } from './components/ModelSelect.vue' 24 | export { default as HistoryDrawerUI } from './components/HistoryDrawer.vue' 25 | export { default as InputPanelUI } from './components/InputPanel.vue' 26 | export { default as MainLayoutUI } from './components/MainLayout.vue' 27 | export { default as ContentCardUI } from './components/ContentCard.vue' 28 | export { default as ActionButtonUI } from './components/ActionButton.vue' 29 | export { default as ThemeToggleUI } from './components/ThemeToggleUI.vue' 30 | export { default as TestPanelUI } from './components/TestPanel.vue' 31 | export { default as LanguageSwitchUI } from './components/LanguageSwitch.vue' 32 | export { default as DataManagerUI } from './components/DataManager.vue' 33 | 34 | // 导出指令 35 | export { clickOutside } from './directives/clickOutside' 36 | 37 | // 导出 composables 38 | export * from './composables' 39 | 40 | // 从core重新导出需要的内容 41 | export { 42 | templateManager, 43 | modelManager, 44 | historyManager, 45 | dataManager, 46 | createLLMService, 47 | createPromptService 48 | } from '@prompt-optimizer/core' 49 | -------------------------------------------------------------------------------- /packages/ui/src/plugins/i18n.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | import type { App } from 'vue' 3 | import zhCN from '../i18n/locales/zh-CN' 4 | import enUS from '../i18n/locales/en-US' 5 | import { StorageFactory } from '@prompt-optimizer/core' 6 | 7 | // 定义支持的语言类型 8 | type SupportedLocale = 'zh-CN' | 'en-US' 9 | const SUPPORTED_LOCALES: SupportedLocale[] = ['zh-CN', 'en-US'] 10 | 11 | // 创建i18n实例 12 | const i18n = createI18n({ 13 | legacy: false, 14 | locale: 'zh-CN' as SupportedLocale, 15 | fallbackLocale: { 16 | 'zh-CN': ['en-US'], 17 | 'default': ['en-US'] 18 | }, 19 | messages: { 20 | 'zh-CN': zhCN, 21 | 'en-US': enUS, 22 | } 23 | }) 24 | 25 | // 初始化语言设置 26 | async function initializeLanguage() { 27 | try { 28 | const storage = StorageFactory.createDefault() 29 | const savedLanguage = await storage.getItem('preferred-language') 30 | 31 | // 优先使用保存的语言设置 32 | if (savedLanguage && SUPPORTED_LOCALES.includes(savedLanguage as SupportedLocale)) { 33 | i18n.global.locale.value = savedLanguage as SupportedLocale 34 | return 35 | } 36 | 37 | // 其次使用浏览器语言设置 38 | const defaultLocale: SupportedLocale = navigator.language.startsWith('zh') ? 'zh-CN' : 'en-US' 39 | i18n.global.locale.value = defaultLocale 40 | await storage.setItem('preferred-language', defaultLocale) 41 | } catch (error) { 42 | console.error('初始化语言设置失败:', error) 43 | // 降级到默认语言 44 | i18n.global.locale.value = 'zh-CN' 45 | } 46 | } 47 | 48 | // 导出插件安装函数 49 | export function installI18n(app: App) { 50 | initializeLanguage() // 异步初始化,不阻塞应用启动 51 | app.use(i18n) 52 | } 53 | 54 | // 导出i18n实例 55 | export { i18n } -------------------------------------------------------------------------------- /packages/ui/src/styles/common.css: -------------------------------------------------------------------------------- 1 | /* 自定义选择器样式 */ 2 | .custom-select { 3 | -webkit-appearance: none !important; 4 | -moz-appearance: none !important; 5 | appearance: none !important; 6 | background-image: none !important; 7 | } 8 | 9 | .custom-select::-ms-expand { 10 | display: none; 11 | } -------------------------------------------------------------------------------- /packages/ui/src/styles/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* 自定义滚动条样式 */ 6 | ::-webkit-scrollbar { 7 | width: 4px; 8 | } 9 | 10 | ::-webkit-scrollbar-track { 11 | background: transparent; 12 | } 13 | 14 | ::-webkit-scrollbar-thumb { 15 | background: rgba(255, 255, 255, 0.2); 16 | border-radius: 2px; 17 | } 18 | 19 | ::-webkit-scrollbar-thumb:hover { 20 | background: rgba(255, 255, 255, 0.3); 21 | } 22 | 23 | /* 隐藏水平滚动条 */ 24 | ::-webkit-scrollbar-horizontal { 25 | display: none; 26 | } 27 | 28 | /* 自定义select样式 */ 29 | .custom-select { 30 | -webkit-appearance: none !important; 31 | -moz-appearance: none !important; 32 | appearance: none !important; 33 | background-image: none !important; 34 | } 35 | 36 | .custom-select::-ms-expand { 37 | display: none; 38 | } -------------------------------------------------------------------------------- /packages/ui/src/styles/scrollbar.css: -------------------------------------------------------------------------------- 1 | /* 自定义滚动条样式 */ 2 | .custom-select { 3 | -webkit-appearance: none !important; 4 | -moz-appearance: none !important; 5 | appearance: none !important; 6 | background-image: none !important; 7 | } 8 | 9 | .custom-select::-ms-expand { 10 | display: none; 11 | } 12 | 13 | /* 优化滚动条样式 */ 14 | ::-webkit-scrollbar { 15 | width: 4px; 16 | } 17 | 18 | ::-webkit-scrollbar-track { 19 | background: transparent; 20 | } 21 | 22 | ::-webkit-scrollbar-thumb { 23 | background: rgba(255, 255, 255, 0.2); 24 | border-radius: 2px; 25 | } 26 | 27 | ::-webkit-scrollbar-thumb:hover { 28 | background: rgba(255, 255, 255, 0.3); 29 | } 30 | 31 | /* 隐藏水平滚动条 */ 32 | ::-webkit-scrollbar-horizontal { 33 | display: none; 34 | } -------------------------------------------------------------------------------- /packages/ui/src/types/images.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg' { 2 | const content: string 3 | export default content 4 | } 5 | 6 | declare module '*.jpeg' { 7 | const content: string 8 | export default content 9 | } 10 | 11 | declare module '*.png' { 12 | const content: string 13 | export default content 14 | } 15 | 16 | declare module '*.svg' { 17 | const content: string 18 | export default content 19 | } -------------------------------------------------------------------------------- /packages/ui/src/utils/error.ts: -------------------------------------------------------------------------------- 1 | import { useToast } from '../composables/useToast' 2 | 3 | export class AppError extends Error { 4 | constructor( 5 | message: string, 6 | public code: string = 'UNKNOWN_ERROR', 7 | public details?: Record 8 | ) { 9 | super(message) 10 | this.name = 'AppError' 11 | } 12 | } 13 | 14 | export function createErrorHandler(context: string) { 15 | const toast = useToast() 16 | 17 | return { 18 | handleError(error: unknown) { 19 | console.error(`[${context}]错误:`, error) 20 | 21 | if (error instanceof AppError) { 22 | toast.error(error.message) 23 | return 24 | } 25 | 26 | if (error instanceof Error) { 27 | toast.error(error.message || `${context}过程中发生错误`) 28 | return 29 | } 30 | 31 | toast.error(`${context}过程中发生未知错误`) 32 | } 33 | } 34 | } 35 | 36 | export const errorMessages = { 37 | SERVICE_NOT_INITIALIZED: '服务未初始化,请稍后重试', 38 | TEMPLATE_NOT_SELECTED: '请先选择提示词模板', 39 | INCOMPLETE_TEST_INFO: '请填写完整的测试信息', 40 | LOAD_TEMPLATE_FAILED: '加载提示词失败', 41 | CLEAR_HISTORY_FAILED: '清空历史记录失败' 42 | } as const -------------------------------------------------------------------------------- /packages/ui/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | ], 7 | darkMode: 'class', // 启用基于类的暗黑模式 8 | theme: { 9 | extend: { 10 | colors: { 11 | primary: 'var(--color-primary)', 12 | secondary: 'var(--color-secondary)', 13 | background: 'var(--color-background)', 14 | card: 'var(--color-card)', 15 | text: 'var(--color-text)', 16 | border: 'var(--color-border)', 17 | }, 18 | }, 19 | }, 20 | plugins: [ 21 | require('@tailwindcss/forms'), 22 | require('@tailwindcss/typography'), 23 | function({ addVariant, e }) { 24 | // 添加自定义主题变体 25 | addVariant('theme-blue', ['.theme-blue &', ':root.theme-blue &']) 26 | addVariant('theme-green', ['.theme-green &', ':root.theme-green &']) 27 | addVariant('theme-purple', ['.theme-purple &', ':root.theme-purple &']) 28 | } 29 | ], 30 | } -------------------------------------------------------------------------------- /packages/ui/tests/setup.js: -------------------------------------------------------------------------------- 1 | import { vi } from 'vitest' 2 | import { config } from '@vue/test-utils' 3 | 4 | // 模拟 window 对象 5 | const windowMock = { 6 | localStorage: { 7 | store: new Map(), 8 | getItem: vi.fn((key) => { 9 | return windowMock.localStorage.store.get(key) || null; 10 | }), 11 | setItem: vi.fn((key, value) => { 12 | windowMock.localStorage.store.set(key, value); 13 | }), 14 | removeItem: vi.fn((key) => { 15 | windowMock.localStorage.store.delete(key); 16 | }), 17 | clear: vi.fn(() => { 18 | windowMock.localStorage.store.clear(); 19 | }) 20 | } 21 | }; 22 | 23 | // 全局注入 window mock 24 | Object.defineProperty(global, 'window', { 25 | value: windowMock, 26 | writable: true, 27 | configurable: true 28 | }); 29 | 30 | // 全局注入 localStorage 31 | Object.defineProperty(global, 'localStorage', { 32 | value: windowMock.localStorage, 33 | writable: true, 34 | configurable: true 35 | }); 36 | 37 | // 模拟 Teleport 组件 38 | config.global.stubs = { 39 | Teleport: { 40 | template: '
' 41 | } 42 | } 43 | 44 | // 在每个测试之前重置 mock 状态 45 | beforeEach(() => { 46 | windowMock.localStorage.store.clear(); 47 | vi.clearAllMocks(); 48 | }); -------------------------------------------------------------------------------- /packages/ui/tests/unit/components.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach } from 'vitest' 2 | import { mount } from '@vue/test-utils' 3 | import { createI18n } from 'vue-i18n' 4 | import { ActionButtonUI, ContentCardUI } from '../../src' 5 | 6 | // 创建i18n实例(Vue 3不需要createLocalVue) 7 | const i18n = createI18n({ 8 | legacy: false, 9 | locale: 'zh-CN', 10 | messages: {} 11 | }) 12 | 13 | describe('基础UI组件测试', () => { 14 | describe('ActionButtonUI', () => { 15 | it('应该正确渲染按钮文本', () => { 16 | const buttonText = '测试按钮' 17 | const wrapper = mount(ActionButtonUI, { 18 | global: { 19 | plugins: [i18n] // 直接使用i18n插件 20 | }, 21 | props: { 22 | text: buttonText, 23 | icon: '🔄' 24 | } 25 | }) 26 | expect(wrapper.text()).toContain(buttonText) 27 | }) 28 | 29 | it('应该正确处理loading状态', async () => { 30 | const wrapper = mount(ActionButtonUI, { 31 | global: { 32 | plugins: [i18n] // 添加i18n插件 33 | }, 34 | props: { 35 | text: '测试按钮', 36 | icon: '🔄', 37 | loading: false 38 | } 39 | }) 40 | 41 | // 初始状态不是loading 42 | expect(wrapper.props('loading')).toBe(false) 43 | 44 | // 修改为loading状态 45 | await wrapper.setProps({ loading: true }) 46 | expect(wrapper.props('loading')).toBe(true) 47 | }) 48 | }) 49 | 50 | describe('ContentCardUI', () => { 51 | it('应该正确渲染slot内容', () => { 52 | const slotContent = '测试内容' 53 | const wrapper = mount(ContentCardUI, { 54 | slots: { 55 | default: slotContent 56 | } 57 | }) 58 | expect(wrapper.text()).toContain(slotContent) 59 | }) 60 | }) 61 | }) -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } -------------------------------------------------------------------------------- /packages/ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | import path from 'path' 5 | 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | resolve: { 9 | alias: { 10 | '@ui': path.resolve(__dirname, '../ui') 11 | } 12 | }, 13 | build: { 14 | lib: { 15 | entry: resolve(__dirname, 'src/index.ts'), 16 | name: 'PromptOptimizerUI', 17 | fileName: (format) => `index.${format === 'es' ? 'js' : 'cjs'}`, 18 | formats: ['es', 'cjs'] 19 | }, 20 | watch: process.env.NODE_ENV === 'development' ? { 21 | // 更精确的监听配置 22 | include: ['src/**/*'], 23 | buildDelay: 100 24 | } : null, 25 | sourcemap: true, 26 | rollupOptions: { 27 | external: ['vue', '@prompt-optimizer/core', 'element-plus', 'element-plus/dist/index.css', 'uuid'], 28 | output: { 29 | globals: { 30 | vue: 'Vue', 31 | '@prompt-optimizer/core': 'PromptOptimizerCore', 32 | 'element-plus': 'ElementPlus', 33 | 'uuid': 'uuid' 34 | }, 35 | assetFileNames: 'style.css' 36 | } 37 | }, 38 | cssCodeSplit: false, 39 | emptyOutDir: false 40 | }, 41 | assetsInclude: ['**/*.jpg', '**/*.jpeg', '**/*.png', '**/*.svg'] 42 | }) -------------------------------------------------------------------------------- /packages/ui/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | import vue from '@vitejs/plugin-vue' 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | test: { 8 | // 全局超时设置为5秒 9 | testTimeout: 5000, 10 | // 环境设置 11 | environment: 'jsdom', 12 | // 包含的文件模式 13 | include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 14 | // 排除的文件模式 15 | exclude: ['**/node_modules/**', '**/dist/**', '**/.{idea,git,cache,output,temp}/**'], 16 | // 全局测试设置 17 | globals: true, 18 | // 测试覆盖率配置 19 | coverage: { 20 | provider: 'v8', 21 | reporter: ['text', 'json', 'html'], 22 | exclude: [ 23 | 'coverage/**', 24 | 'dist/**', 25 | '**/[.]**', 26 | 'packages/*/test?(s)/**', 27 | '**/*.d.ts', 28 | '**/virtual:*', 29 | '**/__x00__*', 30 | '**/\x00*', 31 | 'cypress/**', 32 | ], 33 | }, 34 | }, 35 | }) -------------------------------------------------------------------------------- /packages/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 提示词优化器 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@prompt-optimizer/web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "test": "vitest", 11 | "test:ui": "vitest --ui", 12 | "test:coverage": "vitest run --coverage", 13 | "test:unit": "vitest run tests/unit" 14 | }, 15 | "dependencies": { 16 | "@prompt-optimizer/ui": "workspace:*", 17 | "element-plus": "^2.9.3", 18 | "uuid": "^11.0.5", 19 | "vue": "^3.5.13" 20 | }, 21 | "devDependencies": { 22 | "@pinia/testing": "^0.1.7", 23 | "@tailwindcss/forms": "^0.5.10", 24 | "@tailwindcss/typography": "^0.5.16", 25 | "@vitejs/plugin-basic-ssl": "^1.2.0", 26 | "@vitejs/plugin-vue": "^5.2.1", 27 | "@vue/test-utils": "^2.4.6", 28 | "autoprefixer": "^10.4.20", 29 | "jsdom": "^26.0.0", 30 | "postcss": "^8.5.1", 31 | "tailwindcss": "^3.4.17", 32 | "vite": "^6.0.7", 33 | "vitest": "^3.0.2", 34 | "dotenv": "^16.4.7", 35 | "js-yaml": "^4.1.0" 36 | } 37 | } -------------------------------------------------------------------------------- /packages/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /packages/web/public/config.js: -------------------------------------------------------------------------------- 1 | // 默认空配置文件 2 | // 在docker环境下会被替换为实际的环境配置文件 3 | window.runtime_config = {}; 4 | console.log("使用默认配置"); -------------------------------------------------------------------------------- /packages/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linshenkx/prompt-optimizer/76438cab16bcbc0a357e319658562c2025cfd2d6/packages/web/public/favicon.ico -------------------------------------------------------------------------------- /packages/web/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import { installI18n } from '@prompt-optimizer/ui' 3 | import App from './App.vue' 4 | 5 | import '@prompt-optimizer/ui/dist/style.css' 6 | 7 | const app = createApp(App) 8 | installI18n(app) 9 | app.mount('#app') 10 | 11 | // 只在Vercel环境中加载Analytics 12 | // 当环境变量VITE_VERCEL_DEPLOYMENT为true时才尝试加载 13 | if (import.meta.env.VITE_VERCEL_DEPLOYMENT === 'true') { 14 | // 使用完全运行时方式加载Vercel Analytics 15 | const loadAnalytics = () => { 16 | const script = document.createElement('script') 17 | script.src = '/_vercel/insights/script.js' 18 | script.defer = true 19 | script.onload = () => console.log('Vercel Analytics 已加载') 20 | script.onerror = () => console.log('Vercel Analytics 加载失败') 21 | document.head.appendChild(script) 22 | } 23 | 24 | // 延迟执行以确保DOM已完全加载 25 | window.addEventListener('DOMContentLoaded', loadAnalytics) 26 | }else{ 27 | console.log('Vercel Analytics 未加载') 28 | } -------------------------------------------------------------------------------- /packages/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{vue,js,ts,jsx,tsx}", 6 | "../../node_modules/@prompt-optimizer/ui/src/**/*.{vue,js,ts,jsx,tsx}" 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [ 12 | require('@tailwindcss/forms'), 13 | require('@tailwindcss/typography') 14 | ], 15 | } -------------------------------------------------------------------------------- /packages/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "moduleResolution": "bundler", 9 | "allowImportingTsExtensions": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "noEmit": true, 13 | "jsx": "preserve", 14 | "strict": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "baseUrl": ".", 19 | "paths": { 20 | "@prompt-optimizer/ui": ["../ui/src"] 21 | }, 22 | "types": ["vite/client", "vue"] 23 | }, 24 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "tsconfig.node.json"] 25 | } -------------------------------------------------------------------------------- /packages/web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } -------------------------------------------------------------------------------- /packages/web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import { resolve } from 'path' 4 | import path from 'path' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig(({ mode }) => { 8 | // 加载环境变量(从项目根目录加载) 9 | const env = loadEnv(mode, resolve(process.cwd(), '../../')) 10 | 11 | return { 12 | plugins: [vue()], 13 | server: { 14 | port: 18181, 15 | host: true, 16 | fs: { 17 | // 允许为工作区依赖提供服务 18 | allow: ['..'] 19 | }, 20 | hmr: true, 21 | watch: { 22 | // 确保监视monorepo中其他包的变化 23 | ignored: ['!**/node_modules/@prompt-optimizer/**'] 24 | } 25 | }, 26 | build: { 27 | rollupOptions: { 28 | input: { 29 | main: resolve(__dirname, 'index.html') 30 | } 31 | } 32 | }, 33 | publicDir: 'public', 34 | resolve: { 35 | preserveSymlinks: true, 36 | alias: { 37 | '@': resolve(__dirname, 'src'), 38 | '@prompt-optimizer/core': path.resolve(__dirname, '../core'), 39 | '@prompt-optimizer/ui': path.resolve(__dirname, '../ui'), 40 | '@prompt-optimizer/web': path.resolve(__dirname, '../web'), 41 | '@prompt-optimizer/extension': path.resolve(__dirname, '../extension') 42 | } 43 | }, 44 | optimizeDeps: { 45 | // 预构建依赖 46 | include: ['element-plus'], 47 | }, 48 | define: { 49 | 'process.env': { 50 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'), 51 | ...Object.keys(env).reduce((acc, key) => { 52 | acc[key] = env[key]; 53 | return acc; 54 | }, {}) 55 | } 56 | } 57 | } 58 | }) -------------------------------------------------------------------------------- /packages/web/vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vitest/config' 3 | import vue from '@vitejs/plugin-vue' 4 | 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | test: { 8 | // 全局超时设置为5秒 9 | testTimeout: 5000, 10 | // 环境设置 11 | environment: 'jsdom', 12 | // 包含的文件模式 13 | include: ['tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], 14 | // 排除的文件模式 15 | exclude: ['**/node_modules/**', '**/dist/**', '**/.{idea,git,cache,output,temp}/**'], 16 | // 全局测试设置 17 | globals: true, 18 | // 测试覆盖率配置 19 | coverage: { 20 | provider: 'v8', 21 | reporter: ['text', 'json', 'html'], 22 | exclude: [ 23 | 'coverage/**', 24 | 'dist/**', 25 | '**/[.]**', 26 | 'packages/*/test?(s)/**', 27 | '**/*.d.ts', 28 | '**/virtual:*', 29 | '**/__x00__*', 30 | '**/\x00*', 31 | 'cypress/**', 32 | ], 33 | }, 34 | }, 35 | }) -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'packages/*' -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "if [[ $(pwd) == */packages/extension ]]; then cd ../.. && pnpm build && mkdir -p packages/extension/packages/web && cp -r packages/web/dist packages/extension/packages/web/dist; else pnpm build; fi", 3 | "outputDirectory": "packages/web/dist", 4 | "installCommand": "pwd && if [[ $(pwd) == */packages/extension ]]; then cd ../.. && pnpm install && pnpm i @vercel/analytics -w; else pnpm install && pnpm i @vercel/analytics -w; fi", 5 | "rewrites": [ 6 | { 7 | "source": "/api/:path*", 8 | "destination": "/api/:path*" 9 | }, 10 | { 11 | "source": "/(.*)", 12 | "destination": "/index.html" 13 | } 14 | ], 15 | "github": { 16 | "silent": true 17 | }, 18 | "env": { 19 | "VITE_VERCEL_DEPLOYMENT": "true" 20 | }, 21 | "build": { 22 | "env": { 23 | "VITE_VERCEL_DEPLOYMENT": "true" 24 | } 25 | } 26 | } 27 | --------------------------------------------------------------------------------