Compare commits
138 Commits
hlink/1600
...
feat/qwen3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d618cdf19e | ||
|
|
9120136f45 | ||
|
|
8747974359 | ||
|
|
ecd7518505 | ||
|
|
69e9b9855e | ||
|
|
08ee877676 | ||
|
|
6ee8a72823 | ||
|
|
b0d6f209d7 | ||
|
|
1c5526c020 | ||
|
|
f29b83faab | ||
|
|
57e8b9a592 | ||
|
|
0782b24790 | ||
|
|
0a7bf99f9c | ||
|
|
7be6ddfb59 | ||
|
|
0413884021 | ||
|
|
4225d20760 | ||
|
|
efad8f9ad0 | ||
|
|
a6822d4037 | ||
|
|
8423fb5610 | ||
|
|
350c2735e4 | ||
|
|
f844a7e024 | ||
|
|
74e7c6a327 | ||
|
|
8c05b4f067 | ||
|
|
449d74040f | ||
|
|
712bd11274 | ||
|
|
cc89f5330a | ||
|
|
db7a7e2c2b | ||
|
|
d64ae18bfc | ||
|
|
9f2ac4aa81 | ||
|
|
e227fbf821 | ||
|
|
6595dc9a1e | ||
|
|
d16235d916 | ||
|
|
f5a7258229 | ||
|
|
dfd957434c | ||
|
|
2e0d315ce4 | ||
|
|
47bde9eb36 | ||
|
|
506af6cfb9 | ||
|
|
4e07faf520 | ||
|
|
381377c0eb | ||
|
|
24f59047a5 | ||
|
|
e1a97ccd50 | ||
|
|
f2929d44d8 | ||
|
|
bb02dca7e9 | ||
|
|
54cb4d7ac1 | ||
|
|
b76a609b97 | ||
|
|
9cea0166e6 | ||
|
|
e5e04c8132 | ||
|
|
6b113c19a3 | ||
|
|
bcbb3f294e | ||
|
|
e17765f1bf | ||
|
|
3064b7a3e3 | ||
|
|
da72a5706f | ||
|
|
261eeb097a | ||
|
|
bf17e71445 | ||
|
|
0744e42be9 | ||
|
|
2833c377fa | ||
|
|
ad5769f6b7 | ||
|
|
a6a4a32159 | ||
|
|
a112b143e7 | ||
|
|
a7ee3cbd02 | ||
|
|
32f160444b | ||
|
|
a546c265ee | ||
|
|
ea89a37b1d | ||
|
|
c288b4a8d0 | ||
|
|
e8384db91a | ||
|
|
36c87451d9 | ||
|
|
eaa37fe674 | ||
|
|
308ad9f68f | ||
|
|
62b6584d65 | ||
|
|
5a44f6aca8 | ||
|
|
4c6a904929 | ||
|
|
be4ef2990f | ||
|
|
2807e71f1a | ||
|
|
a5f8ac8587 | ||
|
|
4bd50251ff | ||
|
|
f0d60052c4 | ||
|
|
84f4b565f3 | ||
|
|
ebdacdde3e | ||
|
|
aeb66195a0 | ||
|
|
53ef8b0f32 | ||
|
|
794c23f296 | ||
|
|
62440cbfa1 | ||
|
|
39b723f143 | ||
|
|
bdc75f2f4e | ||
|
|
eb3f136997 | ||
|
|
6ba5768650 | ||
|
|
1f588d242e | ||
|
|
25b1e309ed | ||
|
|
7a7b24fe2f | ||
|
|
0686b2d813 | ||
|
|
4a027892b9 | ||
|
|
be323b6304 | ||
|
|
2f5cfc0162 | ||
|
|
4a00eb57ad | ||
|
|
784b02e62e | ||
|
|
4c2c026f6d | ||
|
|
07b2c6f169 | ||
|
|
aafd04090e | ||
|
|
af10ae3f37 | ||
|
|
3df4680c7b | ||
|
|
5d04ef2508 | ||
|
|
7fb6eb1949 | ||
|
|
4fe99cddce | ||
|
|
a84763def6 | ||
|
|
8125fac309 | ||
|
|
6c6b2f0b9e | ||
|
|
314be9b198 | ||
|
|
409e0096d8 | ||
|
|
a1ffabae41 | ||
|
|
0fa10627bc | ||
|
|
80618b2331 | ||
|
|
bf8baedfcf | ||
|
|
98f2c8a0b6 | ||
|
|
3887cf2a6f | ||
|
|
eb89c6ea21 | ||
|
|
fd09edc2b9 | ||
|
|
55a9447a7b | ||
|
|
c576aa5cb4 | ||
|
|
ca553a2454 | ||
|
|
ef9c8fd037 | ||
|
|
234a5e085f | ||
|
|
cb22b80ead | ||
|
|
a6d9ad6716 | ||
|
|
185900ada6 | ||
|
|
288ebe5222 | ||
|
|
6e91066e5d | ||
|
|
49a7b2dc8b | ||
|
|
4789ba3e8f | ||
|
|
cc18f0f0c3 | ||
|
|
9bb96c212d | ||
|
|
81eab1179b | ||
|
|
24c9a8e8f1 | ||
|
|
c4d0f8e950 | ||
|
|
3bdf0be4ad | ||
|
|
9e4ebf7c6f | ||
|
|
2408566d34 | ||
|
|
cf61ae927c | ||
|
|
60680936d3 |
2
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/#0_bug_report.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 🐛 错误报告 (中文)
|
||||
description: 创建一个报告以帮助我们改进
|
||||
title: '[错误]: '
|
||||
labels: ['bug']
|
||||
labels: ['kind/bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 💡 功能建议 (中文)
|
||||
description: 为项目提出新的想法
|
||||
title: '[功能]: '
|
||||
labels: ['enhancement']
|
||||
labels: ['kind/enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
4
.github/ISSUE_TEMPLATE/#2_question.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: ❓ 讨论 & 提问 (中文)
|
||||
name: ❓ 提问 & 讨论 (中文)
|
||||
description: 寻求帮助、讨论问题、提出疑问等...
|
||||
title: '[讨论]: '
|
||||
labels: ['question']
|
||||
labels: ['kind/question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
76
.github/ISSUE_TEMPLATE/#3_others.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/#3_others.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
name: 🤔 其他问题 (中文)
|
||||
description: 提交不属于错误报告或功能需求的问题
|
||||
title: '[其他]: '
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
感谢您花时间提出问题!
|
||||
在提交此问题之前,请确保您已经了解了[常见问题](https://docs.cherry-ai.com/question-contact/questions)和[知识科普](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: 提交前检查
|
||||
description: |
|
||||
在提交 Issue 前请确保您已经完成了以下所有步骤
|
||||
options:
|
||||
- label: 我理解 Issue 是用于反馈和解决问题的,而非吐槽评论区,将尽可能提供更多信息帮助问题解决。
|
||||
required: true
|
||||
- label: 我已经查看了置顶 Issue 并搜索了现有的 [开放Issue](https://github.com/CherryHQ/cherry-studio/issues)和[已关闭Issue](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20),没有找到类似的问题。
|
||||
required: true
|
||||
- label: 我填写了简短且清晰明确的标题,以便开发者在翻阅 Issue 列表时能快速确定大致问题。而不是"一个问题"、"求助"等。
|
||||
required: true
|
||||
- label: 我的问题不属于错误报告或功能需求类别。
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: 平台
|
||||
description: 您正在使用哪个平台?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: 版本
|
||||
description: 您正在运行的 Cherry Studio 版本是什么?
|
||||
placeholder: 例如 v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: 问题描述
|
||||
description: 请详细描述您的问题或疑问
|
||||
placeholder: 我想了解有关...的更多信息
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: 相关背景
|
||||
description: 请提供与您的问题相关的任何背景信息或上下文
|
||||
placeholder: 我尝试实现...时遇到了疑问
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: attempts
|
||||
attributes:
|
||||
label: 您已尝试的方法
|
||||
description: 请描述您为解决问题已经尝试过的方法(如果有)
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: 附加信息
|
||||
description: 任何能让我们对您的问题有更多了解的信息,包括截图或相关链接
|
||||
2
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/0_bug_report.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 🐛 Bug Report (English)
|
||||
description: Create a report to help us improve
|
||||
title: '[Bug]: '
|
||||
labels: ['bug']
|
||||
labels: ['kind/bug']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
2
.github/ISSUE_TEMPLATE/1_feature_request.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: 💡 Feature Request (English)
|
||||
description: Suggest an idea for this project
|
||||
title: '[Feature]: '
|
||||
labels: ['enhancement']
|
||||
labels: ['kind/enhancement']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/2_question.yml
vendored
4
.github/ISSUE_TEMPLATE/2_question.yml
vendored
@@ -1,7 +1,7 @@
|
||||
name: ❓ Discussion & Questions
|
||||
name: ❓ Questions & Discussion
|
||||
description: Seeking help, discussing issues, asking questions, etc...
|
||||
title: '[Discussion]: '
|
||||
labels: ['question']
|
||||
labels: ['kind/question']
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
76
.github/ISSUE_TEMPLATE/3_others.yml
vendored
Normal file
76
.github/ISSUE_TEMPLATE/3_others.yml
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
name: 🤔 Other Questions (English)
|
||||
description: Submit questions that don't fit into bug reports or feature requests
|
||||
title: '[Other]: '
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for taking the time to ask a question!
|
||||
Before submitting this issue, please make sure you've reviewed the [FAQ](https://docs.cherry-ai.com/question-contact/questions) and [Knowledge Base](https://docs.cherry-ai.com/question-contact/knowledge)
|
||||
|
||||
- type: checkboxes
|
||||
id: checklist
|
||||
attributes:
|
||||
label: Pre-submission Checklist
|
||||
description: |
|
||||
Please ensure you've completed all the steps below before submitting your issue
|
||||
options:
|
||||
- label: I understand that Issues are for feedback and problem-solving, not for complaints, and I will provide as much information as possible to help resolve the issue.
|
||||
required: true
|
||||
- label: I have checked the pinned Issues and searched through existing [open Issues](https://github.com/CherryHQ/cherry-studio/issues) and [closed Issues](https://github.com/CherryHQ/cherry-studio/issues?q=is%3Aissue%20state%3Aclosed%20) and didn't find similar questions.
|
||||
required: true
|
||||
- label: I have written a short and clear title that helps developers quickly understand the nature of my question, rather than vague titles like "A question" or "Help needed".
|
||||
required: true
|
||||
- label: My question doesn't fall under bug reports or feature requests categories.
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: platform
|
||||
attributes:
|
||||
label: Platform
|
||||
description: Which platform are you using?
|
||||
options:
|
||||
- Windows
|
||||
- macOS
|
||||
- Linux
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Version
|
||||
description: What version of Cherry Studio are you running?
|
||||
placeholder: e.g., v1.0.0
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Question Description
|
||||
description: Please describe your question or inquiry in detail
|
||||
placeholder: I would like to know more about...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: context
|
||||
attributes:
|
||||
label: Relevant Context
|
||||
description: Please provide any background information or context related to your question
|
||||
placeholder: I encountered this question while trying to implement...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: attempts
|
||||
attributes:
|
||||
label: Attempted Solutions
|
||||
description: Please describe any methods you've already tried to resolve your question (if applicable)
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional Information
|
||||
description: Any other information that could help us better understand your question, including screenshots or relevant links
|
||||
252
.github/issue-checker.yml
vendored
Normal file
252
.github/issue-checker.yml
vendored
Normal file
@@ -0,0 +1,252 @@
|
||||
default-mode:
|
||||
add:
|
||||
remove: [pull_request_target, issues]
|
||||
|
||||
labels:
|
||||
# <!-- [Ss]kip `LABEL` --> 跳过一个 label
|
||||
# <!-- [Rr]emove `LABEL` --> 去掉一个 label
|
||||
|
||||
# skips and removes
|
||||
- name: skip all
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Aa]ll |)[Ll]abels?"
|
||||
- name: remove all
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Aa]ll |)[Ll]abels?"
|
||||
|
||||
- name: skip kind/bug
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
|
||||
- name: remove kind/bug
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/bug(?:`|)"
|
||||
|
||||
- name: skip kind/enhancement
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
|
||||
- name: remove kind/enhancement
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/enhancement(?:`|)"
|
||||
|
||||
- name: skip kind/question
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
|
||||
- name: remove kind/question
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/question(?:`|)"
|
||||
|
||||
- name: skip area/Connectivity
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
|
||||
- name: remove area/Connectivity
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/Connectivity(?:`|)"
|
||||
|
||||
- name: skip area/UI/UX
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
|
||||
- name: remove area/UI/UX
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)area/UI/UX(?:`|)"
|
||||
|
||||
- name: skip kind/documentation
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
|
||||
- name: remove kind/documentation
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)kind/documentation(?:`|)"
|
||||
|
||||
- name: skip client:linux
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
|
||||
- name: remove client:linux
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:linux(?:`|)"
|
||||
|
||||
- name: skip client:mac
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
|
||||
- name: remove client:mac
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:mac(?:`|)"
|
||||
|
||||
- name: skip client:win
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
|
||||
- name: remove client:win
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)client:win(?:`|)"
|
||||
|
||||
- name: skip sig/Assistant
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
|
||||
- name: remove sig/Assistant
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Assistant(?:`|)"
|
||||
|
||||
- name: skip sig/Data
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
|
||||
- name: remove sig/Data
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/Data(?:`|)"
|
||||
|
||||
- name: skip sig/MCP
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
|
||||
- name: remove sig/MCP
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/MCP(?:`|)"
|
||||
|
||||
- name: skip sig/RAG
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
|
||||
- name: remove sig/RAG
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)sig/RAG(?:`|)"
|
||||
|
||||
- name: skip lgtm
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
|
||||
- name: remove lgtm
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)lgtm(?:`|)"
|
||||
|
||||
- name: skip License
|
||||
content:
|
||||
regexes: "[Ss]kip (?:[Ll]abels? |)(?:`|)License(?:`|)"
|
||||
- name: remove License
|
||||
content:
|
||||
regexes: "[Rr]emove (?:[Ll]abels? |)(?:`|)License(?:`|)"
|
||||
|
||||
# `Dev Team`
|
||||
- name: Dev Team
|
||||
mode:
|
||||
add: [pull_request_target, issues]
|
||||
author_association:
|
||||
- COLLABORATOR
|
||||
|
||||
# Area labels
|
||||
- name: area/Connectivity
|
||||
content: area/Connectivity
|
||||
regexes: "代理|[Pp]roxy"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip area/Connectivity
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove area/Connectivity
|
||||
|
||||
- name: area/UI/UX
|
||||
content: area/UI/UX
|
||||
regexes: "界面|[Uu][Ii]|重叠|按钮|图标|组件|渲染|菜单|栏目|头像|主题|样式|[Cc][Ss][Ss]"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip area/UI/UX
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove area/UI/UX
|
||||
|
||||
# Kind labels
|
||||
- name: kind/documentation
|
||||
content: kind/documentation
|
||||
regexes: "文档|教程|[Dd]oc(s|umentation)|[Rr]eadme"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip kind/documentation
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove kind/documentation
|
||||
|
||||
# Client labels
|
||||
- name: client:linux
|
||||
content: client:linux
|
||||
regexes: "(?:[Ll]inux|[Uu]buntu|[Dd]ebian)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:linux
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove client:linux
|
||||
|
||||
- name: client:mac
|
||||
content: client:mac
|
||||
regexes: "(?:[Mm]ac|[Mm]acOS|[Oo]SX)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:mac
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove client:mac
|
||||
|
||||
- name: client:win
|
||||
content: client:win
|
||||
regexes: "(?:[Ww]in|[Ww]indows)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip client:win
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove client:win
|
||||
|
||||
# SIG labels
|
||||
- name: sig/Assistant
|
||||
content: sig/Assistant
|
||||
regexes: "快捷助手|[Aa]ssistant"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/Assistant
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/Assistant
|
||||
|
||||
- name: sig/Data
|
||||
content: sig/Data
|
||||
regexes: "[Ww]ebdav|坚果云|备份|同步|数据|Obsidian|Notion|Joplin|思源"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/Data
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/Data
|
||||
|
||||
- name: sig/MCP
|
||||
content: sig/MCP
|
||||
regexes: "[Mm][Cc][Pp]"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/MCP
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/MCP
|
||||
|
||||
- name: sig/RAG
|
||||
content: sig/RAG
|
||||
regexes: "知识库|[Rr][Aa][Gg]"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip sig/RAG
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove sig/RAG
|
||||
|
||||
# Other labels
|
||||
- name: lgtm
|
||||
content: lgtm
|
||||
regexes: "(?:[Ll][Gg][Tt][Mm]|[Ll]ooks [Gg]ood [Tt]o [Mm]e)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip lgtm
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove lgtm
|
||||
|
||||
- name: License
|
||||
content: License
|
||||
regexes: "(?:[Ll]icense|[Cc]opyright|[Mm][Ii][Tt]|[Aa]pache)"
|
||||
skip-if:
|
||||
- skip all
|
||||
- skip License
|
||||
remove-if:
|
||||
- remove all
|
||||
- remove License
|
||||
54
.github/pull_request_template.md
vendored
Normal file
54
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
<!-- Template from https://github.com/kubevirt/kubevirt/blob/main/.github/PULL_REQUEST_TEMPLATE.md?-->
|
||||
<!-- Thanks for sending a pull request! Here are some tips for you:
|
||||
1. Consider creating this PR as draft: https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
|
||||
-->
|
||||
|
||||
### What this PR does
|
||||
|
||||
Before this PR:
|
||||
|
||||
After this PR:
|
||||
|
||||
<!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close the issue(s) when PR gets merged)*: -->
|
||||
|
||||
Fixes #
|
||||
|
||||
### Why we need it and why it was done in this way
|
||||
|
||||
The following tradeoffs were made:
|
||||
|
||||
The following alternatives were considered:
|
||||
|
||||
Links to places where the discussion took place: <!-- optional: slack, other GH issue, mailinglist, ... -->
|
||||
|
||||
### Breaking changes
|
||||
|
||||
<!-- optional -->
|
||||
|
||||
If this PR introduces breaking changes, please describe the changes and the impact on users.
|
||||
|
||||
### Special notes for your reviewer
|
||||
|
||||
<!-- optional -->
|
||||
|
||||
### Checklist
|
||||
|
||||
This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
|
||||
Approvers are expected to review this list.
|
||||
|
||||
- [ ] PR: The PR description is expressive enough and will help future contributors
|
||||
- [ ] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
|
||||
- [ ] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
|
||||
- [ ] Upgrade: Impact of this change on upgrade flows was considered and addressed if required
|
||||
- [ ] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. You want a user-guide update if it's a user facing feature.
|
||||
|
||||
### Release note
|
||||
|
||||
<!-- Write your release note:
|
||||
1. Enter your extended release note in the below block. If the PR requires additional action from users switching to the new release, include the string "action required".
|
||||
2. If no release note is required, just write "NONE".
|
||||
-->
|
||||
|
||||
```release-note
|
||||
|
||||
```
|
||||
25
.github/workflows/issue-checker.yml
vendored
Normal file
25
.github/workflows/issue-checker.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: "Issue Checker"
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
pull_request_target:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: MaaAssistantArknights/issue-checker@v1.14
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
configuration-path: .github/issue-checker.yml
|
||||
not-before: 2022-08-05T00:00:00Z
|
||||
include-title: 1
|
||||
23
.github/workflows/issue-management.yml
vendored
23
.github/workflows/issue-management.yml
vendored
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
daysBeforeStale: 30 # Number of days of inactivity before marking as stale
|
||||
daysBeforeClose: 30 # Number of days to wait after marking as stale before closing
|
||||
daysBeforeClose: 10 # Number of days to wait after marking as stale before closing
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
@@ -20,6 +20,25 @@ jobs:
|
||||
pull-requests: none
|
||||
contents: none
|
||||
steps:
|
||||
- name: Close needs-more-info issues
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: "needs-more-info"
|
||||
days-before-stale: ${{ env.daysBeforeStale }}
|
||||
days-before-close: 0 # Close immediately after stale
|
||||
stale-issue-label: "inactive"
|
||||
close-issue-label: "closed:no-response"
|
||||
stale-issue-message: |
|
||||
This issue has been labeled as needing more information and has been inactive for ${{ env.daysBeforeStale }} days.
|
||||
It will be closed now due to lack of additional information.
|
||||
|
||||
该问题被标记为"需要更多信息"且已经 ${{ env.daysBeforeStale }} 天没有任何活动,将立即关闭。
|
||||
operations-per-run: 50
|
||||
exempt-issue-labels: "pending, Dev Team"
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
|
||||
- name: Close inactive issues
|
||||
uses: actions/stale@v9
|
||||
with:
|
||||
@@ -30,7 +49,7 @@ jobs:
|
||||
stale-issue-message: |
|
||||
This issue has been inactive for a prolonged period and will be closed automatically in ${{ env.daysBeforeClose }} days.
|
||||
该问题已长时间处于闲置状态,${{ env.daysBeforeClose }} 天后将自动关闭。
|
||||
exempt-issue-labels: "pending, Dev Team, enhancement"
|
||||
exempt-issue-labels: "pending, Dev Team, kind/enhancement"
|
||||
days-before-pr-stale: -1 # Completely disable stalling for PRs
|
||||
days-before-pr-close: -1 # Completely disable closing for PRs
|
||||
|
||||
|
||||
75
.github/workflows/nightly-build.yml
vendored
75
.github/workflows/nightly-build.yml
vendored
@@ -7,9 +7,41 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write # Required for deleting artifacts
|
||||
|
||||
jobs:
|
||||
cleanup-artifacts:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete old artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
# Calculate the date 14 days ago
|
||||
cutoff_date=$(date -d "14 days ago" +%Y-%m-%d)
|
||||
|
||||
# List and delete artifacts older than cutoff date
|
||||
gh api repos/$REPO/actions/artifacts --paginate | \
|
||||
jq -r '.artifacts[] | select(.name | startswith("cherry-studio-nightly-")) | select(.created_at < "'$cutoff_date'") | .id' | \
|
||||
while read artifact_id; do
|
||||
echo "Deleting artifact $artifact_id"
|
||||
gh api repos/$REPO/actions/artifacts/$artifact_id -X DELETE
|
||||
done
|
||||
|
||||
check-repository:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ github.repository == 'CherryHQ/cherry-studio' }}
|
||||
steps:
|
||||
- name: Check if running in main repository
|
||||
run: |
|
||||
echo "Running in repository: ${{ github.repository }}"
|
||||
echo "Should run: ${{ github.repository == 'CherryHQ/cherry-studio' }}"
|
||||
|
||||
nightly-build:
|
||||
needs: check-repository
|
||||
if: needs.check-repository.outputs.should_run == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
@@ -26,6 +58,11 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
|
||||
@@ -59,6 +96,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Build Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
@@ -73,19 +111,17 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
yarn build:npm windows
|
||||
yarn build:win:x64
|
||||
yarn build:win:arm64
|
||||
yarn build:win
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
|
||||
- name: Replace spaces in filenames
|
||||
run: node scripts/replace-spaces.js
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Rename artifacts with nightly format
|
||||
shell: bash
|
||||
@@ -96,39 +132,24 @@ jobs:
|
||||
# Windows artifacts - based on actual file naming pattern
|
||||
if [ "${{ matrix.os }}" == "windows-latest" ]; then
|
||||
# Setup installer
|
||||
find dist -name "*setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-setup.exe \;
|
||||
|
||||
# Portable exe
|
||||
find dist -name "*portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-portable.exe \;
|
||||
find dist -name "*-x64-setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64-setup.exe \;
|
||||
find dist -name "*-arm64-setup.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64-setup.exe \;
|
||||
|
||||
# Rename blockmap files to match the new exe names
|
||||
if [ -f "dist/*setup.exe.blockmap" ]; then
|
||||
cp dist/*setup.exe.blockmap renamed-artifacts/cherry-studio-nightly-${DATE}-setup.exe.blockmap || true
|
||||
fi
|
||||
# Portable exe
|
||||
find dist -name "*-x64-portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64-portable.exe \;
|
||||
find dist -name "*-arm64-portable.exe" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64-portable.exe \;
|
||||
fi
|
||||
|
||||
# macOS artifacts
|
||||
if [ "${{ matrix.os }}" == "macos-latest" ]; then
|
||||
# 处理arm64架构文件
|
||||
find dist -name "*-arm64.dmg" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.dmg \;
|
||||
find dist -name "*-arm64.dmg.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.dmg.blockmap \;
|
||||
find dist -name "*-arm64.zip" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.zip \;
|
||||
find dist -name "*-arm64.zip.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.zip.blockmap \;
|
||||
|
||||
# 处理x64架构文件
|
||||
find dist -name "*-x64.dmg" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.dmg \;
|
||||
find dist -name "*-x64.dmg.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.dmg.blockmap \;
|
||||
find dist -name "*-x64.zip" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.zip \;
|
||||
find dist -name "*-x64.zip.blockmap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x64.zip.blockmap \;
|
||||
fi
|
||||
|
||||
# Linux artifacts
|
||||
if [ "${{ matrix.os }}" == "ubuntu-latest" ]; then
|
||||
find dist -name "*.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.AppImage \;
|
||||
find dist -name "*.snap" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.snap \;
|
||||
find dist -name "*.deb" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.deb \;
|
||||
find dist -name "*.rpm" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.rpm \;
|
||||
find dist -name "*.tar.gz" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}.tar.gz \;
|
||||
find dist -name "*-x86_64.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-x86_64.AppImage \;
|
||||
find dist -name "*-arm64.AppImage" -exec cp {} renamed-artifacts/cherry-studio-nightly-${DATE}-arm64.AppImage \;
|
||||
fi
|
||||
|
||||
# Copy update files
|
||||
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -6,7 +6,7 @@ on:
|
||||
tag:
|
||||
description: 'Release tag (e.g. v1.0.0)'
|
||||
required: true
|
||||
default: 'v0.9.18'
|
||||
default: 'v1.0.0'
|
||||
push:
|
||||
tags:
|
||||
- v*.*.*
|
||||
@@ -42,6 +42,11 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: macos-latest dependencies fix
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
brew install python-setuptools
|
||||
|
||||
- name: Install corepack
|
||||
run: corepack enable && corepack prepare yarn@4.6.0 --activate
|
||||
|
||||
@@ -71,10 +76,12 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Build Mac
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
sudo -H pip install setuptools
|
||||
yarn build:npm mac
|
||||
yarn build:mac
|
||||
env:
|
||||
@@ -85,6 +92,7 @@ jobs:
|
||||
APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Build Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
@@ -94,6 +102,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RENDERER_VITE_AIHUBMIX_SECRET: ${{ vars.RENDERER_VITE_AIHUBMIX_SECRET }}
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
|
||||
- name: Release
|
||||
uses: ncipollo/release-action@v1
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -46,3 +46,8 @@ local
|
||||
.aider*
|
||||
.cursorrules
|
||||
.cursor/rules
|
||||
|
||||
# test
|
||||
coverage
|
||||
.vitest-cache
|
||||
vitest.config.*.timestamp-*
|
||||
|
||||
1
.vscode/launch.json
vendored
1
.vscode/launch.json
vendored
@@ -7,6 +7,7 @@
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
|
||||
"runtimeVersion": "20",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
|
||||
},
|
||||
|
||||
37698
.yarn/patches/@google-genai-npm-0.8.0-450d0d9a7d.patch
vendored
37698
.yarn/patches/@google-genai-npm-0.8.0-450d0d9a7d.patch
vendored
File diff suppressed because one or more lines are too long
92
.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch
vendored
Normal file
92
.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
diff --git a/out/electron/ElectronFramework.js b/out/electron/ElectronFramework.js
|
||||
index 5a4b4546870ee9e770d5a50d79790d39baabd268..3f0ac05dfd6bbaeaf5f834341a823718bd10f55c 100644
|
||||
--- a/out/electron/ElectronFramework.js
|
||||
+++ b/out/electron/ElectronFramework.js
|
||||
@@ -55,26 +55,27 @@ async function removeUnusedLanguagesIfNeeded(options) {
|
||||
if (!wantedLanguages.length) {
|
||||
return;
|
||||
}
|
||||
- const { dir, langFileExt } = getLocalesConfig(options);
|
||||
+ const { dirs, langFileExt } = getLocalesConfig(options);
|
||||
// noinspection SpellCheckingInspection
|
||||
- await (0, tiny_async_pool_1.default)(builder_util_1.MAX_FILE_REQUESTS, await (0, fs_extra_1.readdir)(dir), async (file) => {
|
||||
- if (!file.endsWith(langFileExt)) {
|
||||
+ const deletedFiles = async (dir) => {
|
||||
+ await (0, tiny_async_pool_1.default)(builder_util_1.MAX_FILE_REQUESTS, await (0, fs_extra_1.readdir)(dir), async (file) => {
|
||||
+ if (!file.endsWith(langFileExt)) {
|
||||
+ return;
|
||||
+ }
|
||||
+ const language = file.substring(0, file.length - langFileExt.length);
|
||||
+ if (!wantedLanguages.includes(language)) {
|
||||
+ return fs.rm(path.join(dir, file), { recursive: true, force: true });
|
||||
+ }
|
||||
return;
|
||||
- }
|
||||
- const language = file.substring(0, file.length - langFileExt.length);
|
||||
- if (!wantedLanguages.includes(language)) {
|
||||
- return fs.rm(path.join(dir, file), { recursive: true, force: true });
|
||||
- }
|
||||
- return;
|
||||
- });
|
||||
+ });
|
||||
+ };
|
||||
+ await Promise.all(dirs.map(deletedFiles));
|
||||
function getLocalesConfig(options) {
|
||||
const { appOutDir, packager } = options;
|
||||
if (packager.platform === index_1.Platform.MAC) {
|
||||
- return { dir: packager.getResourcesDir(appOutDir), langFileExt: ".lproj" };
|
||||
- }
|
||||
- else {
|
||||
- return { dir: path.join(packager.getResourcesDir(appOutDir), "..", "locales"), langFileExt: ".pak" };
|
||||
+ return { dirs: [packager.getResourcesDir(appOutDir), packager.getMacOsElectronFrameworkResourcesDir(appOutDir)], langFileExt: ".lproj" };
|
||||
}
|
||||
+ return { dirs: [path.join(packager.getResourcesDir(appOutDir), "..", "locales")], langFileExt: ".pak" };
|
||||
}
|
||||
}
|
||||
class ElectronFramework {
|
||||
diff --git a/out/node-module-collector/index.d.ts b/out/node-module-collector/index.d.ts
|
||||
index 8e808be0fa0d5971b9f9605c8eb88f71630e34b7..1b97dccd8a150a67c4312d2ba4757960e624045b 100644
|
||||
--- a/out/node-module-collector/index.d.ts
|
||||
+++ b/out/node-module-collector/index.d.ts
|
||||
@@ -2,6 +2,6 @@ import { NpmNodeModulesCollector } from "./npmNodeModulesCollector";
|
||||
import { PnpmNodeModulesCollector } from "./pnpmNodeModulesCollector";
|
||||
import { detect, PM, getPackageManagerVersion } from "./packageManager";
|
||||
import { NodeModuleInfo } from "./types";
|
||||
-export declare function getCollectorByPackageManager(rootDir: string): Promise<NpmNodeModulesCollector | PnpmNodeModulesCollector>;
|
||||
+export declare function getCollectorByPackageManager(rootDir: string): Promise<PnpmNodeModulesCollector | NpmNodeModulesCollector>;
|
||||
export declare function getNodeModules(rootDir: string): Promise<NodeModuleInfo[]>;
|
||||
export { detect, getPackageManagerVersion, PM };
|
||||
diff --git a/out/platformPackager.d.ts b/out/platformPackager.d.ts
|
||||
index 2df1ba2725c54c7b0e8fed67ab52e94f0cdb17bc..c7ff756564cfd216d2c7d8f72f367527010c06f9 100644
|
||||
--- a/out/platformPackager.d.ts
|
||||
+++ b/out/platformPackager.d.ts
|
||||
@@ -67,6 +67,7 @@ export declare abstract class PlatformPackager<DC extends PlatformSpecificBuildO
|
||||
getElectronSrcDir(dist: string): string;
|
||||
getElectronDestinationDir(appOutDir: string): string;
|
||||
getResourcesDir(appOutDir: string): string;
|
||||
+ getMacOsElectronFrameworkResourcesDir(appOutDir: string): string;
|
||||
getMacOsResourcesDir(appOutDir: string): string;
|
||||
private checkFileInPackage;
|
||||
private sanityCheckPackage;
|
||||
diff --git a/out/platformPackager.js b/out/platformPackager.js
|
||||
index 6f799ce0d1cdb5f0b18a9c8187b2db84b3567aa9..879248e6c6786d3473e1a80e3930d3a8d0190aab 100644
|
||||
--- a/out/platformPackager.js
|
||||
+++ b/out/platformPackager.js
|
||||
@@ -465,12 +465,13 @@ class PlatformPackager {
|
||||
if (this.platform === index_1.Platform.MAC) {
|
||||
return this.getMacOsResourcesDir(appOutDir);
|
||||
}
|
||||
- else if ((0, Framework_1.isElectronBased)(this.info.framework)) {
|
||||
+ if ((0, Framework_1.isElectronBased)(this.info.framework)) {
|
||||
return path.join(appOutDir, "resources");
|
||||
}
|
||||
- else {
|
||||
- return appOutDir;
|
||||
- }
|
||||
+ return appOutDir;
|
||||
+ }
|
||||
+ getMacOsElectronFrameworkResourcesDir(appOutDir) {
|
||||
+ return path.join(appOutDir, `${this.appInfo.productFilename}.app`, "Contents", "Frameworks", "Electron Framework.framework", "Resources");
|
||||
}
|
||||
getMacOsResourcesDir(appOutDir) {
|
||||
return path.join(appOutDir, `${this.appInfo.productFilename}.app`, "Contents", "Resources");
|
||||
51
.yarn/patches/electron-updater-npm-6.6.3-9269dbaf84.patch
vendored
Normal file
51
.yarn/patches/electron-updater-npm-6.6.3-9269dbaf84.patch
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
diff --git a/out/MacUpdater.js b/out/MacUpdater.js
|
||||
index 8f18dc5416c91835ded4e47f2358fba680c129ac..a3fb43c2450dc3484bf099b5ea79a362a3b372cc 100644
|
||||
--- a/out/MacUpdater.js
|
||||
+++ b/out/MacUpdater.js
|
||||
@@ -74,7 +74,7 @@ class MacUpdater extends AppUpdater_1.AppUpdater {
|
||||
else {
|
||||
files = files.filter(file => !isArm64(file));
|
||||
}
|
||||
- const zipFileInfo = (0, Provider_1.findFile)(files, "zip", ["pkg", "dmg"]);
|
||||
+ const zipFileInfo = (0, Provider_1.findFile)(files, "zip", ["pkg", "dmg"], false /*has been filtered by myself*/);
|
||||
if (zipFileInfo == null) {
|
||||
throw (0, builder_util_runtime_1.newError)(`ZIP file not provided: ${(0, builder_util_runtime_1.safeStringifyJson)(files)}`, "ERR_UPDATER_ZIP_FILE_NOT_FOUND");
|
||||
}
|
||||
diff --git a/out/providers/Provider.js b/out/providers/Provider.js
|
||||
index 9829dff7e95aa9baa0bfdf29f52e6f761c9b7243..6ecaade9e294c87c03bb42e77ff5463f2782cb3c 100644
|
||||
--- a/out/providers/Provider.js
|
||||
+++ b/out/providers/Provider.js
|
||||
@@ -61,11 +61,18 @@ class Provider {
|
||||
}
|
||||
}
|
||||
exports.Provider = Provider;
|
||||
-function findFile(files, extension, not) {
|
||||
+function findFile(files, extension, not, filterByArch = true) {
|
||||
if (files.length === 0) {
|
||||
throw (0, builder_util_runtime_1.newError)("No files provided", "ERR_UPDATER_NO_FILES_PROVIDED");
|
||||
}
|
||||
- const result = files.find(it => it.url.pathname.toLowerCase().endsWith(`.${extension.toLowerCase()}`));
|
||||
+ const result = files
|
||||
+ .filter(file => {
|
||||
+ if (!filterByArch) {
|
||||
+ return true;
|
||||
+ }
|
||||
+ return (process.arch == "arm64") === (file.url.pathname.includes("arm64") || file.info.url.includes("arm64"));
|
||||
+ })
|
||||
+ .find(it => it.url.pathname.toLowerCase().endsWith(`.${extension.toLowerCase()}`));
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
diff --git a/out/differentialDownloader/multipleRangeDownloader.js b/out/differentialDownloader/multipleRangeDownloader.js
|
||||
index bf7d3a2982c62b94054fed4ef60455b20b26d622..3a924eddc946ec446654a112a33be4e2cea311d2 100644
|
||||
--- a/out/differentialDownloader/multipleRangeDownloader.js
|
||||
+++ b/out/differentialDownloader/multipleRangeDownloader.js
|
||||
@@ -75,7 +75,7 @@ function doExecuteTasks(differentialDownloader, options, out, resolve, reject) {
|
||||
return;
|
||||
}
|
||||
const contentType = (0, builder_util_runtime_1.safeGetHeader)(response, "content-type");
|
||||
- const m = /^multipart\/.+?(?:; boundary=(?:(?:"(.+)")|(?:([^\s]+))))$/i.exec(contentType);
|
||||
+ const m = /^multipart\/.+?\s*;\s*boundary=(?:"([^"]+)"|([^\s";]+))\s*$/i.exec(contentType);
|
||||
if (m == null) {
|
||||
reject(new Error(`Content-Type "multipart/byteranges" is expected, but got "${contentType}"`));
|
||||
return;
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/core.js b/core.js
|
||||
index ebb071d31cd5a14792b62814df072c5971e83300..31e1062d4a7f2422ffec79cf96a35dbb69fe89cb 100644
|
||||
index 862d66101f441fb4f47dfc8cff5e2d39e1f5a11e..6464bebbf696c39d35f0368f061ea4236225c162 100644
|
||||
--- a/core.js
|
||||
+++ b/core.js
|
||||
@@ -157,7 +157,7 @@ class APIClient {
|
||||
@@ -159,7 +159,7 @@ class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
@@ -12,10 +12,10 @@ index ebb071d31cd5a14792b62814df072c5971e83300..31e1062d4a7f2422ffec79cf96a35dbb
|
||||
};
|
||||
}
|
||||
diff --git a/core.mjs b/core.mjs
|
||||
index 9c1a0264dcd73a85de1cf81df4efab9ce9ee2ab7..33f9f1f237f2eb2667a05dae1a7e3dc916f6bfff 100644
|
||||
index 05dbc6cfde51589a2b100d4e4b5b3c1a33b32b89..789fbb4985eb952a0349b779fa83b1a068af6e7e 100644
|
||||
--- a/core.mjs
|
||||
+++ b/core.mjs
|
||||
@@ -150,7 +150,7 @@ export class APIClient {
|
||||
@@ -152,7 +152,7 @@ export class APIClient {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': this.getUserAgent(),
|
||||
@@ -1,45 +1,73 @@
|
||||
# Cherry Studio 贡献者指南
|
||||
[中文](./docs/CONTRIBUTING.zh.md) | [English](./CONTRIBUTING.md)
|
||||
|
||||
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。
|
||||
# Cherry Studio Contributor Guide
|
||||
|
||||
## 如何贡献
|
||||
Welcome to the Cherry Studio contributor community! We are committed to making Cherry Studio a project that provides long-term value and hope to invite more developers to join us. Whether you are an experienced developer or a beginner just starting out, your contributions will help us better serve users and improve software quality.
|
||||
|
||||
以下是您可以参与的几种方式:
|
||||
## How to Contribute
|
||||
|
||||
1. **贡献代码**:帮助我们开发新功能或优化现有代码。请确保您的代码符合我们的编码标准,并通过所有测试。
|
||||
Here are several ways you can participate:
|
||||
|
||||
2. **修复 BUG**:如果您发现了 BUG,欢迎提交修复方案。请在提交前确认问题已被解决,并附上相关测试。
|
||||
1. **Contribute Code**: Help us develop new features or optimize existing code. Please ensure your code adheres to our coding standards and passes all tests.
|
||||
|
||||
3. **维护 Issue**:协助我们管理 GitHub 上的 issue,帮助标记、分类和解决问题。
|
||||
2. **Fix Bugs**: If you find a bug, you are welcome to submit a fix. Please confirm the issue is resolved before submitting and include relevant tests.
|
||||
|
||||
4. **产品设计**:参与产品设计讨论,帮助我们改进用户体验和界面设计。
|
||||
3. **Maintain Issues**: Help us manage issues on GitHub by assisting with tagging, classifying, and resolving problems.
|
||||
|
||||
5. **编写文档**:帮助我们完善用户手册、API 文档和开发者指南。
|
||||
4. **Product Design**: Participate in product design discussions to help us improve user experience and interface design.
|
||||
|
||||
6. **社区维护**:参与社区讨论,帮助解答用户问题,促进社区活跃。
|
||||
5. **Write Documentation**: Help us improve the user manual, API documentation, and developer guides.
|
||||
|
||||
7. **推广使用**:通过博客、社交媒体等渠道推广 Cherry Studio,吸引更多用户和开发者。
|
||||
6. **Community Maintenance**: Participate in community discussions, help answer user questions, and promote community activity.
|
||||
|
||||
## 开始贡献
|
||||
7. **Promote Usage**: Promote Cherry Studio through blogs, social media, and other channels to attract more users and developers.
|
||||
|
||||
1. **Fork 仓库**:在 GitHub 上 fork 我们的仓库,并将其克隆到本地。
|
||||
## Before You Start
|
||||
|
||||
2. **创建分支**:为您要进行的更改创建一个新的分支。
|
||||
Please make sure you have read the [Code of Conduct](CODE_OF_CONDUCT.md) and the [LICENSE](LICENSE).
|
||||
|
||||
3. **提交更改**:在本地进行更改并提交。请确保您的提交信息清晰明了。
|
||||
## Getting Started
|
||||
|
||||
4. **发起 Pull Request**:将您的更改推送到 GitHub,并发起 Pull Request。请描述您的更改内容和原因。
|
||||
To help you get familiar with the codebase, we recommend tackling issues tagged with one or more of the following labels: [good-first-issue](https://github.com/CherryHQ/cherry-studio/labels/good%20first%20issue), [help-wanted](https://github.com/CherryHQ/cherry-studio/labels/help%20wanted), or [kind/bug](https://github.com/CherryHQ/cherry-studio/labels/kind%2Fbug). Any help is welcome.
|
||||
|
||||
### 其他建议
|
||||
### Testing
|
||||
|
||||
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
|
||||
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。
|
||||
Features without tests are considered non-existent. To ensure code is truly effective, relevant processes should be covered by unit tests and functional tests. Therefore, when considering contributions, please also consider testability. All tests can be run locally without dependency on CI. Please refer to the "Testing" section in the [Developer Guide](docs/dev.md).
|
||||
|
||||
## 联系我们
|
||||
### Automated Testing for Pull Requests
|
||||
|
||||
如果您有任何问题或建议,欢迎通过以下方式联系我们:
|
||||
Automated tests are triggered on pull requests (PRs) opened by members of the Cherry Studio organization, except for draft PRs. PRs opened by new contributors will initially be marked with the `needs-ok-to-test` label and will not be automatically tested. Once a Cherry Studio organization member adds `/ok-to-test` to the PR, the test pipeline will be created.
|
||||
|
||||
- 微信:kangfenmao
|
||||
### Consider Opening Your Pull Request as a Draft
|
||||
|
||||
Not all pull requests are ready for review when created. This might be because the author wants to start a discussion, they are not entirely sure if the changes are heading in the right direction, or the changes are not yet complete. Please consider creating these PRs as [draft pull requests](https://github.blog/2019-02-14-introducing-draft-pull-requests/). Draft PRs are skipped by CI, thus saving CI resources. This also means reviewers will not be automatically assigned, and the community will understand that this PR is not yet ready for review.
|
||||
Reviewers will be assigned after you mark the draft pull request as ready for review.
|
||||
|
||||
### Contributor Compliance with Project Terms
|
||||
|
||||
We require every contributor to certify that they have the right to legally contribute to our project. Contributors express this by consciously signing their commits, thereby indicating their compliance with the [LICENSE](LICENSE).
|
||||
A signed commit is one where the commit message includes the following:
|
||||
|
||||
You can generate a signed commit using the following command [git commit --signoff](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff):
|
||||
|
||||
```
|
||||
git commit --signoff -m "Your commit message"
|
||||
```
|
||||
|
||||
### Getting Code Reviewed/Merged
|
||||
|
||||
Maintainers are here to help you implement your use case within a reasonable timeframe. They will do their best to review your code and provide constructive feedback promptly. However, if you get stuck during the review process or feel your Pull Request is not receiving the attention it deserves, please contact us via comments in the Issue or through the [Community](README.md#-community).
|
||||
|
||||
### Other Suggestions
|
||||
|
||||
- **Contact Developers**: Before submitting a PR, you can contact the developers first to discuss or get help.
|
||||
- **Become a Core Developer**: If you contribute to the project consistently, congratulations, you can become a core developer and gain project membership status. Please check our [Membership Guide](https://github.com/CherryHQ/community/blob/main/docs/membership.en.md).
|
||||
|
||||
## Contact Us
|
||||
|
||||
If you have any questions or suggestions, feel free to contact us through the following ways:
|
||||
|
||||
- WeChat: kangfenmao
|
||||
- [GitHub Issues](https://github.com/CherryHQ/cherry-studio/issues)
|
||||
|
||||
感谢您的支持和贡献!我们期待与您一起将 Cherry Studio 打造成更好的产品。
|
||||
Thank you for your support and contributions! We look forward to working with you to make Cherry Studio a better product.
|
||||
|
||||
@@ -23,14 +23,12 @@ https://docs.cherry-ai.com
|
||||
|
||||
# 🌠 Screenshot
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 🌟 Key Features
|
||||
|
||||

|
||||
|
||||
1. **Diverse LLM Provider Support**:
|
||||
|
||||
- ☁️ Major LLM Cloud Services: OpenAI, Gemini, Anthropic, and more
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* 内置的ASR服务器模块
|
||||
* 这个文件可以直接在Electron中运行,不需要外部依赖
|
||||
*/
|
||||
|
||||
// 使用Electron内置的Node.js模块
|
||||
const http = require('http')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
// 输出环境信息
|
||||
console.log('ASR Server (Embedded) starting...')
|
||||
console.log('Node.js version:', process.version)
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
console.log('Command line arguments:', process.argv)
|
||||
|
||||
// 创建HTTP服务器
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
if (req.url === '/' || req.url === '/index.html') {
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 当前目录
|
||||
path.join(__dirname, 'index.html'),
|
||||
// 上级目录
|
||||
path.join(__dirname, '..', 'index.html'),
|
||||
// 应用根目录
|
||||
path.join(process.cwd(), 'index.html')
|
||||
]
|
||||
|
||||
console.log('Possible index.html paths:', possiblePaths)
|
||||
|
||||
// 查找第一个存在的文件
|
||||
let indexPath = null
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
indexPath = p
|
||||
console.log(`Found index.html at: ${p}`)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error checking existence of ${p}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
if (indexPath) {
|
||||
// 读取文件内容并发送
|
||||
fs.readFile(indexPath, (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error reading index.html:', err)
|
||||
res.writeHead(500)
|
||||
res.end('Error reading index.html')
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end(data)
|
||||
})
|
||||
} else {
|
||||
// 如果找不到文件,返回一个简单的HTML页面
|
||||
console.error('Could not find index.html, serving fallback page')
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ASR Server</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 2em; }
|
||||
h1 { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ASR Server is running</h1>
|
||||
<p>This is a fallback page because the index.html file could not be found.</p>
|
||||
<p>Server is running at: http://localhost:34515</p>
|
||||
<p>Current directory: ${__dirname}</p>
|
||||
<p>Working directory: ${process.cwd()}</p>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
} else {
|
||||
// 处理其他请求
|
||||
res.writeHead(404)
|
||||
res.end('Not found')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling request:', error)
|
||||
res.writeHead(500)
|
||||
res.end('Server error')
|
||||
}
|
||||
})
|
||||
|
||||
// 添加进程错误处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Server] Uncaught exception:', error)
|
||||
// 不立即退出,给日志输出的时间
|
||||
setTimeout(() => process.exit(1), 1000)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// 尝试启动服务器
|
||||
try {
|
||||
const port = 34515
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// 处理服务器错误
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Server] Critical error starting server:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cherry Studio ASR</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 1em;
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.5em;
|
||||
min-height: 50px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>浏览器语音识别中继页面</h1>
|
||||
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
|
||||
<div id="status">正在连接到服务器...</div>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
const statusDiv = document.getElementById('status');
|
||||
const resultDiv = document.getElementById('result');
|
||||
// 尝试连接到WebSocket服务器
|
||||
let ws;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 5;
|
||||
const reconnectInterval = 2000; // 2秒
|
||||
|
||||
function connectWebSocket() {
|
||||
try {
|
||||
ws = new WebSocket('ws://localhost:34515');
|
||||
|
||||
ws.onopen = () => {
|
||||
reconnectAttempts = 0;
|
||||
updateStatus('已连接到服务器,等待指令...');
|
||||
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
|
||||
};
|
||||
|
||||
ws.onmessage = handleMessage;
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[Browser Page] WebSocket Error:', error);
|
||||
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[Browser Page] WebSocket Connection Closed');
|
||||
updateStatus('与服务器断开连接。尝试重新连接...');
|
||||
stopRecognition();
|
||||
|
||||
// 尝试重新连接
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
updateStatus(`与服务器断开连接。尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`);
|
||||
setTimeout(connectWebSocket, reconnectInterval);
|
||||
} else {
|
||||
updateStatus('无法连接到服务器。请刷新页面或重启应用。');
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Browser Page] Error creating WebSocket:', error);
|
||||
updateStatus('创建WebSocket连接时出错。请刷新页面或重启应用。');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始连接
|
||||
connectWebSocket();
|
||||
let recognition = null;
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
function updateStatus(message) {
|
||||
console.log(`[Browser Page Status] ${message}`);
|
||||
statusDiv.textContent = message;
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.data);
|
||||
console.log('[Browser Page] Received command:', data);
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Received non-JSON message:', event.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'start') {
|
||||
startRecognition();
|
||||
} else if (data.type === 'stop') {
|
||||
stopRecognition();
|
||||
} else if (data.type === 'reset') {
|
||||
// 强制重置语音识别
|
||||
forceResetRecognition();
|
||||
} else {
|
||||
console.warn('[Browser Page] Received unknown command type:', data.type);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
function setupRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:此浏览器不支持 Web Speech API。');
|
||||
return false;
|
||||
}
|
||||
if (recognition && recognition.recognizing) {
|
||||
console.log('[Browser Page] Recognition already active.');
|
||||
return true;
|
||||
}
|
||||
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.lang = 'zh-CN';
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
// 增加以下设置提高语音识别的可靠性
|
||||
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
|
||||
// 设置较短的语音识别时间,使用户能更快地看到结果
|
||||
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
|
||||
try {
|
||||
// @ts-ignore
|
||||
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
|
||||
} catch (e) {
|
||||
console.log('[Browser Page] audioStart property not supported');
|
||||
}
|
||||
|
||||
recognition.onstart = () => {
|
||||
updateStatus("🎤 正在识别...");
|
||||
console.log('[Browser Page] SpeechRecognition started.');
|
||||
};
|
||||
|
||||
recognition.onresult = (event) => {
|
||||
console.log('[Browser Page] Recognition result event:', event);
|
||||
|
||||
let interim_transcript = '';
|
||||
let final_transcript = '';
|
||||
|
||||
// 输出识别结果的详细信息便于调试
|
||||
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
||||
const confidence = event.results[i][0].confidence;
|
||||
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
|
||||
|
||||
if (event.results[i].isFinal) {
|
||||
final_transcript += event.results[i][0].transcript;
|
||||
} else {
|
||||
interim_transcript += event.results[i][0].transcript;
|
||||
}
|
||||
}
|
||||
|
||||
const resultText = final_transcript || interim_transcript;
|
||||
resultDiv.textContent = resultText;
|
||||
|
||||
// 更新状态显示
|
||||
if (resultText) {
|
||||
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
|
||||
}
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
|
||||
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onerror = (event) => {
|
||||
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
|
||||
|
||||
// 根据错误类型提供更友好的错误提示
|
||||
let errorMessage = '';
|
||||
switch (event.error) {
|
||||
case 'no-speech':
|
||||
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Restarting recognition after no-speech error');
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
break;
|
||||
case 'audio-capture':
|
||||
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
|
||||
break;
|
||||
case 'not-allowed':
|
||||
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
|
||||
break;
|
||||
case 'network':
|
||||
errorMessage = '网络错误导致语音识别失败。';
|
||||
break;
|
||||
case 'aborted':
|
||||
errorMessage = '语音识别被用户或系统中止。';
|
||||
break;
|
||||
default:
|
||||
errorMessage = `识别错误: ${event.error}`;
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMessage}`);
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: {
|
||||
error: event.error,
|
||||
message: errorMessage || event.message || `Recognition error: ${event.error}`
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
console.log('[Browser Page] SpeechRecognition ended.');
|
||||
|
||||
// 检查是否是由于错误或用户手动停止导致的结束
|
||||
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
|
||||
|
||||
if (!isErrorOrStopped) {
|
||||
// 如果不是由于错误或手动停止,则自动重新启动语音识别
|
||||
updateStatus("识别暂停,正在重新启动...");
|
||||
|
||||
// 保存当前的recognition对象
|
||||
const currentRecognition = recognition;
|
||||
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (currentRecognition && currentRecognition === recognition) {
|
||||
currentRecognition.start();
|
||||
console.log('[Browser Page] Automatically restarting recognition');
|
||||
} else {
|
||||
// 如果recognition对象已经变化,重新创建一个
|
||||
setupRecognition();
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Created new recognition instance and started');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
|
||||
}
|
||||
|
||||
// 只有在手动停止或错误时才重置recognition对象
|
||||
recognition = null;
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
function startRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:浏览器不支持 Web Speech API。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示正在准备的状态
|
||||
updateStatus('正在准备麦克风...');
|
||||
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Recognition already exists, stopping first.');
|
||||
stopRecognition();
|
||||
}
|
||||
|
||||
if (!setupRecognition()) return;
|
||||
|
||||
console.log('[Browser Page] Attempting to start recognition...');
|
||||
try {
|
||||
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
|
||||
const micPermissionTimeout = setTimeout(() => {
|
||||
updateStatus('获取麦克风权限超时,请刷新页面重试。');
|
||||
}, 10000); // 10秒超时
|
||||
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
}
|
||||
})
|
||||
.then(stream => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.log('[Browser Page] Microphone access granted.');
|
||||
|
||||
// 检查麦克风音量级别
|
||||
const audioContext = new AudioContext();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
const microphone = audioContext.createMediaStreamSource(stream);
|
||||
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
|
||||
|
||||
analyser.smoothingTimeConstant = 0.8;
|
||||
analyser.fftSize = 1024;
|
||||
|
||||
microphone.connect(analyser);
|
||||
analyser.connect(javascriptNode);
|
||||
javascriptNode.connect(audioContext.destination);
|
||||
|
||||
javascriptNode.onaudioprocess = function () {
|
||||
const array = new Uint8Array(analyser.frequencyBinCount);
|
||||
analyser.getByteFrequencyData(array);
|
||||
let values = 0;
|
||||
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
values += (array[i]);
|
||||
}
|
||||
|
||||
const average = values / length;
|
||||
console.log('[Browser Page] Microphone volume level:', average);
|
||||
|
||||
// 如果音量太低,显示提示
|
||||
if (average < 5) {
|
||||
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
|
||||
} else {
|
||||
updateStatus('🎤 正在识别...');
|
||||
}
|
||||
|
||||
// 只检查一次就断开连接
|
||||
microphone.disconnect();
|
||||
analyser.disconnect();
|
||||
javascriptNode.disconnect();
|
||||
};
|
||||
|
||||
// 释放测试用的音频流
|
||||
setTimeout(() => {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
audioContext.close();
|
||||
}, 1000);
|
||||
|
||||
// 启动语音识别
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
updateStatus('🎤 正在识别...');
|
||||
} else {
|
||||
updateStatus('错误:Recognition 实例丢失。');
|
||||
console.error('[Browser Page] Recognition instance lost before start.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.error('[Browser Page] Microphone access error:', err);
|
||||
|
||||
let errorMsg = `无法访问麦克风 (${err.name})`;
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMsg}`);
|
||||
recognition = null;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.start():', e);
|
||||
updateStatus(`启动识别时出错: ${e.message}`);
|
||||
recognition = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecognition() {
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Stopping recognition...');
|
||||
updateStatus("正在停止识别...");
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.stop():', e);
|
||||
recognition = null;
|
||||
updateStatus("停止时出错,已强制重置。");
|
||||
}
|
||||
} else {
|
||||
console.log('[Browser Page] Recognition not active, nothing to stop.');
|
||||
updateStatus("识别未运行。");
|
||||
}
|
||||
}
|
||||
|
||||
function forceResetRecognition() {
|
||||
console.log('[Browser Page] Force resetting recognition...');
|
||||
updateStatus("强制重置语音识别...");
|
||||
|
||||
// 先尝试停止当前的识别
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error stopping recognition during reset:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 强制设置为null,丢弃所有后续结果
|
||||
recognition = null;
|
||||
|
||||
// 通知服务器已重置
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
|
||||
}
|
||||
|
||||
updateStatus("语音识别已重置,等待新指令。");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
854
asr-server/package-lock.json
generated
854
asr-server/package-lock.json
generated
@@ -1,854 +0,0 @@
|
||||
{
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Cherry Studio ASR Server",
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
// 检查依赖项
|
||||
try {
|
||||
console.log('ASR Server starting...')
|
||||
console.log('Node.js version:', process.version)
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
console.log('Command line arguments:', process.argv)
|
||||
|
||||
// 检查必要的依赖项
|
||||
const checkDependency = (name) => {
|
||||
try {
|
||||
require(name) // Removed unused variable 'module'
|
||||
console.log(`Successfully loaded dependency: ${name}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Failed to load dependency: ${name}`, error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查所有必要的依赖项
|
||||
const dependencies = ['http', 'ws', 'express', 'path', 'fs']
|
||||
const missingDeps = dependencies.filter((dep) => !checkDependency(dep))
|
||||
|
||||
if (missingDeps.length > 0) {
|
||||
console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during dependency check:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 加载依赖项
|
||||
const http = require('http')
|
||||
const WebSocket = require('ws')
|
||||
const express = require('express')
|
||||
const path = require('path') // Need path module
|
||||
// const fs = require('fs') // Commented out unused import 'fs'
|
||||
|
||||
const app = express()
|
||||
const port = 34515 // Define the port
|
||||
|
||||
// 获取index.html文件的路径
|
||||
function getIndexHtmlPath() {
|
||||
const fs = require('fs')
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 开发环境路径
|
||||
path.join(__dirname, 'index.html'),
|
||||
// 当前目录
|
||||
path.join(process.cwd(), 'index.html'),
|
||||
// 相对于可执行文件的路径
|
||||
path.join(path.dirname(process.execPath), 'index.html'),
|
||||
// 相对于可执行文件的上级目录的路径
|
||||
path.join(path.dirname(path.dirname(process.execPath)), 'index.html'),
|
||||
// 相对于可执行文件的resources目录的路径
|
||||
path.join(path.dirname(process.execPath), 'resources', 'index.html'),
|
||||
// 相对于可执行文件的resources/asr-server目录的路径
|
||||
path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'),
|
||||
// 相对于可执行文件的asr-server目录的路径
|
||||
path.join(path.dirname(process.execPath), 'asr-server', 'index.html'),
|
||||
// 如果是pkg打包环境
|
||||
process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null
|
||||
].filter(Boolean) // 过滤掉null值
|
||||
|
||||
console.log('Possible index.html paths:', possiblePaths)
|
||||
|
||||
// 检查每个路径,返回第一个存在的文件
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
console.log(`Found index.html at: ${p}`)
|
||||
return p
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error checking existence of ${p}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到文件,返回默认路径并记录错误
|
||||
console.error('Could not find index.html in any of the expected locations')
|
||||
return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在
|
||||
}
|
||||
|
||||
// 提供网页给浏览器
|
||||
app.get('/', (req, res) => {
|
||||
try {
|
||||
const indexPath = getIndexHtmlPath()
|
||||
console.log(`Serving index.html from: ${indexPath}`)
|
||||
|
||||
// 检查文件是否存在
|
||||
const fs = require('fs')
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.error(`Error: index.html not found at ${indexPath}`)
|
||||
return res.status(404).send(`Error: index.html not found at ${indexPath}. <br>Please check the server logs.`)
|
||||
}
|
||||
|
||||
res.sendFile(indexPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error sending index.html:', err)
|
||||
res.status(500).send(`Error serving index.html: ${err.message}`)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error in route handler:', error)
|
||||
res.status(500).send(`Server error: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
const server = http.createServer(app)
|
||||
const wss = new WebSocket.Server({ server })
|
||||
|
||||
let browserConnection = null
|
||||
let electronConnection = null
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('[Server] WebSocket client connected') // Add log
|
||||
|
||||
ws.on('message', (message) => {
|
||||
let data
|
||||
try {
|
||||
// Ensure message is treated as string before parsing
|
||||
data = JSON.parse(message.toString())
|
||||
console.log('[Server] Received message:', data) // Log parsed data
|
||||
} catch (e) {
|
||||
console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e)
|
||||
return // Ignore non-JSON messages
|
||||
}
|
||||
|
||||
// 识别客户端类型
|
||||
if (data.type === 'identify') {
|
||||
if (data.role === 'browser') {
|
||||
browserConnection = ws
|
||||
console.log('[Server] Browser identified and connected')
|
||||
// Notify Electron that the browser is ready
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
|
||||
console.log('[Server] Sent browser_ready status to Electron')
|
||||
}
|
||||
// Notify Electron if it's already connected
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' }))
|
||||
}
|
||||
ws.on('close', () => {
|
||||
console.log('[Server] Browser disconnected')
|
||||
browserConnection = null
|
||||
// Notify Electron
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' }))
|
||||
}
|
||||
})
|
||||
ws.on('error', (error) => {
|
||||
console.error('[Server] Browser WebSocket error:', error)
|
||||
browserConnection = null // Assume disconnected on error
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
|
||||
}
|
||||
})
|
||||
} else if (data.role === 'electron') {
|
||||
electronConnection = ws
|
||||
console.log('[Server] Electron identified and connected')
|
||||
// If browser is already connected when Electron connects, notify Electron immediately
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
|
||||
console.log('[Server] Sent initial browser_ready status to Electron')
|
||||
}
|
||||
ws.on('close', () => {
|
||||
console.log('[Server] Electron disconnected')
|
||||
electronConnection = null
|
||||
// Maybe send stop to browser if electron disconnects?
|
||||
// if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' }));
|
||||
})
|
||||
ws.on('error', (error) => {
|
||||
console.error('[Server] Electron WebSocket error:', error)
|
||||
electronConnection = null // Assume disconnected on error
|
||||
})
|
||||
}
|
||||
}
|
||||
// Electron 控制开始/停止
|
||||
else if (data.type === 'start' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying START command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'start' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay START: Browser not connected')
|
||||
// Optionally notify Electron back
|
||||
electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' }))
|
||||
}
|
||||
} else if (data.type === 'stop' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying STOP command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'stop' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay STOP: Browser not connected')
|
||||
}
|
||||
} else if (data.type === 'reset' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying RESET command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'reset' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay RESET: Browser not connected')
|
||||
}
|
||||
}
|
||||
// 浏览器发送识别结果
|
||||
else if (data.type === 'result' && ws === browserConnection) {
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
// console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed
|
||||
electronConnection.send(JSON.stringify({ type: 'result', data: data.data }))
|
||||
} else {
|
||||
// console.log('[Server] Cannot relay RESULT: Electron not connected');
|
||||
}
|
||||
}
|
||||
// 浏览器发送状态更新 (例如 'stopped')
|
||||
else if (data.type === 'status' && ws === browserConnection) {
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: data.message }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay STATUS: Electron not connected')
|
||||
}
|
||||
} else {
|
||||
console.log('[Server] Received unknown message type or from unknown source:', data)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('error', (error) => {
|
||||
// Generic error handling for connection before identification
|
||||
console.error('[Server] Initial WebSocket connection error:', error)
|
||||
// Attempt to clean up based on which connection it might be (if identified)
|
||||
if (ws === browserConnection) {
|
||||
browserConnection = null
|
||||
if (electronConnection)
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
|
||||
} else if (ws === electronConnection) {
|
||||
electronConnection = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 添加进程错误处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Server] Uncaught exception:', error)
|
||||
// 不立即退出,给日志输出的时间
|
||||
setTimeout(() => process.exit(1), 1000)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// 尝试启动服务器
|
||||
try {
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// Handle server errors
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Server] Critical error starting server:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* 独立的ASR服务器
|
||||
* 这个文件是一个简化版的server.js,用于在打包后的应用中运行
|
||||
*/
|
||||
|
||||
// 基本依赖
|
||||
const http = require('http')
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
// 输出环境信息
|
||||
console.log('ASR Server starting...')
|
||||
console.log('Node.js version:', process.version)
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
console.log('Command line arguments:', process.argv)
|
||||
|
||||
// 创建Express应用
|
||||
const app = express()
|
||||
const port = 34515
|
||||
|
||||
// 提供静态文件
|
||||
app.use(express.static(__dirname))
|
||||
|
||||
// 提供网页给浏览器
|
||||
app.get('/', (req, res) => {
|
||||
try {
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 当前目录
|
||||
path.join(__dirname, 'index.html'),
|
||||
// 上级目录
|
||||
path.join(__dirname, '..', 'index.html'),
|
||||
// 应用根目录
|
||||
path.join(process.cwd(), 'index.html')
|
||||
]
|
||||
|
||||
console.log('Possible index.html paths:', possiblePaths)
|
||||
|
||||
// 查找第一个存在的文件
|
||||
let indexPath = null
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
indexPath = p
|
||||
console.log(`Found index.html at: ${p}`)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error checking existence of ${p}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
if (indexPath) {
|
||||
res.sendFile(indexPath)
|
||||
} else {
|
||||
// 如果找不到文件,返回一个简单的HTML页面
|
||||
console.error('Could not find index.html, serving fallback page')
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ASR Server</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 2em; }
|
||||
h1 { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ASR Server is running</h1>
|
||||
<p>This is a fallback page because the index.html file could not be found.</p>
|
||||
<p>Server is running at: http://localhost:${port}</p>
|
||||
<p>Current directory: ${__dirname}</p>
|
||||
<p>Working directory: ${process.cwd()}</p>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error serving index.html:', error)
|
||||
res.status(500).send(`Server error: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 创建HTTP服务器
|
||||
const server = http.createServer(app)
|
||||
|
||||
// 添加进程错误处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Server] Uncaught exception:', error)
|
||||
// 不立即退出,给日志输出的时间
|
||||
setTimeout(() => process.exit(1), 1000)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// 尝试启动服务器
|
||||
try {
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// 处理服务器错误
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Server] Critical error starting server:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
@echo off
|
||||
echo Starting ASR Server...
|
||||
cd /d %~dp0
|
||||
node standalone.js
|
||||
pause
|
||||
77
docs/CONTRIBUTING.zh.md
Normal file
77
docs/CONTRIBUTING.zh.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Cherry Studio 贡献者指南
|
||||
|
||||
[**English**](../CONTRIBUTING.md) | [**中文**](./CONTRIBUTING.zh.md)
|
||||
|
||||
欢迎来到 Cherry Studio 的贡献者社区!我们致力于将 Cherry Studio 打造成一个长期提供价值的项目,并希望邀请更多的开发者加入我们的行列。无论您是经验丰富的开发者还是刚刚起步的初学者,您的贡献都将帮助我们更好地服务用户,提升软件质量。
|
||||
|
||||
## 如何贡献
|
||||
|
||||
以下是您可以参与的几种方式:
|
||||
|
||||
1. **贡献代码**:帮助我们开发新功能或优化现有代码。请确保您的代码符合我们的编码标准,并通过所有测试。
|
||||
|
||||
2. **修复 BUG**:如果您发现了 BUG,欢迎提交修复方案。请在提交前确认问题已被解决,并附上相关测试。
|
||||
|
||||
3. **维护 Issue**:协助我们管理 GitHub 上的 issue,帮助标记、分类和解决问题。
|
||||
|
||||
4. **产品设计**:参与产品设计讨论,帮助我们改进用户体验和界面设计。
|
||||
|
||||
5. **编写文档**:帮助我们完善用户手册、API 文档和开发者指南。
|
||||
|
||||
6. **社区维护**:参与社区讨论,帮助解答用户问题,促进社区活跃。
|
||||
|
||||
7. **推广使用**:通过博客、社交媒体等渠道推广 Cherry Studio,吸引更多用户和开发者。
|
||||
|
||||
## 开始之前
|
||||
|
||||
请确保阅读了[行为准则](CODE_OF_CONDUCT.md)和[LICENSE](LICENSE)。
|
||||
|
||||
## 开始贡献
|
||||
|
||||
为了让您更熟悉代码,建议您处理一些标记有以下标签之一或多个的问题:[good-first-issue](https://github.com/CherryHQ/cherry-studio/labels/good%20first%20issue)、[help-wanted](https://github.com/CherryHQ/cherry-studio/labels/help%20wanted) 或 [kind/bug](https://github.com/CherryHQ/cherry-studio/labels/kind%2Fbug)。任何帮助都会收到欢迎。
|
||||
|
||||
### 测试
|
||||
|
||||
未经测试的功能等同于不存在。为确保代码真正有效,应通过单元测试和功能测试覆盖相关流程。因此,在考虑贡献时,也请考虑可测试性。所有测试均可本地运行,无需依赖 CI。请参阅[开发者指南](docs/dev.md#test)中的“Test”部分。
|
||||
|
||||
### 拉取请求的自动化测试
|
||||
|
||||
自动化测试会在 Cherry Studio 组织成员开启的拉取请求(PR)上触发,草稿 PR 除外。新贡献者开启的 PR 最初会标记为 needs-ok-to-test 标签且不自动测试。待 Cherry Studio 组织成员在 PR 上添加 /ok-to-test 后,测试通道将被创建。
|
||||
|
||||
### 考虑将您的拉取请求作为草稿打开
|
||||
|
||||
并非所有拉取请求在创建时就准备好接受审查。这可能是因为作者想发起讨论,或者他们不完全确定更改是否朝着正确的方向发展,甚至可能是因为更改尚未完成。请考虑将这些 PR 创建为[草稿拉取请求](https://github.blog/2019-02-14-introducing-draft-pull-requests/)。草稿 PR 会被CI跳过,从而节省CI资源。这也意味着审阅者不会被自动分配,社区会理解此 PR 尚未准备好接受审阅。
|
||||
在您将草稿拉取请求标记为准备审核后,审核人员将被分配
|
||||
|
||||
### 贡献者遵守项目条款
|
||||
|
||||
我们要求每位贡献者证明他们有权合法地为我们的项目做出贡献。贡献者通过有意识地签署他们的提交来表达这一点,并通过这一行为表明他们遵守许可证[LICENSE](LICENSE)。
|
||||
签名提交是指提交信息中包含以下内容的提交:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <your.email@example.com>
|
||||
```
|
||||
|
||||
您可以通过以下命令[git commit --signoff](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---signoff)生成签名提交:
|
||||
|
||||
```
|
||||
git commit --signoff -m "Your commit message"
|
||||
```
|
||||
|
||||
### 获取代码审查/合并
|
||||
|
||||
维护者在此帮助您在合理时间内实现您的用例。他们会尽力在合理时间内审查您的代码并提供建设性反馈。但如果您在审查过程中受阻,或认为您的 Pull Request 未得到应有的关注,请通过 Issue 中的评论或者[社群](README.md#-community)联系我们
|
||||
|
||||
### 其他建议
|
||||
|
||||
- **联系开发者**:在提交 PR 之前,您可以先和开发者进行联系,共同探讨或者获取帮助。
|
||||
- **成为核心开发者**:如果您能够稳定为项目贡献,恭喜您可以成为项目核心开发者,获取到项目成员身份。请查看我们的[成员指南](https://github.com/CherryHQ/community/blob/main/membership.md)
|
||||
|
||||
## 联系我们
|
||||
|
||||
如果您有任何问题或建议,欢迎通过以下方式联系我们:
|
||||
|
||||
- 微信:kangfenmao
|
||||
- [GitHub Issues](https://github.com/CherryHQ/cherry-studio/issues)
|
||||
|
||||
感谢您的支持和贡献!我们期待与您一起将 Cherry Studio 打造成更好的产品。
|
||||
@@ -24,14 +24,12 @@ https://docs.cherry-ai.com
|
||||
|
||||
# 🌠 スクリーンショット
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 🌟 主な機能
|
||||
|
||||

|
||||
|
||||
1. **多様な LLM サービス対応**:
|
||||
|
||||
- ☁️ 主要な LLM クラウドサービス対応:OpenAI、Gemini、Anthropic など
|
||||
|
||||
@@ -24,14 +24,12 @@ https://docs.cherry-ai.com
|
||||
|
||||
# 🌠 界面
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# 🌟 主要特性
|
||||
|
||||

|
||||
|
||||
1. **多样化 LLM 服务支持**:
|
||||
|
||||
- ☁️ 支持主流 LLM 云服务:OpenAI、Gemini、Anthropic、硅基流动等
|
||||
@@ -116,7 +114,7 @@ https://docs.cherry-ai.com
|
||||
3. **提交更改**:提交并推送您的更改。
|
||||
4. **打开 Pull Request**:描述您的更改和原因。
|
||||
|
||||
有关更详细的指南,请参阅我们的 [贡献指南](../CONTRIBUTING.md)。
|
||||
有关更详细的指南,请参阅我们的 [贡献指南](./CONTRIBUTING.zh.md)。
|
||||
|
||||
感谢您的支持和贡献!
|
||||
|
||||
|
||||
@@ -37,6 +37,12 @@ yarn install
|
||||
yarn dev
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
|
||||
3
docs/technical/Message.md
Normal file
3
docs/technical/Message.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 消息的生命周期
|
||||
|
||||

|
||||
127
docs/technical/how-to-use-messageBlock.md
Normal file
127
docs/technical/how-to-use-messageBlock.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# messageBlock.ts 使用指南
|
||||
|
||||
该文件定义了用于管理应用程序中所有 `MessageBlock` 实体的 Redux Slice。它使用 Redux Toolkit 的 `createSlice` 和 `createEntityAdapter` 来高效地处理规范化的状态,并提供了一系列 actions 和 selectors 用于与消息块数据交互。
|
||||
|
||||
## 核心目标
|
||||
|
||||
- **状态管理**: 集中管理所有 `MessageBlock` 的状态。`MessageBlock` 代表消息中的不同内容单元(如文本、代码、图片、引用等)。
|
||||
- **规范化**: 使用 `createEntityAdapter` 将 `MessageBlock` 数据存储在规范化的结构中(`{ ids: [], entities: {} }`),这有助于提高性能和简化更新逻辑。
|
||||
- **可预测性**: 提供明确的 actions 来修改状态,并通过 selectors 安全地访问状态。
|
||||
|
||||
## 关键概念
|
||||
|
||||
- **Slice (`createSlice`)**: Redux Toolkit 的核心 API,用于创建包含 reducer 逻辑、action creators 和初始状态的 Redux 模块。
|
||||
- **Entity Adapter (`createEntityAdapter`)**: Redux Toolkit 提供的工具,用于简化对规范化数据的 CRUD(创建、读取、更新、删除)操作。它会自动生成 reducer 函数和 selectors。
|
||||
- **Selectors**: 用于从 Redux store 中派生和计算数据的函数。Selectors 可以被记忆化(memoized),以提高性能。
|
||||
|
||||
## State 结构
|
||||
|
||||
`messageBlocks` slice 的状态结构由 `createEntityAdapter` 定义,大致如下:
|
||||
|
||||
```typescript
|
||||
{
|
||||
ids: string[]; // 存储所有 MessageBlock ID 的有序列表
|
||||
entities: { [id: string]: MessageBlock }; // 按 ID 存储 MessageBlock 对象的字典
|
||||
loadingState: 'idle' | 'loading' | 'succeeded' | 'failed'; // (可选) 其他状态,如加载状态
|
||||
error: string | null; // (可选) 错误信息
|
||||
}
|
||||
```
|
||||
|
||||
## Actions
|
||||
|
||||
该 slice 导出以下 actions (由 `createSlice` 和 `createEntityAdapter` 自动生成或自定义):
|
||||
|
||||
- **`upsertOneBlock(payload: MessageBlock)`**:
|
||||
|
||||
- 添加一个新的 `MessageBlock` 或更新一个已存在的 `MessageBlock`。如果 payload 中的 `id` 已存在,则执行更新;否则执行插入。
|
||||
|
||||
- **`upsertManyBlocks(payload: MessageBlock[])`**:
|
||||
|
||||
- 添加或更新多个 `MessageBlock`。常用于批量加载数据(例如,加载一个 Topic 的所有消息块)。
|
||||
|
||||
- **`removeOneBlock(payload: string)`**:
|
||||
|
||||
- 根据提供的 `id` (payload) 移除单个 `MessageBlock`。
|
||||
|
||||
- **`removeManyBlocks(payload: string[])`**:
|
||||
|
||||
- 根据提供的 `id` 数组 (payload) 移除多个 `MessageBlock`。常用于删除消息或清空 Topic 时清理相关的块。
|
||||
|
||||
- **`removeAllBlocks()`**:
|
||||
|
||||
- 移除 state 中的所有 `MessageBlock` 实体。
|
||||
|
||||
- **`updateOneBlock(payload: { id: string; changes: Partial<MessageBlock> })`**:
|
||||
|
||||
- 更新一个已存在的 `MessageBlock`。`payload` 需要包含块的 `id` 和一个包含要更改的字段的 `changes` 对象。
|
||||
|
||||
- **`setMessageBlocksLoading(payload: 'idle' | 'loading')`**:
|
||||
|
||||
- (自定义) 设置 `loadingState` 属性。
|
||||
|
||||
- **`setMessageBlocksError(payload: string)`**:
|
||||
- (自定义) 设置 `loadingState` 为 `'failed'` 并记录错误信息。
|
||||
|
||||
**使用示例 (在 Thunk 或其他 Dispatch 的地方):**
|
||||
|
||||
```typescript
|
||||
import { upsertOneBlock, removeManyBlocks, updateOneBlock } from './messageBlock'
|
||||
import store from './store' // 假设这是你的 Redux store 实例
|
||||
|
||||
// 添加或更新一个块
|
||||
const newBlock: MessageBlock = {
|
||||
/* ... block data ... */
|
||||
}
|
||||
store.dispatch(upsertOneBlock(newBlock))
|
||||
|
||||
// 更新一个块的内容
|
||||
store.dispatch(updateOneBlock({ id: blockId, changes: { content: 'New content' } }))
|
||||
|
||||
// 删除多个块
|
||||
const blockIdsToRemove = ['id1', 'id2']
|
||||
store.dispatch(removeManyBlocks(blockIdsToRemove))
|
||||
```
|
||||
|
||||
## Selectors
|
||||
|
||||
该 slice 导出由 `createEntityAdapter` 生成的基础 selectors,并通过 `messageBlocksSelectors` 对象访问:
|
||||
|
||||
- **`messageBlocksSelectors.selectIds(state: RootState): string[]`**: 返回包含所有块 ID 的数组。
|
||||
- **`messageBlocksSelectors.selectEntities(state: RootState): { [id: string]: MessageBlock }`**: 返回块 ID 到块对象的映射字典。
|
||||
- **`messageBlocksSelectors.selectAll(state: RootState): MessageBlock[]`**: 返回包含所有块对象的数组。
|
||||
- **`messageBlocksSelectors.selectTotal(state: RootState): number`**: 返回块的总数。
|
||||
- **`messageBlocksSelectors.selectById(state: RootState, id: string): MessageBlock | undefined`**: 根据 ID 返回单个块对象,如果找不到则返回 `undefined`。
|
||||
|
||||
**此外,还提供了一个自定义的、记忆化的 selector:**
|
||||
|
||||
- **`selectFormattedCitationsByBlockId(state: RootState, blockId: string | undefined): Citation[]`**:
|
||||
- 接收一个 `blockId`。
|
||||
- 如果该 ID 对应的块是 `CITATION` 类型,则提取并格式化其包含的引用信息(来自网页搜索、知识库等),进行去重和重新编号,最后返回一个 `Citation[]` 数组,用于在 UI 中显示。
|
||||
- 如果块不存在或类型不匹配,返回空数组 `[]`。
|
||||
- 这个 selector 封装了处理不同引用来源(Gemini, OpenAI, OpenRouter, Zhipu 等)的复杂逻辑。
|
||||
|
||||
**使用示例 (在 React 组件或 `useSelector` 中):**
|
||||
|
||||
```typescript
|
||||
import { useSelector } from 'react-redux'
|
||||
import { messageBlocksSelectors, selectFormattedCitationsByBlockId } from './messageBlock'
|
||||
import type { RootState } from './store'
|
||||
|
||||
// 获取所有块
|
||||
const allBlocks = useSelector(messageBlocksSelectors.selectAll)
|
||||
|
||||
// 获取特定 ID 的块
|
||||
const specificBlock = useSelector((state: RootState) => messageBlocksSelectors.selectById(state, someBlockId))
|
||||
|
||||
// 获取特定引用块格式化后的引用列表
|
||||
const formattedCitations = useSelector((state: RootState) => selectFormattedCitationsByBlockId(state, citationBlockId))
|
||||
|
||||
// 在组件中使用引用数据
|
||||
// {formattedCitations.map(citation => ...)}
|
||||
```
|
||||
|
||||
## 集成
|
||||
|
||||
`messageBlock.ts` slice 通常与 `messageThunk.ts` 中的 Thunks 紧密协作。Thunks 负责处理异步逻辑(如 API 调用、数据库操作),并在需要时 dispatch `messageBlock` slice 的 actions 来更新状态。例如,当 `messageThunk` 接收到流式响应时,它会 dispatch `upsertOneBlock` 或 `updateOneBlock` 来实时更新对应的 `MessageBlock`。同样,删除消息的 Thunk 会 dispatch `removeManyBlocks`。
|
||||
|
||||
理解 `messageBlock.ts` 的职责是管理**状态本身**,而 `messageThunk.ts` 负责**触发状态变更**的异步流程,这对于维护清晰的应用架构至关重要。
|
||||
105
docs/technical/how-to-use-messageThunk.md
Normal file
105
docs/technical/how-to-use-messageThunk.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# messageThunk.ts 使用指南
|
||||
|
||||
该文件包含用于管理应用程序中消息流、处理助手交互以及同步 Redux 状态与 IndexedDB 数据库的核心 Thunk Action Creators。主要围绕 `Message` 和 `MessageBlock` 对象进行操作。
|
||||
|
||||
## 核心功能
|
||||
|
||||
1. **发送/接收消息**: 处理用户消息的发送,触发助手响应,并流式处理返回的数据,将其解析为不同的 `MessageBlock`。
|
||||
2. **状态管理**: 确保 Redux store 中的消息和消息块状态与 IndexedDB 中的持久化数据保持一致。
|
||||
3. **消息操作**: 提供删除、重发、重新生成、编辑后重发、追加响应、克隆等消息生命周期管理功能。
|
||||
4. **Block 处理**: 动态创建、更新和保存各种类型的 `MessageBlock`(文本、思考过程、工具调用、引用、图片、错误、翻译等)。
|
||||
|
||||
## 主要 Thunks
|
||||
|
||||
以下是一些关键的 Thunk 函数及其用途:
|
||||
|
||||
1. **`sendMessage(userMessage, userMessageBlocks, assistant, topicId)`**
|
||||
|
||||
- **用途**: 发送一条新的用户消息。
|
||||
- **流程**:
|
||||
- 保存用户消息 (`userMessage`) 及其块 (`userMessageBlocks`) 到 Redux 和 DB。
|
||||
- 检查 `@mentions` 以确定是单模型响应还是多模型响应。
|
||||
- 创建助手消息(们)的存根 (Stub)。
|
||||
- 将存根添加到 Redux 和 DB。
|
||||
- 将核心处理逻辑 `fetchAndProcessAssistantResponseImpl` 添加到该 `topicId` 的队列中以获取实际响应。
|
||||
- **Block 相关**: 主要处理用户消息的初始 `MessageBlock` 保存。
|
||||
|
||||
2. **`fetchAndProcessAssistantResponseImpl(dispatch, getState, topicId, assistant, assistantMessage)`**
|
||||
|
||||
- **用途**: (内部函数) 获取并处理单个助手响应的核心逻辑,被 `sendMessage`, `resend...`, `regenerate...`, `append...` 等调用。
|
||||
- **流程**:
|
||||
- 设置 Topic 加载状态。
|
||||
- 准备上下文消息。
|
||||
- 调用 `fetchChatCompletion` API 服务。
|
||||
- 使用 `createStreamProcessor` 处理流式响应。
|
||||
- 通过各种回调 (`onTextChunk`, `onThinkingChunk`, `onToolCallComplete`, `onImageGenerated`, `onError`, `onComplete` 等) 处理不同类型的事件。
|
||||
- **Block 相关**:
|
||||
- 根据流事件创建初始 `UNKNOWN` 块。
|
||||
- 实时创建和更新 `MAIN_TEXT` 和 `THINKING` 块,使用 `throttledBlockUpdate` 和 `throttledBlockDbUpdate` 进行节流更新。
|
||||
- 创建 `TOOL`, `CITATION`, `IMAGE`, `ERROR` 等类型的块。
|
||||
- 在事件完成时(如 `onTextComplete`, `onToolCallComplete`)将块状态标记为 `SUCCESS` 或 `ERROR`,并使用 `saveUpdatedBlockToDB` 保存最终状态。
|
||||
- 使用 `handleBlockTransition` 管理非流式块(如 `TOOL`, `CITATION`)的添加和状态更新。
|
||||
|
||||
3. **`loadTopicMessagesThunk(topicId, forceReload)`**
|
||||
|
||||
- **用途**: 从数据库加载指定主题的所有消息及其关联的 `MessageBlock`。
|
||||
- **流程**:
|
||||
- 从 DB 获取 `Topic` 及其 `messages` 列表。
|
||||
- 根据消息 ID 列表从 DB 获取所有相关的 `MessageBlock`。
|
||||
- 使用 `upsertManyBlocks` 将块更新到 Redux。
|
||||
- 将消息更新到 Redux。
|
||||
- **Block 相关**: 负责将持久化的 `MessageBlock` 加载到 Redux 状态。
|
||||
|
||||
4. **删除 Thunks**
|
||||
|
||||
- `deleteSingleMessageThunk(topicId, messageId)`: 删除单个消息及其所有 `MessageBlock`。
|
||||
- `deleteMessageGroupThunk(topicId, askId)`: 删除一个用户消息及其所有相关的助手响应消息和它们的所有 `MessageBlock`。
|
||||
- `clearTopicMessagesThunk(topicId)`: 清空主题下的所有消息及其所有 `MessageBlock`。
|
||||
- **Block 相关**: 从 Redux 和 DB 中移除指定的 `MessageBlock`。
|
||||
|
||||
5. **重发/重新生成 Thunks**
|
||||
|
||||
- `resendMessageThunk(topicId, userMessageToResend, assistant)`: 重发用户消息。会重置(清空 Block 并标记为 PENDING)所有与该用户消息关联的助手响应,然后重新请求生成。
|
||||
- `resendUserMessageWithEditThunk(topicId, originalMessage, mainTextBlockId, editedContent, assistant)`: 用户编辑消息内容后重发。先更新用户消息的 `MAIN_TEXT` 块内容,然后调用 `resendMessageThunk`。
|
||||
- `regenerateAssistantResponseThunk(topicId, assistantMessageToRegenerate, assistant)`: 重新生成单个助手响应。重置该助手消息(清空 Block 并标记为 PENDING),然后重新请求生成。
|
||||
- **Block 相关**: 删除旧的 `MessageBlock`,并在重新生成过程中创建新的 `MessageBlock`。
|
||||
|
||||
6. **`appendAssistantResponseThunk(topicId, existingAssistantMessageId, newModel, assistant)`**
|
||||
|
||||
- **用途**: 在已有的对话上下文中,针对同一个用户问题,使用新选择的模型追加一个新的助手响应。
|
||||
- **流程**:
|
||||
- 找到现有助手消息以获取原始 `askId`。
|
||||
- 创建使用 `newModel` 的新助手消息存根(使用相同的 `askId`)。
|
||||
- 添加新存根到 Redux 和 DB。
|
||||
- 将 `fetchAndProcessAssistantResponseImpl` 添加到队列以生成新响应。
|
||||
- **Block 相关**: 为新的助手响应创建全新的 `MessageBlock`。
|
||||
|
||||
7. **`cloneMessagesToNewTopicThunk(sourceTopicId, branchPointIndex, newTopic)`**
|
||||
|
||||
- **用途**: 将源主题的部分消息(及其 Block)克隆到一个**已存在**的新主题中。
|
||||
- **流程**:
|
||||
- 复制指定索引前的消息。
|
||||
- 为所有克隆的消息和 Block 生成新的 UUID。
|
||||
- 正确映射克隆消息之间的 `askId` 关系。
|
||||
- 复制 `MessageBlock` 内容,更新其 `messageId` 指向新的消息 ID。
|
||||
- 更新文件引用计数(如果 Block 是文件或图片)。
|
||||
- 将克隆的消息和 Block 保存到新主题的 Redux 状态和 DB 中。
|
||||
- **Block 相关**: 创建 `MessageBlock` 的副本,并更新其 ID 和 `messageId`。
|
||||
|
||||
8. **`initiateTranslationThunk(messageId, topicId, targetLanguage, sourceBlockId?, sourceLanguage?)`**
|
||||
- **用途**: 为指定消息启动翻译流程,创建一个初始的 `TRANSLATION` 类型的 `MessageBlock`。
|
||||
- **流程**:
|
||||
- 创建一个状态为 `STREAMING` 的 `TranslationMessageBlock`。
|
||||
- 将其添加到 Redux 和 DB。
|
||||
- 更新原消息的 `blocks` 列表以包含新的翻译块 ID。
|
||||
- **Block 相关**: 创建并保存一个占位的 `TranslationMessageBlock`。实际翻译内容的获取和填充需要后续步骤。
|
||||
|
||||
## 内部机制和注意事项
|
||||
|
||||
- **数据库交互**: 通过 `saveMessageAndBlocksToDB`, `updateExistingMessageAndBlocksInDB`, `saveUpdatesToDB`, `saveUpdatedBlockToDB`, `throttledBlockDbUpdate` 等辅助函数与 IndexedDB (`db`) 交互,确保数据持久化。
|
||||
- **状态同步**: Thunks 负责协调 Redux Store 和 IndexedDB 之间的数据一致性。
|
||||
- **队列 (`getTopicQueue`)**: 使用 `AsyncQueue` 确保对同一主题的操作(尤其是 API 请求)按顺序执行,避免竞态条件。
|
||||
- **节流 (`throttle`)**: 对流式响应中频繁的 Block 更新(文本、思考)使用 `lodash.throttle` 优化性能,减少 Redux dispatch 和 DB 写入次数。
|
||||
- **错误处理**: `fetchAndProcessAssistantResponseImpl` 内的回调函数(特别是 `onError`)处理流处理和 API 调用中可能出现的错误,并创建 `ERROR` 类型的 `MessageBlock`。
|
||||
|
||||
开发者在使用这些 Thunks 时,通常需要提供 `dispatch`, `getState` (由 Redux Thunk 中间件注入),以及如 `topicId`, `assistant` 配置对象, 相关的 `Message` 或 `MessageBlock` 对象/ID 等参数。理解每个 Thunk 的职责和它如何影响消息及块的状态至关重要。
|
||||
156
docs/technical/how-to-use-useMessageOperations.md
Normal file
156
docs/technical/how-to-use-useMessageOperations.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# useMessageOperations.ts 使用指南
|
||||
|
||||
该文件定义了一个名为 `useMessageOperations` 的自定义 React Hook。这个 Hook 的主要目的是为 React 组件提供一个便捷的接口,用于执行与特定主题(Topic)相关的各种消息操作。它封装了调用 Redux Thunks (`messageThunk.ts`) 和 Actions (`newMessage.ts`, `messageBlock.ts`) 的逻辑,简化了组件与消息数据交互的代码。
|
||||
|
||||
## 核心目标
|
||||
|
||||
- **封装**: 将复杂的消息操作逻辑(如删除、重发、重新生成、编辑、翻译等)封装在易于使用的函数中。
|
||||
- **简化**: 让组件可以直接调用这些操作函数,而无需直接与 Redux `dispatch` 或 Thunks 交互。
|
||||
- **上下文关联**: 所有操作都与传入的 `topic` 对象相关联,确保操作作用于正确的主题。
|
||||
|
||||
## 如何使用
|
||||
|
||||
在你的 React 函数组件中,导入并调用 `useMessageOperations` Hook,并传入当前活动的 `Topic` 对象。
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations';
|
||||
import type { Topic, Message, Assistant, Model } from '@renderer/types';
|
||||
|
||||
interface MyComponentProps {
|
||||
currentTopic: Topic;
|
||||
currentAssistant: Assistant;
|
||||
}
|
||||
|
||||
function MyComponent({ currentTopic, currentAssistant }: MyComponentProps) {
|
||||
const {
|
||||
deleteMessage,
|
||||
resendMessage,
|
||||
regenerateAssistantMessage,
|
||||
appendAssistantResponse,
|
||||
getTranslationUpdater,
|
||||
createTopicBranch,
|
||||
// ... 其他操作函数
|
||||
} = useMessageOperations(currentTopic);
|
||||
|
||||
const handleDelete = (messageId: string) => {
|
||||
deleteMessage(messageId);
|
||||
};
|
||||
|
||||
const handleResend = (message: Message) => {
|
||||
resendMessage(message, currentAssistant);
|
||||
};
|
||||
|
||||
const handleAppend = (existingMsg: Message, newModel: Model) => {
|
||||
appendAssistantResponse(existingMsg, newModel, currentAssistant);
|
||||
}
|
||||
|
||||
// ... 在组件中使用其他操作函数
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Component UI */}
|
||||
<button onClick={() => handleDelete('some-message-id')}>Delete Message</button>
|
||||
{/* ... */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 返回值
|
||||
|
||||
`useMessageOperations(topic)` Hook 返回一个包含以下函数和值的对象:
|
||||
|
||||
- **`deleteMessage(id: string)`**:
|
||||
|
||||
- 删除指定 `id` 的单个消息。
|
||||
- 内部调用 `deleteSingleMessageThunk`。
|
||||
|
||||
- **`deleteGroupMessages(askId: string)`**:
|
||||
|
||||
- 删除与指定 `askId` 相关联的一组消息(通常是用户提问及其所有助手回答)。
|
||||
- 内部调用 `deleteMessageGroupThunk`。
|
||||
|
||||
- **`editMessage(messageId: string, updates: Partial<Message>)`**:
|
||||
|
||||
- 更新指定 `messageId` 的消息的部分属性。
|
||||
- **注意**: 目前主要用于更新 Redux 状态
|
||||
- 内部调用 `newMessagesActions.updateMessage`。
|
||||
|
||||
- **`resendMessage(message: Message, assistant: Assistant)`**:
|
||||
|
||||
- 重新发送指定的用户消息 (`message`),这将触发其所有关联助手响应的重新生成。
|
||||
- 内部调用 `resendMessageThunk`。
|
||||
|
||||
- **`resendUserMessageWithEdit(message: Message, editedContent: string, assistant: Assistant)`**:
|
||||
|
||||
- 在用户消息的主要文本块被编辑后,重新发送该消息。
|
||||
- 会先查找消息的 `MAIN_TEXT` 块 ID,然后调用 `resendUserMessageWithEditThunk`。
|
||||
|
||||
- **`clearTopicMessages(_topicId?: string)`**:
|
||||
|
||||
- 清除当前主题(或可选的指定 `_topicId`)下的所有消息。
|
||||
- 内部调用 `clearTopicMessagesThunk`。
|
||||
|
||||
- **`createNewContext()`**:
|
||||
|
||||
- 发出一个全局事件 (`EVENT_NAMES.NEW_CONTEXT`),通常用于通知 UI 清空显示,准备新的上下文。不直接修改 Redux 状态。
|
||||
|
||||
- **`displayCount`**:
|
||||
|
||||
- (非操作函数) 从 Redux store 中获取当前的 `displayCount` 值。
|
||||
|
||||
- **`pauseMessages()`**:
|
||||
|
||||
- 尝试中止当前主题中正在进行的消息生成(状态为 `processing` 或 `pending`)。
|
||||
- 通过查找相关的 `askId` 并调用 `abortCompletion` 来实现。
|
||||
- 同时会 dispatch `setTopicLoading` action 将加载状态设为 `false`。
|
||||
|
||||
- **`resumeMessage(message: Message, assistant: Assistant)`**:
|
||||
|
||||
- 恢复/重新发送一个用户消息。目前实现为直接调用 `resendMessage`。
|
||||
|
||||
- **`regenerateAssistantMessage(message: Message, assistant: Assistant)`**:
|
||||
|
||||
- 重新生成指定的**助手**消息 (`message`) 的响应。
|
||||
- 内部调用 `regenerateAssistantResponseThunk`。
|
||||
|
||||
- **`appendAssistantResponse(existingAssistantMessage: Message, newModel: Model, assistant: Assistant)`**:
|
||||
|
||||
- 针对 `existingAssistantMessage` 所回复的**同一用户提问**,使用 `newModel` 追加一个新的助手响应。
|
||||
- 内部调用 `appendAssistantResponseThunk`。
|
||||
|
||||
- **`getTranslationUpdater(messageId: string, targetLanguage: string, sourceBlockId?: string, sourceLanguage?: string)`**:
|
||||
|
||||
- **用途**: 获取一个用于逐步更新翻译块内容的函数。
|
||||
- **流程**:
|
||||
1. 内部调用 `initiateTranslationThunk` 来创建或获取一个 `TRANSLATION` 类型的 `MessageBlock`,并获取其 `blockId`。
|
||||
2. 返回一个**异步更新函数**。
|
||||
- **返回的更新函数 `(accumulatedText: string, isComplete?: boolean) => void`**:
|
||||
- 接收累积的翻译文本和完成状态。
|
||||
- 调用 `updateOneBlock` 更新 Redux 中的翻译块内容和状态 (`STREAMING` 或 `SUCCESS`)。
|
||||
- 调用 `throttledBlockDbUpdate` 将更新(节流地)保存到数据库。
|
||||
- 如果初始化失败(Thunk 返回 `undefined`),则此函数返回 `null`。
|
||||
|
||||
- **`createTopicBranch(sourceTopicId: string, branchPointIndex: number, newTopic: Topic)`**:
|
||||
- 创建一个主题分支,将 `sourceTopicId` 主题中 `branchPointIndex` 索引之前的消息克隆到 `newTopic` 中。
|
||||
- **注意**: `newTopic` 对象必须是调用此函数**之前**已经创建并添加到 Redux 和数据库中的。
|
||||
- 内部调用 `cloneMessagesToNewTopicThunk`。
|
||||
|
||||
## 依赖
|
||||
|
||||
- **`topic: Topic`**: 必须传入当前操作上下文的主题对象。Hook 返回的操作函数将始终作用于这个主题的 `topic.id`。
|
||||
- **Redux `dispatch`**: Hook 内部使用 `useAppDispatch` 获取 `dispatch` 函数来调用 actions 和 thunks。
|
||||
|
||||
## 相关 Hooks
|
||||
|
||||
在同一文件中还定义了两个辅助 Hook:
|
||||
|
||||
- **`useTopicMessages(topic: Topic)`**:
|
||||
|
||||
- 使用 `selectMessagesForTopic` selector 来获取并返回指定主题的消息列表。
|
||||
|
||||
- **`useTopicLoading(topic: Topic)`**:
|
||||
- 使用 `selectNewTopicLoading` selector 来获取并返回指定主题的加载状态。
|
||||
|
||||
这些 Hook 可以与 `useMessageOperations` 结合使用,方便地在组件中获取消息数据、加载状态,并执行相关操作。
|
||||
BIN
docs/technical/message-lifecycle.png
Normal file
BIN
docs/technical/message-lifecycle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 563 KiB |
@@ -3,14 +3,15 @@ productName: Cherry Studio
|
||||
electronLanguages:
|
||||
- zh-CN
|
||||
- zh-TW
|
||||
- en-GB
|
||||
- en-US
|
||||
- ru
|
||||
- ja # macOS/linux/win
|
||||
- ru # macOS/linux/win
|
||||
- zh_CN # for macOS
|
||||
- zh_TW # for macOS
|
||||
- en # for macOS
|
||||
directories:
|
||||
buildResources: build
|
||||
files:
|
||||
- out/**/*
|
||||
- package.json
|
||||
- '!{.vscode,.yarn,.github}'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
@@ -35,18 +36,9 @@ files:
|
||||
- '!node_modules/@tavily/core/node_modules/js-tiktoken'
|
||||
- '!node_modules/pdf-parse/lib/pdf.js/{v1.9.426,v1.10.88,v2.0.550}'
|
||||
- '!node_modules/mammoth/{mammoth.browser.js,mammoth.browser.min.js}'
|
||||
asarUnpack: # Removed ASR server rules from 'files' section
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
- '**/*.{metal,exp,lib}'
|
||||
extraResources: # Add extraResources to copy the prepared asr-server directory
|
||||
- from: asr-server # Copy the folder from project root
|
||||
to: app/asr-server # Copy TO the 'app' subfolder within resources
|
||||
filter:
|
||||
- '**/*' # Include everything inside
|
||||
- from: resources/data # Copy the data folder with agents.json
|
||||
to: data # Copy TO the 'data' subfolder within resources
|
||||
filter:
|
||||
- '**/*' # Include everything inside
|
||||
win:
|
||||
executableName: Cherry Studio
|
||||
artifactName: ${productName}-${version}-${arch}-setup.${ext}
|
||||
@@ -82,6 +74,11 @@ linux:
|
||||
- target: AppImage
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
desktop:
|
||||
entry:
|
||||
StartupWMClass: CherryStudio
|
||||
mimeTypes:
|
||||
- x-scheme-handler/cherrystudio
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://releases.cherry-ai.com
|
||||
@@ -92,6 +89,9 @@ afterSign: scripts/notarize.js
|
||||
artifactBuildCompleted: scripts/artifact-build-completed.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
全新图标风格
|
||||
新的智能体界面
|
||||
WebDAV 增加文件管理功能
|
||||
新增对 grok-2-image 和 gpt-4o-image 图像支持
|
||||
支持 Windows 便携版使用 data 目录存储数据
|
||||
MCP 界面改版,新增描述信息显示
|
||||
Mermaid 渲染逻辑优化
|
||||
支持关闭公示渲染
|
||||
修复 OpenAI 类型渲染错误
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import viteReact from '@vitejs/plugin-react'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import { resolve } from 'path'
|
||||
import { visualizer } from 'rollup-plugin-visualizer'
|
||||
@@ -6,7 +6,7 @@ import { visualizer } from 'rollup-plugin-visualizer'
|
||||
const visualizerPlugin = (type: 'renderer' | 'main') => {
|
||||
return process.env[`VISUALIZER_${type.toUpperCase()}`] ? [visualizer({ open: true })] : []
|
||||
}
|
||||
// const viteReact = await import('@vitejs/plugin-react')
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
plugins: [
|
||||
@@ -51,20 +51,18 @@ export default defineConfig({
|
||||
},
|
||||
renderer: {
|
||||
plugins: [
|
||||
viteReact({
|
||||
babel: {
|
||||
plugins: [
|
||||
[
|
||||
'styled-components',
|
||||
{
|
||||
displayName: true, // 开发环境下启用组件名称
|
||||
fileName: false, // 不在类名中包含文件名
|
||||
pure: true, // 优化性能
|
||||
ssr: false // 不需要服务端渲染
|
||||
}
|
||||
]
|
||||
react({
|
||||
plugins: [
|
||||
[
|
||||
'@swc/plugin-styled-components',
|
||||
{
|
||||
displayName: true, // 开发环境下启用组件名称
|
||||
fileName: false, // 不在类名中包含文件名
|
||||
pure: true, // 优化性能
|
||||
ssr: false // 不需要服务端渲染
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}),
|
||||
...visualizerPlugin('renderer')
|
||||
],
|
||||
@@ -76,17 +74,6 @@ export default defineConfig({
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: []
|
||||
},
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve('src/renderer/index.html')
|
||||
}
|
||||
},
|
||||
// 复制ASR服务器文件
|
||||
assetsInlineLimit: 0,
|
||||
// 确保复制assets目录下的所有文件
|
||||
copyPublicDir: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
425
index.html
425
index.html
@@ -1,425 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cherry Studio ASR</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 1em;
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.5em;
|
||||
min-height: 50px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>浏览器语音识别中继页面</h1>
|
||||
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
|
||||
<div id="status">正在连接到服务器...</div>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
const statusDiv = document.getElementById('status');
|
||||
const resultDiv = document.getElementById('result');
|
||||
// 尝试连接到WebSocket服务器
|
||||
let ws;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 5;
|
||||
const reconnectInterval = 2000; // 2秒
|
||||
|
||||
function connectWebSocket() {
|
||||
try {
|
||||
ws = new WebSocket('ws://localhost:34515');
|
||||
|
||||
ws.onopen = () => {
|
||||
reconnectAttempts = 0;
|
||||
updateStatus('已连接到服务器,等待指令...');
|
||||
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
|
||||
};
|
||||
|
||||
ws.onmessage = handleMessage;
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[Browser Page] WebSocket Error:', error);
|
||||
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[Browser Page] WebSocket Connection Closed');
|
||||
updateStatus('与服务器断开连接。尝试重新连接...');
|
||||
stopRecognition();
|
||||
|
||||
// 尝试重新连接
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
updateStatus(`与服务器断开连接。尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`);
|
||||
setTimeout(connectWebSocket, reconnectInterval);
|
||||
} else {
|
||||
updateStatus('无法连接到服务器。请刷新页面或重启应用。');
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Browser Page] Error creating WebSocket:', error);
|
||||
updateStatus('创建WebSocket连接时出错。请刷新页面或重启应用。');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始连接
|
||||
connectWebSocket();
|
||||
let recognition = null;
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
function updateStatus(message) {
|
||||
console.log(`[Browser Page Status] ${message}`);
|
||||
statusDiv.textContent = message;
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.data);
|
||||
console.log('[Browser Page] Received command:', data);
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Received non-JSON message:', event.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'start') {
|
||||
startRecognition();
|
||||
} else if (data.type === 'stop') {
|
||||
stopRecognition();
|
||||
} else if (data.type === 'reset') {
|
||||
// 强制重置语音识别
|
||||
forceResetRecognition();
|
||||
} else {
|
||||
console.warn('[Browser Page] Received unknown command type:', data.type);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
function setupRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:此浏览器不支持 Web Speech API。');
|
||||
return false;
|
||||
}
|
||||
if (recognition && recognition.recognizing) {
|
||||
console.log('[Browser Page] Recognition already active.');
|
||||
return true;
|
||||
}
|
||||
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.lang = 'zh-CN';
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
// 增加以下设置提高语音识别的可靠性
|
||||
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
|
||||
// 设置较短的语音识别时间,使用户能更快地看到结果
|
||||
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
|
||||
try {
|
||||
// @ts-ignore
|
||||
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
|
||||
} catch (e) {
|
||||
console.log('[Browser Page] audioStart property not supported');
|
||||
}
|
||||
|
||||
recognition.onstart = () => {
|
||||
updateStatus("🎤 正在识别...");
|
||||
console.log('[Browser Page] SpeechRecognition started.');
|
||||
};
|
||||
|
||||
recognition.onresult = (event) => {
|
||||
console.log('[Browser Page] Recognition result event:', event);
|
||||
|
||||
let interim_transcript = '';
|
||||
let final_transcript = '';
|
||||
|
||||
// 输出识别结果的详细信息便于调试
|
||||
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
||||
const confidence = event.results[i][0].confidence;
|
||||
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
|
||||
|
||||
if (event.results[i].isFinal) {
|
||||
final_transcript += event.results[i][0].transcript;
|
||||
} else {
|
||||
interim_transcript += event.results[i][0].transcript;
|
||||
}
|
||||
}
|
||||
|
||||
const resultText = final_transcript || interim_transcript;
|
||||
resultDiv.textContent = resultText;
|
||||
|
||||
// 更新状态显示
|
||||
if (resultText) {
|
||||
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
|
||||
}
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
|
||||
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onerror = (event) => {
|
||||
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
|
||||
|
||||
// 根据错误类型提供更友好的错误提示
|
||||
let errorMessage = '';
|
||||
switch (event.error) {
|
||||
case 'no-speech':
|
||||
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Restarting recognition after no-speech error');
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
break;
|
||||
case 'audio-capture':
|
||||
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
|
||||
break;
|
||||
case 'not-allowed':
|
||||
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
|
||||
break;
|
||||
case 'network':
|
||||
errorMessage = '网络错误导致语音识别失败。';
|
||||
break;
|
||||
case 'aborted':
|
||||
errorMessage = '语音识别被用户或系统中止。';
|
||||
break;
|
||||
default:
|
||||
errorMessage = `识别错误: ${event.error}`;
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMessage}`);
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: {
|
||||
error: event.error,
|
||||
message: errorMessage || event.message || `Recognition error: ${event.error}`
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
console.log('[Browser Page] SpeechRecognition ended.');
|
||||
|
||||
// 检查是否是由于错误或用户手动停止导致的结束
|
||||
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
|
||||
|
||||
if (!isErrorOrStopped) {
|
||||
// 如果不是由于错误或手动停止,则自动重新启动语音识别
|
||||
updateStatus("识别暂停,正在重新启动...");
|
||||
|
||||
// 保存当前的recognition对象
|
||||
const currentRecognition = recognition;
|
||||
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (currentRecognition && currentRecognition === recognition) {
|
||||
currentRecognition.start();
|
||||
console.log('[Browser Page] Automatically restarting recognition');
|
||||
} else {
|
||||
// 如果recognition对象已经变化,重新创建一个
|
||||
setupRecognition();
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Created new recognition instance and started');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
|
||||
}
|
||||
|
||||
// 只有在手动停止或错误时才重置recognition对象
|
||||
recognition = null;
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
function startRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:浏览器不支持 Web Speech API。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示正在准备的状态
|
||||
updateStatus('正在准备麦克风...');
|
||||
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Recognition already exists, stopping first.');
|
||||
stopRecognition();
|
||||
}
|
||||
|
||||
if (!setupRecognition()) return;
|
||||
|
||||
console.log('[Browser Page] Attempting to start recognition...');
|
||||
try {
|
||||
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
|
||||
const micPermissionTimeout = setTimeout(() => {
|
||||
updateStatus('获取麦克风权限超时,请刷新页面重试。');
|
||||
}, 10000); // 10秒超时
|
||||
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
}
|
||||
})
|
||||
.then(stream => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.log('[Browser Page] Microphone access granted.');
|
||||
|
||||
// 检查麦克风音量级别
|
||||
const audioContext = new AudioContext();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
const microphone = audioContext.createMediaStreamSource(stream);
|
||||
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
|
||||
|
||||
analyser.smoothingTimeConstant = 0.8;
|
||||
analyser.fftSize = 1024;
|
||||
|
||||
microphone.connect(analyser);
|
||||
analyser.connect(javascriptNode);
|
||||
javascriptNode.connect(audioContext.destination);
|
||||
|
||||
javascriptNode.onaudioprocess = function () {
|
||||
const array = new Uint8Array(analyser.frequencyBinCount);
|
||||
analyser.getByteFrequencyData(array);
|
||||
let values = 0;
|
||||
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
values += (array[i]);
|
||||
}
|
||||
|
||||
const average = values / length;
|
||||
console.log('[Browser Page] Microphone volume level:', average);
|
||||
|
||||
// 如果音量太低,显示提示
|
||||
if (average < 5) {
|
||||
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
|
||||
} else {
|
||||
updateStatus('🎤 正在识别...');
|
||||
}
|
||||
|
||||
// 只检查一次就断开连接
|
||||
microphone.disconnect();
|
||||
analyser.disconnect();
|
||||
javascriptNode.disconnect();
|
||||
};
|
||||
|
||||
// 释放测试用的音频流
|
||||
setTimeout(() => {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
audioContext.close();
|
||||
}, 1000);
|
||||
|
||||
// 启动语音识别
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
updateStatus('🎤 正在识别...');
|
||||
} else {
|
||||
updateStatus('错误:Recognition 实例丢失。');
|
||||
console.error('[Browser Page] Recognition instance lost before start.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.error('[Browser Page] Microphone access error:', err);
|
||||
|
||||
let errorMsg = `无法访问麦克风 (${err.name})`;
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMsg}`);
|
||||
recognition = null;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.start():', e);
|
||||
updateStatus(`启动识别时出错: ${e.message}`);
|
||||
recognition = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecognition() {
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Stopping recognition...');
|
||||
updateStatus("正在停止识别...");
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.stop():', e);
|
||||
recognition = null;
|
||||
updateStatus("停止时出错,已强制重置。");
|
||||
}
|
||||
} else {
|
||||
console.log('[Browser Page] Recognition not active, nothing to stop.');
|
||||
updateStatus("识别未运行。");
|
||||
}
|
||||
}
|
||||
|
||||
function forceResetRecognition() {
|
||||
console.log('[Browser Page] Force resetting recognition...');
|
||||
updateStatus("强制重置语音识别...");
|
||||
|
||||
// 先尝试停止当前的识别
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error stopping recognition during reset:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 强制设置为null,丢弃所有后续结果
|
||||
recognition = null;
|
||||
|
||||
// 通知服务器已重置
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
|
||||
}
|
||||
|
||||
updateStatus("语音识别已重置,等待新指令。");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
53
package.json
53
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "1.2.5",
|
||||
"version": "1.2.10",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -44,7 +44,12 @@
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"check:i18n": "node scripts/check-i18n.js",
|
||||
"test": "npx -y tsx --test src/**/*.test.ts",
|
||||
"test": "yarn test:renderer",
|
||||
"test:coverage": "yarn test:renderer:coverage",
|
||||
"test:node": "npx -y tsx --test src/**/*.test.ts",
|
||||
"test:renderer": "vitest run",
|
||||
"test:renderer:ui": "vitest --ui",
|
||||
"test:renderer:coverage": "vitest run --coverage",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
@@ -65,32 +70,30 @@
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@electron/notarize": "^2.5.0",
|
||||
"@langchain/community": "^0.3.36",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@strongtz/win32-arm64-msvc": "^0.4.7",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"adm-zip": "^0.5.16",
|
||||
"archiver": "^7.0.1",
|
||||
"async-mutex": "^0.5.0",
|
||||
"bufferutil": "^4.0.9",
|
||||
"color": "^5.0.0",
|
||||
"diff": "^7.0.0",
|
||||
"docx": "^9.0.2",
|
||||
"edge-tts-node": "^1.5.7",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-updater": "patch:electron-updater@npm%3A6.6.3#~/.yarn/patches/electron-updater-npm-6.6.3-9269dbaf84.patch",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||
"extract-zip": "^2.0.1",
|
||||
"fast-xml-parser": "^5.2.0",
|
||||
"fetch-socks": "^1.3.2",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got-scraping": "^4.1.1",
|
||||
"jsdom": "^26.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"node-edge-tts": "^1.2.8",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"os-proxy-config": "^1.1.2",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"tar": "^7.4.3",
|
||||
"turndown": "^7.2.0",
|
||||
@@ -104,7 +107,6 @@
|
||||
"@agentic/exa": "^7.3.3",
|
||||
"@agentic/searxng": "^7.3.3",
|
||||
"@agentic/tavily": "^7.3.3",
|
||||
"@analytics/google-analytics": "^1.1.0",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"@anthropic-ai/sdk": "^0.38.0",
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
@@ -114,19 +116,21 @@
|
||||
"@emotion/is-prop-valid": "^1.3.1",
|
||||
"@eslint-react/eslint-plugin": "^1.36.1",
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@google/genai": "patch:@google/genai@npm%3A0.8.0#~/.yarn/patches/@google-genai-npm-0.8.0-450d0d9a7d.patch",
|
||||
"@google/genai": "^0.10.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"@notionhq/client": "^2.2.15",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@shikijs/markdown-it": "^3.2.2",
|
||||
"@swc/plugin-styled-components": "^7.1.3",
|
||||
"@tavily/core": "patch:@tavily/core@npm%3A0.3.1#~/.yarn/patches/@tavily-core-npm-0.3.1-fe69bf2bea.patch",
|
||||
"@tryfabric/martian": "^1.2.4",
|
||||
"@types/adm-zip": "^0",
|
||||
"@types/diff": "^7",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/js-yaml": "^4",
|
||||
"@types/lodash": "^4.17.16",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^18.19.9",
|
||||
@@ -136,8 +140,10 @@
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@types/ws": "^8",
|
||||
"@vitejs/plugin-react": "4.3.4",
|
||||
"analytics": "^0.8.16",
|
||||
"@vitejs/plugin-react-swc": "^3.9.0",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"@xyflow/react": "^12.4.4",
|
||||
"antd": "^5.22.5",
|
||||
"applescript": "^1.0.0",
|
||||
"axios": "^1.7.3",
|
||||
@@ -167,7 +173,7 @@
|
||||
"lucide-react": "^0.487.0",
|
||||
"mime": "^4.0.4",
|
||||
"npx-scope-finder": "^1.2.0",
|
||||
"openai": "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch",
|
||||
"openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"p-queue": "^8.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"rc-virtual-list": "^3.18.5",
|
||||
@@ -186,13 +192,12 @@
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-mathjax": "^7.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"remark-cjk-friendly": "^1.1.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"sass": "^1.77.2",
|
||||
"shiki": "^3.2.1",
|
||||
"shiki": "^3.2.2",
|
||||
"string-width": "^7.2.0",
|
||||
"styled-components": "^6.1.11",
|
||||
"tiny-pinyin": "^1.3.2",
|
||||
@@ -200,7 +205,8 @@
|
||||
"tokenx": "^0.4.1",
|
||||
"typescript": "^5.6.2",
|
||||
"uuid": "^10.0.0",
|
||||
"vite": "6.2.6"
|
||||
"vite": "6.2.6",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"pdf-parse@npm:1.1.1": "patch:pdf-parse@npm%3A1.1.1#~/.yarn/patches/pdf-parse-npm-1.1.1-04a6109b2a.patch",
|
||||
@@ -208,8 +214,11 @@
|
||||
"@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch",
|
||||
"node-gyp": "^9.1.0",
|
||||
"libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch",
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch",
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch"
|
||||
"openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch",
|
||||
"pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch",
|
||||
"app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch",
|
||||
"shiki": "3.2.2",
|
||||
"openai@npm:^4.87.3": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.6.0",
|
||||
"lint-staged": {
|
||||
|
||||
@@ -20,16 +20,7 @@ export enum IpcChannel {
|
||||
App_InstallUvBinary = 'app:install-uv-binary',
|
||||
App_InstallBunBinary = 'app:install-bun-binary',
|
||||
|
||||
// ASR Server
|
||||
Asr_StartServer = 'start-asr-server',
|
||||
Asr_StopServer = 'stop-asr-server',
|
||||
|
||||
// MsTTS
|
||||
MsTTS_GetVoices = 'mstts:get-voices',
|
||||
MsTTS_Synthesize = 'mstts:synthesize',
|
||||
MsTTS_SynthesizeStream = 'mstts:synthesize-stream',
|
||||
MsTTS_StreamData = 'mstts:stream-data',
|
||||
MsTTS_StreamEnd = 'mstts:stream-end',
|
||||
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
||||
|
||||
// Open
|
||||
Open_Path = 'open:path',
|
||||
@@ -47,6 +38,7 @@ export enum IpcChannel {
|
||||
MiniWindow_SetPin = 'miniwindow:set-pin',
|
||||
|
||||
// Mcp
|
||||
Mcp_AddServer = 'mcp:add-server',
|
||||
Mcp_RemoveServer = 'mcp:remove-server',
|
||||
Mcp_RestartServer = 'mcp:restart-server',
|
||||
Mcp_StopServer = 'mcp:stop-server',
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* 内置的ASR服务器模块
|
||||
* 这个文件可以直接在Electron中运行,不需要外部依赖
|
||||
*/
|
||||
|
||||
// 使用Electron内置的Node.js模块
|
||||
const http = require('http')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
// 输出环境信息
|
||||
console.log('ASR Server (Embedded) starting...')
|
||||
console.log('Node.js version:', process.version)
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
console.log('Command line arguments:', process.argv)
|
||||
|
||||
// 创建HTTP服务器
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
if (req.url === '/' || req.url === '/index.html') {
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 当前目录
|
||||
path.join(__dirname, 'index.html'),
|
||||
// 上级目录
|
||||
path.join(__dirname, '..', 'index.html'),
|
||||
// 应用根目录
|
||||
path.join(process.cwd(), 'index.html')
|
||||
]
|
||||
|
||||
console.log('Possible index.html paths:', possiblePaths)
|
||||
|
||||
// 查找第一个存在的文件
|
||||
let indexPath = null
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
indexPath = p
|
||||
console.log(`Found index.html at: ${p}`)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error checking existence of ${p}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
if (indexPath) {
|
||||
// 读取文件内容并发送
|
||||
fs.readFile(indexPath, (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error reading index.html:', err)
|
||||
res.writeHead(500)
|
||||
res.end('Error reading index.html')
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end(data)
|
||||
})
|
||||
} else {
|
||||
// 如果找不到文件,返回一个简单的HTML页面
|
||||
console.error('Could not find index.html, serving fallback page')
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ASR Server</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 2em; }
|
||||
h1 { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ASR Server is running</h1>
|
||||
<p>This is a fallback page because the index.html file could not be found.</p>
|
||||
<p>Server is running at: http://localhost:34515</p>
|
||||
<p>Current directory: ${__dirname}</p>
|
||||
<p>Working directory: ${process.cwd()}</p>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
} else {
|
||||
// 处理其他请求
|
||||
res.writeHead(404)
|
||||
res.end('Not found')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling request:', error)
|
||||
res.writeHead(500)
|
||||
res.end('Server error')
|
||||
}
|
||||
})
|
||||
|
||||
// 添加进程错误处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Server] Uncaught exception:', error)
|
||||
// 不立即退出,给日志输出的时间
|
||||
setTimeout(() => process.exit(1), 1000)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// 尝试启动服务器
|
||||
try {
|
||||
const port = 34515
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// 处理服务器错误
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Server] Critical error starting server:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cherry Studio ASR</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 1em;
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.5em;
|
||||
min-height: 50px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>浏览器语音识别中继页面</h1>
|
||||
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
|
||||
<div id="status">正在连接到服务器...</div>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
const statusDiv = document.getElementById('status');
|
||||
const resultDiv = document.getElementById('result');
|
||||
// 尝试连接到WebSocket服务器
|
||||
let ws;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 5;
|
||||
const reconnectInterval = 2000; // 2秒
|
||||
|
||||
function connectWebSocket() {
|
||||
try {
|
||||
ws = new WebSocket('ws://localhost:34515');
|
||||
|
||||
ws.onopen = () => {
|
||||
reconnectAttempts = 0;
|
||||
updateStatus('已连接到服务器,等待指令...');
|
||||
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
|
||||
};
|
||||
|
||||
ws.onmessage = handleMessage;
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[Browser Page] WebSocket Error:', error);
|
||||
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[Browser Page] WebSocket Connection Closed');
|
||||
updateStatus('与服务器断开连接。尝试重新连接...');
|
||||
stopRecognition();
|
||||
|
||||
// 尝试重新连接
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
updateStatus(`与服务器断开连接。尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`);
|
||||
setTimeout(connectWebSocket, reconnectInterval);
|
||||
} else {
|
||||
updateStatus('无法连接到服务器。请刷新页面或重启应用。');
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Browser Page] Error creating WebSocket:', error);
|
||||
updateStatus('创建WebSocket连接时出错。请刷新页面或重启应用。');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始连接
|
||||
connectWebSocket();
|
||||
let recognition = null;
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
function updateStatus(message) {
|
||||
console.log(`[Browser Page Status] ${message}`);
|
||||
statusDiv.textContent = message;
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.data);
|
||||
console.log('[Browser Page] Received command:', data);
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Received non-JSON message:', event.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'start') {
|
||||
startRecognition();
|
||||
} else if (data.type === 'stop') {
|
||||
stopRecognition();
|
||||
} else if (data.type === 'reset') {
|
||||
// 强制重置语音识别
|
||||
forceResetRecognition();
|
||||
} else {
|
||||
console.warn('[Browser Page] Received unknown command type:', data.type);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
function setupRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:此浏览器不支持 Web Speech API。');
|
||||
return false;
|
||||
}
|
||||
if (recognition && recognition.recognizing) {
|
||||
console.log('[Browser Page] Recognition already active.');
|
||||
return true;
|
||||
}
|
||||
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.lang = 'zh-CN';
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
// 增加以下设置提高语音识别的可靠性
|
||||
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
|
||||
// 设置较短的语音识别时间,使用户能更快地看到结果
|
||||
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
|
||||
try {
|
||||
// @ts-ignore
|
||||
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
|
||||
} catch (e) {
|
||||
console.log('[Browser Page] audioStart property not supported');
|
||||
}
|
||||
|
||||
recognition.onstart = () => {
|
||||
updateStatus("🎤 正在识别...");
|
||||
console.log('[Browser Page] SpeechRecognition started.');
|
||||
};
|
||||
|
||||
recognition.onresult = (event) => {
|
||||
console.log('[Browser Page] Recognition result event:', event);
|
||||
|
||||
let interim_transcript = '';
|
||||
let final_transcript = '';
|
||||
|
||||
// 输出识别结果的详细信息便于调试
|
||||
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
||||
const confidence = event.results[i][0].confidence;
|
||||
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
|
||||
|
||||
if (event.results[i].isFinal) {
|
||||
final_transcript += event.results[i][0].transcript;
|
||||
} else {
|
||||
interim_transcript += event.results[i][0].transcript;
|
||||
}
|
||||
}
|
||||
|
||||
const resultText = final_transcript || interim_transcript;
|
||||
resultDiv.textContent = resultText;
|
||||
|
||||
// 更新状态显示
|
||||
if (resultText) {
|
||||
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
|
||||
}
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
|
||||
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onerror = (event) => {
|
||||
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
|
||||
|
||||
// 根据错误类型提供更友好的错误提示
|
||||
let errorMessage = '';
|
||||
switch (event.error) {
|
||||
case 'no-speech':
|
||||
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Restarting recognition after no-speech error');
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
break;
|
||||
case 'audio-capture':
|
||||
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
|
||||
break;
|
||||
case 'not-allowed':
|
||||
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
|
||||
break;
|
||||
case 'network':
|
||||
errorMessage = '网络错误导致语音识别失败。';
|
||||
break;
|
||||
case 'aborted':
|
||||
errorMessage = '语音识别被用户或系统中止。';
|
||||
break;
|
||||
default:
|
||||
errorMessage = `识别错误: ${event.error}`;
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMessage}`);
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: {
|
||||
error: event.error,
|
||||
message: errorMessage || event.message || `Recognition error: ${event.error}`
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
console.log('[Browser Page] SpeechRecognition ended.');
|
||||
|
||||
// 检查是否是由于错误或用户手动停止导致的结束
|
||||
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
|
||||
|
||||
if (!isErrorOrStopped) {
|
||||
// 如果不是由于错误或手动停止,则自动重新启动语音识别
|
||||
updateStatus("识别暂停,正在重新启动...");
|
||||
|
||||
// 保存当前的recognition对象
|
||||
const currentRecognition = recognition;
|
||||
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (currentRecognition && currentRecognition === recognition) {
|
||||
currentRecognition.start();
|
||||
console.log('[Browser Page] Automatically restarting recognition');
|
||||
} else {
|
||||
// 如果recognition对象已经变化,重新创建一个
|
||||
setupRecognition();
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Created new recognition instance and started');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
|
||||
}
|
||||
|
||||
// 只有在手动停止或错误时才重置recognition对象
|
||||
recognition = null;
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
function startRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:浏览器不支持 Web Speech API。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示正在准备的状态
|
||||
updateStatus('正在准备麦克风...');
|
||||
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Recognition already exists, stopping first.');
|
||||
stopRecognition();
|
||||
}
|
||||
|
||||
if (!setupRecognition()) return;
|
||||
|
||||
console.log('[Browser Page] Attempting to start recognition...');
|
||||
try {
|
||||
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
|
||||
const micPermissionTimeout = setTimeout(() => {
|
||||
updateStatus('获取麦克风权限超时,请刷新页面重试。');
|
||||
}, 10000); // 10秒超时
|
||||
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
}
|
||||
})
|
||||
.then(stream => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.log('[Browser Page] Microphone access granted.');
|
||||
|
||||
// 检查麦克风音量级别
|
||||
const audioContext = new AudioContext();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
const microphone = audioContext.createMediaStreamSource(stream);
|
||||
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
|
||||
|
||||
analyser.smoothingTimeConstant = 0.8;
|
||||
analyser.fftSize = 1024;
|
||||
|
||||
microphone.connect(analyser);
|
||||
analyser.connect(javascriptNode);
|
||||
javascriptNode.connect(audioContext.destination);
|
||||
|
||||
javascriptNode.onaudioprocess = function () {
|
||||
const array = new Uint8Array(analyser.frequencyBinCount);
|
||||
analyser.getByteFrequencyData(array);
|
||||
let values = 0;
|
||||
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
values += (array[i]);
|
||||
}
|
||||
|
||||
const average = values / length;
|
||||
console.log('[Browser Page] Microphone volume level:', average);
|
||||
|
||||
// 如果音量太低,显示提示
|
||||
if (average < 5) {
|
||||
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
|
||||
} else {
|
||||
updateStatus('🎤 正在识别...');
|
||||
}
|
||||
|
||||
// 只检查一次就断开连接
|
||||
microphone.disconnect();
|
||||
analyser.disconnect();
|
||||
javascriptNode.disconnect();
|
||||
};
|
||||
|
||||
// 释放测试用的音频流
|
||||
setTimeout(() => {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
audioContext.close();
|
||||
}, 1000);
|
||||
|
||||
// 启动语音识别
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
updateStatus('🎤 正在识别...');
|
||||
} else {
|
||||
updateStatus('错误:Recognition 实例丢失。');
|
||||
console.error('[Browser Page] Recognition instance lost before start.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.error('[Browser Page] Microphone access error:', err);
|
||||
|
||||
let errorMsg = `无法访问麦克风 (${err.name})`;
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMsg}`);
|
||||
recognition = null;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.start():', e);
|
||||
updateStatus(`启动识别时出错: ${e.message}`);
|
||||
recognition = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecognition() {
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Stopping recognition...');
|
||||
updateStatus("正在停止识别...");
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.stop():', e);
|
||||
recognition = null;
|
||||
updateStatus("停止时出错,已强制重置。");
|
||||
}
|
||||
} else {
|
||||
console.log('[Browser Page] Recognition not active, nothing to stop.');
|
||||
updateStatus("识别未运行。");
|
||||
}
|
||||
}
|
||||
|
||||
function forceResetRecognition() {
|
||||
console.log('[Browser Page] Force resetting recognition...');
|
||||
updateStatus("强制重置语音识别...");
|
||||
|
||||
// 先尝试停止当前的识别
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error stopping recognition during reset:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 强制设置为null,丢弃所有后续结果
|
||||
recognition = null;
|
||||
|
||||
// 通知服务器已重置
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
|
||||
}
|
||||
|
||||
updateStatus("语音识别已重置,等待新指令。");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
854
public/asr-server/package-lock.json
generated
854
public/asr-server/package-lock.json
generated
@@ -1,854 +0,0 @@
|
||||
{
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Cherry Studio ASR Server",
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
// 检查依赖项
|
||||
try {
|
||||
console.log('ASR Server starting...')
|
||||
console.log('Node.js version:', process.version)
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
console.log('Command line arguments:', process.argv)
|
||||
|
||||
// 检查必要的依赖项
|
||||
const checkDependency = (name) => {
|
||||
try {
|
||||
require(name) // Removed unused variable 'module'
|
||||
console.log(`Successfully loaded dependency: ${name}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Failed to load dependency: ${name}`, error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查所有必要的依赖项
|
||||
const dependencies = ['http', 'ws', 'express', 'path', 'fs']
|
||||
const missingDeps = dependencies.filter((dep) => !checkDependency(dep))
|
||||
|
||||
if (missingDeps.length > 0) {
|
||||
console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during dependency check:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 加载依赖项
|
||||
const http = require('http')
|
||||
const WebSocket = require('ws')
|
||||
const express = require('express')
|
||||
const path = require('path') // Need path module
|
||||
// const fs = require('fs') // Commented out unused import 'fs'
|
||||
|
||||
const app = express()
|
||||
const port = 34515 // Define the port
|
||||
|
||||
// 获取index.html文件的路径
|
||||
function getIndexHtmlPath() {
|
||||
const fs = require('fs')
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 开发环境路径
|
||||
path.join(__dirname, 'index.html'),
|
||||
// 当前目录
|
||||
path.join(process.cwd(), 'index.html'),
|
||||
// 相对于可执行文件的路径
|
||||
path.join(path.dirname(process.execPath), 'index.html'),
|
||||
// 相对于可执行文件的上级目录的路径
|
||||
path.join(path.dirname(path.dirname(process.execPath)), 'index.html'),
|
||||
// 相对于可执行文件的resources目录的路径
|
||||
path.join(path.dirname(process.execPath), 'resources', 'index.html'),
|
||||
// 相对于可执行文件的resources/asr-server目录的路径
|
||||
path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'),
|
||||
// 相对于可执行文件的asr-server目录的路径
|
||||
path.join(path.dirname(process.execPath), 'asr-server', 'index.html'),
|
||||
// 如果是pkg打包环境
|
||||
process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null
|
||||
].filter(Boolean) // 过滤掉null值
|
||||
|
||||
console.log('Possible index.html paths:', possiblePaths)
|
||||
|
||||
// 检查每个路径,返回第一个存在的文件
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
console.log(`Found index.html at: ${p}`)
|
||||
return p
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error checking existence of ${p}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到文件,返回默认路径并记录错误
|
||||
console.error('Could not find index.html in any of the expected locations')
|
||||
return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在
|
||||
}
|
||||
|
||||
// 提供网页给浏览器
|
||||
app.get('/', (req, res) => {
|
||||
try {
|
||||
const indexPath = getIndexHtmlPath()
|
||||
console.log(`Serving index.html from: ${indexPath}`)
|
||||
|
||||
// 检查文件是否存在
|
||||
const fs = require('fs')
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.error(`Error: index.html not found at ${indexPath}`)
|
||||
return res.status(404).send(`Error: index.html not found at ${indexPath}. <br>Please check the server logs.`)
|
||||
}
|
||||
|
||||
res.sendFile(indexPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error sending index.html:', err)
|
||||
res.status(500).send(`Error serving index.html: ${err.message}`)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error in route handler:', error)
|
||||
res.status(500).send(`Server error: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
const server = http.createServer(app)
|
||||
const wss = new WebSocket.Server({ server })
|
||||
|
||||
let browserConnection = null
|
||||
let electronConnection = null
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('[Server] WebSocket client connected') // Add log
|
||||
|
||||
ws.on('message', (message) => {
|
||||
let data
|
||||
try {
|
||||
// Ensure message is treated as string before parsing
|
||||
data = JSON.parse(message.toString())
|
||||
console.log('[Server] Received message:', data) // Log parsed data
|
||||
} catch (e) {
|
||||
console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e)
|
||||
return // Ignore non-JSON messages
|
||||
}
|
||||
|
||||
// 识别客户端类型
|
||||
if (data.type === 'identify') {
|
||||
if (data.role === 'browser') {
|
||||
browserConnection = ws
|
||||
console.log('[Server] Browser identified and connected')
|
||||
// Notify Electron that the browser is ready
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
|
||||
console.log('[Server] Sent browser_ready status to Electron')
|
||||
}
|
||||
// Notify Electron if it's already connected
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' }))
|
||||
}
|
||||
ws.on('close', () => {
|
||||
console.log('[Server] Browser disconnected')
|
||||
browserConnection = null
|
||||
// Notify Electron
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' }))
|
||||
}
|
||||
})
|
||||
ws.on('error', (error) => {
|
||||
console.error('[Server] Browser WebSocket error:', error)
|
||||
browserConnection = null // Assume disconnected on error
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
|
||||
}
|
||||
})
|
||||
} else if (data.role === 'electron') {
|
||||
electronConnection = ws
|
||||
console.log('[Server] Electron identified and connected')
|
||||
// If browser is already connected when Electron connects, notify Electron immediately
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
|
||||
console.log('[Server] Sent initial browser_ready status to Electron')
|
||||
}
|
||||
ws.on('close', () => {
|
||||
console.log('[Server] Electron disconnected')
|
||||
electronConnection = null
|
||||
// Maybe send stop to browser if electron disconnects?
|
||||
// if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' }));
|
||||
})
|
||||
ws.on('error', (error) => {
|
||||
console.error('[Server] Electron WebSocket error:', error)
|
||||
electronConnection = null // Assume disconnected on error
|
||||
})
|
||||
}
|
||||
}
|
||||
// Electron 控制开始/停止
|
||||
else if (data.type === 'start' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying START command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'start' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay START: Browser not connected')
|
||||
// Optionally notify Electron back
|
||||
electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' }))
|
||||
}
|
||||
} else if (data.type === 'stop' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying STOP command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'stop' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay STOP: Browser not connected')
|
||||
}
|
||||
} else if (data.type === 'reset' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying RESET command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'reset' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay RESET: Browser not connected')
|
||||
}
|
||||
}
|
||||
// 浏览器发送识别结果
|
||||
else if (data.type === 'result' && ws === browserConnection) {
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
// console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed
|
||||
electronConnection.send(JSON.stringify({ type: 'result', data: data.data }))
|
||||
} else {
|
||||
// console.log('[Server] Cannot relay RESULT: Electron not connected');
|
||||
}
|
||||
}
|
||||
// 浏览器发送状态更新 (例如 'stopped')
|
||||
else if (data.type === 'status' && ws === browserConnection) {
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: data.message }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay STATUS: Electron not connected')
|
||||
}
|
||||
} else {
|
||||
console.log('[Server] Received unknown message type or from unknown source:', data)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('error', (error) => {
|
||||
// Generic error handling for connection before identification
|
||||
console.error('[Server] Initial WebSocket connection error:', error)
|
||||
// Attempt to clean up based on which connection it might be (if identified)
|
||||
if (ws === browserConnection) {
|
||||
browserConnection = null
|
||||
if (electronConnection)
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
|
||||
} else if (ws === electronConnection) {
|
||||
electronConnection = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 添加进程错误处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Server] Uncaught exception:', error)
|
||||
// 不立即退出,给日志输出的时间
|
||||
setTimeout(() => process.exit(1), 1000)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// 尝试启动服务器
|
||||
try {
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// Handle server errors
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Server] Critical error starting server:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* 独立的ASR服务器
|
||||
* 这个文件是一个简化版的server.js,用于在打包后的应用中运行
|
||||
*/
|
||||
|
||||
// 基本依赖
|
||||
const http = require('http')
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
// 输出环境信息
|
||||
console.log('ASR Server starting...')
|
||||
console.log('Node.js version:', process.version)
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
console.log('Command line arguments:', process.argv)
|
||||
|
||||
// 创建Express应用
|
||||
const app = express()
|
||||
const port = 34515
|
||||
|
||||
// 提供静态文件
|
||||
app.use(express.static(__dirname))
|
||||
|
||||
// 提供网页给浏览器
|
||||
app.get('/', (req, res) => {
|
||||
try {
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 当前目录
|
||||
path.join(__dirname, 'index.html'),
|
||||
// 上级目录
|
||||
path.join(__dirname, '..', 'index.html'),
|
||||
// 应用根目录
|
||||
path.join(process.cwd(), 'index.html')
|
||||
]
|
||||
|
||||
console.log('Possible index.html paths:', possiblePaths)
|
||||
|
||||
// 查找第一个存在的文件
|
||||
let indexPath = null
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
indexPath = p
|
||||
console.log(`Found index.html at: ${p}`)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error checking existence of ${p}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
if (indexPath) {
|
||||
res.sendFile(indexPath)
|
||||
} else {
|
||||
// 如果找不到文件,返回一个简单的HTML页面
|
||||
console.error('Could not find index.html, serving fallback page')
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ASR Server</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 2em; }
|
||||
h1 { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ASR Server is running</h1>
|
||||
<p>This is a fallback page because the index.html file could not be found.</p>
|
||||
<p>Server is running at: http://localhost:${port}</p>
|
||||
<p>Current directory: ${__dirname}</p>
|
||||
<p>Working directory: ${process.cwd()}</p>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error serving index.html:', error)
|
||||
res.status(500).send(`Server error: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 创建HTTP服务器
|
||||
const server = http.createServer(app)
|
||||
|
||||
// 添加进程错误处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Server] Uncaught exception:', error)
|
||||
// 不立即退出,给日志输出的时间
|
||||
setTimeout(() => process.exit(1), 1000)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// 尝试启动服务器
|
||||
try {
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// 处理服务器错误
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Server] Critical error starting server:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
@echo off
|
||||
echo Starting ASR Server...
|
||||
cd /d %~dp0
|
||||
node standalone.js
|
||||
pause
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* 内置的ASR服务器模块
|
||||
* 这个文件可以直接在Electron中运行,不需要外部依赖
|
||||
*/
|
||||
|
||||
// 使用Electron内置的Node.js模块
|
||||
const http = require('http')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
// 输出环境信息
|
||||
console.log('ASR Server (Embedded) starting...')
|
||||
console.log('Node.js version:', process.version)
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
console.log('Command line arguments:', process.argv)
|
||||
|
||||
// 创建HTTP服务器
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
if (req.url === '/' || req.url === '/index.html') {
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 当前目录
|
||||
path.join(__dirname, 'index.html'),
|
||||
// 上级目录
|
||||
path.join(__dirname, '..', 'index.html'),
|
||||
// 应用根目录
|
||||
path.join(process.cwd(), 'index.html')
|
||||
]
|
||||
|
||||
console.log('Possible index.html paths:', possiblePaths)
|
||||
|
||||
// 查找第一个存在的文件
|
||||
let indexPath = null
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
indexPath = p
|
||||
console.log(`Found index.html at: ${p}`)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error checking existence of ${p}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
if (indexPath) {
|
||||
// 读取文件内容并发送
|
||||
fs.readFile(indexPath, (err, data) => {
|
||||
if (err) {
|
||||
console.error('Error reading index.html:', err)
|
||||
res.writeHead(500)
|
||||
res.end('Error reading index.html')
|
||||
return
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end(data)
|
||||
})
|
||||
} else {
|
||||
// 如果找不到文件,返回一个简单的HTML页面
|
||||
console.error('Could not find index.html, serving fallback page')
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||
res.end(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ASR Server</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 2em; }
|
||||
h1 { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ASR Server is running</h1>
|
||||
<p>This is a fallback page because the index.html file could not be found.</p>
|
||||
<p>Server is running at: http://localhost:34515</p>
|
||||
<p>Current directory: ${__dirname}</p>
|
||||
<p>Working directory: ${process.cwd()}</p>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
} else {
|
||||
// 处理其他请求
|
||||
res.writeHead(404)
|
||||
res.end('Not found')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling request:', error)
|
||||
res.writeHead(500)
|
||||
res.end('Server error')
|
||||
}
|
||||
})
|
||||
|
||||
// 添加进程错误处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Server] Uncaught exception:', error)
|
||||
// 不立即退出,给日志输出的时间
|
||||
setTimeout(() => process.exit(1), 1000)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// 尝试启动服务器
|
||||
try {
|
||||
const port = 34515
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// 处理服务器错误
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Server] Critical error starting server:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cherry Studio ASR</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 1em;
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.5em;
|
||||
min-height: 50px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>浏览器语音识别中继页面</h1>
|
||||
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
|
||||
<div id="status">正在连接到服务器...</div>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
const statusDiv = document.getElementById('status');
|
||||
const resultDiv = document.getElementById('result');
|
||||
// 尝试连接到WebSocket服务器
|
||||
let ws;
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 5;
|
||||
const reconnectInterval = 2000; // 2秒
|
||||
|
||||
function connectWebSocket() {
|
||||
try {
|
||||
ws = new WebSocket('ws://localhost:34515');
|
||||
|
||||
ws.onopen = () => {
|
||||
reconnectAttempts = 0;
|
||||
updateStatus('已连接到服务器,等待指令...');
|
||||
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
|
||||
};
|
||||
|
||||
ws.onmessage = handleMessage;
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[Browser Page] WebSocket Error:', error);
|
||||
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[Browser Page] WebSocket Connection Closed');
|
||||
updateStatus('与服务器断开连接。尝试重新连接...');
|
||||
stopRecognition();
|
||||
|
||||
// 尝试重新连接
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
updateStatus(`与服务器断开连接。尝试重新连接 (${reconnectAttempts}/${maxReconnectAttempts})...`);
|
||||
setTimeout(connectWebSocket, reconnectInterval);
|
||||
} else {
|
||||
updateStatus('无法连接到服务器。请刷新页面或重启应用。');
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[Browser Page] Error creating WebSocket:', error);
|
||||
updateStatus('创建WebSocket连接时出错。请刷新页面或重启应用。');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始连接
|
||||
connectWebSocket();
|
||||
let recognition = null;
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
function updateStatus(message) {
|
||||
console.log(`[Browser Page Status] ${message}`);
|
||||
statusDiv.textContent = message;
|
||||
}
|
||||
|
||||
function handleMessage(event) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.data);
|
||||
console.log('[Browser Page] Received command:', data);
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Received non-JSON message:', event.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'start') {
|
||||
startRecognition();
|
||||
} else if (data.type === 'stop') {
|
||||
stopRecognition();
|
||||
} else if (data.type === 'reset') {
|
||||
// 强制重置语音识别
|
||||
forceResetRecognition();
|
||||
} else {
|
||||
console.warn('[Browser Page] Received unknown command type:', data.type);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
function setupRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:此浏览器不支持 Web Speech API。');
|
||||
return false;
|
||||
}
|
||||
if (recognition && recognition.recognizing) {
|
||||
console.log('[Browser Page] Recognition already active.');
|
||||
return true;
|
||||
}
|
||||
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.lang = 'zh-CN';
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
// 增加以下设置提高语音识别的可靠性
|
||||
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
|
||||
// 设置较短的语音识别时间,使用户能更快地看到结果
|
||||
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
|
||||
try {
|
||||
// @ts-ignore
|
||||
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
|
||||
} catch (e) {
|
||||
console.log('[Browser Page] audioStart property not supported');
|
||||
}
|
||||
|
||||
recognition.onstart = () => {
|
||||
updateStatus("🎤 正在识别...");
|
||||
console.log('[Browser Page] SpeechRecognition started.');
|
||||
};
|
||||
|
||||
recognition.onresult = (event) => {
|
||||
console.log('[Browser Page] Recognition result event:', event);
|
||||
|
||||
let interim_transcript = '';
|
||||
let final_transcript = '';
|
||||
|
||||
// 输出识别结果的详细信息便于调试
|
||||
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
||||
const confidence = event.results[i][0].confidence;
|
||||
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
|
||||
|
||||
if (event.results[i].isFinal) {
|
||||
final_transcript += event.results[i][0].transcript;
|
||||
} else {
|
||||
interim_transcript += event.results[i][0].transcript;
|
||||
}
|
||||
}
|
||||
|
||||
const resultText = final_transcript || interim_transcript;
|
||||
resultDiv.textContent = resultText;
|
||||
|
||||
// 更新状态显示
|
||||
if (resultText) {
|
||||
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
|
||||
}
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
|
||||
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onerror = (event) => {
|
||||
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
|
||||
|
||||
// 根据错误类型提供更友好的错误提示
|
||||
let errorMessage = '';
|
||||
switch (event.error) {
|
||||
case 'no-speech':
|
||||
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Restarting recognition after no-speech error');
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
break;
|
||||
case 'audio-capture':
|
||||
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
|
||||
break;
|
||||
case 'not-allowed':
|
||||
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
|
||||
break;
|
||||
case 'network':
|
||||
errorMessage = '网络错误导致语音识别失败。';
|
||||
break;
|
||||
case 'aborted':
|
||||
errorMessage = '语音识别被用户或系统中止。';
|
||||
break;
|
||||
default:
|
||||
errorMessage = `识别错误: ${event.error}`;
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMessage}`);
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: {
|
||||
error: event.error,
|
||||
message: errorMessage || event.message || `Recognition error: ${event.error}`
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
console.log('[Browser Page] SpeechRecognition ended.');
|
||||
|
||||
// 检查是否是由于错误或用户手动停止导致的结束
|
||||
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
|
||||
|
||||
if (!isErrorOrStopped) {
|
||||
// 如果不是由于错误或手动停止,则自动重新启动语音识别
|
||||
updateStatus("识别暂停,正在重新启动...");
|
||||
|
||||
// 保存当前的recognition对象
|
||||
const currentRecognition = recognition;
|
||||
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (currentRecognition && currentRecognition === recognition) {
|
||||
currentRecognition.start();
|
||||
console.log('[Browser Page] Automatically restarting recognition');
|
||||
} else {
|
||||
// 如果recognition对象已经变化,重新创建一个
|
||||
setupRecognition();
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Created new recognition instance and started');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
|
||||
}
|
||||
|
||||
// 只有在手动停止或错误时才重置recognition对象
|
||||
recognition = null;
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
function startRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:浏览器不支持 Web Speech API。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示正在准备的状态
|
||||
updateStatus('正在准备麦克风...');
|
||||
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Recognition already exists, stopping first.');
|
||||
stopRecognition();
|
||||
}
|
||||
|
||||
if (!setupRecognition()) return;
|
||||
|
||||
console.log('[Browser Page] Attempting to start recognition...');
|
||||
try {
|
||||
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
|
||||
const micPermissionTimeout = setTimeout(() => {
|
||||
updateStatus('获取麦克风权限超时,请刷新页面重试。');
|
||||
}, 10000); // 10秒超时
|
||||
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
}
|
||||
})
|
||||
.then(stream => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.log('[Browser Page] Microphone access granted.');
|
||||
|
||||
// 检查麦克风音量级别
|
||||
const audioContext = new AudioContext();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
const microphone = audioContext.createMediaStreamSource(stream);
|
||||
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
|
||||
|
||||
analyser.smoothingTimeConstant = 0.8;
|
||||
analyser.fftSize = 1024;
|
||||
|
||||
microphone.connect(analyser);
|
||||
analyser.connect(javascriptNode);
|
||||
javascriptNode.connect(audioContext.destination);
|
||||
|
||||
javascriptNode.onaudioprocess = function () {
|
||||
const array = new Uint8Array(analyser.frequencyBinCount);
|
||||
analyser.getByteFrequencyData(array);
|
||||
let values = 0;
|
||||
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
values += (array[i]);
|
||||
}
|
||||
|
||||
const average = values / length;
|
||||
console.log('[Browser Page] Microphone volume level:', average);
|
||||
|
||||
// 如果音量太低,显示提示
|
||||
if (average < 5) {
|
||||
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
|
||||
} else {
|
||||
updateStatus('🎤 正在识别...');
|
||||
}
|
||||
|
||||
// 只检查一次就断开连接
|
||||
microphone.disconnect();
|
||||
analyser.disconnect();
|
||||
javascriptNode.disconnect();
|
||||
};
|
||||
|
||||
// 释放测试用的音频流
|
||||
setTimeout(() => {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
audioContext.close();
|
||||
}, 1000);
|
||||
|
||||
// 启动语音识别
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
updateStatus('🎤 正在识别...');
|
||||
} else {
|
||||
updateStatus('错误:Recognition 实例丢失。');
|
||||
console.error('[Browser Page] Recognition instance lost before start.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.error('[Browser Page] Microphone access error:', err);
|
||||
|
||||
let errorMsg = `无法访问麦克风 (${err.name})`;
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMsg}`);
|
||||
recognition = null;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.start():', e);
|
||||
updateStatus(`启动识别时出错: ${e.message}`);
|
||||
recognition = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecognition() {
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Stopping recognition...');
|
||||
updateStatus("正在停止识别...");
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.stop():', e);
|
||||
recognition = null;
|
||||
updateStatus("停止时出错,已强制重置。");
|
||||
}
|
||||
} else {
|
||||
console.log('[Browser Page] Recognition not active, nothing to stop.');
|
||||
updateStatus("识别未运行。");
|
||||
}
|
||||
}
|
||||
|
||||
function forceResetRecognition() {
|
||||
console.log('[Browser Page] Force resetting recognition...');
|
||||
updateStatus("强制重置语音识别...");
|
||||
|
||||
// 先尝试停止当前的识别
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error stopping recognition during reset:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 强制设置为null,丢弃所有后续结果
|
||||
recognition = null;
|
||||
|
||||
// 通知服务器已重置
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
|
||||
}
|
||||
|
||||
updateStatus("语音识别已重置,等待新指令。");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
854
resources/asr-server/package-lock.json
generated
854
resources/asr-server/package-lock.json
generated
@@ -1,854 +0,0 @@
|
||||
{
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.13.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmmirror.com/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "2.0.1",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmmirror.com/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.2.tgz",
|
||||
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"unpipe": "1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmmirror.com/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Cherry Studio ASR Server",
|
||||
"main": "server.js",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
// 检查依赖项
|
||||
try {
|
||||
console.log('ASR Server starting...')
|
||||
console.log('Node.js version:', process.version)
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
console.log('Command line arguments:', process.argv)
|
||||
|
||||
// 检查必要的依赖项
|
||||
const checkDependency = (name) => {
|
||||
try {
|
||||
require(name) // Removed unused variable 'module'
|
||||
console.log(`Successfully loaded dependency: ${name}`)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(`Failed to load dependency: ${name}`, error.message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 检查所有必要的依赖项
|
||||
const dependencies = ['http', 'ws', 'express', 'path', 'fs']
|
||||
const missingDeps = dependencies.filter((dep) => !checkDependency(dep))
|
||||
|
||||
if (missingDeps.length > 0) {
|
||||
console.error(`Missing dependencies: ${missingDeps.join(', ')}. Server cannot start.`)
|
||||
process.exit(1)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during dependency check:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// 加载依赖项
|
||||
const http = require('http')
|
||||
const WebSocket = require('ws')
|
||||
const express = require('express')
|
||||
const path = require('path') // Need path module
|
||||
// const fs = require('fs') // Commented out unused import 'fs'
|
||||
|
||||
const app = express()
|
||||
const port = 34515 // Define the port
|
||||
|
||||
// 获取index.html文件的路径
|
||||
function getIndexHtmlPath() {
|
||||
const fs = require('fs')
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 开发环境路径
|
||||
path.join(__dirname, 'index.html'),
|
||||
// 当前目录
|
||||
path.join(process.cwd(), 'index.html'),
|
||||
// 相对于可执行文件的路径
|
||||
path.join(path.dirname(process.execPath), 'index.html'),
|
||||
// 相对于可执行文件的上级目录的路径
|
||||
path.join(path.dirname(path.dirname(process.execPath)), 'index.html'),
|
||||
// 相对于可执行文件的resources目录的路径
|
||||
path.join(path.dirname(process.execPath), 'resources', 'index.html'),
|
||||
// 相对于可执行文件的resources/asr-server目录的路径
|
||||
path.join(path.dirname(process.execPath), 'resources', 'asr-server', 'index.html'),
|
||||
// 相对于可执行文件的asr-server目录的路径
|
||||
path.join(path.dirname(process.execPath), 'asr-server', 'index.html'),
|
||||
// 如果是pkg打包环境
|
||||
process.pkg ? path.join(path.dirname(process.execPath), 'index.html') : null
|
||||
].filter(Boolean) // 过滤掉null值
|
||||
|
||||
console.log('Possible index.html paths:', possiblePaths)
|
||||
|
||||
// 检查每个路径,返回第一个存在的文件
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
console.log(`Found index.html at: ${p}`)
|
||||
return p
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error checking existence of ${p}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到文件,返回默认路径并记录错误
|
||||
console.error('Could not find index.html in any of the expected locations')
|
||||
return path.join(__dirname, 'index.html') // 返回默认路径,即使它可能不存在
|
||||
}
|
||||
|
||||
// 提供网页给浏览器
|
||||
app.get('/', (req, res) => {
|
||||
try {
|
||||
const indexPath = getIndexHtmlPath()
|
||||
console.log(`Serving index.html from: ${indexPath}`)
|
||||
|
||||
// 检查文件是否存在
|
||||
const fs = require('fs')
|
||||
if (!fs.existsSync(indexPath)) {
|
||||
console.error(`Error: index.html not found at ${indexPath}`)
|
||||
return res.status(404).send(`Error: index.html not found at ${indexPath}. <br>Please check the server logs.`)
|
||||
}
|
||||
|
||||
res.sendFile(indexPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Error sending index.html:', err)
|
||||
res.status(500).send(`Error serving index.html: ${err.message}`)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error in route handler:', error)
|
||||
res.status(500).send(`Server error: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
const server = http.createServer(app)
|
||||
const wss = new WebSocket.Server({ server })
|
||||
|
||||
let browserConnection = null
|
||||
let electronConnection = null
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('[Server] WebSocket client connected') // Add log
|
||||
|
||||
ws.on('message', (message) => {
|
||||
let data
|
||||
try {
|
||||
// Ensure message is treated as string before parsing
|
||||
data = JSON.parse(message.toString())
|
||||
console.log('[Server] Received message:', data) // Log parsed data
|
||||
} catch (e) {
|
||||
console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e)
|
||||
return // Ignore non-JSON messages
|
||||
}
|
||||
|
||||
// 识别客户端类型
|
||||
if (data.type === 'identify') {
|
||||
if (data.role === 'browser') {
|
||||
browserConnection = ws
|
||||
console.log('[Server] Browser identified and connected')
|
||||
// Notify Electron that the browser is ready
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
|
||||
console.log('[Server] Sent browser_ready status to Electron')
|
||||
}
|
||||
// Notify Electron if it's already connected
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' }))
|
||||
}
|
||||
ws.on('close', () => {
|
||||
console.log('[Server] Browser disconnected')
|
||||
browserConnection = null
|
||||
// Notify Electron
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' }))
|
||||
}
|
||||
})
|
||||
ws.on('error', (error) => {
|
||||
console.error('[Server] Browser WebSocket error:', error)
|
||||
browserConnection = null // Assume disconnected on error
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
|
||||
}
|
||||
})
|
||||
} else if (data.role === 'electron') {
|
||||
electronConnection = ws
|
||||
console.log('[Server] Electron identified and connected')
|
||||
// If browser is already connected when Electron connects, notify Electron immediately
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
|
||||
console.log('[Server] Sent initial browser_ready status to Electron')
|
||||
}
|
||||
ws.on('close', () => {
|
||||
console.log('[Server] Electron disconnected')
|
||||
electronConnection = null
|
||||
// Maybe send stop to browser if electron disconnects?
|
||||
// if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' }));
|
||||
})
|
||||
ws.on('error', (error) => {
|
||||
console.error('[Server] Electron WebSocket error:', error)
|
||||
electronConnection = null // Assume disconnected on error
|
||||
})
|
||||
}
|
||||
}
|
||||
// Electron 控制开始/停止
|
||||
else if (data.type === 'start' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying START command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'start' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay START: Browser not connected')
|
||||
// Optionally notify Electron back
|
||||
electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' }))
|
||||
}
|
||||
} else if (data.type === 'stop' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying STOP command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'stop' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay STOP: Browser not connected')
|
||||
}
|
||||
} else if (data.type === 'reset' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying RESET command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'reset' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay RESET: Browser not connected')
|
||||
}
|
||||
}
|
||||
// 浏览器发送识别结果
|
||||
else if (data.type === 'result' && ws === browserConnection) {
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
// console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed
|
||||
electronConnection.send(JSON.stringify({ type: 'result', data: data.data }))
|
||||
} else {
|
||||
// console.log('[Server] Cannot relay RESULT: Electron not connected');
|
||||
}
|
||||
}
|
||||
// 浏览器发送状态更新 (例如 'stopped')
|
||||
else if (data.type === 'status' && ws === browserConnection) {
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: data.message }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay STATUS: Electron not connected')
|
||||
}
|
||||
} else {
|
||||
console.log('[Server] Received unknown message type or from unknown source:', data)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('error', (error) => {
|
||||
// Generic error handling for connection before identification
|
||||
console.error('[Server] Initial WebSocket connection error:', error)
|
||||
// Attempt to clean up based on which connection it might be (if identified)
|
||||
if (ws === browserConnection) {
|
||||
browserConnection = null
|
||||
if (electronConnection)
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
|
||||
} else if (ws === electronConnection) {
|
||||
electronConnection = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 添加进程错误处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Server] Uncaught exception:', error)
|
||||
// 不立即退出,给日志输出的时间
|
||||
setTimeout(() => process.exit(1), 1000)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// 尝试启动服务器
|
||||
try {
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// Handle server errors
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Server] Critical error starting server:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
/**
|
||||
* 独立的ASR服务器
|
||||
* 这个文件是一个简化版的server.js,用于在打包后的应用中运行
|
||||
*/
|
||||
|
||||
// 基本依赖
|
||||
const http = require('http')
|
||||
const express = require('express')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
// 输出环境信息
|
||||
console.log('ASR Server starting...')
|
||||
console.log('Node.js version:', process.version)
|
||||
console.log('Current directory:', __dirname)
|
||||
console.log('Current working directory:', process.cwd())
|
||||
console.log('Command line arguments:', process.argv)
|
||||
|
||||
// 创建Express应用
|
||||
const app = express()
|
||||
const port = 34515
|
||||
|
||||
// 提供静态文件
|
||||
app.use(express.static(__dirname))
|
||||
|
||||
// 提供网页给浏览器
|
||||
app.get('/', (req, res) => {
|
||||
try {
|
||||
// 尝试多个可能的路径
|
||||
const possiblePaths = [
|
||||
// 当前目录
|
||||
path.join(__dirname, 'index.html'),
|
||||
// 上级目录
|
||||
path.join(__dirname, '..', 'index.html'),
|
||||
// 应用根目录
|
||||
path.join(process.cwd(), 'index.html')
|
||||
]
|
||||
|
||||
console.log('Possible index.html paths:', possiblePaths)
|
||||
|
||||
// 查找第一个存在的文件
|
||||
let indexPath = null
|
||||
for (const p of possiblePaths) {
|
||||
try {
|
||||
if (fs.existsSync(p)) {
|
||||
indexPath = p
|
||||
console.log(`Found index.html at: ${p}`)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error checking existence of ${p}:`, e)
|
||||
}
|
||||
}
|
||||
|
||||
if (indexPath) {
|
||||
res.sendFile(indexPath)
|
||||
} else {
|
||||
// 如果找不到文件,返回一个简单的HTML页面
|
||||
console.error('Could not find index.html, serving fallback page')
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ASR Server</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; padding: 2em; }
|
||||
h1 { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ASR Server is running</h1>
|
||||
<p>This is a fallback page because the index.html file could not be found.</p>
|
||||
<p>Server is running at: http://localhost:${port}</p>
|
||||
<p>Current directory: ${__dirname}</p>
|
||||
<p>Working directory: ${process.cwd()}</p>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error serving index.html:', error)
|
||||
res.status(500).send(`Server error: ${error.message}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 创建HTTP服务器
|
||||
const server = http.createServer(app)
|
||||
|
||||
// 添加进程错误处理
|
||||
process.on('uncaughtException', (error) => {
|
||||
console.error('[Server] Uncaught exception:', error)
|
||||
// 不立即退出,给日志输出的时间
|
||||
setTimeout(() => process.exit(1), 1000)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('[Server] Unhandled rejection at:', promise, 'reason:', reason)
|
||||
})
|
||||
|
||||
// 尝试启动服务器
|
||||
try {
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// 处理服务器错误
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[Server] Critical error starting server:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
@echo off
|
||||
echo Starting ASR Server...
|
||||
cd /d %~dp0
|
||||
node standalone.js
|
||||
pause
|
||||
@@ -14,35 +14,76 @@
|
||||
<div class="mb-12">
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">许可协议</h1>
|
||||
|
||||
<p class="mb-6 text-gray-700">采用 Apache License 2.0 修改版许可,并附加以下条件:</p>
|
||||
<p class="mb-6 text-gray-700">本项目采用<strong>区分用户的双重许可 (User-Segmented Dual Licensing)</strong> 模式。</p>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">一. 商用许可</h2>
|
||||
<p class="mb-4 text-gray-700">在以下任何一种情况下,您需要联系我们并获得明确的书面商业授权后,方可继续使用 Cherry Studio 材料:</p>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>修改与衍生</strong>: 您对 Cherry Studio 材料进行修改或基于其进行衍生开发(包括但不限于修改应用名称、Logo、代码、功能、界面,数据等)。</li>
|
||||
<li><strong>企业服务</strong>: 在您的企业内部,或为企业客户提供基于 Cherry Studio 的服务,且该服务支持 10 人及以上累计用户使用。</li>
|
||||
<li><strong>硬件捆绑销售</strong>: 您将 Cherry Studio 预装或集成到硬件设备或产品中进行捆绑销售。</li>
|
||||
<li><strong>政府或教育机构大规模采购</strong>: 您的使用场景属于政府或教育机构的大规模采购项目,特别是涉及安全、数据隐私等敏感需求时。</li>
|
||||
<li><strong>面向公众的公有云服务</strong>:基于 Cherry Studio,提供面向公众的公有云服务。</li>
|
||||
</ol>
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">核心原则</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>个人用户 和 10人及以下企业/组织:</strong> 默认适用 <strong>GNU Affero 通用公共许可证 v3.0 (AGPLv3)</strong>。</li>
|
||||
<li><strong>超过10人的企业/组织:</strong> <strong>必须</strong> 获取 <strong>商业许可证 (Commercial License)</strong>。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">二. 贡献者协议</h2>
|
||||
<p class="mb-4 text-gray-700">作为 Cherry Studio 的贡献者,您应当同意以下条款:</p>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>许可调整</strong>:生产者有权根据需要对开源协议进行调整,使其更加严格或宽松。</li>
|
||||
<li><strong>商业用途</strong>:您贡献的代码可能会被用于商业用途,包括但不限于云业务运营。</li>
|
||||
</ol>
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">定义:"10人及以下"</h2>
|
||||
<p class="text-gray-700">
|
||||
指在您的组织(包括公司、非营利组织、政府机构、教育机构等任何实体)中,能够访问、使用或以任何方式直接或间接受益于本软件(Cherry
|
||||
Studio)功能的个人总数不超过10人。这包括但不限于开发者、测试人员、运营人员、最终用户、通过集成系统间接使用者等。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">三. 其他条款</h2>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li>本协议条款的解释权归 Cherry Studio 开发者所有。</li>
|
||||
<li>本协议可能根据实际情况进行更新,更新时将通过本软件通知用户。</li>
|
||||
</ol>
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. 开源许可证 (Open Source License): AGPLv3 - 适用于个人及10人及以下组织
|
||||
</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>如果您是个人用户,或者您的组织满足上述"10人及以下"的定义,您可以在 <strong>AGPLv3</strong> 的条款下自由使用、修改和分发 Cherry Studio。AGPLv3 的完整文本可以访问
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a> 获取。
|
||||
</li>
|
||||
<li><strong>核心义务:</strong> AGPLv3 的一个关键要求是,如果您修改了 Cherry Studio 并通过网络提供服务,或者分发了修改后的版本,您必须以 AGPLv3
|
||||
许可证向接收者提供相应的<strong>完整源代码</strong>。即使您符合"10人及以下"的标准,如果您希望避免此源代码公开义务,您也需要考虑获取商业许可证(见下文)。</li>
|
||||
<li>使用前请务必仔细阅读并理解 AGPLv3 的所有条款。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. 商业许可证 (Commercial License) - 适用于超过10人的组织,或希望规避 AGPLv3
|
||||
义务的用户</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>强制要求:</strong>
|
||||
如果您的组织<strong>不</strong>满足上述"10人及以下"的定义(即有11人或更多人可以访问、使用或受益于本软件),您<strong>必须</strong>联系我们获取并签署一份商业许可证才能使用
|
||||
Cherry Studio。</li>
|
||||
<li><strong>自愿选择:</strong> 即使您的组织满足"10人及以下"的条件,但如果您的使用场景<strong>无法满足 AGPLv3
|
||||
的条款要求</strong>(特别是关于<strong>源代码公开</strong>的义务),或者您需要 AGPLv3 <strong>未提供</strong>的特定商业条款(如保证、赔偿、无 Copyleft
|
||||
限制等),您也<strong>必须</strong>联系我们获取并签署一份商业许可证。</li>
|
||||
<li><strong>需要商业许可证的常见情况包括(但不限于):</strong>
|
||||
<ul class="list-disc pl-6 mt-2 space-y-1">
|
||||
<li>您的组织规模超过10人。</li>
|
||||
<li>(无论组织规模)您希望分发修改过的 Cherry Studio 版本,但<strong>不希望</strong>根据 AGPLv3 公开您修改部分的源代码。</li>
|
||||
<li>(无论组织规模)您希望基于修改过的 Cherry Studio 提供网络服务(SaaS),但<strong>不希望</strong>根据 AGPLv3 向服务使用者提供修改后的源代码。</li>
|
||||
<li>(无论组织规模)您的公司政策、客户合同或项目要求不允许使用 AGPLv3 许可的软件,或要求闭源分发及保密。</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>获取商业许可:</strong> 请通过邮箱 <a href="mailto:bd@cherry-ai.com"
|
||||
class="text-blue-600 hover:underline">bd@cherry-ai.com</a> 联系 Cherry Studio 开发团队洽谈商业授权事宜。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. 贡献 (Contributions)</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>我们欢迎社区对 Cherry Studio 的贡献。所有向本项目提交的贡献都将被视为在 <strong>AGPLv3</strong> 许可证下提供。</li>
|
||||
<li>通过向本项目提交贡献(例如通过 Pull Request),即表示您同意您的代码以 AGPLv3 许可证授权给本项目及所有后续使用者(无论这些使用者最终遵循 AGPLv3 还是商业许可)。</li>
|
||||
<li>您也理解并同意,您的贡献可能会被包含在根据商业许可证分发的 Cherry Studio 版本中。</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. 其他条款 (Other Terms)</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>关于商业许可证的具体条款和条件,以双方签署的正式商业许可协议为准。</li>
|
||||
<li>项目维护者保留根据需要更新本许可政策(包括用户规模定义和阈值)的权利。相关更新将通过项目官方渠道(如代码仓库、官方网站)进行通知。</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -50,58 +91,107 @@
|
||||
|
||||
<!-- English Version -->
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">License Agreement</h1>
|
||||
<h1 class="text-3xl font-bold mb-8 text-gray-900">Licensing</h1>
|
||||
|
||||
<p class="mb-6 text-gray-700">This software is licensed under a modified version of the Apache License 2.0, with
|
||||
the following additional conditions.</p>
|
||||
<p class="mb-6 text-gray-700">This project employs a <strong>User-Segmented Dual Licensing</strong> model.</p>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">I. Commercial Licensing</h2>
|
||||
<p class="mb-4 text-gray-700">You must contact us and obtain explicit written commercial authorization to
|
||||
continue using Cherry Studio materials under any of the following circumstances:</p>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>Modifications and Derivatives:</strong> You modify Cherry Studio materials or perform derivative
|
||||
development based on them (including but not limited to changing the application's name, logo, code,
|
||||
functionality, user interface, data, etc.).</li>
|
||||
<li><strong>Enterprise Services:</strong> You use Cherry Studio internally within your enterprise, or you
|
||||
provide Cherry Studio-based services for enterprise customers, and such services support cumulative usage by
|
||||
10 or more users.</li>
|
||||
<li><strong>Hardware Bundling and Sales:</strong> You pre-install or integrate Cherry Studio into hardware
|
||||
devices or products for bundled sale.</li>
|
||||
<li><strong>Large-scale Procurement by Government or Educational Institutions:</strong> Your usage scenario
|
||||
involves large-scale procurement projects by government or educational institutions, especially in cases
|
||||
involving sensitive requirements such as security and data privacy.</li>
|
||||
<li><strong>Public Cloud Services:</strong> You provide public cloud-based product services utilizing Cherry
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">Core Principle</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>Individual Users and Organizations with 10 or Fewer Individuals:</strong> Governed by default
|
||||
under the <strong>GNU Affero General Public License v3.0 (AGPLv3)</strong>.</li>
|
||||
<li><strong>Organizations with More Than 10 Individuals:</strong> <strong>Must</strong> obtain a
|
||||
<strong>Commercial License</strong>.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">Definition: "10 or Fewer Individuals"</h2>
|
||||
<p class="text-gray-700">
|
||||
Refers to any organization (including companies, non-profits, government agencies, educational institutions,
|
||||
etc.) where the total number of individuals who can access, use, or in any way directly or indirectly benefit
|
||||
from the functionality of this software (Cherry Studio) does not exceed 10. This includes, but is not limited
|
||||
to, developers, testers, operations staff, end-users, and indirect users via integrated systems.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">1. Open Source License: AGPLv3 - For Individuals and
|
||||
Organizations of 10 or Fewer</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>If you are an individual user, or if your organization meets the "10 or Fewer Individuals" definition
|
||||
above, you are free to use, modify, and distribute Cherry Studio under the terms of the
|
||||
<strong>AGPLv3</strong>. The full text of the AGPLv3 can be found at <a
|
||||
href="https://www.gnu.org/licenses/agpl-3.0.html"
|
||||
class="text-blue-600 hover:underline">https://www.gnu.org/licenses/agpl-3.0.html</a>.
|
||||
</li>
|
||||
<li><strong>Core Obligation:</strong> A key requirement of the AGPLv3 is that if you modify Cherry Studio and
|
||||
make it available over a network, or distribute the modified version, you must provide the <strong>complete
|
||||
corresponding source code</strong> under the AGPLv3 license to the recipients. Even if you qualify under
|
||||
the "10 or Fewer Individuals" rule, if you wish to avoid this source code disclosure obligation, you will
|
||||
need to obtain a Commercial License (see below).</li>
|
||||
<li>Please read and understand the full terms of the AGPLv3 carefully before use.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">2. Commercial License - For Organizations with More Than 10
|
||||
Individuals, or Users Needing to Avoid AGPLv3 Obligations</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>Mandatory Requirement:</strong> If your organization does <strong>not</strong> meet the "10 or
|
||||
Fewer Individuals" definition above (i.e., 11 or more individuals can access, use, or benefit from the
|
||||
software), you <strong>must</strong> contact us to obtain and execute a Commercial License to use Cherry
|
||||
Studio.</li>
|
||||
</ol>
|
||||
<li><strong>Voluntary Option:</strong> Even if your organization meets the "10 or Fewer Individuals"
|
||||
condition, if your intended use case <strong>cannot comply with the terms of the AGPLv3</strong>
|
||||
(particularly the obligations regarding <strong>source code disclosure</strong>), or if you require specific
|
||||
commercial terms <strong>not offered</strong> by the AGPLv3 (such as warranties, indemnities, or freedom
|
||||
from copyleft restrictions), you also <strong>must</strong> contact us to obtain and execute a Commercial
|
||||
License.</li>
|
||||
<li><strong>Common scenarios requiring a Commercial License include (but are not limited to):</strong>
|
||||
<ul class="list-disc pl-6 mt-2 space-y-1">
|
||||
<li>Your organization has more than 10 individuals who can access, use, or benefit from the software.</li>
|
||||
<li>(Regardless of organization size) You wish to distribute a modified version of Cherry Studio but
|
||||
<strong>do not want</strong> to disclose the source code of your modifications under AGPLv3.
|
||||
</li>
|
||||
<li>(Regardless of organization size) You wish to provide a network service (SaaS) based on a modified
|
||||
version of Cherry Studio but <strong>do not want</strong> to provide the modified source code to users
|
||||
of the service under AGPLv3.</li>
|
||||
<li>(Regardless of organization size) Your corporate policies, client contracts, or project requirements
|
||||
prohibit the use of AGPLv3-licensed software or mandate closed-source distribution and confidentiality.
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>Obtaining a Commercial License:</strong> Please contact the Cherry Studio development team via
|
||||
email at <a href="mailto:bd@cherry-ai.com" class="text-blue-600 hover:underline">bd@cherry-ai.com</a> to
|
||||
discuss commercial licensing options.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">II. Contributor Agreement</h2>
|
||||
<p class="mb-4 text-gray-700">As a contributor to Cherry Studio, you must agree to the following terms:</p>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li><strong>License Adjustments:</strong> The producer reserves the right to adjust the open-source license as
|
||||
necessary, making it more strict or permissive.</li>
|
||||
<li><strong>Commercial Usage:</strong> Your contributed code may be used commercially, including but not
|
||||
limited to cloud business operations.</li>
|
||||
</ol>
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">3. Contributions</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>We welcome community contributions to Cherry Studio. All contributions submitted to this project are
|
||||
considered to be offered under the <strong>AGPLv3</strong> license.</li>
|
||||
<li>By submitting a contribution to this project (e.g., via a Pull Request), you agree to license your code
|
||||
under the AGPLv3 to the project and all its downstream users (regardless of whether those users ultimately
|
||||
operate under AGPLv3 or a Commercial License).</li>
|
||||
<li>You also understand and agree that your contribution may be included in distributions of Cherry Studio
|
||||
offered under our commercial license.</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mb-8">
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">III. Other Terms</h2>
|
||||
<ol class="list-decimal pl-6 space-y-2 text-gray-700">
|
||||
<li>Cherry Studio developers reserve the right of final interpretation of these agreement terms.</li>
|
||||
<li>This agreement may be updated according to practical circumstances, and users will be notified of updates
|
||||
through this software.</li>
|
||||
</ol>
|
||||
<h2 class="text-xl font-semibold mb-4 text-gray-900">4. Other Terms</h2>
|
||||
<ul class="list-disc pl-6 space-y-2 text-gray-700">
|
||||
<li>The specific terms and conditions of the Commercial License are governed by the formal commercial license
|
||||
agreement signed by both parties.</li>
|
||||
<li>The project maintainers reserve the right to update this licensing policy (including the definition and
|
||||
threshold for user count) as needed. Updates will be communicated through official project channels (e.g.,
|
||||
code repository, official website).</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<p class="mt-8 text-gray-700">
|
||||
Other than these specific conditions, all remaining rights and restrictions follow the Apache License 2.0. For
|
||||
more detailed information regarding Apache License 2.0, please visit
|
||||
<a href="http://www.apache.org/licenses/LICENSE-2.0"
|
||||
class="text-blue-600 hover:underline">http://www.apache.org/licenses/LICENSE-2.0</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
const { Arch } = require('electron-builder')
|
||||
const { default: removeLocales } = require('./remove-locales')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
exports.default = async function (context) {
|
||||
await removeLocales(context)
|
||||
const platform = context.packager.platform.name
|
||||
const arch = context.arch
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ Object.defineProperty(exports, '__esModule', { value: true })
|
||||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
var translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||
var baseLocale = 'zh-cn'
|
||||
var baseLocale = 'zh-CN'
|
||||
var baseFileName = ''.concat(baseLocale, '.json')
|
||||
var baseFilePath = path.join(translationsDir, baseFileName)
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
const translationsDir = path.join(__dirname, '../src/renderer/src/i18n/locales')
|
||||
const baseLocale = 'zh-cn'
|
||||
const baseLocale = 'zh-CN'
|
||||
const baseFileName = `${baseLocale}.json`
|
||||
const baseFilePath = path.join(translationsDir, baseFileName)
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
exports.default = async function (context) {
|
||||
const platform = context.packager.platform.name
|
||||
|
||||
// 根据平台确定 locales 目录位置
|
||||
let resourceDirs = []
|
||||
if (platform === 'mac') {
|
||||
// macOS 的语言文件位置
|
||||
resourceDirs = [
|
||||
path.join(context.appOutDir, 'Cherry Studio.app', 'Contents', 'Resources'),
|
||||
path.join(
|
||||
context.appOutDir,
|
||||
'Cherry Studio.app',
|
||||
'Contents',
|
||||
'Frameworks',
|
||||
'Electron Framework.framework',
|
||||
'Resources'
|
||||
)
|
||||
]
|
||||
} else {
|
||||
// Windows 和 Linux 的语言文件位置
|
||||
resourceDirs = [path.join(context.appOutDir, 'locales')]
|
||||
}
|
||||
|
||||
// 处理每个资源目录
|
||||
for (const resourceDir of resourceDirs) {
|
||||
if (!fs.existsSync(resourceDir)) {
|
||||
console.log(`Resource directory not found: ${resourceDir}, skipping...`)
|
||||
continue
|
||||
}
|
||||
|
||||
// 读取所有文件和目录
|
||||
const items = fs.readdirSync(resourceDir)
|
||||
|
||||
// 遍历并删除不需要的语言文件
|
||||
for (const item of items) {
|
||||
if (platform === 'mac') {
|
||||
// 在 macOS 上检查 .lproj 目录
|
||||
if (item.endsWith('.lproj') && !item.match(/^(en|zh|ru)/)) {
|
||||
const dirPath = path.join(resourceDir, item)
|
||||
fs.rmSync(dirPath, { recursive: true, force: true })
|
||||
console.log(`Removed locale directory: ${item} from ${resourceDir}`)
|
||||
}
|
||||
} else {
|
||||
// 其他平台处理 .pak 文件
|
||||
if (!item.match(/^(en|zh|ru)/)) {
|
||||
const filePath = path.join(resourceDir, item)
|
||||
fs.unlinkSync(filePath)
|
||||
console.log(`Removed locale file: ${item} from ${resourceDir}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Locale cleanup completed!')
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export const isMac = process.platform === 'darwin'
|
||||
export const isWin = process.platform === 'win32'
|
||||
export const isLinux = process.platform === 'linux'
|
||||
export const isDev = process.env.NODE_ENV === 'development'
|
||||
export const isPortable = isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env
|
||||
|
||||
@@ -11,6 +11,7 @@ export default class VoyageEmbeddings extends BaseEmbeddings {
|
||||
if (!this.configuration.outputDimension) {
|
||||
throw new Error('You need to pass in the optional dimensions parameter for this model')
|
||||
}
|
||||
console.log('VoyageEmbeddings', this.configuration)
|
||||
this.model = new _VoyageEmbeddings(this.configuration)
|
||||
}
|
||||
override async getDimensions(): Promise<number> {
|
||||
|
||||
@@ -8,10 +8,16 @@ import Logger from 'electron-log'
|
||||
import { registerIpc } from './ipc'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import mcpService from './services/MCPService'
|
||||
import { CHERRY_STUDIO_PROTOCOL, handleProtocolUrl, registerProtocolClient } from './services/ProtocolClient'
|
||||
import {
|
||||
CHERRY_STUDIO_PROTOCOL,
|
||||
handleProtocolUrl,
|
||||
registerProtocolClient,
|
||||
setupAppImageDeepLink
|
||||
} from './services/ProtocolClient'
|
||||
import { registerShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { setUserDataDir } from './utils/file'
|
||||
|
||||
// Check for single instance lock
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
@@ -48,11 +54,13 @@ if (!app.requestSingleInstanceLock()) {
|
||||
|
||||
registerIpc(mainWindow, app)
|
||||
|
||||
// 注意: MsTTS IPC处理程序已在ipc.ts中注册
|
||||
// 不需要再次调用registerMsTTSIpcHandlers()
|
||||
|
||||
replaceDevtoolsFont(mainWindow)
|
||||
|
||||
setUserDataDir()
|
||||
|
||||
// Setup deep link for AppImage on Linux
|
||||
await setupAppImageDeepLink()
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
installExtension([REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS])
|
||||
.then((name) => console.log(`Added Extension: ${name}`))
|
||||
@@ -75,14 +83,6 @@ if (!app.requestSingleInstanceLock()) {
|
||||
handleProtocolUrl(url)
|
||||
})
|
||||
|
||||
registerProtocolClient(app)
|
||||
|
||||
// macOS specific: handle protocol when app is already running
|
||||
app.on('open-url', (event, url) => {
|
||||
event.preventDefault()
|
||||
handleProtocolUrl(url)
|
||||
})
|
||||
|
||||
// Listen for second instance
|
||||
app.on('second-instance', (_event, argv) => {
|
||||
windowService.showMainWindow()
|
||||
|
||||
@@ -5,12 +5,11 @@ import { isMac, isWin } from '@main/constant'
|
||||
import { getBinaryPath, isBinaryExists, runInstallScript } from '@main/utils/process'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
import { BrowserWindow, ipcMain, nativeTheme, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
import { asrServerService } from './services/ASRServerService'
|
||||
import BackupManager from './services/BackupManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import CopilotService from './services/CopilotService'
|
||||
@@ -20,13 +19,13 @@ import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import mcpService from './services/MCPService'
|
||||
import * as MsTTSService from './services/MsTTSService'
|
||||
import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
import { searchService } from './services/SearchService'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { setOpenLinkExternal } from './services/WebviewService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { getResourcePath } from './utils'
|
||||
import { decrypt, encrypt } from './utils/aes'
|
||||
@@ -50,7 +49,8 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
appDataPath: app.getPath('userData'),
|
||||
resourcesPath: getResourcePath(),
|
||||
logsPath: log.transports.file.getFile().path,
|
||||
arch: arch()
|
||||
arch: arch(),
|
||||
isPortable: isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env
|
||||
}))
|
||||
|
||||
ipcMain.handle(IpcChannel.App_Proxy, async (_, proxy: string) => {
|
||||
@@ -104,6 +104,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// auto update
|
||||
ipcMain.handle(IpcChannel.App_SetAutoUpdate, (_, isActive: boolean) => {
|
||||
appUpdater.setAutoUpdate(isActive)
|
||||
configManager.setAutoUpdate(isActive)
|
||||
})
|
||||
|
||||
@@ -118,23 +119,26 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
|
||||
// theme
|
||||
ipcMain.handle(IpcChannel.App_SetTheme, (event, theme: ThemeMode) => {
|
||||
if (theme === configManager.getTheme()) return
|
||||
ipcMain.handle(IpcChannel.App_SetTheme, (_, theme: ThemeMode) => {
|
||||
const notifyThemeChange = () => {
|
||||
const windows = BrowserWindow.getAllWindows()
|
||||
windows.forEach((win) =>
|
||||
win.webContents.send(IpcChannel.ThemeChange, nativeTheme.shouldUseDarkColors ? ThemeMode.dark : ThemeMode.light)
|
||||
)
|
||||
}
|
||||
|
||||
configManager.setTheme(theme)
|
||||
|
||||
// should sync theme change to all windows
|
||||
const senderWindowId = event.sender.id
|
||||
const windows = BrowserWindow.getAllWindows()
|
||||
// 向其他窗口广播主题变化
|
||||
windows.forEach((win) => {
|
||||
if (win.webContents.id !== senderWindowId) {
|
||||
win.webContents.send(IpcChannel.ThemeChange, theme)
|
||||
}
|
||||
})
|
||||
if (theme === ThemeMode.auto) {
|
||||
nativeTheme.themeSource = 'system'
|
||||
nativeTheme.on('updated', notifyThemeChange)
|
||||
} else {
|
||||
nativeTheme.themeSource = theme
|
||||
nativeTheme.removeAllListeners('updated')
|
||||
}
|
||||
|
||||
mainWindow?.setTitleBarOverlay &&
|
||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
mainWindow.setTitleBarOverlay(nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
configManager.setTheme(theme)
|
||||
notifyThemeChange()
|
||||
})
|
||||
|
||||
// custom css
|
||||
@@ -167,7 +171,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
)
|
||||
await fileManager.clearTemp()
|
||||
fs.writeFileSync(log.transports.file.getFile().path, '')
|
||||
await fs.writeFileSync(log.transports.file.getFile().path, '')
|
||||
return { success: true }
|
||||
} catch (error: any) {
|
||||
log.error('Failed to clear cache:', error)
|
||||
@@ -177,12 +181,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
|
||||
// check for update
|
||||
ipcMain.handle(IpcChannel.App_CheckForUpdate, async () => {
|
||||
const update = await appUpdater.autoUpdater.checkForUpdates()
|
||||
|
||||
return {
|
||||
currentVersion: appUpdater.autoUpdater.currentVersion,
|
||||
updateInfo: update?.updateInfo
|
||||
}
|
||||
await appUpdater.checkForUpdates()
|
||||
})
|
||||
|
||||
// zip
|
||||
@@ -325,18 +324,18 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
)
|
||||
|
||||
// search window
|
||||
ipcMain.handle(IpcChannel.SearchWindow_Open, (_, uid: string) => searchService.openSearchWindow(uid))
|
||||
ipcMain.handle(IpcChannel.SearchWindow_Close, (_, uid: string) => searchService.closeSearchWindow(uid))
|
||||
ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, (_, uid: string, url: string) =>
|
||||
searchService.openUrlInSearchWindow(uid, url)
|
||||
)
|
||||
ipcMain.handle(IpcChannel.SearchWindow_Open, async (_, uid: string) => {
|
||||
await searchService.openSearchWindow(uid)
|
||||
})
|
||||
ipcMain.handle(IpcChannel.SearchWindow_Close, async (_, uid: string) => {
|
||||
await searchService.closeSearchWindow(uid)
|
||||
})
|
||||
ipcMain.handle(IpcChannel.SearchWindow_OpenUrl, async (_, uid: string, url: string) => {
|
||||
return await searchService.openUrlInSearchWindow(uid, url)
|
||||
})
|
||||
|
||||
// 注册ASR服务器IPC处理程序
|
||||
asrServerService.registerIpcHandlers()
|
||||
|
||||
// 注册MsTTS IPC处理程序
|
||||
ipcMain.handle(IpcChannel.MsTTS_GetVoices, MsTTSService.getVoices)
|
||||
ipcMain.handle(IpcChannel.MsTTS_Synthesize, (_, text: string, voice: string, outputFormat: string) =>
|
||||
MsTTSService.synthesize(text, voice, outputFormat)
|
||||
// webview
|
||||
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
||||
setOpenLinkExternal(webviewId, isExternal)
|
||||
)
|
||||
}
|
||||
|
||||
263
src/main/mcpServers/dify-knowledge.ts
Normal file
263
src/main/mcpServers/dify-knowledge.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
// inspired by https://dify.ai/blog/turn-your-dify-app-into-an-mcp-server
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema, ToolSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
import { z } from 'zod'
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema'
|
||||
|
||||
interface DifyKnowledgeServerConfig {
|
||||
difyKey: string
|
||||
apiHost: string
|
||||
}
|
||||
|
||||
interface DifyListKnowledgeResponse {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface DifySearchKnowledgeResponse {
|
||||
query: {
|
||||
content: string
|
||||
}
|
||||
records: Array<{
|
||||
segment: {
|
||||
id: string
|
||||
position: number
|
||||
document_id: string
|
||||
content: string
|
||||
keywords: string[]
|
||||
document?: {
|
||||
id: string
|
||||
data_source_type: string
|
||||
name: string
|
||||
}
|
||||
}
|
||||
score: number
|
||||
}>
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const ToolInputSchema = ToolSchema.shape.inputSchema
|
||||
type ToolInput = z.infer<typeof ToolInputSchema>
|
||||
|
||||
const SearchKnowledgeArgsSchema = z.object({
|
||||
id: z.string().describe('Knowledge ID'),
|
||||
query: z.string().describe('Query string'),
|
||||
topK: z.number().optional().describe('Number of top results to return')
|
||||
})
|
||||
|
||||
type McpResponse = {
|
||||
content: Array<{ type: 'text'; text: string }>
|
||||
isError?: boolean
|
||||
}
|
||||
|
||||
class DifyKnowledgeServer {
|
||||
public server: Server
|
||||
private config: DifyKnowledgeServerConfig
|
||||
|
||||
constructor(difyKey: string, args: string[]) {
|
||||
console.log('DifyKnowledgeServer args', args)
|
||||
if (args.length === 0) {
|
||||
throw new Error('DifyKnowledgeServer requires at least one argument')
|
||||
}
|
||||
this.config = {
|
||||
difyKey: difyKey,
|
||||
apiHost: args[0]
|
||||
}
|
||||
this.server = new Server(
|
||||
{
|
||||
name: '@cherry/dify-knowledge-server',
|
||||
version: '0.1.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: 'list_knowledges',
|
||||
description: 'List all knowledges',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: []
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search_knowledge',
|
||||
description: 'Search knowledge by id and query',
|
||||
inputSchema: zodToJsonSchema(SearchKnowledgeArgsSchema) as ToolInput
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
try {
|
||||
const { name, arguments: args } = request.params
|
||||
switch (name) {
|
||||
case 'list_knowledges': {
|
||||
return await this.performListKnowledges(this.config.difyKey, this.config.apiHost)
|
||||
}
|
||||
case 'search_knowledge': {
|
||||
const parsed = SearchKnowledgeArgsSchema.safeParse(args)
|
||||
if (!parsed.success) {
|
||||
const errorDetails = JSON.stringify(parsed.error.format(), null, 2)
|
||||
throw new Error(`无效的参数:\n${errorDetails}`)
|
||||
}
|
||||
|
||||
console.log('DifyKnowledgeServer search_knowledge parsed', parsed.data)
|
||||
return await this.performSearchKnowledge(
|
||||
parsed.data.id,
|
||||
parsed.data.query,
|
||||
parsed.data.topK || 6,
|
||||
this.config.difyKey,
|
||||
this.config.apiHost
|
||||
)
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async performListKnowledges(difyKey: string, apiHost: string): Promise<McpResponse> {
|
||||
try {
|
||||
const url = `${apiHost.replace(/\/$/, '')}/datasets`
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${difyKey}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const apiResponse = await response.json()
|
||||
|
||||
const knowledges: DifyListKnowledgeResponse[] =
|
||||
apiResponse?.data?.map((item: any) => ({
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
description: item.description || ''
|
||||
})) || []
|
||||
|
||||
const listText =
|
||||
knowledges.length > 0
|
||||
? knowledges.map((k) => `- **${k.name}** (ID: ${k.id})\n ${k.description || 'No Description'}`).join('\n')
|
||||
: '- No knowledges found.'
|
||||
|
||||
const formattedText = `### 可用知识库:\n\n${listText}`
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedText }]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取知识库列表时出错:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
// 返回包含错误信息的 MCP 响应
|
||||
return {
|
||||
content: [{ type: 'text', text: `Accessing Knowledge Error: ${errorMessage}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async performSearchKnowledge(
|
||||
id: string,
|
||||
query: string,
|
||||
topK: number,
|
||||
difyKey: string,
|
||||
apiHost: string
|
||||
): Promise<McpResponse> {
|
||||
try {
|
||||
const url = `${apiHost.replace(/\/$/, '')}/datasets/${id}/retrieve`
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${difyKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
retrieval_model: {
|
||||
top_k: topK,
|
||||
// will be error if not set
|
||||
reranking_enable: null,
|
||||
score_threshold_enabled: null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
throw new Error(`API 请求失败,状态码 ${response.status}: ${errorText}`)
|
||||
}
|
||||
|
||||
const searchResponse: DifySearchKnowledgeResponse = await response.json()
|
||||
|
||||
if (!searchResponse || !Array.isArray(searchResponse.records)) {
|
||||
throw new Error(`从 Dify API 收到的响应格式无效: ${JSON.stringify(searchResponse)}`)
|
||||
}
|
||||
|
||||
const header = `### Query: ${query}\n\n`
|
||||
let body: string
|
||||
|
||||
if (searchResponse.records.length === 0) {
|
||||
body = 'No results found.'
|
||||
} else {
|
||||
const resultsText = searchResponse.records
|
||||
.map((record, index) => {
|
||||
const docName = record.segment.document?.name || 'Unknown Document'
|
||||
const content = record.segment.content.trim()
|
||||
const score = record.score
|
||||
const keywords = record.segment.keywords || []
|
||||
|
||||
let resultEntry = `#### ${index + 1}. ${docName} (Relevant Score: ${(score * 100).toFixed(1)}%)`
|
||||
resultEntry += `\n${content}`
|
||||
if (keywords.length > 0) {
|
||||
resultEntry += `\n*Keywords: ${keywords.join(', ')}*`
|
||||
}
|
||||
return resultEntry
|
||||
})
|
||||
.join('\n\n')
|
||||
|
||||
body = `Found ${searchResponse.records.length} results:\n\n${resultsText}`
|
||||
}
|
||||
|
||||
const formattedText = header + body
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: formattedText }]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索知识库时出错:', error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
return {
|
||||
content: [{ type: 'text', text: `Search Knowledge Error: ${errorMessage}` }],
|
||||
isError: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DifyKnowledgeServer
|
||||
@@ -2,6 +2,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import BraveSearchServer from './brave-search'
|
||||
import DifyKnowledgeServer from './dify-knowledge'
|
||||
import FetchServer from './fetch'
|
||||
import FileSystemServer from './filesystem'
|
||||
import MemoryServer from './memory'
|
||||
@@ -26,6 +27,10 @@ export function createInMemoryMCPServer(name: string, args: string[] = [], envs:
|
||||
case '@cherry/filesystem': {
|
||||
return new FileSystemServer(args).server
|
||||
}
|
||||
case '@cherry/dify-knowledge': {
|
||||
const difyKey = envs.DIFY_KEY
|
||||
return new DifyKnowledgeServer(difyKey, args).server
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
||||
}
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { ChildProcess, spawn } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
/**
|
||||
* ASR服务器服务,用于管理ASR服务器进程
|
||||
*/
|
||||
class ASRServerService {
|
||||
private asrServerProcess: ChildProcess | null = null
|
||||
|
||||
/**
|
||||
* 注册IPC处理程序
|
||||
*/
|
||||
public registerIpcHandlers(): void {
|
||||
// 启动ASR服务器
|
||||
ipcMain.handle(IpcChannel.Asr_StartServer, this.startServer.bind(this))
|
||||
|
||||
// 停止ASR服务器
|
||||
ipcMain.handle(IpcChannel.Asr_StopServer, this.stopServer.bind(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动ASR服务器
|
||||
* @returns Promise<{success: boolean, pid?: number, error?: string}>
|
||||
*/
|
||||
private async startServer(): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||
try {
|
||||
if (this.asrServerProcess) {
|
||||
return { success: true, pid: this.asrServerProcess.pid }
|
||||
}
|
||||
|
||||
// 获取服务器文件路径
|
||||
log.info('App path:', app.getAppPath())
|
||||
// 在开发环境和生产环境中使用不同的路径
|
||||
let serverPath = ''
|
||||
const isPackaged = app.isPackaged
|
||||
|
||||
if (isPackaged) {
|
||||
// 生产环境 (打包后) - 使用 extraResources 复制的路径
|
||||
// 注意: 'app' 是 extraResources 配置中 'to' 字段的一部分
|
||||
serverPath = path.join(process.resourcesPath, 'app', 'asr-server', 'server.js')
|
||||
log.info('生产环境,ASR 服务器路径:', serverPath)
|
||||
} else {
|
||||
// 开发环境 - 指向项目根目录的 asr-server
|
||||
serverPath = path.join(app.getAppPath(), 'asr-server', 'server.js')
|
||||
log.info('开发环境,ASR 服务器路径:', serverPath)
|
||||
}
|
||||
|
||||
// 注意:删除了 isExeFile 检查逻辑, 假设总是用 node 启动
|
||||
// Removed unused variable 'isExeFile'
|
||||
log.info('ASR服务器路径:', serverPath)
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(serverPath)) {
|
||||
return { success: false, error: '服务器文件不存在' }
|
||||
}
|
||||
|
||||
// 启动服务器进程
|
||||
// 始终使用 node 启动 server.js
|
||||
log.info(`尝试使用 node 启动: ${serverPath}`)
|
||||
this.asrServerProcess = spawn('node', [serverPath], {
|
||||
stdio: 'pipe', // 'pipe' 用于捕获输出, 如果需要调试可以临时改为 'inherit'
|
||||
detached: false // false 通常足够
|
||||
})
|
||||
|
||||
// 处理服务器输出
|
||||
this.asrServerProcess.stdout?.on('data', (data) => {
|
||||
log.info(`[ASR Server] ${data.toString()}`)
|
||||
})
|
||||
|
||||
this.asrServerProcess.stderr?.on('data', (data) => {
|
||||
log.error(`[ASR Server Error] ${data.toString()}`)
|
||||
})
|
||||
|
||||
// 处理服务器退出
|
||||
this.asrServerProcess.on('close', (code) => {
|
||||
log.info(`[ASR Server] 进程退出,退出码: ${code}`)
|
||||
this.asrServerProcess = null
|
||||
})
|
||||
|
||||
// 等待一段时间确保服务器启动
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
return { success: true, pid: this.asrServerProcess.pid }
|
||||
} catch (error) {
|
||||
log.error('启动ASR服务器失败:', error)
|
||||
return { success: false, error: (error as Error).message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止ASR服务器
|
||||
* @param _event IPC事件
|
||||
* @param pid 进程ID
|
||||
* @returns Promise<{success: boolean, error?: string}>
|
||||
*/
|
||||
private async stopServer(
|
||||
_event: Electron.IpcMainInvokeEvent,
|
||||
pid?: number
|
||||
): Promise<{ success: boolean; error?: string }> {
|
||||
try {
|
||||
if (!this.asrServerProcess) {
|
||||
return { success: true }
|
||||
}
|
||||
|
||||
// 检查PID是否匹配
|
||||
if (pid && this.asrServerProcess.pid !== pid) {
|
||||
log.warn(`请求停止的PID (${pid}) 与当前运行的ASR服务器PID (${this.asrServerProcess.pid}) 不匹配`)
|
||||
}
|
||||
|
||||
// 杀死进程
|
||||
this.asrServerProcess.kill()
|
||||
|
||||
// 等待一段时间确保进程已经退出
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
|
||||
this.asrServerProcess = null
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
log.error('停止ASR服务器失败:', error)
|
||||
return { success: false, error: (error as Error).message }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const asrServerService = new ASRServerService()
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isWin } from '@main/constant'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { UpdateInfo } from 'builder-util-runtime'
|
||||
import { app, BrowserWindow, dialog } from 'electron'
|
||||
@@ -55,6 +56,40 @@ export default class AppUpdater {
|
||||
this.autoUpdater = autoUpdater
|
||||
}
|
||||
|
||||
public setAutoUpdate(isActive: boolean) {
|
||||
autoUpdater.autoDownload = isActive
|
||||
autoUpdater.autoInstallOnAppQuit = isActive
|
||||
}
|
||||
|
||||
public async checkForUpdates() {
|
||||
if (isWin && 'PORTABLE_EXECUTABLE_DIR' in process.env) {
|
||||
return {
|
||||
currentVersion: app.getVersion(),
|
||||
updateInfo: null
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const update = await this.autoUpdater.checkForUpdates()
|
||||
if (update?.isUpdateAvailable && !this.autoUpdater.autoDownload) {
|
||||
// 如果 autoDownload 为 false,则需要再调用下面的函数触发下
|
||||
// do not use await, because it will block the return of this function
|
||||
this.autoUpdater.downloadUpdate()
|
||||
}
|
||||
|
||||
return {
|
||||
currentVersion: this.autoUpdater.currentVersion,
|
||||
updateInfo: update?.updateInfo
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to check for update:', error)
|
||||
return {
|
||||
currentVersion: app.getVersion(),
|
||||
updateInfo: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async showUpdateDialog(mainWindow: BrowserWindow) {
|
||||
if (!this.releaseInfo) {
|
||||
return
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
import { AxiosInstance, default as axios_ } from 'axios'
|
||||
import { ProxyAgent } from 'proxy-agent'
|
||||
|
||||
import { proxyManager } from './ProxyManager'
|
||||
|
||||
class AxiosProxy {
|
||||
private cacheAxios: AxiosInstance | undefined
|
||||
private proxyURL: string | undefined
|
||||
private cacheAxios: AxiosInstance | null = null
|
||||
private proxyAgent: ProxyAgent | null = null
|
||||
|
||||
get axios(): AxiosInstance {
|
||||
const currentProxyURL = proxyManager.getProxyUrl()
|
||||
if (this.proxyURL !== currentProxyURL) {
|
||||
this.proxyURL = currentProxyURL
|
||||
const agent = proxyManager.getProxyAgent()
|
||||
const currentProxyAgent = proxyManager.getProxyAgent()
|
||||
|
||||
// 如果代理发生变化或尚未初始化,则重新创建 axios 实例
|
||||
if (this.cacheAxios === null || (currentProxyAgent !== null && this.proxyAgent !== currentProxyAgent)) {
|
||||
this.proxyAgent = currentProxyAgent
|
||||
|
||||
// 创建带有代理配置的 axios 实例
|
||||
this.cacheAxios = axios_.create({
|
||||
proxy: false,
|
||||
...(agent && { httpAgent: agent, httpsAgent: agent })
|
||||
httpAgent: currentProxyAgent || undefined,
|
||||
httpsAgent: currentProxyAgent || undefined
|
||||
})
|
||||
}
|
||||
|
||||
if (this.cacheAxios === undefined) {
|
||||
this.cacheAxios = axios_.create({ proxy: false })
|
||||
}
|
||||
return this.cacheAxios
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { WebDavConfig } from '@types'
|
||||
import AdmZip from 'adm-zip'
|
||||
import archiver from 'archiver'
|
||||
import { exec } from 'child_process'
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import extract from 'extract-zip'
|
||||
import * as fs from 'fs-extra'
|
||||
import * as path from 'path'
|
||||
import { createClient, CreateDirectoryOptions, FileStat } from 'webdav'
|
||||
@@ -91,6 +92,7 @@ class BackupManager {
|
||||
|
||||
// 使用流的方式写入 data.json
|
||||
const tempDataPath = path.join(this.tempDir, 'data.json')
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const writeStream = fs.createWriteStream(tempDataPath)
|
||||
writeStream.write(data)
|
||||
@@ -99,6 +101,7 @@ class BackupManager {
|
||||
writeStream.on('finish', () => resolve())
|
||||
writeStream.on('error', (error) => reject(error))
|
||||
})
|
||||
|
||||
onProgress({ stage: 'writing_data', progress: 20, total: 100 })
|
||||
|
||||
// 复制 Data 目录到临时目录
|
||||
@@ -112,18 +115,92 @@ class BackupManager {
|
||||
// 使用流式复制
|
||||
await this.copyDirWithProgress(sourcePath, tempDataDir, (size) => {
|
||||
copiedSize += size
|
||||
const progress = Math.min(80, 20 + Math.floor((copiedSize / totalSize) * 60))
|
||||
const progress = Math.min(50, Math.floor((copiedSize / totalSize) * 50))
|
||||
onProgress({ stage: 'copying_files', progress, total: 100 })
|
||||
})
|
||||
|
||||
await this.setWritableRecursive(tempDataDir)
|
||||
onProgress({ stage: 'compressing', progress: 80, total: 100 })
|
||||
onProgress({ stage: 'preparing_compression', progress: 50, total: 100 })
|
||||
|
||||
// 使用 adm-zip 创建压缩文件
|
||||
const zip = new AdmZip()
|
||||
zip.addLocalFolder(this.tempDir)
|
||||
// 创建输出文件流
|
||||
const backupedFilePath = path.join(destinationPath, fileName)
|
||||
zip.writeZip(backupedFilePath)
|
||||
const output = fs.createWriteStream(backupedFilePath)
|
||||
|
||||
// 创建 archiver 实例,启用 ZIP64 支持
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 1 }, // 使用最低压缩级别以提高速度
|
||||
zip64: true // 启用 ZIP64 支持以处理大文件
|
||||
})
|
||||
|
||||
let lastProgress = 50
|
||||
let totalEntries = 0
|
||||
let processedEntries = 0
|
||||
let totalBytes = 0
|
||||
let processedBytes = 0
|
||||
|
||||
// 首先计算总文件数和总大小
|
||||
const calculateTotals = async (dirPath: string) => {
|
||||
const items = await fs.readdir(dirPath, { withFileTypes: true })
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dirPath, item.name)
|
||||
if (item.isDirectory()) {
|
||||
await calculateTotals(fullPath)
|
||||
} else {
|
||||
totalEntries++
|
||||
const stats = await fs.stat(fullPath)
|
||||
totalBytes += stats.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await calculateTotals(this.tempDir)
|
||||
|
||||
// 监听文件添加事件
|
||||
archive.on('entry', () => {
|
||||
processedEntries++
|
||||
if (totalEntries > 0) {
|
||||
const progressPercent = Math.min(55, 50 + Math.floor((processedEntries / totalEntries) * 5))
|
||||
if (progressPercent > lastProgress) {
|
||||
lastProgress = progressPercent
|
||||
onProgress({ stage: 'compressing', progress: progressPercent, total: 100 })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听数据写入事件
|
||||
archive.on('data', (chunk) => {
|
||||
processedBytes += chunk.length
|
||||
if (totalBytes > 0) {
|
||||
const progressPercent = Math.min(99, 55 + Math.floor((processedBytes / totalBytes) * 44))
|
||||
if (progressPercent > lastProgress) {
|
||||
lastProgress = progressPercent
|
||||
onProgress({ stage: 'compressing', progress: progressPercent, total: 100 })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 使用 Promise 等待压缩完成
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
output.on('close', () => {
|
||||
onProgress({ stage: 'compressing', progress: 100, total: 100 })
|
||||
resolve()
|
||||
})
|
||||
archive.on('error', reject)
|
||||
archive.on('warning', (err: any) => {
|
||||
if (err.code !== 'ENOENT') {
|
||||
Logger.warn('[BackupManager] Archive warning:', err)
|
||||
}
|
||||
})
|
||||
|
||||
// 将输出流连接到压缩器
|
||||
archive.pipe(output)
|
||||
|
||||
// 添加整个临时目录到压缩文件
|
||||
archive.directory(this.tempDir, false)
|
||||
|
||||
// 完成压缩
|
||||
archive.finalize()
|
||||
})
|
||||
|
||||
// 清理临时目录
|
||||
await fs.remove(this.tempDir)
|
||||
@@ -133,6 +210,8 @@ class BackupManager {
|
||||
return backupedFilePath
|
||||
} catch (error) {
|
||||
Logger.error('[BackupManager] Backup failed:', error)
|
||||
// 确保清理临时目录
|
||||
await fs.remove(this.tempDir).catch(() => {})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -151,16 +230,22 @@ class BackupManager {
|
||||
onProgress({ stage: 'preparing', progress: 0, total: 100 })
|
||||
|
||||
Logger.log('[backup] step 1: unzip backup file', this.tempDir)
|
||||
// 使用 adm-zip 解压
|
||||
const zip = new AdmZip(backupPath)
|
||||
zip.extractAllTo(this.tempDir, true) // true 表示覆盖已存在的文件
|
||||
onProgress({ stage: 'extracting', progress: 20, total: 100 })
|
||||
|
||||
// 使用 extract-zip 解压
|
||||
await extract(backupPath, {
|
||||
dir: this.tempDir,
|
||||
onEntry: () => {
|
||||
// 这里可以处理进度,但 extract-zip 不提供总条目数信息
|
||||
onProgress({ stage: 'extracting', progress: 15, total: 100 })
|
||||
}
|
||||
})
|
||||
onProgress({ stage: 'extracting', progress: 25, total: 100 })
|
||||
|
||||
Logger.log('[backup] step 2: read data.json')
|
||||
// 读取 data.json
|
||||
const dataPath = path.join(this.tempDir, 'data.json')
|
||||
const data = await fs.readFile(dataPath, 'utf-8')
|
||||
onProgress({ stage: 'reading_data', progress: 40, total: 100 })
|
||||
onProgress({ stage: 'reading_data', progress: 35, total: 100 })
|
||||
|
||||
Logger.log('[backup] step 3: restore Data directory')
|
||||
// 恢复 Data 目录
|
||||
@@ -177,7 +262,7 @@ class BackupManager {
|
||||
// 使用流式复制
|
||||
await this.copyDirWithProgress(sourcePath, destPath, (size) => {
|
||||
copiedSize += size
|
||||
const progress = Math.min(90, 40 + Math.floor((copiedSize / totalSize) * 50))
|
||||
const progress = Math.min(85, 35 + Math.floor((copiedSize / totalSize) * 50))
|
||||
onProgress({ stage: 'copying_files', progress, total: 100 })
|
||||
})
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ enum ConfigKeys {
|
||||
Shortcuts = 'shortcuts',
|
||||
ClickTrayToShowQuickAssistant = 'clickTrayToShowQuickAssistant',
|
||||
EnableQuickAssistant = 'enableQuickAssistant',
|
||||
AutoUpdate = 'autoUpdate'
|
||||
AutoUpdate = 'autoUpdate',
|
||||
EnableDataCollection = 'enableDataCollection'
|
||||
}
|
||||
|
||||
export class ConfigManager {
|
||||
@@ -36,7 +37,7 @@ export class ConfigManager {
|
||||
}
|
||||
|
||||
getTheme(): ThemeMode {
|
||||
return this.get(ConfigKeys.Theme, ThemeMode.light)
|
||||
return this.get(ConfigKeys.Theme, ThemeMode.auto)
|
||||
}
|
||||
|
||||
setTheme(theme: ThemeMode) {
|
||||
@@ -145,6 +146,14 @@ export class ConfigManager {
|
||||
this.set(ConfigKeys.AutoUpdate, value)
|
||||
}
|
||||
|
||||
getEnableDataCollection(): boolean {
|
||||
return this.get<boolean>(ConfigKeys.EnableDataCollection, true)
|
||||
}
|
||||
|
||||
setEnableDataCollection(value: boolean) {
|
||||
this.set(ConfigKeys.EnableDataCollection, value)
|
||||
}
|
||||
|
||||
set(key: string, value: unknown) {
|
||||
this.store.set(key, value)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
export default class FileService {
|
||||
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string, encoding?: BufferEncoding) {
|
||||
// 如果指定了编码,则返回字符串,否则返回二进制数据
|
||||
if (encoding) {
|
||||
return fs.readFileSync(path, encoding)
|
||||
} else {
|
||||
return fs.readFileSync(path)
|
||||
}
|
||||
public static async readFile(_: Electron.IpcMainInvokeEvent, path: string) {
|
||||
return fs.readFileSync(path, 'utf8')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ import { getBinaryName, getBinaryPath } from '@main/utils/process'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { SSEClientTransport, SSEClientTransportOptions } from '@modelcontextprotocol/sdk/client/sse.js'
|
||||
import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import {
|
||||
StreamableHTTPClientTransport,
|
||||
type StreamableHTTPClientTransportOptions
|
||||
} from '@modelcontextprotocol/sdk/client/streamableHttp'
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory'
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import {
|
||||
@@ -29,7 +33,6 @@ import { memoize } from 'lodash'
|
||||
import { CacheService } from './CacheService'
|
||||
import { CallBackServer } from './mcp/oauth/callback'
|
||||
import { McpOAuthClientProvider } from './mcp/oauth/provider'
|
||||
import { StreamableHTTPClientTransport, type StreamableHTTPClientTransportOptions } from './MCPStreamableHttpClient'
|
||||
|
||||
// Generic type for caching wrapped functions
|
||||
type CachedFunction<T extends unknown[], R> = (...args: T) => Promise<R>
|
||||
@@ -158,6 +161,9 @@ class McpService {
|
||||
return new StreamableHTTPClientTransport(new URL(server.baseUrl!), options)
|
||||
} else if (server.type === 'sse') {
|
||||
const options: SSEClientTransportOptions = {
|
||||
eventSourceInit: {
|
||||
fetch: (url, init) => fetch(url, { ...init, headers: server.headers || {} })
|
||||
},
|
||||
requestInit: {
|
||||
headers: server.headers || {}
|
||||
},
|
||||
@@ -388,8 +394,17 @@ class McpService {
|
||||
): Promise<MCPCallToolResponse> {
|
||||
try {
|
||||
Logger.info('[MCP] Calling:', server.name, name, args)
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
args = JSON.parse(args)
|
||||
} catch (e) {
|
||||
Logger.error('[MCP] args parse error', args)
|
||||
}
|
||||
}
|
||||
const client = await this.initClient(server)
|
||||
const result = await client.callTool({ name, arguments: args })
|
||||
const result = await client.callTool({ name, arguments: args }, undefined, {
|
||||
timeout: server.timeout ? server.timeout * 1000 : 60000 // Default timeout of 1 minute
|
||||
})
|
||||
return result as MCPCallToolResponse
|
||||
} catch (error) {
|
||||
Logger.error(`[MCP] Error calling tool ${name} on ${server.name}:`, error)
|
||||
@@ -559,13 +574,26 @@ class McpService {
|
||||
return await cachedGetResource(server, uri)
|
||||
}
|
||||
|
||||
private findPowerShellExecutable() {
|
||||
const psPath = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' // Standard WinPS path
|
||||
const pwshPath = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
|
||||
|
||||
if (fs.existsSync(psPath)) {
|
||||
return psPath
|
||||
}
|
||||
if (fs.existsSync(pwshPath)) {
|
||||
return pwshPath
|
||||
}
|
||||
return 'powershell.exe'
|
||||
}
|
||||
|
||||
private getSystemPath = memoize(async (): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let command: string
|
||||
let shell: string
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
shell = 'powershell.exe'
|
||||
shell = this.findPowerShellExecutable()
|
||||
command = '$env:PATH'
|
||||
} else {
|
||||
// 尝试获取当前用户的默认 shell
|
||||
@@ -617,6 +645,10 @@ class McpService {
|
||||
console.error('Error getting PATH:', data.toString())
|
||||
})
|
||||
|
||||
child.on('error', (error: Error) => {
|
||||
reject(new Error(`Failed to get system PATH, ${error.message}`))
|
||||
})
|
||||
|
||||
child.on('close', (code: number) => {
|
||||
if (code === 0) {
|
||||
const trimmedPath = path.trim()
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
import { auth, AuthResult, OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
||||
import { JSONRPCMessage, JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||
|
||||
export class StreamableHTTPError extends Error {
|
||||
constructor(
|
||||
public readonly code: number | undefined,
|
||||
message: string | undefined,
|
||||
public readonly event: ErrorEvent
|
||||
) {
|
||||
super(`Streamable HTTP error: ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for the `StreamableHTTPClientTransport`.
|
||||
*/
|
||||
export type StreamableHTTPClientTransportOptions = {
|
||||
/**
|
||||
* An OAuth client provider to use for authentication.
|
||||
*
|
||||
* When an `authProvider` is specified and the connection is started:
|
||||
* 1. The connection is attempted with any existing access token from the `authProvider`.
|
||||
* 2. If the access token has expired, the `authProvider` is used to refresh the token.
|
||||
* 3. If token refresh fails or no access token exists, and auth is required, `OAuthClientProvider.redirectToAuthorization` is called, and an `UnauthorizedError` will be thrown from `connect`/`start`.
|
||||
*
|
||||
* After the user has finished authorizing via their user agent, and is redirected back to the MCP client application, call `StreamableHTTPClientTransport.finishAuth` with the authorization code before retrying the connection.
|
||||
*
|
||||
* If an `authProvider` is not provided, and auth is required, an `UnauthorizedError` will be thrown.
|
||||
*
|
||||
* `UnauthorizedError` might also be thrown when sending any message over the transport, indicating that the session has expired, and needs to be re-authed and reconnected.
|
||||
*/
|
||||
authProvider?: OAuthClientProvider
|
||||
|
||||
/**
|
||||
* Customizes HTTP requests to the server.
|
||||
*/
|
||||
requestInit?: RequestInit
|
||||
}
|
||||
|
||||
/**
|
||||
* Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
|
||||
* It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events
|
||||
* for receiving messages.
|
||||
*/
|
||||
export class StreamableHTTPClientTransport implements Transport {
|
||||
private _activeStreams: Map<string, ReadableStreamDefaultReader<Uint8Array>> = new Map()
|
||||
private _abortController?: AbortController
|
||||
private _url: URL
|
||||
private _requestInit?: RequestInit
|
||||
private _authProvider?: OAuthClientProvider
|
||||
private _sessionId?: string
|
||||
private _lastEventId?: string
|
||||
|
||||
onclose?: () => void
|
||||
onerror?: (error: Error) => void
|
||||
onmessage?: (message: JSONRPCMessage) => void
|
||||
|
||||
constructor(url: URL, opts?: StreamableHTTPClientTransportOptions) {
|
||||
this._url = url
|
||||
this._requestInit = opts?.requestInit
|
||||
this._authProvider = opts?.authProvider
|
||||
}
|
||||
|
||||
private async _authThenStart(): Promise<void> {
|
||||
if (!this._authProvider) {
|
||||
throw new UnauthorizedError('No auth provider')
|
||||
}
|
||||
|
||||
let result: AuthResult
|
||||
try {
|
||||
result = await auth(this._authProvider, { serverUrl: this._url })
|
||||
} catch (error) {
|
||||
this.onerror?.(error as Error)
|
||||
throw error
|
||||
}
|
||||
|
||||
if (result !== 'AUTHORIZED') {
|
||||
throw new UnauthorizedError()
|
||||
}
|
||||
|
||||
return await this._startOrAuth()
|
||||
}
|
||||
|
||||
private async _commonHeaders(): Promise<HeadersInit> {
|
||||
const headers: HeadersInit = {}
|
||||
if (this._authProvider) {
|
||||
const tokens = await this._authProvider.tokens()
|
||||
if (tokens) {
|
||||
headers['Authorization'] = `Bearer ${tokens.access_token}`
|
||||
}
|
||||
}
|
||||
|
||||
if (this._sessionId) {
|
||||
headers['mcp-session-id'] = this._sessionId
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
private async _startOrAuth(): Promise<void> {
|
||||
try {
|
||||
// Try to open an initial SSE stream with GET to listen for server messages
|
||||
// This is optional according to the spec - server may not support it
|
||||
const commonHeaders = await this._commonHeaders()
|
||||
const headers = new Headers(commonHeaders)
|
||||
headers.set('Accept', 'text/event-stream')
|
||||
|
||||
// Include Last-Event-ID header for resumable streams
|
||||
if (this._lastEventId) {
|
||||
headers.set('last-event-id', this._lastEventId)
|
||||
}
|
||||
|
||||
const response = await fetch(this._url, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
signal: this._abortController?.signal
|
||||
})
|
||||
|
||||
if (response.status === 405) {
|
||||
// Server doesn't support GET for SSE, which is allowed by the spec
|
||||
// We'll rely on SSE responses to POST requests for communication
|
||||
return
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 && this._authProvider) {
|
||||
// Need to authenticate
|
||||
return await this._authThenStart()
|
||||
}
|
||||
|
||||
const error = new Error(`Failed to open SSE stream: ${response.status} ${response.statusText}`)
|
||||
this.onerror?.(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
// Successful connection, handle the SSE stream as a standalone listener
|
||||
const streamId = `initial-${Date.now()}`
|
||||
this._handleSseStream(response.body, streamId)
|
||||
} catch (error) {
|
||||
this.onerror?.(error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this._activeStreams.size > 0) {
|
||||
throw new Error(
|
||||
'StreamableHTTPClientTransport already started! If using Client class, note that connect() calls start() automatically.'
|
||||
)
|
||||
}
|
||||
|
||||
this._abortController = new AbortController()
|
||||
return await this._startOrAuth()
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method after the user has finished authorizing via their user agent and is redirected back to the MCP client application. This will exchange the authorization code for an access token, enabling the next connection attempt to successfully auth.
|
||||
*/
|
||||
async finishAuth(authorizationCode: string): Promise<void> {
|
||||
if (!this._authProvider) {
|
||||
throw new UnauthorizedError('No auth provider')
|
||||
}
|
||||
|
||||
const result = await auth(this._authProvider, { serverUrl: this._url, authorizationCode })
|
||||
if (result !== 'AUTHORIZED') {
|
||||
throw new UnauthorizedError('Failed to authorize')
|
||||
}
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
// Close all active streams
|
||||
for (const reader of this._activeStreams.values()) {
|
||||
try {
|
||||
reader.cancel()
|
||||
} catch (error) {
|
||||
this.onerror?.(error as Error)
|
||||
}
|
||||
}
|
||||
this._activeStreams.clear()
|
||||
|
||||
// Abort any pending requests
|
||||
this._abortController?.abort()
|
||||
|
||||
// If we have a session ID, send a DELETE request to explicitly terminate the session
|
||||
if (this._sessionId) {
|
||||
try {
|
||||
const commonHeaders = await this._commonHeaders()
|
||||
const response = await fetch(this._url, {
|
||||
method: 'DELETE',
|
||||
headers: commonHeaders,
|
||||
signal: this._abortController?.signal
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
// Server might respond with 405 if it doesn't support explicit session termination
|
||||
// We don't throw an error in that case
|
||||
if (response.status !== 405) {
|
||||
const text = await response.text().catch(() => null)
|
||||
throw new Error(`Error terminating session (HTTP ${response.status}): ${text}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// We still want to invoke onclose even if the session termination fails
|
||||
this.onerror?.(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
this.onclose?.()
|
||||
}
|
||||
|
||||
async send(message: JSONRPCMessage | JSONRPCMessage[]): Promise<void> {
|
||||
try {
|
||||
const commonHeaders = await this._commonHeaders()
|
||||
const headers = new Headers({ ...commonHeaders, ...this._requestInit?.headers })
|
||||
headers.set('content-type', 'application/json')
|
||||
headers.set('accept', 'application/json, text/event-stream')
|
||||
|
||||
const init = {
|
||||
...this._requestInit,
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(message),
|
||||
signal: this._abortController?.signal
|
||||
}
|
||||
|
||||
const response = await fetch(this._url, init)
|
||||
|
||||
// Handle session ID received during initialization
|
||||
const sessionId = response.headers.get('mcp-session-id')
|
||||
if (sessionId) {
|
||||
this._sessionId = sessionId
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401 && this._authProvider) {
|
||||
const result = await auth(this._authProvider, { serverUrl: this._url })
|
||||
if (result !== 'AUTHORIZED') {
|
||||
throw new UnauthorizedError()
|
||||
}
|
||||
|
||||
// Purposely _not_ awaited, so we don't call onerror twice
|
||||
return this.send(message)
|
||||
}
|
||||
|
||||
const text = await response.text().catch(() => null)
|
||||
throw new Error(`Error POSTing to endpoint (HTTP ${response.status}): ${text}`)
|
||||
}
|
||||
|
||||
// If the response is 202 Accepted, there's no body to process
|
||||
if (response.status === 202) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get original message(s) for detecting request IDs
|
||||
const messages = Array.isArray(message) ? message : [message]
|
||||
|
||||
// Extract IDs from request messages for tracking responses
|
||||
const requestIds = messages
|
||||
.filter((msg) => 'method' in msg && 'id' in msg)
|
||||
.map((msg) => ('id' in msg ? msg.id : undefined))
|
||||
.filter((id) => id !== undefined)
|
||||
|
||||
// If we have request IDs and an SSE response, create a unique stream ID
|
||||
const hasRequests = requestIds.length > 0
|
||||
|
||||
// Check the response type
|
||||
const contentType = response.headers.get('content-type')
|
||||
|
||||
if (hasRequests) {
|
||||
if (contentType?.includes('text/event-stream')) {
|
||||
// For streaming responses, create a unique stream ID based on request IDs
|
||||
const streamId = `req-${requestIds.join('-')}-${Date.now()}`
|
||||
this._handleSseStream(response.body, streamId)
|
||||
} else if (contentType?.includes('application/json')) {
|
||||
// For non-streaming servers, we might get direct JSON responses
|
||||
const data = await response.json()
|
||||
const responseMessages = Array.isArray(data)
|
||||
? data.map((msg) => JSONRPCMessageSchema.parse(msg))
|
||||
: [JSONRPCMessageSchema.parse(data)]
|
||||
|
||||
for (const msg of responseMessages) {
|
||||
this.onmessage?.(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.onerror?.(error as Error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSseStream(stream: ReadableStream<Uint8Array> | null, streamId: string): void {
|
||||
if (!stream) {
|
||||
return
|
||||
}
|
||||
|
||||
// Set up stream handling for server-sent events
|
||||
const reader = stream.getReader()
|
||||
this._activeStreams.set(streamId, reader)
|
||||
const decoder = new TextDecoder()
|
||||
let buffer = ''
|
||||
|
||||
const processStream = async () => {
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) {
|
||||
// Stream closed by server
|
||||
this._activeStreams.delete(streamId)
|
||||
break
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true })
|
||||
|
||||
// Process SSE messages in the buffer
|
||||
const events = buffer.split('\n\n')
|
||||
buffer = events.pop() || ''
|
||||
|
||||
for (const event of events) {
|
||||
const lines = event.split('\n')
|
||||
let id: string | undefined
|
||||
let eventType: string | undefined
|
||||
let data: string | undefined
|
||||
|
||||
// Parse SSE message according to the format
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('id:')) {
|
||||
id = line.slice(3).trim()
|
||||
} else if (line.startsWith('event:')) {
|
||||
eventType = line.slice(6).trim()
|
||||
} else if (line.startsWith('data:')) {
|
||||
data = line.slice(5).trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Update last event ID if provided by server
|
||||
// As per spec: the ID MUST be globally unique across all streams within that session
|
||||
if (id) {
|
||||
this._lastEventId = id
|
||||
}
|
||||
|
||||
// Handle message event
|
||||
if (data) {
|
||||
// Default event type is 'message' per SSE spec if not specified
|
||||
if (!eventType || eventType === 'message') {
|
||||
try {
|
||||
const message = JSONRPCMessageSchema.parse(JSON.parse(data))
|
||||
this.onmessage?.(message)
|
||||
} catch (error) {
|
||||
this.onerror?.(error as Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this._activeStreams.delete(streamId)
|
||||
this.onerror?.(error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
processStream()
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { app } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { EdgeTTS } from 'node-edge-tts'
|
||||
|
||||
/**
|
||||
* Microsoft Edge TTS服务
|
||||
* 使用Microsoft Edge的在线TTS服务,不需要API密钥
|
||||
*/
|
||||
class MsEdgeTTSService {
|
||||
private static instance: MsEdgeTTSService
|
||||
private tempDir: string
|
||||
|
||||
private constructor() {
|
||||
this.tempDir = path.join(app.getPath('temp'), 'cherry-tts')
|
||||
|
||||
// 确保临时目录存在
|
||||
if (!fs.existsSync(this.tempDir)) {
|
||||
fs.mkdirSync(this.tempDir, { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
public static getInstance(): MsEdgeTTSService {
|
||||
if (!MsEdgeTTSService.instance) {
|
||||
MsEdgeTTSService.instance = new MsEdgeTTSService()
|
||||
}
|
||||
return MsEdgeTTSService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的语音列表
|
||||
* @returns 语音列表
|
||||
*/
|
||||
public async getVoices(): Promise<any[]> {
|
||||
try {
|
||||
// 返回预定义的中文语音列表
|
||||
return [
|
||||
{ name: 'zh-CN-XiaoxiaoNeural', locale: 'zh-CN', gender: 'Female' },
|
||||
{ name: 'zh-CN-YunxiNeural', locale: 'zh-CN', gender: 'Male' },
|
||||
{ name: 'zh-CN-YunyangNeural', locale: 'zh-CN', gender: 'Male' },
|
||||
{ name: 'zh-CN-XiaohanNeural', locale: 'zh-CN', gender: 'Female' },
|
||||
{ name: 'zh-CN-XiaomoNeural', locale: 'zh-CN', gender: 'Female' },
|
||||
{ name: 'zh-CN-XiaoxuanNeural', locale: 'zh-CN', gender: 'Female' },
|
||||
{ name: 'zh-CN-XiaoruiNeural', locale: 'zh-CN', gender: 'Female' },
|
||||
{ name: 'zh-CN-YunfengNeural', locale: 'zh-CN', gender: 'Male' }
|
||||
]
|
||||
} catch (error) {
|
||||
log.error('获取Microsoft Edge TTS语音列表失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合成语音
|
||||
* @param text 要合成的文本
|
||||
* @param voice 语音
|
||||
* @param outputFormat 输出格式
|
||||
* @returns 音频文件路径
|
||||
*/
|
||||
public async synthesize(text: string, voice: string, outputFormat: string): Promise<string> {
|
||||
try {
|
||||
log.info(`Microsoft Edge TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`)
|
||||
|
||||
// 验证输入参数
|
||||
if (!text || text.trim() === '') {
|
||||
throw new Error('要合成的文本不能为空')
|
||||
}
|
||||
|
||||
if (!voice || voice.trim() === '') {
|
||||
throw new Error('语音名称不能为空')
|
||||
}
|
||||
|
||||
// 创建一个新的EdgeTTS实例,并设置参数
|
||||
const tts = new EdgeTTS({
|
||||
voice: voice,
|
||||
outputFormat: outputFormat,
|
||||
timeout: 30000, // 30秒超时
|
||||
rate: '+0%', // 正常语速
|
||||
pitch: '+0Hz', // 正常音调
|
||||
volume: '+0%' // 正常音量
|
||||
})
|
||||
|
||||
// 生成临时文件路径
|
||||
const timestamp = Date.now()
|
||||
const fileExtension = outputFormat.includes('mp3') ? 'mp3' : outputFormat.split('-').pop() || 'audio'
|
||||
const outputPath = path.join(this.tempDir, `tts_${timestamp}.${fileExtension}`)
|
||||
|
||||
log.info(`开始生成语音文件: ${outputPath}`)
|
||||
|
||||
// 使用ttsPromise方法生成文件
|
||||
await tts.ttsPromise(text, outputPath)
|
||||
|
||||
// 验证生成的文件是否存在且大小大于0
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
throw new Error(`生成的语音文件不存在: ${outputPath}`)
|
||||
}
|
||||
|
||||
const stats = fs.statSync(outputPath)
|
||||
if (stats.size === 0) {
|
||||
throw new Error(`生成的语音文件大小为0: ${outputPath}`)
|
||||
}
|
||||
|
||||
log.info(`Microsoft Edge TTS合成成功: ${outputPath}, 文件大小: ${stats.size} 字节`)
|
||||
return outputPath
|
||||
} catch (error: any) {
|
||||
// 记录详细的错误信息
|
||||
log.error(`Microsoft Edge TTS语音合成失败 (语音=${voice}):`, error)
|
||||
|
||||
// 尝试提供更有用的错误信息
|
||||
if (error.message && typeof error.message === 'string') {
|
||||
if (error.message.includes('Timed out')) {
|
||||
throw new Error(`语音合成超时,请检查网络连接或尝试其他语音`)
|
||||
} else if (error.message.includes('ENOTFOUND')) {
|
||||
throw new Error(`无法连接到Microsoft语音服务,请检查网络连接`)
|
||||
} else if (error.message.includes('ECONNREFUSED')) {
|
||||
throw new Error(`连接被拒绝,请检查网络设置或代理配置`)
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例方法
|
||||
export const getVoices = async () => {
|
||||
return await MsEdgeTTSService.getInstance().getVoices()
|
||||
}
|
||||
|
||||
export const synthesize = async (text: string, voice: string, outputFormat: string) => {
|
||||
return await MsEdgeTTSService.getInstance().synthesize(text, voice, outputFormat)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
|
||||
import * as MsTTSService from './MsTTSService'
|
||||
|
||||
/**
|
||||
* 注册MsTTS相关的IPC处理程序
|
||||
*/
|
||||
export function registerMsTTSIpcHandlers(): void {
|
||||
// 获取可用的语音列表
|
||||
ipcMain.handle(IpcChannel.MsTTS_GetVoices, MsTTSService.getVoices)
|
||||
|
||||
// 合成语音
|
||||
ipcMain.handle(IpcChannel.MsTTS_Synthesize, (_, text: string, voice: string, outputFormat: string) =>
|
||||
MsTTSService.synthesize(text, voice, outputFormat)
|
||||
)
|
||||
|
||||
// 流式合成语音
|
||||
ipcMain.handle(
|
||||
IpcChannel.MsTTS_SynthesizeStream,
|
||||
async (event, requestId: string, text: string, voice: string, outputFormat: string) => {
|
||||
const window = BrowserWindow.fromWebContents(event.sender)
|
||||
if (!window) return
|
||||
|
||||
try {
|
||||
await MsTTSService.synthesizeStream(
|
||||
text,
|
||||
voice,
|
||||
outputFormat,
|
||||
(chunk: Uint8Array) => {
|
||||
// 发送音频数据块
|
||||
if (!window.isDestroyed()) {
|
||||
window.webContents.send(IpcChannel.MsTTS_StreamData, requestId, chunk)
|
||||
}
|
||||
},
|
||||
() => {
|
||||
// 发送流结束信号
|
||||
if (!window.isDestroyed()) {
|
||||
window.webContents.send(IpcChannel.MsTTS_StreamEnd, requestId)
|
||||
}
|
||||
}
|
||||
)
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('流式TTS合成失败:', error)
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) }
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -1,643 +0,0 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { MsEdgeTTS, OUTPUT_FORMAT } from 'edge-tts-node' // 新版支持流式的TTS库
|
||||
import { app } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { EdgeTTS } from 'node-edge-tts' // 旧版TTS库
|
||||
|
||||
// --- START OF HARDCODED VOICE LIST ---
|
||||
// WARNING: This list is static and may become outdated.
|
||||
// It's generally recommended to use listVoices() for the most up-to-date list.
|
||||
const hardcodedVoices = [
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (af-ZA, AdriNeural)',
|
||||
ShortName: 'af-ZA-AdriNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'af-ZA'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (am-ET, MekdesNeural)',
|
||||
ShortName: 'am-ET-MekdesNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'am-ET'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (ar-AE, FatimaNeural)',
|
||||
ShortName: 'ar-AE-FatimaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'ar-AE'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (ar-AE, HamdanNeural)',
|
||||
ShortName: 'ar-AE-HamdanNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'ar-AE'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (ar-BH, AliNeural)',
|
||||
ShortName: 'ar-BH-AliNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'ar-BH'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (ar-BH, LailaNeural)',
|
||||
ShortName: 'ar-BH-LailaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'ar-BH'
|
||||
},
|
||||
// ... (Many other Arabic locales/voices) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (ar-SA, ZariyahNeural)',
|
||||
ShortName: 'ar-SA-ZariyahNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'ar-SA'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (az-AZ, BabekNeural)',
|
||||
ShortName: 'az-AZ-BabekNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'az-AZ'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (az-AZ, BanuNeural)',
|
||||
ShortName: 'az-AZ-BanuNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'az-AZ'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (bg-BG, BorislavNeural)',
|
||||
ShortName: 'bg-BG-BorislavNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'bg-BG'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (bg-BG, KalinaNeural)',
|
||||
ShortName: 'bg-BG-KalinaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'bg-BG'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (bn-BD, NabanitaNeural)',
|
||||
ShortName: 'bn-BD-NabanitaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'bn-BD'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (bn-BD, PradeepNeural)',
|
||||
ShortName: 'bn-BD-PradeepNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'bn-BD'
|
||||
},
|
||||
// ... (Catalan, Czech, Welsh, Danish, German, Greek, English variants) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-AU, NatashaNeural)',
|
||||
ShortName: 'en-AU-NatashaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-AU'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-AU, WilliamNeural)',
|
||||
ShortName: 'en-AU-WilliamNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-AU'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-CA, ClaraNeural)',
|
||||
ShortName: 'en-CA-ClaraNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-CA'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-CA, LiamNeural)',
|
||||
ShortName: 'en-CA-LiamNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-CA'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, LibbyNeural)',
|
||||
ShortName: 'en-GB-LibbyNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-GB'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, MaisieNeural)',
|
||||
ShortName: 'en-GB-MaisieNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-GB'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, RyanNeural)',
|
||||
ShortName: 'en-GB-RyanNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-GB'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, SoniaNeural)',
|
||||
ShortName: 'en-GB-SoniaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-GB'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-GB, ThomasNeural)',
|
||||
ShortName: 'en-GB-ThomasNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-GB'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-HK, SamNeural)',
|
||||
ShortName: 'en-HK-SamNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-HK'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-HK, YanNeural)',
|
||||
ShortName: 'en-HK-YanNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-HK'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-IE, ConnorNeural)',
|
||||
ShortName: 'en-IE-ConnorNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-IE'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-IE, EmilyNeural)',
|
||||
ShortName: 'en-IE-EmilyNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-IE'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-IN, NeerjaNeural)',
|
||||
ShortName: 'en-IN-NeerjaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-IN'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-IN, PrabhatNeural)',
|
||||
ShortName: 'en-IN-PrabhatNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-IN'
|
||||
},
|
||||
// ... (Many more English variants: KE, NG, NZ, PH, SG, TZ, US, ZA) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, AriaNeural)',
|
||||
ShortName: 'en-US-AriaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-US'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, AnaNeural)',
|
||||
ShortName: 'en-US-AnaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-US'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, ChristopherNeural)',
|
||||
ShortName: 'en-US-ChristopherNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-US'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, EricNeural)',
|
||||
ShortName: 'en-US-EricNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-US'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, GuyNeural)',
|
||||
ShortName: 'en-US-GuyNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-US'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, JennyNeural)',
|
||||
ShortName: 'en-US-JennyNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-US'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, MichelleNeural)',
|
||||
ShortName: 'en-US-MichelleNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'en-US'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, RogerNeural)',
|
||||
ShortName: 'en-US-RogerNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-US'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (en-US, SteffanNeural)',
|
||||
ShortName: 'en-US-SteffanNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'en-US'
|
||||
},
|
||||
// ... (Spanish variants) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (es-MX, DaliaNeural)',
|
||||
ShortName: 'es-MX-DaliaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'es-MX'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (es-MX, JorgeNeural)',
|
||||
ShortName: 'es-MX-JorgeNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'es-MX'
|
||||
},
|
||||
// ... (Estonian, Basque, Persian, Finnish, Filipino, French, Irish, Galician, Gujarati, Hebrew, Hindi, Croatian, Hungarian, Indonesian, Icelandic, Italian, Japanese) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (ja-JP, KeitaNeural)',
|
||||
ShortName: 'ja-JP-KeitaNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'ja-JP'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (ja-JP, NanamiNeural)',
|
||||
ShortName: 'ja-JP-NanamiNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'ja-JP'
|
||||
},
|
||||
// ... (Javanese, Georgian, Kazakh, Khmer, Kannada, Korean) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (ko-KR, InJoonNeural)',
|
||||
ShortName: 'ko-KR-InJoonNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'ko-KR'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (ko-KR, SunHiNeural)',
|
||||
ShortName: 'ko-KR-SunHiNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'ko-KR'
|
||||
},
|
||||
// ... (Lao, Lithuanian, Latvian, Macedonian, Malayalam, Mongolian, Marathi, Malay, Maltese, Burmese, Norwegian, Dutch, Polish, Pashto, Portuguese) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (pt-BR, AntonioNeural)',
|
||||
ShortName: 'pt-BR-AntonioNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'pt-BR'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (pt-BR, FranciscaNeural)',
|
||||
ShortName: 'pt-BR-FranciscaNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'pt-BR'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (pt-PT, DuarteNeural)',
|
||||
ShortName: 'pt-PT-DuarteNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'pt-PT'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (pt-PT, RaquelNeural)',
|
||||
ShortName: 'pt-PT-RaquelNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'pt-PT'
|
||||
},
|
||||
// ... (Romanian, Russian, Sinhala, Slovak, Slovenian, Somali, Albanian, Serbian, Sundanese, Swedish, Swahili, Tamil, Telugu, Thai) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (th-TH, NiwatNeural)',
|
||||
ShortName: 'th-TH-NiwatNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'th-TH'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (th-TH, PremwadeeNeural)',
|
||||
ShortName: 'th-TH-PremwadeeNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'th-TH'
|
||||
},
|
||||
// ... (Turkish, Ukrainian, Urdu, Uzbek, Vietnamese) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (vi-VN, HoaiMyNeural)',
|
||||
ShortName: 'vi-VN-HoaiMyNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'vi-VN'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (vi-VN, NamMinhNeural)',
|
||||
ShortName: 'vi-VN-NamMinhNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'vi-VN'
|
||||
},
|
||||
// ... (Chinese variants) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, XiaoxiaoNeural)',
|
||||
ShortName: 'zh-CN-XiaoxiaoNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'zh-CN'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiNeural)',
|
||||
ShortName: 'zh-CN-YunxiNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'zh-CN'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunjianNeural)',
|
||||
ShortName: 'zh-CN-YunjianNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'zh-CN'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunxiaNeural)',
|
||||
ShortName: 'zh-CN-YunxiaNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'zh-CN'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN, YunyangNeural)',
|
||||
ShortName: 'zh-CN-YunyangNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'zh-CN'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN-liaoning, XiaobeiNeural)',
|
||||
ShortName: 'zh-CN-liaoning-XiaobeiNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'zh-CN-liaoning'
|
||||
},
|
||||
// { Name: 'Microsoft Server Speech Text to Speech Voice (zh-CN-shaanxi, XiaoniNeural)', ShortName: 'zh-CN-shaanxi-XiaoniNeural', Gender: 'Female', Locale: 'zh-CN-shaanxi' }, // Example regional voice
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-HK, HiuGaaiNeural)',
|
||||
ShortName: 'zh-HK-HiuGaaiNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'zh-HK'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-HK, HiuMaanNeural)',
|
||||
ShortName: 'zh-HK-HiuMaanNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'zh-HK'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-HK, WanLungNeural)',
|
||||
ShortName: 'zh-HK-WanLungNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'zh-HK'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-TW, HsiaoChenNeural)',
|
||||
ShortName: 'zh-TW-HsiaoChenNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'zh-TW'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-TW, HsiaoYuNeural)',
|
||||
ShortName: 'zh-TW-HsiaoYuNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'zh-TW'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zh-TW, YunJheNeural)',
|
||||
ShortName: 'zh-TW-YunJheNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'zh-TW'
|
||||
},
|
||||
// ... (Zulu) ...
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zu-ZA, ThandoNeural)',
|
||||
ShortName: 'zu-ZA-ThandoNeural',
|
||||
Gender: 'Female',
|
||||
Locale: 'zu-ZA'
|
||||
},
|
||||
{
|
||||
Name: 'Microsoft Server Speech Text to Speech Voice (zu-ZA, ThembaNeural)',
|
||||
ShortName: 'zu-ZA-ThembaNeural',
|
||||
Gender: 'Male',
|
||||
Locale: 'zu-ZA'
|
||||
}
|
||||
]
|
||||
// --- END OF HARDCODED VOICE LIST ---
|
||||
|
||||
/**
|
||||
* 免费在线TTS服务
|
||||
* 使用免费的在线TTS服务,不需要API密钥
|
||||
*/
|
||||
class MsTTSService {
|
||||
private static instance: MsTTSService
|
||||
private tempDir: string
|
||||
|
||||
private constructor() {
|
||||
this.tempDir = path.join(app.getPath('temp'), 'cherry-tts')
|
||||
if (!fs.existsSync(this.tempDir)) {
|
||||
fs.mkdirSync(this.tempDir, { recursive: true })
|
||||
}
|
||||
log.info('初始化免费在线TTS服务 (使用硬编码语音列表)')
|
||||
}
|
||||
|
||||
public static getInstance(): MsTTSService {
|
||||
if (!MsTTSService.instance) {
|
||||
MsTTSService.instance = new MsTTSService()
|
||||
}
|
||||
return MsTTSService.instance
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式合成语音
|
||||
* @param text 要合成的文本
|
||||
* @param voice 语音的 ShortName (例如 'zh-CN-XiaoxiaoNeural')
|
||||
* @param outputFormat 输出格式 (例如 'audio-24khz-48kbitrate-mono-mp3')
|
||||
* @param onData 数据块回调
|
||||
* @param onEnd 结束回调
|
||||
*/
|
||||
public async synthesizeStream(
|
||||
text: string,
|
||||
voice: string,
|
||||
outputFormat: string,
|
||||
onData: (chunk: Uint8Array) => void,
|
||||
onEnd: () => void
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 记录详细的请求信息
|
||||
log.info(`流式微软在线TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`)
|
||||
|
||||
// 验证输入参数
|
||||
if (!text || text.trim() === '') {
|
||||
throw new Error('要合成的文本不能为空')
|
||||
}
|
||||
|
||||
if (!voice || voice.trim() === '') {
|
||||
throw new Error('语音名称不能为空')
|
||||
}
|
||||
|
||||
// 创建一个新的MsEdgeTTS实例
|
||||
const tts = new MsEdgeTTS({
|
||||
enableLogger: false // 禁用内部日志
|
||||
})
|
||||
|
||||
// 设置元数据
|
||||
let msOutputFormat: OUTPUT_FORMAT
|
||||
if (outputFormat.includes('mp3')) {
|
||||
msOutputFormat = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3
|
||||
} else if (outputFormat.includes('webm')) {
|
||||
msOutputFormat = OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS
|
||||
} else {
|
||||
msOutputFormat = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3
|
||||
}
|
||||
|
||||
await tts.setMetadata(voice, msOutputFormat)
|
||||
|
||||
// 创建流
|
||||
const audioStream = tts.toStream(text)
|
||||
|
||||
// 监听数据事件
|
||||
audioStream.on('data', (data: Buffer) => {
|
||||
onData(data)
|
||||
})
|
||||
|
||||
// 监听结束事件
|
||||
audioStream.on('end', () => {
|
||||
log.info(`流式微软在线TTS合成成功`)
|
||||
onEnd()
|
||||
})
|
||||
|
||||
// 监听错误事件
|
||||
audioStream.on('error', (error: Error) => {
|
||||
log.error(`流式微软在线TTS语音合成失败:`, error)
|
||||
throw error
|
||||
})
|
||||
} catch (error: any) {
|
||||
// 记录详细的错误信息
|
||||
log.error(`流式微软在线TTS语音合成失败 (语音=${voice}):`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的语音列表 (返回硬编码列表)
|
||||
* @returns 语音列表
|
||||
*/
|
||||
public async getVoices(): Promise<any[]> {
|
||||
try {
|
||||
log.info(`返回硬编码的 ${hardcodedVoices.length} 个语音列表`)
|
||||
// 直接返回硬编码的列表
|
||||
// 注意:保持 async 是为了接口兼容性,虽然这里没有实际的异步操作
|
||||
return hardcodedVoices
|
||||
} catch (error) {
|
||||
// 这个 try/catch 在这里意义不大了,因为返回静态数据不会出错
|
||||
// 但保留结构以防未来改动
|
||||
log.error('获取硬编码语音列表时出错 (理论上不应发生):', error)
|
||||
return [] // 返回空列表以防万一
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合成语音
|
||||
* @param text 要合成的文本
|
||||
* @param voice 语音的 ShortName (例如 'zh-CN-XiaoxiaoNeural')
|
||||
* @param outputFormat 输出格式 (例如 'audio-24khz-48kbitrate-mono-mp3')
|
||||
* @returns 音频文件路径
|
||||
*/
|
||||
public async synthesize(text: string, voice: string, outputFormat: string): Promise<string> {
|
||||
try {
|
||||
// 记录详细的请求信息
|
||||
log.info(`微软在线TTS合成语音: 文本="${text.substring(0, 30)}...", 语音=${voice}, 格式=${outputFormat}`)
|
||||
|
||||
// 验证输入参数
|
||||
if (!text || text.trim() === '') {
|
||||
throw new Error('要合成的文本不能为空')
|
||||
}
|
||||
|
||||
if (!voice || voice.trim() === '') {
|
||||
throw new Error('语音名称不能为空')
|
||||
}
|
||||
|
||||
// 创建一个新的EdgeTTS实例,并设置参数
|
||||
// 添加超时设置,默认为30秒
|
||||
const tts = new EdgeTTS({
|
||||
voice: voice,
|
||||
outputFormat: outputFormat,
|
||||
timeout: 30000, // 30秒超时
|
||||
rate: '+0%', // 正常语速
|
||||
pitch: '+0Hz', // 正常音调
|
||||
volume: '+0%' // 正常音量
|
||||
})
|
||||
|
||||
// 生成临时文件路径
|
||||
const timestamp = Date.now()
|
||||
const fileExtension = outputFormat.includes('mp3') ? 'mp3' : outputFormat.split('-').pop() || 'audio'
|
||||
const outputPath = path.join(this.tempDir, `tts_${timestamp}.${fileExtension}`)
|
||||
|
||||
log.info(`开始生成语音文件: ${outputPath}`)
|
||||
|
||||
// 使用ttsPromise方法生成文件
|
||||
await tts.ttsPromise(text, outputPath)
|
||||
|
||||
// 验证生成的文件是否存在且大小大于0
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
throw new Error(`生成的语音文件不存在: ${outputPath}`)
|
||||
}
|
||||
|
||||
const stats = fs.statSync(outputPath)
|
||||
if (stats.size === 0) {
|
||||
throw new Error(`生成的语音文件大小为0: ${outputPath}`)
|
||||
}
|
||||
|
||||
log.info(`微软在线TTS合成成功: ${outputPath}, 文件大小: ${stats.size} 字节`)
|
||||
return outputPath
|
||||
} catch (error: any) {
|
||||
// 记录详细的错误信息
|
||||
log.error(`微软在线TTS语音合成失败 (语音=${voice}):`, error)
|
||||
|
||||
// 尝试提供更有用的错误信息
|
||||
if (error.message && typeof error.message === 'string') {
|
||||
if (error.message.includes('Timed out')) {
|
||||
throw new Error(`语音合成超时,请检查网络连接或尝试其他语音`)
|
||||
} else if (error.message.includes('ENOTFOUND')) {
|
||||
throw new Error(`无法连接到微软语音服务,请检查网络连接`)
|
||||
} else if (error.message.includes('ECONNREFUSED')) {
|
||||
throw new Error(`连接被拒绝,请检查网络设置或代理配置`)
|
||||
}
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* (可选) 清理临时文件目录
|
||||
*/
|
||||
public async cleanupTempDir(): Promise<void> {
|
||||
// (Cleanup method remains the same)
|
||||
try {
|
||||
const files = await fs.promises.readdir(this.tempDir)
|
||||
for (const file of files) {
|
||||
if (file.startsWith('tts_')) {
|
||||
await fs.promises.unlink(path.join(this.tempDir, file))
|
||||
}
|
||||
}
|
||||
log.info('TTS 临时文件已清理')
|
||||
} catch (error) {
|
||||
log.error('清理 TTS 临时文件失败:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例方法 (保持不变)
|
||||
export const getVoices = async () => {
|
||||
return await MsTTSService.getInstance().getVoices()
|
||||
}
|
||||
|
||||
export const synthesize = async (text: string, voice: string, outputFormat: string) => {
|
||||
return await MsTTSService.getInstance().synthesize(text, voice, outputFormat)
|
||||
}
|
||||
|
||||
export const synthesizeStream = async (
|
||||
text: string,
|
||||
voice: string,
|
||||
outputFormat: string,
|
||||
onData: (chunk: Uint8Array) => void,
|
||||
onEnd: () => void
|
||||
) => {
|
||||
return await MsTTSService.getInstance().synthesizeStream(text, voice, outputFormat, onData, onEnd)
|
||||
}
|
||||
|
||||
export const cleanupTtsTempFiles = async () => {
|
||||
await MsTTSService.getInstance().cleanupTempDir()
|
||||
}
|
||||
@@ -1,3 +1,12 @@
|
||||
import { exec } from 'node:child_process'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
|
||||
import { app } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { handleMcpProtocolUrl } from './urlschema/mcp-install'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
export const CHERRY_STUDIO_PROTOCOL = 'cherrystudio'
|
||||
@@ -22,6 +31,12 @@ export function handleProtocolUrl(url: string) {
|
||||
const urlObj = new URL(url)
|
||||
const params = new URLSearchParams(urlObj.search)
|
||||
|
||||
switch (urlObj.hostname.toLowerCase()) {
|
||||
case 'mcp':
|
||||
handleMcpProtocolUrl(urlObj)
|
||||
return
|
||||
}
|
||||
|
||||
// You can send the data to your renderer process
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
@@ -32,3 +47,78 @@ export function handleProtocolUrl(url: string) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
const DESKTOP_FILE_NAME = 'cherrystudio-url-handler.desktop'
|
||||
|
||||
/**
|
||||
* Sets up deep linking for the AppImage build on Linux by creating a .desktop file.
|
||||
* This allows the OS to open cherrystudio:// URLs with this App.
|
||||
*/
|
||||
export async function setupAppImageDeepLink(): Promise<void> {
|
||||
// Only run on Linux and when packaged as an AppImage
|
||||
if (process.platform !== 'linux' || !process.env.APPIMAGE) {
|
||||
return
|
||||
}
|
||||
|
||||
Logger.info('AppImage environment detected on Linux, setting up deep link.')
|
||||
|
||||
try {
|
||||
const appPath = app.getPath('exe')
|
||||
if (!appPath) {
|
||||
Logger.error('Could not determine App path.')
|
||||
return
|
||||
}
|
||||
|
||||
const homeDir = app.getPath('home')
|
||||
const applicationsDir = path.join(homeDir, '.local', 'share', 'applications')
|
||||
const desktopFilePath = path.join(applicationsDir, DESKTOP_FILE_NAME)
|
||||
|
||||
// Ensure the applications directory exists
|
||||
await fs.mkdir(applicationsDir, { recursive: true })
|
||||
|
||||
// Content of the .desktop file
|
||||
// %U allows passing the URL to the application
|
||||
// NoDisplay=true hides it from the regular application menu
|
||||
const desktopFileContent = `[Desktop Entry]
|
||||
Name=Cherry Studio
|
||||
Exec=${escapePathForExec(appPath)} %U
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/${CHERRY_STUDIO_PROTOCOL};
|
||||
NoDisplay=true
|
||||
`
|
||||
|
||||
// Write the .desktop file (overwrite if exists)
|
||||
await fs.writeFile(desktopFilePath, desktopFileContent, 'utf-8')
|
||||
Logger.info(`Created/Updated desktop file: ${desktopFilePath}`)
|
||||
|
||||
// Update the desktop database
|
||||
// It's important to update the database for the changes to take effect
|
||||
try {
|
||||
const { stdout, stderr } = await execAsync(`update-desktop-database ${escapePathForExec(applicationsDir)}`)
|
||||
if (stderr) {
|
||||
Logger.warn(`update-desktop-database stderr: ${stderr}`)
|
||||
}
|
||||
Logger.info(`update-desktop-database stdout: ${stdout}`)
|
||||
Logger.info('Desktop database updated successfully.')
|
||||
} catch (updateError) {
|
||||
Logger.error('Failed to update desktop database:', updateError)
|
||||
// Continue even if update fails, as the file is still created.
|
||||
}
|
||||
} catch (error) {
|
||||
// Log the error but don't prevent the app from starting
|
||||
Logger.error('Failed to setup AppImage deep link:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a path for safe use within the Exec field of a .desktop file
|
||||
* and for shell commands. Handles spaces and potentially other special characters
|
||||
* by quoting.
|
||||
*/
|
||||
function escapePathForExec(filePath: string): string {
|
||||
// Simple quoting for paths with spaces.
|
||||
return `'${filePath.replace(/'/g, "'\\''")}'`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ProxyConfig as _ProxyConfig, session } from 'electron'
|
||||
import { socksDispatcher } from 'fetch-socks'
|
||||
import { getSystemProxy } from 'os-proxy-config'
|
||||
import { ProxyAgent as GeneralProxyAgent } from 'proxy-agent'
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
@@ -70,15 +71,14 @@ export class ProxyManager {
|
||||
|
||||
private async setSystemProxy(): Promise<void> {
|
||||
try {
|
||||
await this.setSessionsProxy({ mode: 'system' })
|
||||
const proxyString = await session.defaultSession.resolveProxy('https://dummy.com')
|
||||
const [protocol, address] = proxyString.split(';')[0].split(' ')
|
||||
const url = protocol === 'PROXY' ? `http://${address}` : null
|
||||
if (url && url !== this.config.url) {
|
||||
this.config.url = url.toLowerCase()
|
||||
this.setEnvironment(this.config.url)
|
||||
this.proxyAgent = new GeneralProxyAgent()
|
||||
const currentProxy = await getSystemProxy()
|
||||
if (!currentProxy || currentProxy.proxyUrl === this.config.url) {
|
||||
return
|
||||
}
|
||||
await this.setSessionsProxy({ mode: 'system' })
|
||||
this.config.url = currentProxy.proxyUrl.toLowerCase()
|
||||
this.setEnvironment(this.config.url)
|
||||
this.proxyAgent = new GeneralProxyAgent()
|
||||
} catch (error) {
|
||||
console.error('Failed to set system proxy:', error)
|
||||
throw error
|
||||
|
||||
35
src/main/services/WebviewService.ts
Normal file
35
src/main/services/WebviewService.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { session, shell, webContents } from 'electron'
|
||||
|
||||
/**
|
||||
* init the useragent of the webview session
|
||||
* remove the CherryStudio and Electron from the useragent
|
||||
*/
|
||||
export function initSessionUserAgent() {
|
||||
const wvSession = session.fromPartition('persist:webview')
|
||||
const newChromeVersion = '135.0.7049.96'
|
||||
const originUA = wvSession.getUserAgent()
|
||||
const newUA = originUA
|
||||
.replace(/CherryStudio\/\S+\s/, '')
|
||||
.replace(/Electron\/\S+\s/, '')
|
||||
.replace(/Chrome\/\d+\.\d+\.\d+\.\d+/, `Chrome/${newChromeVersion}`)
|
||||
|
||||
wvSession.setUserAgent(newUA)
|
||||
}
|
||||
|
||||
/**
|
||||
* WebviewService handles the behavior of links opened from webview elements
|
||||
* It controls whether links should be opened within the application or in an external browser
|
||||
*/
|
||||
export function setOpenLinkExternal(webviewId: number, isExternal: boolean) {
|
||||
const webview = webContents.fromId(webviewId)
|
||||
if (!webview) return
|
||||
|
||||
webview.setWindowOpenHandler(({ url }) => {
|
||||
if (isExternal) {
|
||||
shell.openExternal(url)
|
||||
return { action: 'deny' }
|
||||
} else {
|
||||
return { action: 'allow' }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,8 @@ import { is } from '@electron-toolkit/utils'
|
||||
import { isDev, isLinux, isMac, isWin } from '@main/constant'
|
||||
import { getFilesDir } from '@main/utils/file'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
|
||||
import { ThemeMode } from '@types'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, nativeTheme, shell } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
@@ -11,6 +12,7 @@ import icon from '../../../build/icon.png?asset'
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
||||
import { locales } from '../utils/locales'
|
||||
import { configManager } from './ConfigManager'
|
||||
import { initSessionUserAgent } from './WebviewService'
|
||||
|
||||
export class WindowService {
|
||||
private static instance: WindowService | null = null
|
||||
@@ -41,10 +43,16 @@ export class WindowService {
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1080,
|
||||
defaultHeight: 670,
|
||||
fullScreen: false
|
||||
fullScreen: false,
|
||||
maximize: false
|
||||
})
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
if (theme === ThemeMode.auto) {
|
||||
nativeTheme.themeSource = 'system'
|
||||
} else {
|
||||
nativeTheme.themeSource = theme
|
||||
}
|
||||
|
||||
this.mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
@@ -59,8 +67,9 @@ export class WindowService {
|
||||
vibrancy: 'sidebar',
|
||||
visualEffectState: 'active',
|
||||
titleBarStyle: isLinux ? 'default' : 'hidden',
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
|
||||
darkTheme: nativeTheme.shouldUseDarkColors,
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(isLinux ? { icon } : {}),
|
||||
webPreferences: {
|
||||
@@ -80,12 +89,16 @@ export class WindowService {
|
||||
this.miniWindow = this.createMiniWindow(true)
|
||||
}
|
||||
|
||||
//init the MinApp webviews' useragent
|
||||
initSessionUserAgent()
|
||||
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
private setupMainWindow(mainWindow: BrowserWindow, mainWindowState: any) {
|
||||
mainWindowState.manage(mainWindow)
|
||||
|
||||
this.setupMaximize(mainWindow, mainWindowState.isMaximized)
|
||||
this.setupContextMenu(mainWindow)
|
||||
this.setupWindowEvents(mainWindow)
|
||||
this.setupWebContentsHandlers(mainWindow)
|
||||
@@ -93,6 +106,17 @@ export class WindowService {
|
||||
this.loadMainWindowContent(mainWindow)
|
||||
}
|
||||
|
||||
private setupMaximize(mainWindow: BrowserWindow, isMaximized: boolean) {
|
||||
if (isMaximized) {
|
||||
// 如果是从托盘启动,则需要延迟最大化,否则显示的就不是重启前的最大化窗口了
|
||||
configManager.getLaunchToTray()
|
||||
? mainWindow.once('show', () => {
|
||||
mainWindow.maximize()
|
||||
})
|
||||
: mainWindow.maximize()
|
||||
}
|
||||
}
|
||||
|
||||
private setupContextMenu(mainWindow: BrowserWindow) {
|
||||
if (!this.contextMenu) {
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
@@ -191,9 +215,11 @@ export class WindowService {
|
||||
|
||||
const oauthProviderUrls = [
|
||||
'https://account.siliconflow.cn/oauth',
|
||||
'https://cloud.siliconflow.cn/bills',
|
||||
'https://cloud.siliconflow.cn/expensebill',
|
||||
'https://aihubmix.com/token',
|
||||
'https://aihubmix.com/topup'
|
||||
'https://aihubmix.com/topup',
|
||||
'https://aihubmix.com/statistics'
|
||||
]
|
||||
|
||||
if (oauthProviderUrls.some((link) => url.startsWith(link))) {
|
||||
|
||||
@@ -21,7 +21,7 @@ export class CallBackServer {
|
||||
if (req.url?.startsWith(path)) {
|
||||
try {
|
||||
// Parse the URL to extract the authorization code
|
||||
const url = new URL(req.url, `http://localhost:${port}`)
|
||||
const url = new URL(req.url, `http://127.0.0.1:${port}`)
|
||||
const code = url.searchParams.get('code')
|
||||
if (code) {
|
||||
// Emit the code event
|
||||
|
||||
@@ -27,7 +27,7 @@ export class McpOAuthClientProvider implements OAuthClientProvider {
|
||||
}
|
||||
|
||||
get redirectUrl(): string {
|
||||
return `http://localhost:${this.config.callbackPort}${this.config.callbackPath}`
|
||||
return `http://127.0.0.1:${this.config.callbackPort}${this.config.callbackPath}`
|
||||
}
|
||||
|
||||
get clientMetadata() {
|
||||
|
||||
76
src/main/services/urlschema/mcp-install.ts
Normal file
76
src/main/services/urlschema/mcp-install.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { nanoid } from '@reduxjs/toolkit'
|
||||
import { IpcChannel } from '@shared/IpcChannel'
|
||||
import { MCPServer } from '@types'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { windowService } from '../WindowService'
|
||||
|
||||
function installMCPServer(server: MCPServer) {
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
|
||||
if (!server.id) {
|
||||
server.id = nanoid()
|
||||
}
|
||||
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.send(IpcChannel.Mcp_AddServer, server)
|
||||
}
|
||||
}
|
||||
|
||||
function installMCPServers(servers: Record<string, MCPServer>) {
|
||||
for (const name in servers) {
|
||||
const server = servers[name]
|
||||
if (!server.name) {
|
||||
server.name = name
|
||||
}
|
||||
installMCPServer(server)
|
||||
}
|
||||
}
|
||||
|
||||
export function handleMcpProtocolUrl(url: URL) {
|
||||
const params = new URLSearchParams(url.search)
|
||||
switch (url.pathname) {
|
||||
case '/install': {
|
||||
// jsonConfig example:
|
||||
// {
|
||||
// "mcpServers": {
|
||||
// "everything": {
|
||||
// "command": "npx",
|
||||
// "args": [
|
||||
// "-y",
|
||||
// "@modelcontextprotocol/server-everything"
|
||||
// ]
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// cherrystudio://mcp/install?servers={base64Encode(JSON.stringify(jsonConfig))}
|
||||
const data = params.get('servers')
|
||||
if (data) {
|
||||
const stringify = Buffer.from(data, 'base64').toString('utf8')
|
||||
Logger.info('install MCP servers from urlschema: ', stringify)
|
||||
const jsonConfig = JSON.parse(stringify)
|
||||
Logger.info('install MCP servers from urlschema: ', jsonConfig)
|
||||
|
||||
// support both {mcpServers: [servers]}, [servers] and {server}
|
||||
if (jsonConfig.mcpServers) {
|
||||
installMCPServers(jsonConfig.mcpServers)
|
||||
} else if (Array.isArray(jsonConfig)) {
|
||||
for (const server of jsonConfig) {
|
||||
installMCPServer(server)
|
||||
}
|
||||
} else {
|
||||
installMCPServer(jsonConfig)
|
||||
}
|
||||
}
|
||||
|
||||
const mainWindow = windowService.getMainWindow()
|
||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||
mainWindow.webContents.executeJavaScript("window.navigate('/settings/mcp')")
|
||||
}
|
||||
break
|
||||
}
|
||||
default:
|
||||
console.error(`Unknown MCP protocol URL: ${url}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
|
||||
import { isMac } from '@main/constant'
|
||||
import { audioExts, documentExts, imageExts, textExts, videoExts } from '@shared/config/constant'
|
||||
import { FileType, FileTypes } from '@types'
|
||||
import { app } from 'electron'
|
||||
@@ -83,3 +84,12 @@ export function getConfigDir() {
|
||||
export function getAppConfigDir(name: string) {
|
||||
return path.join(getConfigDir(), name)
|
||||
}
|
||||
|
||||
export function setUserDataDir() {
|
||||
if (!isMac) {
|
||||
const dir = path.join(path.dirname(app.getPath('exe')), 'data')
|
||||
if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
|
||||
app.setPath('userData', dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import path from 'node:path'
|
||||
import { app } from 'electron'
|
||||
|
||||
export function getResourcePath() {
|
||||
// 在打包环境中,使用process.resourcesPath,否则使用app.getAppPath()/resources
|
||||
return app.isPackaged ? process.resourcesPath : path.join(app.getAppPath(), 'resources')
|
||||
return path.join(app.getAppPath(), 'resources')
|
||||
}
|
||||
|
||||
export function getDataPath() {
|
||||
|
||||
209
src/preload/index.d.ts
vendored
209
src/preload/index.d.ts
vendored
@@ -1,209 +0,0 @@
|
||||
import { ExtractChunkData } from '@cherrystudio/embedjs-interfaces'
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import type { File } from '@google/genai'
|
||||
import type { GetMCPPromptResponse, MCPPrompt, MCPResource, MCPServer, MCPTool } from '@renderer/types'
|
||||
import { AppInfo, FileType, KnowledgeBaseParams, KnowledgeItem, LanguageVarious, WebDavConfig } from '@renderer/types'
|
||||
import type { LoaderReturn } from '@shared/config/types'
|
||||
import type { OpenDialogOptions } from 'electron'
|
||||
import type { UpdateInfo } from 'electron-updater'
|
||||
|
||||
interface BackupFile {
|
||||
fileName: string
|
||||
modifiedTime: string
|
||||
size: number
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: {
|
||||
getAppInfo: () => Promise<AppInfo>
|
||||
checkForUpdate: () => Promise<{ currentVersion: string; updateInfo: UpdateInfo | null }>
|
||||
showUpdateDialog: () => Promise<void>
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
setLanguage: (theme: LanguageVarious) => void
|
||||
setLaunchOnBoot: (isActive: boolean) => void
|
||||
setLaunchToTray: (isActive: boolean) => void
|
||||
setTray: (isActive: boolean) => void
|
||||
setTrayOnClose: (isActive: boolean) => void
|
||||
restartTray: () => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
setCustomCss: (css: string) => void
|
||||
setAutoUpdate: (isActive: boolean) => void
|
||||
reload: () => void
|
||||
clearCache: () => Promise<{ success: boolean; error?: string }>
|
||||
system: {
|
||||
getDeviceType: () => Promise<'mac' | 'windows' | 'linux'>
|
||||
getHostname: () => Promise<string>
|
||||
}
|
||||
zip: {
|
||||
compress: (text: string) => Promise<Buffer>
|
||||
decompress: (text: Buffer) => Promise<string>
|
||||
}
|
||||
backup: {
|
||||
backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable>
|
||||
restore: (backupPath: string) => Promise<string>
|
||||
backupToWebdav: (data: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||
restoreFromWebdav: (webdavConfig: WebDavConfig) => Promise<string>
|
||||
listWebdavFiles: (webdavConfig: WebDavConfig) => Promise<BackupFile[]>
|
||||
checkConnection: (webdavConfig: WebDavConfig) => Promise<boolean>
|
||||
createDirectory: (webdavConfig: WebDavConfig, path: string, options?: CreateDirectoryOptions) => Promise<void>
|
||||
deleteWebdavFile: (fileName: string, webdavConfig: WebDavConfig) => Promise<boolean>
|
||||
}
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||
upload: (file: FileType) => Promise<FileType>
|
||||
delete: (fileId: string) => Promise<void>
|
||||
read: (fileId: string) => Promise<string>
|
||||
clear: () => Promise<void>
|
||||
get: (filePath: string) => Promise<FileType | null>
|
||||
selectFolder: () => Promise<string | null>
|
||||
create: (fileName: string) => Promise<string>
|
||||
write: (filePath: string, data: Uint8Array | string) => Promise<void>
|
||||
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
|
||||
openPath: (path: string) => Promise<void>
|
||||
save: (
|
||||
path: string,
|
||||
content: string | NodeJS.ArrayBufferView,
|
||||
options?: SaveDialogOptions
|
||||
) => Promise<string | null>
|
||||
saveImage: (name: string, data: string) => void
|
||||
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
|
||||
download: (url: string) => Promise<FileType | null>
|
||||
copy: (fileId: string, destPath: string) => Promise<void>
|
||||
binaryFile: (fileId: string) => Promise<{ data: Buffer; mime: string }>
|
||||
}
|
||||
fs: {
|
||||
read: (path: string) => Promise<string>
|
||||
}
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => Promise<void>
|
||||
}
|
||||
openPath: (path: string) => Promise<void>
|
||||
shortcuts: {
|
||||
update: (shortcuts: Shortcut[]) => Promise<void>
|
||||
}
|
||||
knowledgeBase: {
|
||||
create: (base: KnowledgeBaseParams) => Promise<void>
|
||||
reset: (base: KnowledgeBaseParams) => Promise<void>
|
||||
delete: (id: string) => Promise<void>
|
||||
add: ({
|
||||
base,
|
||||
item,
|
||||
forceReload = false
|
||||
}: {
|
||||
base: KnowledgeBaseParams
|
||||
item: KnowledgeItem
|
||||
forceReload?: boolean
|
||||
}) => Promise<LoaderReturn>
|
||||
remove: ({
|
||||
uniqueId,
|
||||
uniqueIds,
|
||||
base
|
||||
}: {
|
||||
uniqueId: string
|
||||
uniqueIds: string[]
|
||||
base: KnowledgeBaseParams
|
||||
}) => Promise<void>
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
|
||||
rerank: ({
|
||||
search,
|
||||
base,
|
||||
results
|
||||
}: {
|
||||
search: string
|
||||
base: KnowledgeBaseParams
|
||||
results: ExtractChunkData[]
|
||||
}) => Promise<ExtractChunkData[]>
|
||||
}
|
||||
window: {
|
||||
setMinimumSize: (width: number, height: number) => Promise<void>
|
||||
resetMinimumSize: () => Promise<void>
|
||||
}
|
||||
gemini: {
|
||||
uploadFile: (file: FileType, apiKey: string) => Promise<File>
|
||||
retrieveFile: (file: FileType, apiKey: string) => Promise<File | undefined>
|
||||
base64File: (file: FileType) => Promise<{ data: string; mimeType: string }>
|
||||
listFiles: (apiKey: string) => Promise<File[]>
|
||||
deleteFile: (fileId: string, apiKey: string) => Promise<void>
|
||||
}
|
||||
selectionMenu: {
|
||||
action: (action: string) => Promise<void>
|
||||
}
|
||||
config: {
|
||||
set: (key: string, value: any) => Promise<void>
|
||||
get: (key: string) => Promise<any>
|
||||
}
|
||||
miniWindow: {
|
||||
show: () => Promise<void>
|
||||
hide: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
toggle: () => Promise<void>
|
||||
setPin: (isPinned: boolean) => Promise<void>
|
||||
}
|
||||
aes: {
|
||||
encrypt: (text: string, secretKey: string, iv: string) => Promise<{ iv: string; encryptedData: string }>
|
||||
decrypt: (encryptedData: string, iv: string, secretKey: string) => Promise<string>
|
||||
}
|
||||
shell: {
|
||||
openExternal: (url: string, options?: OpenExternalOptions) => Promise<void>
|
||||
}
|
||||
mcp: {
|
||||
removeServer: (server: MCPServer) => Promise<void>
|
||||
restartServer: (server: MCPServer) => Promise<void>
|
||||
stopServer: (server: MCPServer) => Promise<void>
|
||||
listTools: (server: MCPServer) => Promise<MCPTool[]>
|
||||
callTool: ({
|
||||
server,
|
||||
name,
|
||||
args
|
||||
}: {
|
||||
server: MCPServer
|
||||
name: string
|
||||
args: any
|
||||
}) => Promise<MCPCallToolResponse>
|
||||
listPrompts: (server: MCPServer) => Promise<MCPPrompt[]>
|
||||
getPrompt: ({
|
||||
server,
|
||||
name,
|
||||
args
|
||||
}: {
|
||||
server: MCPServer
|
||||
name: string
|
||||
args?: Record<string, any>
|
||||
}) => Promise<GetMCPPromptResponse>
|
||||
listResources: (server: MCPServer) => Promise<MCPResource[]>
|
||||
getResource: ({ server, uri }: { server: MCPServer; uri: string }) => Promise<GetResourceResponse>
|
||||
getInstallInfo: () => Promise<{ dir: string; uvPath: string; bunPath: string }>
|
||||
}
|
||||
copilot: {
|
||||
getAuthMessage: (
|
||||
headers?: Record<string, string>
|
||||
) => Promise<{ device_code: string; user_code: string; verification_uri: string }>
|
||||
getCopilotToken: (device_code: string, headers?: Record<string, string>) => Promise<{ access_token: string }>
|
||||
saveCopilotToken: (access_token: string) => Promise<void>
|
||||
getToken: (headers?: Record<string, string>) => Promise<{ token: string }>
|
||||
logout: () => Promise<void>
|
||||
getUser: (token: string) => Promise<{ login: string; avatar: string }>
|
||||
}
|
||||
isBinaryExist: (name: string) => Promise<boolean>
|
||||
getBinaryPath: (name: string) => Promise<string>
|
||||
installUVBinary: () => Promise<void>
|
||||
installBunBinary: () => Promise<void>
|
||||
protocol: {
|
||||
onReceiveData: (callback: (data: { url: string; params: any }) => void) => () => void
|
||||
}
|
||||
nutstore: {
|
||||
getSSOUrl: () => Promise<string>
|
||||
decryptToken: (token: string) => Promise<{ username: string; access_token: string }>
|
||||
getDirectoryContents: (token: string, path: string) => Promise<any>
|
||||
}
|
||||
searchService: {
|
||||
openSearchWindow: (uid: string) => Promise<string>
|
||||
closeSearchWindow: (uid: string) => Promise<string>
|
||||
openUrlInSearchWindow: (uid: string, url: string) => Promise<string>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { CreateDirectoryOptions } from 'webdav'
|
||||
const api = {
|
||||
getAppInfo: () => ipcRenderer.invoke(IpcChannel.App_Info),
|
||||
reload: () => ipcRenderer.invoke(IpcChannel.App_Reload),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy),
|
||||
setProxy: (proxy: string | undefined) => ipcRenderer.invoke(IpcChannel.App_Proxy, proxy),
|
||||
checkForUpdate: () => ipcRenderer.invoke(IpcChannel.App_CheckForUpdate),
|
||||
showUpdateDialog: () => ipcRenderer.invoke(IpcChannel.App_ShowUpdateDialog),
|
||||
setLanguage: (lang: string) => ipcRenderer.invoke(IpcChannel.App_SetLanguage, lang),
|
||||
@@ -18,7 +18,7 @@ const api = {
|
||||
setTray: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTray, isActive),
|
||||
setTrayOnClose: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetTrayOnClose, isActive),
|
||||
restartTray: () => ipcRenderer.invoke(IpcChannel.App_RestartTray),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
|
||||
setTheme: (theme: 'light' | 'dark' | 'auto') => ipcRenderer.invoke(IpcChannel.App_SetTheme, theme),
|
||||
setCustomCss: (css: string) => ipcRenderer.invoke(IpcChannel.App_SetCustomCss, css),
|
||||
setAutoUpdate: (isActive: boolean) => ipcRenderer.invoke(IpcChannel.App_SetAutoUpdate, isActive),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke(IpcChannel.Open_Website, url),
|
||||
@@ -50,16 +50,16 @@ const api = {
|
||||
},
|
||||
file: {
|
||||
select: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Select, options),
|
||||
upload: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Upload, filePath),
|
||||
upload: (file: FileType) => ipcRenderer.invoke(IpcChannel.File_Upload, file),
|
||||
delete: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Delete, fileId),
|
||||
read: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_Read, fileId),
|
||||
clear: () => ipcRenderer.invoke(IpcChannel.File_Clear),
|
||||
get: (filePath: string) => ipcRenderer.invoke(IpcChannel.File_Get, filePath),
|
||||
create: (fileName: string) => ipcRenderer.invoke(IpcChannel.File_Create, fileName),
|
||||
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke(IpcChannel.File_Write, filePath, data),
|
||||
open: (options?: { decompress: boolean }) => ipcRenderer.invoke(IpcChannel.File_Open, options),
|
||||
open: (options?: OpenDialogOptions) => ipcRenderer.invoke(IpcChannel.File_Open, options),
|
||||
openPath: (path: string) => ipcRenderer.invoke(IpcChannel.File_OpenPath, path),
|
||||
save: (path: string, content: string, options?: { compress: boolean }) =>
|
||||
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: any) =>
|
||||
ipcRenderer.invoke(IpcChannel.File_Save, path, content, options),
|
||||
selectFolder: () => ipcRenderer.invoke(IpcChannel.File_SelectFolder),
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke(IpcChannel.File_SaveImage, name, data),
|
||||
@@ -69,7 +69,7 @@ const api = {
|
||||
binaryFile: (fileId: string) => ipcRenderer.invoke(IpcChannel.File_BinaryFile, fileId)
|
||||
},
|
||||
fs: {
|
||||
read: (path: string, encoding?: BufferEncoding) => ipcRenderer.invoke(IpcChannel.Fs_Read, path, encoding)
|
||||
read: (path: string) => ipcRenderer.invoke(IpcChannel.Fs_Read, path)
|
||||
},
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke(IpcChannel.Export_Word, markdown, fileName)
|
||||
@@ -108,7 +108,7 @@ const api = {
|
||||
base64File: (file: FileType) => ipcRenderer.invoke(IpcChannel.Gemini_Base64File, file),
|
||||
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_RetrieveFile, file, apiKey),
|
||||
listFiles: (apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_ListFiles, apiKey),
|
||||
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke(IpcChannel.Gemini_DeleteFile, apiKey, fileId)
|
||||
deleteFile: (fileId: string, apiKey: string) => ipcRenderer.invoke(IpcChannel.Gemini_DeleteFile, fileId, apiKey)
|
||||
},
|
||||
selectionMenu: {
|
||||
action: (action: string) => ipcRenderer.invoke(IpcChannel.SelectionMenu_Action, action)
|
||||
@@ -124,11 +124,6 @@ const api = {
|
||||
toggle: () => ipcRenderer.invoke(IpcChannel.MiniWindow_Toggle),
|
||||
setPin: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.MiniWindow_SetPin, isPinned)
|
||||
},
|
||||
msTTS: {
|
||||
getVoices: () => ipcRenderer.invoke(IpcChannel.MsTTS_GetVoices),
|
||||
synthesize: (text: string, voice: string, outputFormat: string) =>
|
||||
ipcRenderer.invoke(IpcChannel.MsTTS_Synthesize, text, voice, outputFormat)
|
||||
},
|
||||
aes: {
|
||||
encrypt: (text: string, secretKey: string, iv: string) =>
|
||||
ipcRenderer.invoke(IpcChannel.Aes_Encrypt, text, secretKey, iv),
|
||||
@@ -140,7 +135,7 @@ const api = {
|
||||
restartServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_RestartServer, server),
|
||||
stopServer: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_StopServer, server),
|
||||
listTools: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListTools, server),
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
|
||||
callTool: ({ server, name, args }: { server: MCPServer; name: string; args: any }) =>
|
||||
ipcRenderer.invoke(IpcChannel.Mcp_CallTool, { server, name, args }),
|
||||
listPrompts: (server: MCPServer) => ipcRenderer.invoke(IpcChannel.Mcp_ListPrompts, server),
|
||||
getPrompt: ({ server, name, args }: { server: MCPServer; name: string; args?: Record<string, any> }) =>
|
||||
@@ -151,7 +146,7 @@ const api = {
|
||||
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo)
|
||||
},
|
||||
shell: {
|
||||
openExternal: shell.openExternal
|
||||
openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options)
|
||||
},
|
||||
copilot: {
|
||||
getAuthMessage: (headers?: Record<string, string>) =>
|
||||
@@ -191,9 +186,9 @@ const api = {
|
||||
closeSearchWindow: (uid: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_Close, uid),
|
||||
openUrlInSearchWindow: (uid: string, url: string) => ipcRenderer.invoke(IpcChannel.SearchWindow_OpenUrl, uid, url)
|
||||
},
|
||||
asrServer: {
|
||||
startServer: () => ipcRenderer.invoke(IpcChannel.Asr_StartServer),
|
||||
stopServer: (pid: number) => ipcRenderer.invoke(IpcChannel.Asr_StopServer, pid)
|
||||
webview: {
|
||||
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
|
||||
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,3 +213,5 @@ if (process.contextIsolated) {
|
||||
// @ts-ignore (define in dts)
|
||||
window.api = api
|
||||
}
|
||||
|
||||
export type WindowApiType = typeof api
|
||||
|
||||
11
src/preload/preload.d.ts
vendored
Normal file
11
src/preload/preload.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
import type { WindowApiType } from './index'
|
||||
|
||||
/** you don't need to declare this in your code, it's automatically generated */
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI
|
||||
api: WindowApiType
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||
<title>Cherry Studio</title>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; media-src blob: *; frame-src * file:" />
|
||||
<title>Cherry Studio</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
#spinner {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#spinner {
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
#spinner img {
|
||||
width: 100px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
#spinner img {
|
||||
width: 100px;
|
||||
border-radius: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="spinner">
|
||||
<img src="/src/assets/images/logo.png" />
|
||||
</div>
|
||||
<script type="module" src="/src/init.ts"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<div id="spinner">
|
||||
<img src="/src/assets/images/logo.png" />
|
||||
</div>
|
||||
<script type="module" src="/src/init.ts"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,7 +17,7 @@ import AppsPage from './pages/apps/AppsPage'
|
||||
import FilesPage from './pages/files/FilesPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||
import PaintingsPage from './pages/paintings/PaintingsPage'
|
||||
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
|
||||
@@ -36,7 +36,7 @@ function App(): React.ReactElement {
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/agents" element={<AgentsPage />} />
|
||||
<Route path="/paintings" element={<PaintingsPage />} />
|
||||
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/files" element={<FilesPage />} />
|
||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Browser ASR (External)</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
#status {
|
||||
margin-top: 1em;
|
||||
font-style: italic;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 0.5em;
|
||||
border: 1px solid #ccc;
|
||||
padding: 0.5em;
|
||||
min-height: 50px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>浏览器语音识别中继页面</h1>
|
||||
<p>这个页面需要在浏览器中保持打开,以便应用使用其语音识别功能。</p>
|
||||
<div id="status">正在连接到服务器...</div>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
const statusDiv = document.getElementById('status');
|
||||
const resultDiv = document.getElementById('result');
|
||||
const ws = new WebSocket('ws://localhost:8080'); // Use the defined port
|
||||
let recognition = null;
|
||||
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
||||
|
||||
function updateStatus(message) {
|
||||
console.log(`[Browser Page Status] ${message}`);
|
||||
statusDiv.textContent = message;
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
updateStatus('已连接到服务器,等待指令...');
|
||||
ws.send(JSON.stringify({ type: 'identify', role: 'browser' }));
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(event.data);
|
||||
console.log('[Browser Page] Received command:', data);
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Received non-JSON message:', event.data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'start') {
|
||||
startRecognition();
|
||||
} else if (data.type === 'stop') {
|
||||
stopRecognition();
|
||||
} else if (data.type === 'reset') {
|
||||
// 强制重置语音识别
|
||||
forceResetRecognition();
|
||||
} else {
|
||||
console.warn('[Browser Page] Received unknown command type:', data.type);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[Browser Page] WebSocket Error:', error);
|
||||
updateStatus('WebSocket 连接错误!请检查服务器是否运行。');
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[Browser Page] WebSocket Connection Closed');
|
||||
updateStatus('与服务器断开连接。请刷新页面或重启服务器。');
|
||||
stopRecognition();
|
||||
};
|
||||
|
||||
function setupRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:此浏览器不支持 Web Speech API。');
|
||||
return false;
|
||||
}
|
||||
if (recognition && recognition.recognizing) {
|
||||
console.log('[Browser Page] Recognition already active.');
|
||||
return true;
|
||||
}
|
||||
|
||||
recognition = new SpeechRecognition();
|
||||
recognition.lang = 'zh-CN';
|
||||
recognition.continuous = true;
|
||||
recognition.interimResults = true;
|
||||
// 增加以下设置提高语音识别的可靠性
|
||||
recognition.maxAlternatives = 3; // 返回多个可能的识别结果
|
||||
// 设置较短的语音识别时间,使用户能更快地看到结果
|
||||
// 注意:这个属性不是标准的,可能不是所有浏览器都支持
|
||||
try {
|
||||
// @ts-ignore
|
||||
recognition.audioStart = 0.1; // 尝试设置较低的起始音量阈值
|
||||
} catch (e) {
|
||||
console.log('[Browser Page] audioStart property not supported');
|
||||
}
|
||||
|
||||
recognition.onstart = () => {
|
||||
updateStatus("🎤 正在识别...");
|
||||
console.log('[Browser Page] SpeechRecognition started.');
|
||||
};
|
||||
|
||||
recognition.onresult = (event) => {
|
||||
console.log('[Browser Page] Recognition result event:', event);
|
||||
|
||||
let interim_transcript = '';
|
||||
let final_transcript = '';
|
||||
|
||||
// 输出识别结果的详细信息便于调试
|
||||
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
||||
const confidence = event.results[i][0].confidence;
|
||||
console.log(`[Browser Page] Result ${i}: ${event.results[i][0].transcript} (Confidence: ${confidence.toFixed(2)})`);
|
||||
|
||||
if (event.results[i].isFinal) {
|
||||
final_transcript += event.results[i][0].transcript;
|
||||
} else {
|
||||
interim_transcript += event.results[i][0].transcript;
|
||||
}
|
||||
}
|
||||
|
||||
const resultText = final_transcript || interim_transcript;
|
||||
resultDiv.textContent = resultText;
|
||||
|
||||
// 更新状态显示
|
||||
if (resultText) {
|
||||
updateStatus(`🎤 正在识别... (已捕捉到语音)`);
|
||||
}
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
console.log(`[Browser Page] Sending ${final_transcript ? 'final' : 'interim'} result to server:`, resultText);
|
||||
ws.send(JSON.stringify({ type: 'result', data: { text: resultText, isFinal: !!final_transcript } }));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onerror = (event) => {
|
||||
console.error(`[Browser Page] SpeechRecognition Error - Type: ${event.error}, Message: ${event.message}`);
|
||||
|
||||
// 根据错误类型提供更友好的错误提示
|
||||
let errorMessage = '';
|
||||
switch (event.error) {
|
||||
case 'no-speech':
|
||||
errorMessage = '未检测到语音,请确保麦克风工作正常并尝试说话。';
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Restarting recognition after no-speech error');
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
break;
|
||||
case 'audio-capture':
|
||||
errorMessage = '无法捕获音频,请确保麦克风已连接并已授权。';
|
||||
break;
|
||||
case 'not-allowed':
|
||||
errorMessage = '浏览器不允许使用麦克风,请检查权限设置。';
|
||||
break;
|
||||
case 'network':
|
||||
errorMessage = '网络错误导致语音识别失败。';
|
||||
break;
|
||||
case 'aborted':
|
||||
errorMessage = '语音识别被用户或系统中止。';
|
||||
break;
|
||||
default:
|
||||
errorMessage = `识别错误: ${event.error}`;
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMessage}`);
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
data: {
|
||||
error: event.error,
|
||||
message: errorMessage || event.message || `Recognition error: ${event.error}`
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
recognition.onend = () => {
|
||||
console.log('[Browser Page] SpeechRecognition ended.');
|
||||
|
||||
// 检查是否是由于错误或用户手动停止导致的结束
|
||||
const isErrorOrStopped = statusDiv.textContent.includes('错误') || statusDiv.textContent.includes('停止');
|
||||
|
||||
if (!isErrorOrStopped) {
|
||||
// 如果不是由于错误或手动停止,则自动重新启动语音识别
|
||||
updateStatus("识别暂停,正在重新启动...");
|
||||
|
||||
// 保存当前的recognition对象
|
||||
const currentRecognition = recognition;
|
||||
|
||||
// 尝试重新启动语音识别
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (currentRecognition && currentRecognition === recognition) {
|
||||
currentRecognition.start();
|
||||
console.log('[Browser Page] Automatically restarting recognition');
|
||||
} else {
|
||||
// 如果recognition对象已经变化,重新创建一个
|
||||
setupRecognition();
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
console.log('[Browser Page] Created new recognition instance and started');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Failed to restart recognition:', e);
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
}
|
||||
}, 300);
|
||||
} else {
|
||||
updateStatus("识别已停止。等待指令...");
|
||||
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'stopped' }));
|
||||
}
|
||||
|
||||
// 只有在手动停止或错误时才重置recognition对象
|
||||
recognition = null;
|
||||
}
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
function startRecognition() {
|
||||
if (!SpeechRecognition) {
|
||||
updateStatus('错误:浏览器不支持 Web Speech API。');
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示正在准备的状态
|
||||
updateStatus('正在准备麦克风...');
|
||||
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Recognition already exists, stopping first.');
|
||||
stopRecognition();
|
||||
}
|
||||
|
||||
if (!setupRecognition()) return;
|
||||
|
||||
console.log('[Browser Page] Attempting to start recognition...');
|
||||
try {
|
||||
// 设置更长的超时时间,确保有足够的时间获取麦克风权限
|
||||
const micPermissionTimeout = setTimeout(() => {
|
||||
updateStatus('获取麦克风权限超时,请刷新页面重试。');
|
||||
}, 10000); // 10秒超时
|
||||
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
autoGainControl: true
|
||||
}
|
||||
})
|
||||
.then(stream => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.log('[Browser Page] Microphone access granted.');
|
||||
|
||||
// 检查麦克风音量级别
|
||||
const audioContext = new AudioContext();
|
||||
const analyser = audioContext.createAnalyser();
|
||||
const microphone = audioContext.createMediaStreamSource(stream);
|
||||
const javascriptNode = audioContext.createScriptProcessor(2048, 1, 1);
|
||||
|
||||
analyser.smoothingTimeConstant = 0.8;
|
||||
analyser.fftSize = 1024;
|
||||
|
||||
microphone.connect(analyser);
|
||||
analyser.connect(javascriptNode);
|
||||
javascriptNode.connect(audioContext.destination);
|
||||
|
||||
javascriptNode.onaudioprocess = function () {
|
||||
const array = new Uint8Array(analyser.frequencyBinCount);
|
||||
analyser.getByteFrequencyData(array);
|
||||
let values = 0;
|
||||
|
||||
const length = array.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
values += (array[i]);
|
||||
}
|
||||
|
||||
const average = values / length;
|
||||
console.log('[Browser Page] Microphone volume level:', average);
|
||||
|
||||
// 如果音量太低,显示提示
|
||||
if (average < 5) {
|
||||
updateStatus('麦克风音量很低,请说话或检查麦克风设置。');
|
||||
} else {
|
||||
updateStatus('🎤 正在识别...');
|
||||
}
|
||||
|
||||
// 只检查一次就断开连接
|
||||
microphone.disconnect();
|
||||
analyser.disconnect();
|
||||
javascriptNode.disconnect();
|
||||
};
|
||||
|
||||
// 释放测试用的音频流
|
||||
setTimeout(() => {
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
audioContext.close();
|
||||
}, 1000);
|
||||
|
||||
// 启动语音识别
|
||||
if (recognition) {
|
||||
recognition.start();
|
||||
updateStatus('🎤 正在识别...');
|
||||
} else {
|
||||
updateStatus('错误:Recognition 实例丢失。');
|
||||
console.error('[Browser Page] Recognition instance lost before start.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
clearTimeout(micPermissionTimeout);
|
||||
console.error('[Browser Page] Microphone access error:', err);
|
||||
|
||||
let errorMsg = `无法访问麦克风 (${err.name})`;
|
||||
if (err.name === 'NotAllowedError') {
|
||||
errorMsg = '麦克风访问被拒绝。请在浏览器设置中允许麦克风访问权限。';
|
||||
} else if (err.name === 'NotFoundError') {
|
||||
errorMsg = '未找到麦克风设备。请确保麦克风已连接。';
|
||||
}
|
||||
|
||||
updateStatus(`错误: ${errorMsg}`);
|
||||
recognition = null;
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.start():', e);
|
||||
updateStatus(`启动识别时出错: ${e.message}`);
|
||||
recognition = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopRecognition() {
|
||||
if (recognition) {
|
||||
console.log('[Browser Page] Stopping recognition...');
|
||||
updateStatus("正在停止识别...");
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error calling recognition.stop():', e);
|
||||
recognition = null;
|
||||
updateStatus("停止时出错,已强制重置。");
|
||||
}
|
||||
} else {
|
||||
console.log('[Browser Page] Recognition not active, nothing to stop.');
|
||||
updateStatus("识别未运行。");
|
||||
}
|
||||
}
|
||||
|
||||
function forceResetRecognition() {
|
||||
console.log('[Browser Page] Force resetting recognition...');
|
||||
updateStatus("强制重置语音识别...");
|
||||
|
||||
// 先尝试停止当前的识别
|
||||
if (recognition) {
|
||||
try {
|
||||
recognition.stop();
|
||||
} catch (e) {
|
||||
console.error('[Browser Page] Error stopping recognition during reset:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 强制设置为null,丢弃所有后续结果
|
||||
recognition = null;
|
||||
|
||||
// 通知服务器已重置
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'status', message: 'reset_complete' }));
|
||||
}
|
||||
|
||||
updateStatus("语音识别已重置,等待新指令。");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,27 +0,0 @@
|
||||
{
|
||||
"name": "cherry-asr-server",
|
||||
"version": "1.0.0",
|
||||
"description": "Cherry Studio ASR Server",
|
||||
"main": "server.js",
|
||||
"bin": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"build": "pkg ."
|
||||
},
|
||||
"pkg": {
|
||||
"targets": [
|
||||
"node16-win-x64"
|
||||
],
|
||||
"outputPath": "dist",
|
||||
"assets": [
|
||||
"index.html"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"pkg": "^5.8.1"
|
||||
}
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
const http = require('http')
|
||||
const WebSocket = require('ws')
|
||||
const express = require('express')
|
||||
const path = require('path') // Need path module
|
||||
|
||||
const app = express()
|
||||
const port = 34515 // Define the port
|
||||
|
||||
// 获取index.html文件的路径
|
||||
function getIndexHtmlPath() {
|
||||
// 在开发环境中,直接使用相对路径
|
||||
const devPath = path.join(__dirname, 'index.html')
|
||||
|
||||
// 在pkg打包后,文件会被包含在可执行文件中
|
||||
// 使用process.pkg检测是否是打包环境
|
||||
if (process.pkg) {
|
||||
// 在打包环境中,使用绝对路径
|
||||
return path.join(path.dirname(process.execPath), 'index.html')
|
||||
}
|
||||
|
||||
// 如果文件存在,返回开发路径
|
||||
try {
|
||||
if (require('fs').existsSync(devPath)) {
|
||||
return devPath
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error checking file existence:', e)
|
||||
}
|
||||
|
||||
// 如果都不存在,尝试使用当前目录
|
||||
return path.join(process.cwd(), 'index.html')
|
||||
}
|
||||
|
||||
// 提供网页给浏览器
|
||||
app.get('/', (req, res) => {
|
||||
const indexPath = getIndexHtmlPath()
|
||||
console.log(`Serving index.html from: ${indexPath}`)
|
||||
res.sendFile(indexPath)
|
||||
})
|
||||
|
||||
const server = http.createServer(app)
|
||||
const wss = new WebSocket.Server({ server })
|
||||
|
||||
let browserConnection = null
|
||||
let electronConnection = null
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('[Server] WebSocket client connected') // Add log
|
||||
|
||||
ws.on('message', (message) => {
|
||||
let data
|
||||
try {
|
||||
// Ensure message is treated as string before parsing
|
||||
data = JSON.parse(message.toString())
|
||||
console.log('[Server] Received message:', data) // Log parsed data
|
||||
} catch (e) {
|
||||
console.error('[Server] Failed to parse message or message is not JSON:', message.toString(), e)
|
||||
return // Ignore non-JSON messages
|
||||
}
|
||||
|
||||
// 识别客户端类型
|
||||
if (data.type === 'identify') {
|
||||
if (data.role === 'browser') {
|
||||
browserConnection = ws
|
||||
console.log('[Server] Browser identified and connected')
|
||||
// Notify Electron that the browser is ready
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
|
||||
console.log('[Server] Sent browser_ready status to Electron')
|
||||
}
|
||||
// Notify Electron if it's already connected
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connected' }))
|
||||
}
|
||||
ws.on('close', () => {
|
||||
console.log('[Server] Browser disconnected')
|
||||
browserConnection = null
|
||||
// Notify Electron
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser disconnected' }))
|
||||
}
|
||||
})
|
||||
ws.on('error', (error) => {
|
||||
console.error('[Server] Browser WebSocket error:', error)
|
||||
browserConnection = null // Assume disconnected on error
|
||||
if (electronConnection) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
|
||||
}
|
||||
})
|
||||
} else if (data.role === 'electron') {
|
||||
electronConnection = ws
|
||||
console.log('[Server] Electron identified and connected')
|
||||
// If browser is already connected when Electron connects, notify Electron immediately
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'browser_ready' }))
|
||||
console.log('[Server] Sent initial browser_ready status to Electron')
|
||||
}
|
||||
ws.on('close', () => {
|
||||
console.log('[Server] Electron disconnected')
|
||||
electronConnection = null
|
||||
// Maybe send stop to browser if electron disconnects?
|
||||
// if (browserConnection) browserConnection.send(JSON.stringify({ type: 'stop' }));
|
||||
})
|
||||
ws.on('error', (error) => {
|
||||
console.error('[Server] Electron WebSocket error:', error)
|
||||
electronConnection = null // Assume disconnected on error
|
||||
})
|
||||
}
|
||||
}
|
||||
// Electron 控制开始/停止
|
||||
else if (data.type === 'start' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying START command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'start' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay START: Browser not connected')
|
||||
// Optionally notify Electron back
|
||||
electronConnection.send(JSON.stringify({ type: 'error', message: 'Browser not connected for ASR' }))
|
||||
}
|
||||
} else if (data.type === 'stop' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying STOP command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'stop' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay STOP: Browser not connected')
|
||||
}
|
||||
} else if (data.type === 'reset' && ws === electronConnection) {
|
||||
if (browserConnection && browserConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying RESET command to browser')
|
||||
browserConnection.send(JSON.stringify({ type: 'reset' }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay RESET: Browser not connected')
|
||||
}
|
||||
}
|
||||
// 浏览器发送识别结果
|
||||
else if (data.type === 'result' && ws === browserConnection) {
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
// console.log('[Server] Relaying RESULT to Electron:', data.data); // Log less frequently if needed
|
||||
electronConnection.send(JSON.stringify({ type: 'result', data: data.data }))
|
||||
} else {
|
||||
// console.log('[Server] Cannot relay RESULT: Electron not connected');
|
||||
}
|
||||
}
|
||||
// 浏览器发送状态更新 (例如 'stopped')
|
||||
else if (data.type === 'status' && ws === browserConnection) {
|
||||
if (electronConnection && electronConnection.readyState === WebSocket.OPEN) {
|
||||
console.log('[Server] Relaying STATUS to Electron:', data.message) // Log status being relayed
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: data.message }))
|
||||
} else {
|
||||
console.log('[Server] Cannot relay STATUS: Electron not connected')
|
||||
}
|
||||
} else {
|
||||
console.log('[Server] Received unknown message type or from unknown source:', data)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('error', (error) => {
|
||||
// Generic error handling for connection before identification
|
||||
console.error('[Server] Initial WebSocket connection error:', error)
|
||||
// Attempt to clean up based on which connection it might be (if identified)
|
||||
if (ws === browserConnection) {
|
||||
browserConnection = null
|
||||
if (electronConnection)
|
||||
electronConnection.send(JSON.stringify({ type: 'status', message: 'Browser connection error' }))
|
||||
} else if (ws === electronConnection) {
|
||||
electronConnection = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`[Server] Server running at http://localhost:${port}`)
|
||||
})
|
||||
|
||||
// Handle server errors
|
||||
server.on('error', (error) => {
|
||||
console.error(`[Server] Failed to start server:`, error)
|
||||
process.exit(1) // Exit if server fails to start
|
||||
})
|
||||
55
src/renderer/src/assets/images/cherry-text-logo.svg
Normal file
55
src/renderer/src/assets/images/cherry-text-logo.svg
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_图层_2" data-name="图层_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.45 66.73">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ea5e5d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #23af69;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #ea5756;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_图层_1-2" data-name="图层_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-1" d="M16.72,51.21c-4.45,0-8.64-1.78-11.81-5.01-3.17-3.23-4.91-7.51-4.91-12.04s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.71,1.82,11.82,4.99c2.32,2.36,2.32,6.2,0,8.56-2.32,2.36-6.08,2.36-8.4,0-.9-.92-2.15-1.45-3.43-1.45-2.63,0-4.85,2.26-4.85,4.94s2.22,4.94,4.85,4.94c1.28,0,2.52-.53,3.43-1.45,2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-3.11,3.17-7.42,4.99-11.82,4.99Z"/>
|
||||
<path class="cls-1" d="M32.05,66.73c-4.45,0-8.64-1.78-11.81-5.01s-4.91-7.51-4.91-12.04,1.79-8.88,4.9-12.06c2.32-2.36,6.08-2.36,8.4,0,2.32,2.36,2.32,6.2,0,8.56-.9.92-1.42,2.19-1.42,3.49,0,2.68,2.22,4.94,4.85,4.94s4.85-2.26,4.85-4.94c0-.95-.23-2.31-1.32-3.43-3.13-3.19-4.92-7.6-4.92-12.09s1.74-8.81,4.91-12.04,7.36-5.01,11.81-5.01,8.64,1.78,11.81,5.01,4.91,7.51,4.91,12.04-1.79,8.88-4.9,12.06c-2.32,2.36-6.08,2.36-8.4,0-2.32-2.36-2.32-6.2,0-8.56.9-.92,1.42-2.19,1.42-3.49,0-2.68-2.22-4.94-4.85-4.94s-4.85,2.26-4.85,4.94c0,1.31.53,2.6,1.45,3.53,3.1,3.16,4.8,7.42,4.8,11.99s-1.74,8.81-4.91,12.04c-3.17,3.23-7.36,5.01-11.81,5.01Z"/>
|
||||
</g>
|
||||
<path class="cls-2" d="M32.05,19.09l-9.72-9.12c-1.5-1.4-1.57-3.75-.17-5.25,1.4-1.49,3.75-1.57,5.25-.17l3.89,3.65,5.53-6.83c1.29-1.59,3.63-1.84,5.22-.55,1.59,1.29,1.84,3.63.55,5.22l-10.56,13.05Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M93.93,24.6l.55-.39c.69-.4,1.17-.61,1.46-.61.63,0,1.3.57,2.03,1.7.44.71.67,1.27.67,1.7s-.14.78-.41,1.06c-.27.28-.59.54-.96.76-.36.22-.71.43-1.05.64-.33.2-1.02.47-2.05.79-1.03.32-2.03.49-2.99.49s-1.93-.13-2.91-.38c-.98-.25-1.99-.68-3.03-1.27-1.04-.6-1.98-1.32-2.81-2.18-.83-.86-1.51-1.96-2.05-3.31-.54-1.35-.8-2.81-.8-4.38s.26-3.01.79-4.29c.53-1.28,1.2-2.35,2.02-3.19.82-.84,1.75-1.54,2.81-2.11,1.98-1.09,3.97-1.64,5.98-1.64.95,0,1.92.15,2.9.44.98.29,1.72.59,2.23.9l.73.42c.36.22.65.4.85.55.53.42.79.91.79,1.44s-.21,1.1-.64,1.68c-.79,1.09-1.5,1.64-2.12,1.64-.36,0-.88-.22-1.55-.67-.85-.69-1.98-1.03-3.4-1.03-1.31,0-2.61.46-3.88,1.36-.61.44-1.11,1.07-1.52,1.88-.4.81-.61,1.72-.61,2.75s.2,1.94.61,2.75c.4.81.92,1.45,1.55,1.91,1.23.89,2.52,1.34,3.85,1.34.63,0,1.22-.08,1.77-.24.56-.16.96-.32,1.2-.49Z"/>
|
||||
<path class="cls-3" d="M114.38,9.07c.16-.3.43-.52.82-.64.38-.12.87-.18,1.46-.18s1.05.05,1.4.15c.34.1.61.22.79.36.18.14.32.34.42.61.1.34.15.87.15,1.58v16.84c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58v-6.16h-8.04v6.19c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V10.92c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v6.19h8.04v-6.22c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8Z"/>
|
||||
<path class="cls-3" d="M127.21,25.1h9.34c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-12.01c-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55V10.9c0-1.03.19-1.73.58-2.11.38-.37,1.11-.56,2.18-.56h11.95c.47,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.37,2.25-1.12,2.49-.34.12-.87.18-1.58.18h-9.31v3.06h6.01c.46,0,.81.02,1.05.05.23.03.5.13.8.29.55.28.82,1.07.82,2.37,0,1.42-.38,2.25-1.15,2.49-.34.12-.87.18-1.58.18h-5.95v3.06Z"/>
|
||||
<path class="cls-3" d="M196.96,8.79c.99.69,1.49,1.35,1.49,2,0,.38-.23.92-.7,1.61l-6.55,9.8v5.79c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.16.3-.43.52-.82.64-.38.12-.9.18-1.55.18s-1.16-.06-1.55-.18c-.38-.12-.66-.34-.82-.65-.16-.31-.26-.59-.29-.82-.03-.23-.05-.59-.05-1.08v-5.73l-6.55-9.8c-.47-.69-.7-1.22-.7-1.61,0-.65.44-1.27,1.33-1.87.89-.6,1.53-.9,1.91-.9s.69.08.91.24c.34.22.71.64,1.09,1.24l4.7,7.52,4.7-7.52c.38-.61.72-1.01,1-1.2s.61-.29.99-.29.97.25,1.77.76Z"/>
|
||||
<g>
|
||||
<path class="cls-3" d="M81.93,56.63c-.53-.65-.79-1.23-.79-1.74s.43-1.2,1.3-2.05c.51-.49,1.04-.73,1.61-.73s1.36.51,2.37,1.52c.28.34.69.67,1.21.99.53.31,1.01.47,1.46.47,1.88,0,2.82-.77,2.82-2.31,0-.46-.26-.85-.77-1.17-.52-.31-1.16-.54-1.93-.68-.77-.14-1.6-.37-2.49-.68-.89-.31-1.72-.68-2.49-1.11-.77-.42-1.41-1.1-1.93-2.02-.52-.92-.77-2.03-.77-3.32,0-1.78.66-3.33,1.99-4.66s3.13-1.99,5.42-1.99c1.21,0,2.32.16,3.32.47,1,.31,1.69.63,2.08.96l.76.58c.63.59.94,1.08.94,1.49s-.24.96-.73,1.67c-.69,1.01-1.4,1.52-2.12,1.52-.42,0-.95-.2-1.58-.61-.06-.04-.18-.14-.35-.3-.17-.16-.33-.29-.47-.39-.42-.26-.97-.39-1.62-.39s-1.2.16-1.64.47c-.43.31-.65.75-.65,1.3s.26,1.01.77,1.35c.52.34,1.16.58,1.93.7.77.12,1.61.31,2.52.56.91.25,1.75.56,2.52.93.77.36,1.41,1,1.93,1.9.52.9.77,2.01.77,3.32s-.26,2.47-.79,3.47c-.53,1-1.21,1.77-2.06,2.32-1.64,1.07-3.39,1.61-5.25,1.61-.95,0-1.85-.12-2.7-.35-.85-.23-1.54-.52-2.06-.86-1.07-.65-1.82-1.27-2.24-1.88l-.27-.33Z"/>
|
||||
<path class="cls-3" d="M100.74,37.49h16.87c.65,0,1.12.08,1.43.23.3.15.51.39.61.71.1.32.15.75.15,1.27s-.05.95-.15,1.26c-.1.31-.27.53-.52.65-.36.18-.88.27-1.55.27h-5.79v15.26c0,.47-.02.81-.05,1.03s-.12.48-.27.77c-.15.29-.42.5-.8.62-.38.12-.89.18-1.52.18s-1.13-.06-1.5-.18c-.37-.12-.64-.33-.79-.62-.15-.29-.24-.56-.27-.79-.03-.23-.05-.58-.05-1.05v-15.23h-5.82c-.65,0-1.12-.08-1.43-.23-.3-.15-.51-.39-.61-.71-.1-.32-.15-.75-.15-1.27s.05-.95.15-1.26c.1-.31.27-.53.52-.65.36-.18.88-.27,1.55-.27Z"/>
|
||||
<path class="cls-3" d="M135.99,38.34c.2-.32.5-.55.88-.67.38-.12.86-.18,1.44-.18s1.04.05,1.38.15c.34.1.61.22.79.36.18.14.31.35.39.64.12.34.18.87.18,1.58v9.16c0,2.67-.83,5.1-2.49,7.28-.81,1.03-1.85,1.87-3.12,2.5s-2.68.96-4.23.96-2.95-.32-4.22-.97c-1.26-.65-2.29-1.5-3.08-2.55-1.64-2.14-2.46-4.57-2.46-7.28v-9.13c0-.49.02-.84.05-1.08.03-.23.13-.5.29-.8.16-.3.43-.52.82-.64.38-.12.9-.18,1.55-.18s1.16.06,1.55.18c.38.12.65.33.79.64.24.47.36,1.1.36,1.91v9.1c0,1.23.3,2.41.91,3.52.3.57.76,1.02,1.37,1.36.61.34,1.32.52,2.15.52,1.48,0,2.58-.55,3.31-1.64.73-1.09,1.09-2.36,1.09-3.79v-9.28c0-.79.1-1.34.3-1.67Z"/>
|
||||
<path class="cls-3" d="M146.18,37.49l5.61.03c2.93,0,5.51,1.06,7.74,3.17,2.22,2.11,3.34,4.71,3.34,7.8s-1.09,5.73-3.26,7.93c-2.17,2.2-4.81,3.31-7.9,3.31h-5.55c-1.23,0-2-.25-2.31-.76-.24-.42-.36-1.07-.36-1.94v-16.87c0-.49.02-.84.05-1.06s.13-.49.29-.79c.28-.55,1.07-.82,2.37-.82ZM151.79,54.35c1.46,0,2.77-.54,3.94-1.62,1.17-1.08,1.76-2.44,1.76-4.08s-.57-3.01-1.71-4.11c-1.14-1.1-2.48-1.65-4.02-1.65h-2.91v11.47h2.94Z"/>
|
||||
<path class="cls-3" d="M164.84,40.19c0-.46.02-.81.05-1.05.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82,1.42,0,2.25.37,2.52,1.12.1.34.15.87.15,1.58v16.87c0,.49-.02.84-.05,1.06s-.13.49-.29.79c-.28.55-1.07.82-2.37.82-1.42,0-2.25-.38-2.49-1.15-.12-.32-.18-.84-.18-1.55v-16.87Z"/>
|
||||
<path class="cls-3" d="M183.07,37.24c2.99,0,5.59,1.08,7.8,3.25,2.2,2.16,3.31,4.85,3.31,8.05s-1.05,5.94-3.16,8.19c-2.1,2.26-4.69,3.38-7.77,3.38s-5.69-1.11-7.84-3.34c-2.15-2.22-3.23-4.87-3.23-7.95,0-1.68.3-3.25.91-4.72.61-1.47,1.42-2.7,2.43-3.69,1.01-.99,2.17-1.77,3.49-2.34,1.31-.57,2.67-.85,4.07-.85ZM177.55,48.68c0,1.8.58,3.26,1.74,4.38,1.16,1.12,2.46,1.68,3.9,1.68s2.73-.55,3.88-1.64c1.15-1.09,1.73-2.56,1.73-4.4s-.58-3.32-1.74-4.43c-1.16-1.11-2.46-1.67-3.9-1.67s-2.73.56-3.88,1.68c-1.15,1.12-1.73,2.58-1.73,4.38Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M176.92,11.06c-.03-.23-.13-.5-.29-.8-.28-.55-1.07-.82-2.37-.82h-6.55c-1.78,0-3.51.65-5.19,1.94-.81.63-1.48,1.48-2,2.55-.53,1.07-.79,2.27-.79,3.58,0,2.29.76,4.17,2.28,5.64-.44,1.07-1.13,2.66-2.06,4.76-.3.73-.45,1.25-.45,1.58,0,.77.63,1.42,1.88,1.94.65.28,1.17.43,1.56.43s.72-.1.97-.29c.25-.19.44-.39.56-.59.2-.38.99-2.21,2.37-5.49l.94.06h3.82v3.43c0,.47.02.81.05,1.05.03.23.13.5.29.8.28.55,1.07.82,2.37.82,1.42,0,2.25-.37,2.49-1.12.12-.34.18-.87.18-1.58V12.11c0-.46-.02-.81-.05-1.05ZM172.81,19.44c-.09.14-.48.77-1.24.91-.2.04-.37.03-.48.02-.02.14-.04.26-.06.38-.16.83-.38,1.05-.57,1.07-.29.05-.51-.35-.93-.9-.23.01-.46.02-.69.02-.51,0-1.01-.03-1.49-.09-.25-.03-.5-.07-.74-.11-1.18-.32-2.03-1.27-2.03-2.4v-1.37c0-1.13.86-2.08,2.03-2.4.24-.04.49-.08.74-.11.48-.06.98-.09,1.49-.09s1.01.03,1.49.09c.25.03.5.07.74.11.6.16,1.12.49,1.49.93.34.41.55.92.55,1.47v1.37c0,.23-.01.66-.29,1.1Z"/>
|
||||
<circle class="cls-2" cx="167.24" cy="17.67" r=".49"/>
|
||||
<circle class="cls-2" cx="168.88" cy="17.71" r=".49"/>
|
||||
<circle class="cls-2" cx="170.59" cy="17.71" r=".49"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-3" d="M141.01,8.24c.03-.23.13-.5.29-.8.28-.55,1.07-.82,2.37-.82h6.55c1.78,0,3.51.65,5.19,1.94.81.63,1.48,1.48,2,2.55.53,1.07.79,2.27.79,3.58,0,2.29-.76,4.17-2.28,5.64.44,1.07,1.13,2.66,2.06,4.76.3.73.45,1.25.45,1.58,0,.77-.63,1.42-1.88,1.94-.65.28-1.17.43-1.56.43s-.72-.1-.97-.29c-.25-.19-.44-.39-.56-.59-.2-.38-.99-2.21-2.37-5.49l-.94.06h-3.82v3.43c0,.47-.02.81-.05,1.05-.03.23-.13.5-.29.8-.28.55-1.07.82-2.37.82-1.42,0-2.25-.37-2.49-1.12-.12-.34-.18-.87-.18-1.58V9.28c0-.46.02-.81.05-1.05ZM145.12,16.62c.09.14.48.77,1.24.91.2.04.37.03.48.02.02.14.04.26.06.38.16.83.38,1.05.57,1.07.29.05.51-.35.93-.9.23.01.46.02.69.02.51,0,1.01-.03,1.49-.09.25-.03.5-.07.74-.11,1.18-.32,2.03-1.27,2.03-2.4v-1.37c0-1.13-.86-2.08-2.03-2.4-.24-.04-.49-.08-.74-.11-.48-.06-.98-.09-1.49-.09s-1.01.03-1.49.09c-.25.03-.5.07-.74.11-.6.16-1.12.49-1.49.93-.34.41-.55.92-.55,1.47v1.37c0,.23.01.66.29,1.1Z"/>
|
||||
<circle class="cls-2" cx="150.69" cy="14.84" r=".49"/>
|
||||
<circle class="cls-2" cx="149.05" cy="14.89" r=".49"/>
|
||||
<circle class="cls-2" cx="147.35" cy="14.89" r=".49"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.5 KiB |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1744456106953" class="icon" viewBox="0 0 2633 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1486" xmlns:xlink="http://www.w3.org/1999/xlink" width="514.2578125" height="200"><path d="M0 0v877.843607h731.440328v146.156393H1316.724263v-146.156393h1316.724262V0z m731.440328 731.196014h-146.156393V292.312786h-146.485574v438.883228H146.485574V146.238688h584.954754z m438.880656 0v146.567869h-292.405369V146.238688H1463.209837v585.037049H1170.320984z m1316.888853 0H2341.30033V292.312786h-146.56787v438.883228h-146.485574V292.312786h-145.909508v438.883228H1609.283935V146.238688h878.008197zM1170.238688 292.477377H1316.724263v292.644539h-146.485575z" fill="#CB3837" p-id="1487"></path></svg>
|
||||
|
Before Width: | Height: | Size: 845 B |
8
src/renderer/src/assets/images/paintings/ic_ImageUp.svg
Normal file
8
src/renderer/src/assets/images/paintings/ic_ImageUp.svg
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="ic_ImageUp">
|
||||
<path id="Vector" d="M10.8 21.5H5.5C4.96957 21.5 4.46086 21.2893 4.08579 20.9142C3.71071 20.5391 3.5 20.0304 3.5 19.5V5.5C3.5 4.96957 3.71071 4.46086 4.08579 4.08579C4.46086 3.71071 4.96957 3.5 5.5 3.5H19.5C20.0304 3.5 20.5391 3.71071 20.9142 4.08579C21.2893 4.46086 21.5 4.96957 21.5 5.5V15.5L18.4 12.4C18.0237 12.0312 17.517 11.8258 16.9901 11.8284C16.4632 11.831 15.9586 12.0415 15.586 12.414L6.5 21.5" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_2" d="M14.5 20L17.5 17L20.5 20" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_3" d="M17.5 22.5V17" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path id="Vector_4" d="M9.5 11.5C10.6046 11.5 11.5 10.6046 11.5 9.5C11.5 8.39543 10.6046 7.5 9.5 7.5C8.39543 7.5 7.5 8.39543 7.5 9.5C7.5 10.6046 8.39543 11.5 9.5 11.5Z" stroke="#353941" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/renderer/src/assets/images/providers/aihubmix.png
Normal file
BIN
src/renderer/src/assets/images/providers/aihubmix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user