mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-04 03:30:15 +08:00
Compare commits
14 Commits
fix/5587
...
feat/confi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a743b49740 | ||
|
|
34bc57b9c6 | ||
|
|
b63f06bffa | ||
|
|
9d4472cb2d | ||
|
|
e8d6938d31 | ||
|
|
206973e8ad | ||
|
|
0ecddb4c06 | ||
|
|
2de23184d0 | ||
|
|
4d2791aa9a | ||
|
|
3dd7799f27 | ||
|
|
deedf85360 | ||
|
|
4d9dce184f | ||
|
|
788d103a36 | ||
|
|
328748bd63 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -64,3 +64,5 @@ GenieData/
|
||||
.worktrees/
|
||||
|
||||
dashboard/bun.lock
|
||||
.claude
|
||||
.env
|
||||
|
||||
@@ -12,6 +12,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
bash \
|
||||
ffmpeg \
|
||||
libavcodec-extra \
|
||||
curl \
|
||||
gnupg \
|
||||
git \
|
||||
|
||||
@@ -184,6 +184,7 @@ Connect AstrBot to your favorite chat platform.
|
||||
| Coze | LLMOps Platforms |
|
||||
| OpenAI Whisper | Speech-to-Text Services |
|
||||
| SenseVoice | Speech-to-Text Services |
|
||||
| Xiaomi MiMo Omni | Speech-to-Text Services |
|
||||
| OpenAI TTS | Text-to-Speech Services |
|
||||
| Gemini TTS | Text-to-Speech Services |
|
||||
| GPT-Sovits-Inference | Text-to-Speech Services |
|
||||
@@ -193,6 +194,7 @@ Connect AstrBot to your favorite chat platform.
|
||||
| Alibaba Cloud Bailian TTS | Text-to-Speech Services |
|
||||
| Azure TTS | Text-to-Speech Services |
|
||||
| Minimax TTS | Text-to-Speech Services |
|
||||
| Xiaomi MiMo TTS | Text-to-Speech Services |
|
||||
| Volcano Engine TTS | Text-to-Speech Services |
|
||||
|
||||
## ❤️ Sponsors
|
||||
|
||||
@@ -184,6 +184,7 @@ Connectez AstrBot à vos plateformes de chat préférées.
|
||||
| Coze | Plateformes LLMOps |
|
||||
| OpenAI Whisper | Services de reconnaissance vocale |
|
||||
| SenseVoice | Services de reconnaissance vocale |
|
||||
| Xiaomi MiMo Omni | Services de reconnaissance vocale |
|
||||
| OpenAI TTS | Services de synthèse vocale |
|
||||
| Gemini TTS | Services de synthèse vocale |
|
||||
| GPT-Sovits-Inference | Services de synthèse vocale |
|
||||
@@ -193,6 +194,7 @@ Connectez AstrBot à vos plateformes de chat préférées.
|
||||
| Alibaba Cloud Bailian TTS | Services de synthèse vocale |
|
||||
| Azure TTS | Services de synthèse vocale |
|
||||
| Minimax TTS | Services de synthèse vocale |
|
||||
| Xiaomi MiMo TTS | Services de synthèse vocale |
|
||||
| Volcano Engine TTS | Services de synthèse vocale |
|
||||
|
||||
## ❤️ Contribuer
|
||||
|
||||
@@ -185,6 +185,7 @@ AstrBot をよく使うチャットプラットフォームに接続できます
|
||||
| Coze | LLMOps プラットフォーム |
|
||||
| OpenAI Whisper | 音声認識サービス |
|
||||
| SenseVoice | 音声認識サービス |
|
||||
| Xiaomi MiMo Omni | 音声認識サービス |
|
||||
| OpenAI TTS | 音声合成サービス |
|
||||
| Gemini TTS | 音声合成サービス |
|
||||
| GPT-Sovits-Inference | 音声合成サービス |
|
||||
@@ -194,6 +195,7 @@ AstrBot をよく使うチャットプラットフォームに接続できます
|
||||
| Alibaba Cloud 百炼 TTS | 音声合成サービス |
|
||||
| Azure TTS | 音声合成サービス |
|
||||
| Minimax TTS | 音声合成サービス |
|
||||
| Xiaomi MiMo TTS | 音声合成サービス |
|
||||
| Volcano Engine TTS | 音声合成サービス |
|
||||
|
||||
## ❤️ コントリビューション
|
||||
|
||||
@@ -184,6 +184,7 @@ yay -S astrbot-git
|
||||
| Coze | Платформы LLMOps |
|
||||
| OpenAI Whisper | Сервисы распознавания речи |
|
||||
| SenseVoice | Сервисы распознавания речи |
|
||||
| Xiaomi MiMo Omni | Сервисы распознавания речи |
|
||||
| OpenAI TTS | Сервисы синтеза речи |
|
||||
| Gemini TTS | Сервисы синтеза речи |
|
||||
| GPT-Sovits-Inference | Сервисы синтеза речи |
|
||||
@@ -193,6 +194,7 @@ yay -S astrbot-git
|
||||
| Alibaba Cloud Bailian TTS | Сервисы синтеза речи |
|
||||
| Azure TTS | Сервисы синтеза речи |
|
||||
| Minimax TTS | Сервисы синтеза речи |
|
||||
| Xiaomi MiMo TTS | Сервисы синтеза речи |
|
||||
| Volcano Engine TTS | Сервисы синтеза речи |
|
||||
|
||||
## ❤️ Вклад в проект
|
||||
|
||||
@@ -184,6 +184,7 @@ yay -S astrbot-git
|
||||
| Coze | LLMOps 平台 |
|
||||
| OpenAI Whisper | 語音轉文字服務 |
|
||||
| SenseVoice | 語音轉文字服務 |
|
||||
| Xiaomi MiMo Omni | 語音轉文字服務 |
|
||||
| OpenAI TTS | 文字轉語音服務 |
|
||||
| Gemini TTS | 文字轉語音服務 |
|
||||
| GPT-Sovits-Inference | 文字轉語音服務 |
|
||||
@@ -193,6 +194,7 @@ yay -S astrbot-git
|
||||
| 阿里雲百煉 TTS | 文字轉語音服務 |
|
||||
| Azure TTS | 文字轉語音服務 |
|
||||
| Minimax TTS | 文字轉語音服務 |
|
||||
| Xiaomi MiMo TTS | 文字轉語音服務 |
|
||||
| 火山引擎 TTS | 文字轉語音服務 |
|
||||
|
||||
## ❤️ 貢獻
|
||||
|
||||
@@ -185,6 +185,7 @@ yay -S astrbot-git
|
||||
| Coze | LLMOps 平台 |
|
||||
| OpenAI Whisper | 语音转文本 |
|
||||
| SenseVoice | 语音转文本 |
|
||||
| Xiaomi MiMo Omni | 语音转文本 |
|
||||
| OpenAI TTS | 文本转语音 |
|
||||
| Gemini TTS | 文本转语音 |
|
||||
| GPT-Sovits-Inference | 文本转语音 |
|
||||
@@ -194,6 +195,7 @@ yay -S astrbot-git
|
||||
| 阿里云百炼 TTS | 文本转语音 |
|
||||
| Azure TTS | 文本转语音 |
|
||||
| Minimax TTS | 文本转语音 |
|
||||
| Xiaomi MiMo TTS | 文本转语音 |
|
||||
| 火山引擎 TTS | 文本转语音 |
|
||||
|
||||
## ❤️ 贡献
|
||||
|
||||
@@ -15,7 +15,6 @@ class HandoffTool(FunctionTool, Generic[TContext]):
|
||||
tool_description: str | None = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
||||
# Avoid passing duplicate `description` to the FunctionTool dataclass.
|
||||
# Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs
|
||||
# to override what the main agent sees, while we also compute a default
|
||||
|
||||
@@ -4040,9 +4040,9 @@ CONFIG_METADATA_3_SYSTEM = {
|
||||
"hint": "时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab",
|
||||
},
|
||||
"http_proxy": {
|
||||
"description": "HTTP 代理",
|
||||
"description": "代理",
|
||||
"type": "string",
|
||||
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
|
||||
"hint": "启用后,会以添加环境变量的方式设置代理。支持 http://、https://、socks5:// 格式,例如:http://127.0.0.1:7890 或 socks5://127.0.0.1:7891",
|
||||
},
|
||||
"no_proxy": {
|
||||
"description": "直连地址列表",
|
||||
|
||||
@@ -108,6 +108,7 @@ Text chunk to process:
|
||||
class KBHelper:
|
||||
vec_db: BaseVecDB
|
||||
kb: KnowledgeBase
|
||||
init_error: str | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -122,6 +123,7 @@ class KBHelper:
|
||||
self.prov_mgr = provider_manager
|
||||
self.kb_root_dir = kb_root_dir
|
||||
self.chunker = chunker
|
||||
self.init_error = None
|
||||
|
||||
self.kb_dir = Path(self.kb_root_dir) / self.kb.kb_id
|
||||
self.kb_medias_dir = Path(self.kb_dir) / "medias" / self.kb.kb_id
|
||||
@@ -148,13 +150,14 @@ class KBHelper:
|
||||
async def get_rp(self) -> RerankProvider | None:
|
||||
if not self.kb.rerank_provider_id:
|
||||
return None
|
||||
rp: RerankProvider = await self.prov_mgr.get_provider_by_id(
|
||||
rp: RerankProvider | None = await self.prov_mgr.get_provider_by_id(
|
||||
self.kb.rerank_provider_id,
|
||||
) # type: ignore
|
||||
if not rp:
|
||||
raise ValueError(
|
||||
f"无法找到 ID 为 {self.kb.rerank_provider_id} 的 Rerank Provider",
|
||||
logger.warning(
|
||||
f"知识库 {self.kb.kb_name}({self.kb.kb_id}) 的 Rerank Provider({self.kb.rerank_provider_id}) 不可用,将跳过重排序。",
|
||||
)
|
||||
return None
|
||||
return rp
|
||||
|
||||
async def _ensure_vec_db(self) -> FaissVecDB:
|
||||
@@ -162,7 +165,13 @@ class KBHelper:
|
||||
raise ValueError(f"知识库 {self.kb.kb_name} 未配置 Embedding Provider")
|
||||
|
||||
ep = await self.get_ep()
|
||||
rp = await self.get_rp()
|
||||
rp: RerankProvider | None = None
|
||||
try:
|
||||
rp = await self.get_rp()
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"知识库 {self.kb.kb_name}({self.kb.kb_id}) 初始化重排序能力失败,将跳过重排序: {e}",
|
||||
)
|
||||
|
||||
vec_db = FaissVecDB(
|
||||
doc_store_path=str(self.kb_dir / "doc.db"),
|
||||
@@ -172,6 +181,8 @@ class KBHelper:
|
||||
)
|
||||
await vec_db.initialize()
|
||||
self.vec_db = vec_db
|
||||
# Clear stale init_error once initialization succeeds.
|
||||
self.init_error = None
|
||||
return vec_db
|
||||
|
||||
async def delete_vec_db(self) -> None:
|
||||
@@ -183,7 +194,7 @@ class KBHelper:
|
||||
shutil.rmtree(self.kb_dir)
|
||||
|
||||
async def terminate(self) -> None:
|
||||
if self.vec_db:
|
||||
if hasattr(self, "vec_db") and self.vec_db:
|
||||
await self.vec_db.close()
|
||||
|
||||
async def upload_document(
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
from astrbot.core import logger
|
||||
@@ -56,8 +55,7 @@ class KnowledgeBaseManager:
|
||||
logger.error(f"知识库模块导入失败: {e}")
|
||||
logger.warning("请确保已安装所需依赖: pypdf, aiofiles, Pillow, rank-bm25")
|
||||
except Exception as e:
|
||||
logger.error(f"知识库模块初始化失败: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
logger.error(f"知识库模块初始化失败: {e}", exc_info=True)
|
||||
|
||||
async def _init_kb_database(self) -> None:
|
||||
self.kb_db = KBSQLiteDatabase(DB_PATH.as_posix())
|
||||
@@ -76,7 +74,14 @@ class KnowledgeBaseManager:
|
||||
kb_root_dir=FILES_PATH,
|
||||
chunker=CHUNKER,
|
||||
)
|
||||
await kb_helper.initialize()
|
||||
try:
|
||||
await kb_helper.initialize()
|
||||
except Exception as e:
|
||||
kb_helper.init_error = str(e)
|
||||
logger.error(
|
||||
f"知识库 {record.kb_name}({record.kb_id}) 初始化失败: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
self.kb_insts[record.kb_id] = kb_helper
|
||||
|
||||
async def create_kb(
|
||||
@@ -179,6 +184,20 @@ class KnowledgeBaseManager:
|
||||
return None
|
||||
|
||||
kb = kb_helper.kb
|
||||
previous_state = {
|
||||
"kb_name": kb.kb_name,
|
||||
"description": kb.description,
|
||||
"emoji": kb.emoji,
|
||||
"embedding_provider_id": kb.embedding_provider_id,
|
||||
"rerank_provider_id": kb.rerank_provider_id,
|
||||
"chunk_size": kb.chunk_size,
|
||||
"chunk_overlap": kb.chunk_overlap,
|
||||
"top_k_dense": kb.top_k_dense,
|
||||
"top_k_sparse": kb.top_k_sparse,
|
||||
"top_m_final": kb.top_m_final,
|
||||
}
|
||||
previous_init_error = kb_helper.init_error
|
||||
|
||||
if kb_name is not None:
|
||||
kb.kb_name = kb_name
|
||||
if description is not None:
|
||||
@@ -198,12 +217,47 @@ class KnowledgeBaseManager:
|
||||
kb.top_k_sparse = top_k_sparse
|
||||
if top_m_final is not None:
|
||||
kb.top_m_final = top_m_final
|
||||
|
||||
# Build a new helper first. Keep current vec_db alive until new init succeeds.
|
||||
new_helper = KBHelper(
|
||||
kb_db=self.kb_db,
|
||||
kb=kb,
|
||||
provider_manager=self.provider_manager,
|
||||
kb_root_dir=FILES_PATH,
|
||||
chunker=CHUNKER,
|
||||
)
|
||||
|
||||
try:
|
||||
await new_helper.initialize()
|
||||
except Exception as e:
|
||||
# Roll back in-memory settings and keep current helper available.
|
||||
kb.kb_name = previous_state["kb_name"]
|
||||
kb.description = previous_state["description"]
|
||||
kb.emoji = previous_state["emoji"]
|
||||
kb.embedding_provider_id = previous_state["embedding_provider_id"]
|
||||
kb.rerank_provider_id = previous_state["rerank_provider_id"]
|
||||
kb.chunk_size = previous_state["chunk_size"]
|
||||
kb.chunk_overlap = previous_state["chunk_overlap"]
|
||||
kb.top_k_dense = previous_state["top_k_dense"]
|
||||
kb.top_k_sparse = previous_state["top_k_sparse"]
|
||||
kb.top_m_final = previous_state["top_m_final"]
|
||||
kb_helper.init_error = previous_init_error
|
||||
logger.error(
|
||||
f"知识库 {kb.kb_name}({kb.kb_id}) 重新初始化失败,继续使用旧实例: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
return kb_helper
|
||||
|
||||
async with self.kb_db.get_db() as session:
|
||||
session.add(kb)
|
||||
await session.commit()
|
||||
await session.refresh(kb)
|
||||
|
||||
return kb_helper
|
||||
old_helper = kb_helper
|
||||
self.kb_insts[kb_id] = new_helper
|
||||
await old_helper.terminate()
|
||||
new_helper.init_error = None
|
||||
return new_helper
|
||||
|
||||
async def retrieve(
|
||||
self,
|
||||
@@ -215,11 +269,21 @@ class KnowledgeBaseManager:
|
||||
"""从指定知识库中检索相关内容"""
|
||||
kb_ids = []
|
||||
kb_id_helper_map = {}
|
||||
unavailable_kbs = []
|
||||
for kb_name in kb_names:
|
||||
if kb_helper := await self.get_kb_by_name(kb_name):
|
||||
if kb_helper.init_error:
|
||||
unavailable_kbs.append((kb_name, kb_helper.init_error))
|
||||
logger.warning(f"知识库 {kb_name} 不可用: {kb_helper.init_error}")
|
||||
continue
|
||||
kb_ids.append(kb_helper.kb.kb_id)
|
||||
kb_id_helper_map[kb_helper.kb.kb_id] = kb_helper
|
||||
|
||||
# all requested KBs are unavailable
|
||||
if not kb_ids and unavailable_kbs:
|
||||
errors = "; ".join(f"{n}: {e}" for n, e in unavailable_kbs)
|
||||
raise ValueError(f"所有请求的知识库均不可用: {errors}")
|
||||
|
||||
if not kb_ids:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -184,12 +184,15 @@ class RetrievalManager:
|
||||
first_rerank = vec_db.rerank_provider
|
||||
break
|
||||
if first_rerank and retrieval_results:
|
||||
retrieval_results = await self._rerank(
|
||||
query=query,
|
||||
results=retrieval_results,
|
||||
top_k=top_m_final,
|
||||
rerank_provider=first_rerank,
|
||||
)
|
||||
try:
|
||||
retrieval_results = await self._rerank(
|
||||
query=query,
|
||||
results=retrieval_results,
|
||||
top_k=top_m_final,
|
||||
rerank_provider=first_rerank,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Rerank 执行失败,已跳过重排序并使用融合结果: {e}")
|
||||
|
||||
return retrieval_results[:top_m_final]
|
||||
|
||||
@@ -229,10 +232,10 @@ class RetrievalManager:
|
||||
|
||||
all_results.extend(vec_results)
|
||||
except Exception as e:
|
||||
from astrbot.core import logger
|
||||
|
||||
logger.warning(f"知识库 {kb_id} 稠密检索失败: {e}")
|
||||
continue
|
||||
logger.error(f"知识库 {kb_id} 稠密检索失败: {e}", exc_info=True)
|
||||
if len(kb_ids) == 1:
|
||||
raise RuntimeError(f"知识库 {kb_id} 稠密检索失败: {e}") from e
|
||||
# multi-KB: skip the faulty KB and continue
|
||||
|
||||
# 按相似度排序并返回 top_k
|
||||
all_results.sort(key=lambda x: x.similarity, reverse=True)
|
||||
|
||||
@@ -142,7 +142,8 @@ class BailianRerankProvider(RerankProvider):
|
||||
f"百炼 API 错误: {data.get('code')} – {data.get('message', '')}"
|
||||
)
|
||||
|
||||
results = data.get("output", {}).get("results", [])
|
||||
# 兼容旧版 API (output.results) 和新版 compatible API (results)
|
||||
results = (data.get("output") or {}).get("results") or data.get("results") or []
|
||||
if not results:
|
||||
logger.warning(f"百炼 Rerank 返回空结果: {data}")
|
||||
return []
|
||||
|
||||
@@ -457,6 +457,27 @@ class ProviderOpenAIOfficial(Provider):
|
||||
|
||||
model = payloads.get("model", "").lower()
|
||||
|
||||
if "messages" in payloads and isinstance(payloads["messages"], list):
|
||||
cleaned_messages = []
|
||||
for idx, msg in enumerate(payloads["messages"]):
|
||||
# 过滤空的 assistant 消息,防止严格 API(如 Moonshot)返回 400 错误
|
||||
if msg.get("role") == "assistant":
|
||||
content = msg.get("content")
|
||||
tool_calls = msg.get("tool_calls")
|
||||
|
||||
# 情况1: 空/null content 且无 tool_calls -> 过滤掉
|
||||
if not tool_calls and (content == "" or content is None):
|
||||
logger.warning(f"过滤第 {idx} 条空 assistant 消息 (无工具调用)")
|
||||
continue
|
||||
|
||||
# 情况2: 空 content 但有 tool_calls -> 设为 None (符合 OpenAI 规范)
|
||||
if content == "" and tool_calls:
|
||||
msg["content"] = None
|
||||
|
||||
cleaned_messages.append(msg)
|
||||
|
||||
payloads["messages"] = cleaned_messages
|
||||
|
||||
completion = await self.client.chat.completions.create(
|
||||
**payloads,
|
||||
stream=False,
|
||||
@@ -589,7 +610,10 @@ class ProviderOpenAIOfficial(Provider):
|
||||
def _extract_usage(self, usage: CompletionUsage | dict) -> TokenUsage:
|
||||
ptd = getattr(usage, "prompt_tokens_details", None)
|
||||
cached = getattr(ptd, "cached_tokens", 0) if ptd else 0
|
||||
prompt_tokens = getattr(usage, "prompt_tokens", 0) or 0
|
||||
cached = (
|
||||
cached if isinstance(cached, int) else 0
|
||||
) # ptd.cached_tokens 可能为None
|
||||
prompt_tokens = getattr(usage, "prompt_tokens", 0) or 0 # 安全
|
||||
completion_tokens = getattr(usage, "completion_tokens", 0) or 0
|
||||
cached = cached or 0
|
||||
prompt_tokens = prompt_tokens or 0
|
||||
|
||||
@@ -394,7 +394,6 @@ def find_missing_requirements(requirements_path: str) -> set[str] | None:
|
||||
def find_missing_requirements_from_lines(
|
||||
requirement_lines: Sequence[str],
|
||||
) -> set[str] | None:
|
||||
|
||||
required = list(iter_requirements(lines=requirement_lines))
|
||||
if not required:
|
||||
return set()
|
||||
|
||||
@@ -314,7 +314,12 @@ class KnowledgeBaseRoute(Route):
|
||||
# 转换为字典列表
|
||||
kb_list = []
|
||||
for kb in kbs:
|
||||
kb_list.append(kb.model_dump())
|
||||
kb_dict = kb.model_dump()
|
||||
# include init_error from KBHelper if present
|
||||
kb_helper = await kb_manager.get_kb(kb.kb_id)
|
||||
if kb_helper and kb_helper.init_error:
|
||||
kb_dict["init_error"] = kb_helper.init_error
|
||||
kb_list.append(kb_dict)
|
||||
|
||||
return (
|
||||
Response()
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"qrcode": "^1.5.4",
|
||||
"@guolao/vue-monaco-editor": "^1.5.4",
|
||||
"@tiptap/starter-kit": "2.1.7",
|
||||
"@tiptap/vue-3": "2.1.7",
|
||||
@@ -35,6 +34,7 @@
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pinia": "2.1.6",
|
||||
"pinyin-pro": "^3.26.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"shiki": "^3.20.0",
|
||||
"stream-markdown": "^0.0.13",
|
||||
"vee-validate": "4.11.3",
|
||||
@@ -61,7 +61,7 @@
|
||||
"eslint": "8.48.0",
|
||||
"eslint-plugin-vue": "9.17.0",
|
||||
"prettier": "3.0.2",
|
||||
"sass": "1.66.1",
|
||||
"sass": "1.98.0",
|
||||
"sass-loader": "13.3.2",
|
||||
"subset-font": "^2.4.0",
|
||||
"typescript": "5.1.6",
|
||||
|
||||
271
dashboard/pnpm-lock.yaml
generated
271
dashboard/pnpm-lock.yaml
generated
@@ -86,7 +86,7 @@ importers:
|
||||
version: 4.11.3(vue@3.3.4)
|
||||
vite-plugin-vuetify:
|
||||
specifier: 2.1.3
|
||||
version: 2.1.3(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11)
|
||||
version: 2.1.3(vite@6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11)
|
||||
vue:
|
||||
specifier: 3.3.4
|
||||
version: 3.3.4
|
||||
@@ -129,7 +129,7 @@ importers:
|
||||
version: 20.19.32
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: 5.2.4
|
||||
version: 5.2.4(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)
|
||||
version: 5.2.4(vite@6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0))(vue@3.3.4)
|
||||
'@vue/eslint-config-prettier':
|
||||
specifier: 8.0.0
|
||||
version: 8.0.0(@types/eslint@9.6.1)(eslint@8.48.0)(prettier@3.0.2)
|
||||
@@ -149,11 +149,11 @@ importers:
|
||||
specifier: 3.0.2
|
||||
version: 3.0.2
|
||||
sass:
|
||||
specifier: 1.66.1
|
||||
version: 1.66.1
|
||||
specifier: 1.98.0
|
||||
version: 1.98.0
|
||||
sass-loader:
|
||||
specifier: 13.3.2
|
||||
version: 13.3.2(sass@1.66.1)(webpack@5.105.0)
|
||||
version: 13.3.2(sass@1.98.0)(webpack@5.105.0)
|
||||
subset-font:
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0
|
||||
@@ -162,13 +162,13 @@ importers:
|
||||
version: 5.1.6
|
||||
vite:
|
||||
specifier: 6.4.1
|
||||
version: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)
|
||||
version: 6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0)
|
||||
vite-plugin-webfont-dl:
|
||||
specifier: ^3.12.0
|
||||
version: 3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))
|
||||
version: 3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0))
|
||||
vue-cli-plugin-vuetify:
|
||||
specifier: 2.5.8
|
||||
version: 2.5.8(sass-loader@13.3.2(sass@1.66.1)(webpack@5.105.0))(vue@3.3.4)(vuetify-loader@2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0))(webpack@5.105.0)
|
||||
version: 2.5.8(sass-loader@13.3.2(sass@1.98.0)(webpack@5.105.0))(vue@3.3.4)(vuetify-loader@2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0))(webpack@5.105.0)
|
||||
vue-tsc:
|
||||
specifier: 1.8.8
|
||||
version: 1.8.8(typescript@5.1.6)
|
||||
@@ -496,6 +496,94 @@ packages:
|
||||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.6':
|
||||
resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@parcel/watcher-darwin-arm64@2.5.6':
|
||||
resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@parcel/watcher-darwin-x64@2.5.6':
|
||||
resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@parcel/watcher-freebsd-x64@2.5.6':
|
||||
resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@parcel/watcher-linux-arm-glibc@2.5.6':
|
||||
resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.6':
|
||||
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher-win32-ia32@2.5.6':
|
||||
resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher-win32-x64@2.5.6':
|
||||
resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@parcel/watcher@2.5.6':
|
||||
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
'@pkgr/core@0.2.9':
|
||||
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
@@ -1218,10 +1306,6 @@ packages:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
anymatch@3.1.3:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
apexcharts@3.42.0:
|
||||
resolution: {integrity: sha512-hYhzZqh2Efny9uiutkGU2M/EarJ4Nn8s6dxZ0C7E7N+SV4d1xjTioXi2NLn4UKVJabZkb3HnpXDoumXgtAymwg==}
|
||||
|
||||
@@ -1254,10 +1338,6 @@ packages:
|
||||
big.js@5.2.2:
|
||||
resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
@@ -1324,9 +1404,9 @@ packages:
|
||||
chevrotain@11.0.3:
|
||||
resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
chokidar@4.0.3:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
chrome-trace-event@1.0.4:
|
||||
resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==}
|
||||
@@ -1590,6 +1670,10 @@ packages:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
@@ -1966,10 +2050,6 @@ packages:
|
||||
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-buffer@2.0.5:
|
||||
resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2235,13 +2315,12 @@ packages:
|
||||
neo-async@2.6.2:
|
||||
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
|
||||
|
||||
node-addon-api@7.1.1:
|
||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||
|
||||
node-releases@2.0.36:
|
||||
resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==}
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
@@ -2481,9 +2560,9 @@ packages:
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
readdirp@4.1.2:
|
||||
resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
|
||||
engines: {node: '>= 14.18.0'}
|
||||
|
||||
rechoir@0.6.2:
|
||||
resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==}
|
||||
@@ -2569,8 +2648,8 @@ packages:
|
||||
sass-embedded:
|
||||
optional: true
|
||||
|
||||
sass@1.66.1:
|
||||
resolution: {integrity: sha512-50c+zTsZOJVgFfTgwwEzkjA3/QACgdNsKueWPyAR0mRINIvLAStVQBbPg14iuqEQ74NPDbXzJARJ/O4SI1zftA==}
|
||||
sass@1.98.0:
|
||||
resolution: {integrity: sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -3339,6 +3418,67 @@ snapshots:
|
||||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.20.1
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-darwin-arm64@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-darwin-x64@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-freebsd-x64@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm-glibc@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm-musl@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm64-glibc@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-arm64-musl@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-x64-glibc@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-linux-x64-musl@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-arm64@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-ia32@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher-win32-x64@2.5.6':
|
||||
optional: true
|
||||
|
||||
'@parcel/watcher@2.5.6':
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
is-glob: 4.0.3
|
||||
node-addon-api: 7.1.1
|
||||
picomatch: 4.0.3
|
||||
optionalDependencies:
|
||||
'@parcel/watcher-android-arm64': 2.5.6
|
||||
'@parcel/watcher-darwin-arm64': 2.5.6
|
||||
'@parcel/watcher-darwin-x64': 2.5.6
|
||||
'@parcel/watcher-freebsd-x64': 2.5.6
|
||||
'@parcel/watcher-linux-arm-glibc': 2.5.6
|
||||
'@parcel/watcher-linux-arm-musl': 2.5.6
|
||||
'@parcel/watcher-linux-arm64-glibc': 2.5.6
|
||||
'@parcel/watcher-linux-arm64-musl': 2.5.6
|
||||
'@parcel/watcher-linux-x64-glibc': 2.5.6
|
||||
'@parcel/watcher-linux-x64-musl': 2.5.6
|
||||
'@parcel/watcher-win32-arm64': 2.5.6
|
||||
'@parcel/watcher-win32-ia32': 2.5.6
|
||||
'@parcel/watcher-win32-x64': 2.5.6
|
||||
optional: true
|
||||
|
||||
'@pkgr/core@0.2.9': {}
|
||||
|
||||
'@popperjs/core@2.11.8': {}
|
||||
@@ -3865,9 +4005,9 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)':
|
||||
'@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0))(vue@3.3.4)':
|
||||
dependencies:
|
||||
vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)
|
||||
vite: 6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0)
|
||||
vue: 3.3.4
|
||||
|
||||
'@volar/language-core@1.10.10':
|
||||
@@ -4151,11 +4291,6 @@ snapshots:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
anymatch@3.1.3:
|
||||
dependencies:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.1
|
||||
|
||||
apexcharts@3.42.0:
|
||||
dependencies:
|
||||
'@yr/monotone-cubic-spline': 1.0.3
|
||||
@@ -4192,8 +4327,6 @@ snapshots:
|
||||
|
||||
big.js@5.2.2: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
brace-expansion@1.1.12:
|
||||
@@ -4267,17 +4400,9 @@ snapshots:
|
||||
'@chevrotain/utils': 11.0.3
|
||||
lodash-es: 4.17.23
|
||||
|
||||
chokidar@3.6.0:
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
braces: 3.0.3
|
||||
glob-parent: 5.1.2
|
||||
is-binary-path: 2.1.0
|
||||
is-glob: 4.0.3
|
||||
normalize-path: 3.0.0
|
||||
readdirp: 3.6.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
readdirp: 4.1.2
|
||||
|
||||
chrome-trace-event@1.0.4: {}
|
||||
|
||||
@@ -4547,6 +4672,9 @@ snapshots:
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
optional: true
|
||||
|
||||
devlop@1.1.0:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
@@ -4967,10 +5095,6 @@ snapshots:
|
||||
|
||||
interpret@1.4.0: {}
|
||||
|
||||
is-binary-path@2.1.0:
|
||||
dependencies:
|
||||
binary-extensions: 2.3.0
|
||||
|
||||
is-buffer@2.0.5: {}
|
||||
|
||||
is-core-module@2.16.1:
|
||||
@@ -5226,9 +5350,10 @@ snapshots:
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
|
||||
node-releases@2.0.36: {}
|
||||
node-addon-api@7.1.1:
|
||||
optional: true
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
node-releases@2.0.36: {}
|
||||
|
||||
nth-check@2.1.1:
|
||||
dependencies:
|
||||
@@ -5486,9 +5611,7 @@ snapshots:
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
readdirp@3.6.0:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
readdirp@4.1.2: {}
|
||||
|
||||
rechoir@0.6.2:
|
||||
dependencies:
|
||||
@@ -5574,18 +5697,20 @@ snapshots:
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
sass-loader@13.3.2(sass@1.66.1)(webpack@5.105.0):
|
||||
sass-loader@13.3.2(sass@1.98.0)(webpack@5.105.0):
|
||||
dependencies:
|
||||
neo-async: 2.6.2
|
||||
webpack: 5.105.0
|
||||
optionalDependencies:
|
||||
sass: 1.66.1
|
||||
sass: 1.98.0
|
||||
|
||||
sass@1.66.1:
|
||||
sass@1.98.0:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
chokidar: 4.0.3
|
||||
immutable: 4.3.8
|
||||
source-map-js: 1.2.1
|
||||
optionalDependencies:
|
||||
'@parcel/watcher': 2.5.6
|
||||
|
||||
schema-utils@3.3.0:
|
||||
dependencies:
|
||||
@@ -5860,28 +5985,28 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-plugin-vuetify@2.1.3(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11):
|
||||
vite-plugin-vuetify@2.1.3(vite@6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11):
|
||||
dependencies:
|
||||
'@vuetify/loader-shared': 2.1.2(vue@3.3.4)(vuetify@3.7.11)
|
||||
debug: 4.4.3
|
||||
upath: 2.0.1
|
||||
vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)
|
||||
vite: 6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0)
|
||||
vue: 3.3.4
|
||||
vuetify: 3.7.11(typescript@5.1.6)(vite-plugin-vuetify@2.1.3)(vue@3.3.4)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-webfont-dl@3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)):
|
||||
vite-plugin-webfont-dl@3.12.0(vite@6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0)):
|
||||
dependencies:
|
||||
axios: 1.13.5
|
||||
clean-css: 5.3.3
|
||||
flat-cache: 6.1.20
|
||||
picocolors: 1.1.1
|
||||
vite: 6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0)
|
||||
vite: 6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0)
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0):
|
||||
vite@6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0):
|
||||
dependencies:
|
||||
esbuild: 0.25.12
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -5892,7 +6017,7 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 20.19.32
|
||||
fsevents: 2.3.3
|
||||
sass: 1.66.1
|
||||
sass: 1.98.0
|
||||
terser: 5.46.0
|
||||
|
||||
vscode-jsonrpc@8.2.0: {}
|
||||
@@ -5912,7 +6037,7 @@ snapshots:
|
||||
|
||||
vscode-uri@3.0.8: {}
|
||||
|
||||
vue-cli-plugin-vuetify@2.5.8(sass-loader@13.3.2(sass@1.66.1)(webpack@5.105.0))(vue@3.3.4)(vuetify-loader@2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0))(webpack@5.105.0):
|
||||
vue-cli-plugin-vuetify@2.5.8(sass-loader@13.3.2(sass@1.98.0)(webpack@5.105.0))(vue@3.3.4)(vuetify-loader@2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0))(webpack@5.105.0):
|
||||
dependencies:
|
||||
null-loader: 4.0.1(webpack@5.105.0)
|
||||
semver: 7.7.4
|
||||
@@ -5920,7 +6045,7 @@ snapshots:
|
||||
vue: 3.3.4
|
||||
webpack: 5.105.0
|
||||
optionalDependencies:
|
||||
sass-loader: 13.3.2(sass@1.66.1)(webpack@5.105.0)
|
||||
sass-loader: 13.3.2(sass@1.98.0)(webpack@5.105.0)
|
||||
vuetify-loader: 2.0.0-alpha.9(@vue/compiler-sfc@3.3.4)(vue@3.3.4)(vuetify@3.7.11)(webpack@5.105.0)
|
||||
|
||||
vue-demi@0.14.10(vue@3.3.4):
|
||||
@@ -6002,7 +6127,7 @@ snapshots:
|
||||
vue: 3.3.4
|
||||
optionalDependencies:
|
||||
typescript: 5.1.6
|
||||
vite-plugin-vuetify: 2.1.3(vite@6.4.1(@types/node@20.19.32)(sass@1.66.1)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11)
|
||||
vite-plugin-vuetify: 2.1.3(vite@6.4.1(@types/node@20.19.32)(sass@1.98.0)(terser@5.46.0))(vue@3.3.4)(vuetify@3.7.11)
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* Auto-generated MDI subset – 256 icons */
|
||||
/* Auto-generated MDI subset – 255 icons */
|
||||
/* Do not edit manually. Run: pnpm run subset-icons */
|
||||
|
||||
@font-face {
|
||||
@@ -132,10 +132,6 @@
|
||||
content: "\F0B66";
|
||||
}
|
||||
|
||||
.mdi-calendar-clock-outline::before {
|
||||
content: "\F16E1";
|
||||
}
|
||||
|
||||
.mdi-calendar-edit::before {
|
||||
content: "\F08A7";
|
||||
}
|
||||
@@ -296,6 +292,10 @@
|
||||
content: "\F0193";
|
||||
}
|
||||
|
||||
.mdi-content-save-outline::before {
|
||||
content: "\F0818";
|
||||
}
|
||||
|
||||
.mdi-creation::before {
|
||||
content: "\F0674";
|
||||
}
|
||||
@@ -844,10 +844,6 @@
|
||||
content: "\F048A";
|
||||
}
|
||||
|
||||
.mdi-send-clock-outline::before {
|
||||
content: "\F1164";
|
||||
}
|
||||
|
||||
.mdi-server::before {
|
||||
content: "\F048B";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -65,18 +65,26 @@ const authorDisplay = computed(() => {
|
||||
>
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-avatar
|
||||
v-bind="menuProps"
|
||||
size="72"
|
||||
class="pinned-avatar activator-avatar"
|
||||
:title="plugin.display_name || plugin.name"
|
||||
>
|
||||
<img
|
||||
:src="(typeof plugin.logo === 'string' && plugin.logo.trim()) ? plugin.logo : defaultPluginIcon"
|
||||
:alt="plugin.name"
|
||||
@error="handlePinnedImgError"
|
||||
/>
|
||||
</v-avatar>
|
||||
<div class="d-flex flex-column align-center" style="cursor: pointer; width: 80px;">
|
||||
<v-avatar
|
||||
v-bind="menuProps"
|
||||
size="72"
|
||||
class="pinned-avatar activator-avatar mb-1"
|
||||
:title="plugin.display_name || plugin.name"
|
||||
>
|
||||
<img
|
||||
:src="(typeof plugin.logo === 'string' && plugin.logo.trim()) ? plugin.logo : defaultPluginIcon"
|
||||
:alt="plugin.name"
|
||||
@error="handlePinnedImgError"
|
||||
/>
|
||||
</v-avatar>
|
||||
<span
|
||||
class="text-caption text-center text-truncate"
|
||||
style="width: 100%; font-size: 0.75rem; opacity: 0.9; line-height: 1.2;"
|
||||
>
|
||||
{{ plugin.display_name || plugin.name }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
@@ -182,8 +190,7 @@ const authorDisplay = computed(() => {
|
||||
.pinned-card-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.pinned-item {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<template>
|
||||
<div class="mt-4">
|
||||
<div class="d-flex align-center ga-2 mb-2">
|
||||
<h3 class="text-h5 font-weight-bold mb-0">{{ tm('models.configured') }}</h3>
|
||||
<small style="color: grey;" v-if="availableCount">{{ tm('models.available') }} {{ availableCount }}</small>
|
||||
<div class="provider-models-panel">
|
||||
<div class="provider-models-head">
|
||||
<div class="provider-models-title-wrap">
|
||||
<h3 class="provider-models-title">{{ tm('models.configured') }}</h3>
|
||||
<small v-if="availableCount" class="provider-models-subtitle">{{ tm('models.available') }} {{ availableCount }}</small>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="modelSearchProxy"
|
||||
density="compact"
|
||||
@@ -11,37 +13,35 @@
|
||||
hide-details
|
||||
variant="solo-filled"
|
||||
flat
|
||||
class="ml-1"
|
||||
style="max-width: 240px;"
|
||||
class="provider-models-search"
|
||||
:placeholder="tm('models.searchPlaceholder')"
|
||||
/>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-download"
|
||||
:loading="loadingModels"
|
||||
@click="emit('fetch-models')"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
>
|
||||
{{ isSourceModified ? tm('providerSources.saveAndFetchModels') : tm('providerSources.fetchModels') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-pencil-plus"
|
||||
variant="text"
|
||||
size="small"
|
||||
class="ml-1"
|
||||
@click="emit('open-manual-model')"
|
||||
>
|
||||
{{ tm('models.manualAddButton') }}
|
||||
</v-btn>
|
||||
<div class="provider-models-actions">
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-download"
|
||||
:loading="loadingModels"
|
||||
@click="emit('fetch-models')"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
>
|
||||
{{ isSourceModified ? tm('providerSources.saveAndFetchModels') : tm('providerSources.fetchModels') }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-pencil-plus"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="emit('open-manual-model')"
|
||||
>
|
||||
{{ tm('models.manualAddButton') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-list
|
||||
density="compact"
|
||||
class="rounded-lg border"
|
||||
style="max-height: 520px; overflow-y: auto; font-family:system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;"
|
||||
class="provider-models-list"
|
||||
>
|
||||
<template v-if="entries.length > 0">
|
||||
<template v-for="entry in entries" :key="entry.type === 'configured' ? `provider-${entry.provider.id}` : `model-${entry.model}`">
|
||||
@@ -55,7 +55,7 @@
|
||||
<v-list-item-title class="font-weight-medium text-truncate">
|
||||
{{ entry.provider.id }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption text-grey d-flex align-center ga-1" style="font-family: monospace;">
|
||||
<v-list-item-subtitle class="provider-model-subtitle d-flex align-center ga-1">
|
||||
<span>{{ entry.provider.model }}</span>
|
||||
<v-icon v-if="supportsImageInput(entry.metadata)" size="14" color="grey">
|
||||
mdi-eye-outline
|
||||
@@ -124,7 +124,7 @@
|
||||
<template #activator="{ props }">
|
||||
<v-list-item v-bind="props" class="cursor-pointer" @click="emit('add-model-provider', entry.model)">
|
||||
<v-list-item-title>{{ entry.model }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-caption text-grey d-flex align-center ga-1">
|
||||
<v-list-item-subtitle class="provider-model-subtitle d-flex align-center ga-1">
|
||||
<span>{{ entry.model }}</span>
|
||||
<v-icon v-if="supportsImageInput(entry.metadata)" size="14" color="grey">
|
||||
mdi-eye-outline
|
||||
@@ -231,11 +231,87 @@ const isProviderTesting = (providerId) => props.testingProviders.includes(provid
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.border {
|
||||
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
.provider-models-panel {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.provider-models-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.provider-models-title-wrap {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.provider-models-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.3;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.provider-models-subtitle {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: rgba(var(--v-theme-on-surface), 0.6);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.provider-models-search {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.provider-models-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.provider-models-list {
|
||||
max-height: 520px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
border-radius: 14px;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
.provider-compact-item {
|
||||
border-bottom: 1px solid rgba(var(--v-theme-on-surface), 0.08);
|
||||
}
|
||||
|
||||
.provider-models-list :deep(.v-list-item:last-child) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.provider-model-subtitle {
|
||||
color: rgba(var(--v-theme-on-surface), 0.62);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.provider-models-head {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.provider-models-search {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.provider-models-actions {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<template>
|
||||
<v-card class="provider-sources-panel h-100" elevation="0">
|
||||
<div class="d-flex align-center justify-space-between px-4 pt-4 pb-2">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<h3 class="mb-0">{{ tm('providerSources.title') }}</h3>
|
||||
<div class="provider-sources-head">
|
||||
<div class="provider-sources-title-wrap">
|
||||
<div class="provider-sources-title-row">
|
||||
<h3 class="provider-sources-title">{{ tm('providerSources.title') }}</h3>
|
||||
<v-chip size="x-small" variant="tonal" label>
|
||||
{{ displayedProviderSources.length }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<StyledMenu>
|
||||
<template #activator="{ props }">
|
||||
@@ -11,7 +16,6 @@
|
||||
prepend-icon="mdi-plus"
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
rounded="xl"
|
||||
size="small"
|
||||
>
|
||||
{{ tm('providerSources.add') }}
|
||||
@@ -34,7 +38,7 @@
|
||||
</StyledMenu>
|
||||
</div>
|
||||
|
||||
<div v-if="isMobile && displayedProviderSources.length > 0" class="px-4 pb-3">
|
||||
<div v-if="isMobile && displayedProviderSources.length > 0" class="provider-sources-mobile">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-select
|
||||
:model-value="selectedId"
|
||||
@@ -71,7 +75,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="displayedProviderSources.length > 0">
|
||||
<div v-else-if="displayedProviderSources.length > 0" class="provider-sources-list-wrap">
|
||||
<v-list class="provider-source-list" nav density="compact" lines="two">
|
||||
<v-list-item
|
||||
v-for="source in displayedProviderSources"
|
||||
@@ -83,13 +87,13 @@
|
||||
@click="emitSelectSource(source)"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-avatar size="32" class="bg-grey-lighten-4" rounded="0">
|
||||
<v-avatar size="28" class="provider-source-avatar" rounded="0">
|
||||
<v-img v-if="source?.provider" :src="resolveSourceIcon(source)" alt="logo" cover></v-img>
|
||||
<v-icon v-else size="32">mdi-creation</v-icon>
|
||||
<v-icon v-else size="20">mdi-creation</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<v-list-item-title class="font-weight-bold mb-1" style="font-family: Arial, Helvetica, sans-serif; font-size: 16px;">{{ getSourceDisplayName(source) }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="text-truncate">{{ source.api_base || 'N/A' }}</v-list-item-subtitle>
|
||||
<v-list-item-title class="provider-source-title">{{ getSourceDisplayName(source) }}</v-list-item-title>
|
||||
<v-list-item-subtitle class="provider-source-subtitle text-truncate">{{ source.api_base || 'N/A' }}</v-list-item-subtitle>
|
||||
<template #append>
|
||||
<div class="d-flex align-center ga-1">
|
||||
<v-btn
|
||||
@@ -98,6 +102,7 @@
|
||||
variant="text"
|
||||
size="x-small"
|
||||
color="error"
|
||||
:ripple="false"
|
||||
@click.stop="emitDeleteSource(source)"
|
||||
></v-btn>
|
||||
</div>
|
||||
@@ -181,22 +186,98 @@ const emitDeleteSource = (source) => emit('delete-provider-source', source)
|
||||
|
||||
<style scoped>
|
||||
.provider-sources-panel {
|
||||
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
border-radius: 16px;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
min-height: 320px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.provider-sources-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 18px 18px 12px;
|
||||
border-bottom: 1px solid rgba(var(--v-theme-on-surface), 0.1);
|
||||
}
|
||||
|
||||
.provider-sources-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.provider-sources-title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
line-height: 1.2;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.provider-sources-mobile {
|
||||
padding: 14px 18px 0;
|
||||
}
|
||||
|
||||
.provider-sources-list-wrap {
|
||||
padding: 8px 8px 10px;
|
||||
}
|
||||
|
||||
.provider-source-list {
|
||||
max-height: calc(100vh - 335px);
|
||||
overflow-y: auto;
|
||||
padding: 6px 8px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.provider-source-list-item {
|
||||
margin-bottom: 2px;
|
||||
border: 1px solid transparent;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.provider-source-list-item--active {
|
||||
background-color: #E8F0FE;
|
||||
border: 1px solid rgba(var(--v-theme-primary), 0.25);
|
||||
background-color: rgba(var(--v-theme-primary), 0.06);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.provider-source-avatar {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.provider-source-title {
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.provider-source-subtitle {
|
||||
color: rgba(var(--v-theme-on-surface), 0.62);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.provider-source-list :deep(.v-list-item__prepend) {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
|
||||
.provider-source-list :deep(.v-list-item__content) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.provider-source-list :deep(.v-list-item__append) {
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.provider-source-list-item:hover {
|
||||
background-color: rgba(var(--v-theme-on-surface), 0.025);
|
||||
}
|
||||
|
||||
.provider-source-list-item:hover :deep(.v-list-item__append),
|
||||
.provider-source-list-item--active :deep(.v-list-item__append) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
@@ -212,7 +293,7 @@ const emitDeleteSource = (source) => emit('delete-provider-source', source)
|
||||
|
||||
<style>
|
||||
.v-theme--PurpleThemeDark .provider-source-list-item--active {
|
||||
background-color: #2d2d2d;
|
||||
border: none;
|
||||
background-color: rgba(var(--v-theme-primary), 0.1);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="console-displayer-wrapper" id="console-wrapper">
|
||||
<div class="filter-controls mb-2" v-if="showLevelBtns">
|
||||
<v-chip-group v-model="selectedLevels" column multiple>
|
||||
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter variant="flat" size="small"
|
||||
@@ -13,6 +13,14 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
{{ level }}
|
||||
</v-chip>
|
||||
</v-chip-group>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
:icon="isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
|
||||
variant="text"
|
||||
density="compact"
|
||||
class="me-4 fullscreen-btn"
|
||||
@click="toggleFullscreen"
|
||||
></v-btn>
|
||||
</div>
|
||||
|
||||
<div id="term" style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; overflow-y:auto; height: 100%">
|
||||
@@ -26,6 +34,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
autoScroll: true,
|
||||
isFullscreen: false,
|
||||
logColorAnsiMap: {
|
||||
'\u001b[1;34m': 'color: #39C5BB; font-weight: bold;',
|
||||
'\u001b[1;36m': 'color: #00FFFF; font-weight: bold;',
|
||||
@@ -80,8 +89,10 @@ export default {
|
||||
async mounted() {
|
||||
await this.fetchLogHistory();
|
||||
this.connectSSE();
|
||||
document.addEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
},
|
||||
beforeUnmount() {
|
||||
document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
|
||||
if (this.eventSource) {
|
||||
this.eventSource.close();
|
||||
this.eventSource = null;
|
||||
@@ -253,6 +264,21 @@ export default {
|
||||
this.autoScroll = !this.autoScroll;
|
||||
},
|
||||
|
||||
toggleFullscreen() {
|
||||
const container = document.getElementById('console-wrapper');
|
||||
if (!document.fullscreenElement) {
|
||||
container.requestFullscreen().catch(err => {
|
||||
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
|
||||
});
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
},
|
||||
|
||||
handleFullscreenChange() {
|
||||
this.isFullscreen = !!document.fullscreenElement;
|
||||
},
|
||||
|
||||
printLog(log) {
|
||||
let ele = document.getElementById('term')
|
||||
if (!ele) {
|
||||
@@ -282,14 +308,30 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.console-displayer-wrapper {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#console-wrapper:fullscreen {
|
||||
background-color: #1e1e1e;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
color: rgba(255, 255, 255, 0.7) !important; /* 提高在深色背景下的对比度 */
|
||||
}
|
||||
|
||||
:deep(.console-log-line) {
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"loading": "Loading...",
|
||||
"documents": "Documents",
|
||||
"chunks": "Chunks",
|
||||
"sessionConfig": "Session Config"
|
||||
"sessionConfig": "Session Config",
|
||||
"initError": "Initialization Failed"
|
||||
},
|
||||
"card": {
|
||||
"edit": "Edit",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"loading": "Загрузка...",
|
||||
"documents": "док.",
|
||||
"chunks": "фрагм.",
|
||||
"sessionConfig": "Профиль"
|
||||
"sessionConfig": "Профиль",
|
||||
"initError": "Ошибка инициализации"
|
||||
},
|
||||
"card": {
|
||||
"edit": "Изменить",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"loading": "正在加载...",
|
||||
"documents": "文档",
|
||||
"chunks": "分块",
|
||||
"sessionConfig": "会话配置"
|
||||
"sessionConfig": "会话配置",
|
||||
"initError": "初始化失败"
|
||||
},
|
||||
"card": {
|
||||
"edit": "编辑",
|
||||
|
||||
@@ -1,119 +1,193 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import App from './App.vue';
|
||||
import { router } from './router';
|
||||
import vuetify from './plugins/vuetify';
|
||||
import confirmPlugin from './plugins/confirmPlugin';
|
||||
import { setupI18n } from './i18n/composables';
|
||||
import '@/scss/style.scss';
|
||||
import VueApexCharts from 'vue3-apexcharts';
|
||||
import { createApp } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import { router } from "./router";
|
||||
import vuetify from "./plugins/vuetify";
|
||||
import confirmPlugin from "./plugins/confirmPlugin";
|
||||
import { setupI18n } from "./i18n/composables";
|
||||
import "@/scss/style.scss";
|
||||
import VueApexCharts from "vue3-apexcharts";
|
||||
|
||||
import print from 'vue3-print-nb';
|
||||
import { loader } from '@guolao/vue-monaco-editor'
|
||||
import axios from 'axios';
|
||||
import { waitForRouterReadyInBackground } from './utils/routerReadiness.mjs';
|
||||
import print from "vue3-print-nb";
|
||||
import { loader } from "@guolao/vue-monaco-editor";
|
||||
import * as monaco from "monaco-editor";
|
||||
import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker";
|
||||
import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker";
|
||||
import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker";
|
||||
import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker";
|
||||
import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker";
|
||||
import axios from "axios";
|
||||
import { waitForRouterReadyInBackground } from "./utils/routerReadiness.mjs";
|
||||
import {
|
||||
getApiBaseUrl,
|
||||
resolveApiUrl,
|
||||
resolvePublicUrl,
|
||||
setApiBaseUrl,
|
||||
} from "@/utils/request";
|
||||
|
||||
(self as any).MonacoEnvironment = {
|
||||
getWorker(_: string, label: string) {
|
||||
if (label === "json") {
|
||||
return new jsonWorker();
|
||||
}
|
||||
if (label === "css" || label === "scss" || label === "less") {
|
||||
return new cssWorker();
|
||||
}
|
||||
if (label === "html" || label === "handlebars" || label === "razor") {
|
||||
return new htmlWorker();
|
||||
}
|
||||
if (label === "typescript" || label === "javascript") {
|
||||
return new tsWorker();
|
||||
}
|
||||
return new editorWorker();
|
||||
},
|
||||
};
|
||||
|
||||
// 初始化新的i18n系统,等待完成后再挂载应用
|
||||
setupI18n().then(async () => {
|
||||
console.log('🌍 新i18n系统初始化完成');
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(print);
|
||||
app.use(VueApexCharts);
|
||||
app.use(vuetify);
|
||||
app.use(confirmPlugin);
|
||||
await router.isReady();
|
||||
app.mount('#app');
|
||||
|
||||
// 挂载后同步 Vuetify 主题
|
||||
import('./stores/customizer').then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
const storedPrimary = localStorage.getItem('themePrimary');
|
||||
const storedSecondary = localStorage.getItem('themeSecondary');
|
||||
if (storedPrimary || storedSecondary) {
|
||||
const themes = vuetify.theme.themes.value;
|
||||
['PurpleTheme', 'PurpleThemeDark'].forEach((name) => {
|
||||
const theme = themes[name];
|
||||
if (!theme?.colors) return;
|
||||
if (storedPrimary) theme.colors.primary = storedPrimary;
|
||||
if (storedSecondary) theme.colors.secondary = storedSecondary;
|
||||
if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary;
|
||||
if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary;
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('❌ 新i18n系统初始化失败:', error);
|
||||
|
||||
// 即使i18n初始化失败,也要挂载应用(使用回退机制)
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(print);
|
||||
app.use(VueApexCharts);
|
||||
app.use(vuetify);
|
||||
app.use(confirmPlugin);
|
||||
app.mount('#app');
|
||||
waitForRouterReadyInBackground(router);
|
||||
|
||||
// 挂载后同步 Vuetify 主题
|
||||
import('./stores/customizer').then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
const storedPrimary = localStorage.getItem('themePrimary');
|
||||
const storedSecondary = localStorage.getItem('themeSecondary');
|
||||
if (storedPrimary || storedSecondary) {
|
||||
const themes = vuetify.theme.themes.value;
|
||||
['PurpleTheme', 'PurpleThemeDark'].forEach((name) => {
|
||||
const theme = themes[name];
|
||||
if (!theme?.colors) return;
|
||||
if (storedPrimary) theme.colors.primary = storedPrimary;
|
||||
if (storedSecondary) theme.colors.secondary = storedSecondary;
|
||||
if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary;
|
||||
if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
setupI18n()
|
||||
.then(async () => {
|
||||
console.log("🌍 新i18n系统初始化完成");
|
||||
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(print);
|
||||
app.use(VueApexCharts);
|
||||
app.use(vuetify);
|
||||
app.use(confirmPlugin);
|
||||
await router.isReady();
|
||||
app.mount("#app");
|
||||
|
||||
// 挂载后同步 Vuetify 主题
|
||||
import("./stores/customizer").then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
const storedPrimary = localStorage.getItem("themePrimary");
|
||||
const storedSecondary = localStorage.getItem("themeSecondary");
|
||||
if (storedPrimary || storedSecondary) {
|
||||
const themes = vuetify.theme.themes.value;
|
||||
["PurpleTheme", "PurpleThemeDark"].forEach((name) => {
|
||||
const theme = themes[name];
|
||||
if (!theme?.colors) return;
|
||||
if (storedPrimary) theme.colors.primary = storedPrimary;
|
||||
if (storedSecondary) theme.colors.secondary = storedSecondary;
|
||||
if (storedPrimary && theme.colors.darkprimary)
|
||||
theme.colors.darkprimary = storedPrimary;
|
||||
if (storedSecondary && theme.colors.darksecondary)
|
||||
theme.colors.darksecondary = storedSecondary;
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("❌ 新i18n系统初始化失败:", error);
|
||||
|
||||
// 即使i18n初始化失败,也要挂载应用(使用回退机制)
|
||||
const app = createApp(App);
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(print);
|
||||
app.use(VueApexCharts);
|
||||
app.use(vuetify);
|
||||
app.use(confirmPlugin);
|
||||
app.mount("#app");
|
||||
waitForRouterReadyInBackground(router);
|
||||
|
||||
// 挂载后同步 Vuetify 主题
|
||||
import("./stores/customizer").then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
const storedPrimary = localStorage.getItem("themePrimary");
|
||||
const storedSecondary = localStorage.getItem("themeSecondary");
|
||||
if (storedPrimary || storedSecondary) {
|
||||
const themes = vuetify.theme.themes.value;
|
||||
["PurpleTheme", "PurpleThemeDark"].forEach((name) => {
|
||||
const theme = themes[name];
|
||||
if (!theme?.colors) return;
|
||||
if (storedPrimary) theme.colors.primary = storedPrimary;
|
||||
if (storedSecondary) theme.colors.secondary = storedSecondary;
|
||||
if (storedPrimary && theme.colors.darkprimary)
|
||||
theme.colors.darkprimary = storedPrimary;
|
||||
if (storedSecondary && theme.colors.darksecondary)
|
||||
theme.colors.darksecondary = storedSecondary;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
axios.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token');
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
config.headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
const locale = localStorage.getItem('astrbot-locale');
|
||||
const locale = localStorage.getItem("astrbot-locale");
|
||||
if (locale) {
|
||||
config.headers['Accept-Language'] = locale;
|
||||
config.headers["Accept-Language"] = locale;
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// Keep fetch() calls consistent with axios by automatically attaching the JWT.
|
||||
// Some parts of the UI use fetch directly; without this, those requests will 401.
|
||||
const _origFetch = window.fetch.bind(window);
|
||||
window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return _origFetch(input, init);
|
||||
|
||||
const headers = new Headers(init?.headers || (typeof input !== 'string' && 'headers' in input ? (input as Request).headers : undefined));
|
||||
if (!headers.has('Authorization')) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
// 1. 定义加载配置的函数
|
||||
async function loadAppConfig() {
|
||||
try {
|
||||
const configUrl = new URL(resolvePublicUrl("config.json"));
|
||||
configUrl.searchParams.set("t", `${Date.now()}`);
|
||||
const response = await fetch(configUrl.toString());
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.warn("Failed to load config.json, falling back to default.", error);
|
||||
return {};
|
||||
}
|
||||
const locale = localStorage.getItem('astrbot-locale');
|
||||
if (locale && !headers.has('Accept-Language')) {
|
||||
headers.set('Accept-Language', locale);
|
||||
}
|
||||
return _origFetch(input, { ...init, headers });
|
||||
};
|
||||
}
|
||||
|
||||
loader.config({
|
||||
paths: {
|
||||
vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.54.0/min/vs',
|
||||
},
|
||||
})
|
||||
async function initApp() {
|
||||
const config = await loadAppConfig();
|
||||
const configApiUrl = config.apiBaseUrl || "";
|
||||
const envApiUrl = import.meta.env.VITE_API_BASE || "";
|
||||
|
||||
const localApiUrl = localStorage.getItem("apiBaseUrl");
|
||||
const apiBaseUrl =
|
||||
localApiUrl !== null ? localApiUrl : configApiUrl || envApiUrl;
|
||||
|
||||
if (apiBaseUrl) {
|
||||
console.log(`API Base URL set to: ${apiBaseUrl}`);
|
||||
}
|
||||
|
||||
setApiBaseUrl(apiBaseUrl);
|
||||
|
||||
// Keep fetch() calls consistent with axios by automatically attaching the JWT.
|
||||
// Some parts of the UI use fetch directly; without this, those requests will 401.
|
||||
const _origFetch = window.fetch.bind(window);
|
||||
window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
let url = input;
|
||||
if (typeof input === "string" && input.startsWith("/api")) {
|
||||
url = resolveApiUrl(input, getApiBaseUrl());
|
||||
}
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
const headers = new Headers(
|
||||
init?.headers ||
|
||||
(typeof input !== "string" && "headers" in input
|
||||
? (input as Request).headers
|
||||
: undefined),
|
||||
);
|
||||
|
||||
if (token && !headers.has("Authorization")) {
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
const locale = localStorage.getItem("astrbot-locale");
|
||||
if (locale && !headers.has("Accept-Language")) {
|
||||
headers.set("Accept-Language", locale);
|
||||
}
|
||||
return _origFetch(url, { ...init, headers });
|
||||
};
|
||||
}
|
||||
|
||||
initApp();
|
||||
|
||||
loader.config({ monaco });
|
||||
|
||||
@@ -25,12 +25,47 @@ export const useAuthStore = defineStore({
|
||||
localStorage.setItem('user', this.username);
|
||||
localStorage.setItem('token', res.data.data.token);
|
||||
localStorage.setItem('change_pwd_hint', res.data.data?.change_pwd_hint);
|
||||
|
||||
const onboardingCompleted = await this.checkOnboardingCompleted();
|
||||
this.returnUrl = null;
|
||||
router.push('/welcome');
|
||||
if (onboardingCompleted) {
|
||||
router.push('/dashboard/default');
|
||||
} else {
|
||||
router.push('/welcome');
|
||||
}
|
||||
} catch (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
},
|
||||
async checkOnboardingCompleted(): Promise<boolean> {
|
||||
try {
|
||||
// 1. 检查平台配置
|
||||
const platformRes = await axios.get('/api/config/get');
|
||||
const hasPlatform = (platformRes.data.data.config.platform || []).length > 0;
|
||||
if (!hasPlatform) return false;
|
||||
|
||||
// 2. 检查提供者配置
|
||||
const providerRes = await axios.get('/api/config/provider/template');
|
||||
const providers = providerRes.data.data?.providers || [];
|
||||
const sources = providerRes.data.data?.provider_sources || [];
|
||||
const sourceMap = new Map();
|
||||
sources.forEach((s: any) => sourceMap.set(s.id, s.provider_type));
|
||||
|
||||
const hasProvider = providers.some((provider: any) => {
|
||||
if (provider.provider_type) return provider.provider_type === 'chat_completion';
|
||||
if (provider.provider_source_id) {
|
||||
const type = sourceMap.get(provider.provider_source_id);
|
||||
if (type === 'chat_completion') return true;
|
||||
}
|
||||
return String(provider.type || '').includes('chat_completion');
|
||||
});
|
||||
|
||||
return hasProvider;
|
||||
} catch (e) {
|
||||
console.error('Failed to check onboarding status:', e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
logout() {
|
||||
this.username = '';
|
||||
localStorage.removeItem('user');
|
||||
|
||||
182
dashboard/src/utils/request.ts
Normal file
182
dashboard/src/utils/request.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import axios, { type InternalAxiosRequestConfig } from "axios";
|
||||
|
||||
const ABSOLUTE_URL_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//;
|
||||
|
||||
function isAbsoluteUrl(value: string): boolean {
|
||||
return ABSOLUTE_URL_PATTERN.test(value);
|
||||
}
|
||||
|
||||
function stripTrailingSlashes(value: string): string {
|
||||
return value.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function ensureLeadingSlash(value: string): string {
|
||||
if (!value) {
|
||||
return "/";
|
||||
}
|
||||
return value.startsWith("/") ? value : `/${value}`;
|
||||
}
|
||||
|
||||
function stripLeadingApiPrefix(path: string): string {
|
||||
const normalizedPath = ensureLeadingSlash(path);
|
||||
const strippedPath = normalizedPath.replace(/^\/api(?=\/|$)/, "");
|
||||
return strippedPath || "/";
|
||||
}
|
||||
|
||||
function baseEndsWithApi(baseUrl: string): boolean {
|
||||
if (!baseUrl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAbsoluteUrl(baseUrl)) {
|
||||
try {
|
||||
return new URL(baseUrl).pathname.replace(/\/+$/, "").endsWith("/api");
|
||||
} catch {
|
||||
return baseUrl.replace(/\/+$/, "").endsWith("/api");
|
||||
}
|
||||
}
|
||||
|
||||
return stripTrailingSlashes(baseUrl).endsWith("/api");
|
||||
}
|
||||
|
||||
function normalizePathForBase(path: string, baseUrl = ""): string {
|
||||
if (!path) {
|
||||
return "/";
|
||||
}
|
||||
|
||||
if (isAbsoluteUrl(path)) {
|
||||
return path;
|
||||
}
|
||||
|
||||
const normalizedPath = ensureLeadingSlash(path);
|
||||
if (baseEndsWithApi(baseUrl)) {
|
||||
return stripLeadingApiPrefix(normalizedPath);
|
||||
}
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
function joinBaseAndPath(baseUrl: string, path: string): string {
|
||||
const cleanBase = stripTrailingSlashes(baseUrl);
|
||||
const cleanPath = path.replace(/^\/+/, "");
|
||||
return `${cleanBase}/${cleanPath}`;
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string | null | undefined): string {
|
||||
return stripTrailingSlashes(baseUrl?.trim() || "");
|
||||
}
|
||||
|
||||
export function normalizeConfiguredApiBaseUrl(
|
||||
baseUrl: string | null | undefined,
|
||||
): string {
|
||||
return normalizeBaseUrl(baseUrl);
|
||||
}
|
||||
|
||||
export function getApiBaseUrlValidationError(
|
||||
baseUrl: string | null | undefined,
|
||||
): string {
|
||||
const normalizedBaseUrl = normalizeConfiguredApiBaseUrl(baseUrl);
|
||||
|
||||
if (!normalizedBaseUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(normalizedBaseUrl);
|
||||
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
||||
return "API Base URL must use http:// or https://";
|
||||
}
|
||||
} catch {
|
||||
return "API Base URL must be a valid absolute URL";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
export function getApiBaseUrl(): string {
|
||||
return normalizeBaseUrl(service.defaults.baseURL);
|
||||
}
|
||||
|
||||
export function setApiBaseUrl(baseUrl: string | null | undefined): string {
|
||||
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
||||
service.defaults.baseURL = normalizedBaseUrl;
|
||||
return normalizedBaseUrl;
|
||||
}
|
||||
|
||||
export function resolveApiUrl(
|
||||
path: string,
|
||||
baseUrl: string | null | undefined = getApiBaseUrl(),
|
||||
): string {
|
||||
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
||||
const normalizedPath = normalizePathForBase(path, normalizedBaseUrl);
|
||||
|
||||
if (isAbsoluteUrl(normalizedPath)) {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
if (!normalizedBaseUrl) {
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
return joinBaseAndPath(normalizedBaseUrl, normalizedPath);
|
||||
}
|
||||
|
||||
export function resolvePublicUrl(path: string): string {
|
||||
const base = import.meta.env.BASE_URL || "/";
|
||||
const cleanBase = base.endsWith("/") ? base : `${base}/`;
|
||||
return new URL(
|
||||
path.replace(/^\/+/, ""),
|
||||
window.location.origin + cleanBase,
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function resolveWebSocketUrl(
|
||||
path: string,
|
||||
queryParams?: Record<string, string>,
|
||||
): string {
|
||||
const resolvedApiUrl = resolveApiUrl(path);
|
||||
const url = new URL(resolvedApiUrl, window.location.href);
|
||||
|
||||
if (url.protocol === "https:") {
|
||||
url.protocol = "wss:";
|
||||
} else if (url.protocol === "http:") {
|
||||
url.protocol = "ws:";
|
||||
}
|
||||
|
||||
if (queryParams) {
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
url.searchParams.set(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: normalizeBaseUrl(import.meta.env.VITE_API_BASE),
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
|
||||
const normalizedBaseUrl = normalizeBaseUrl(
|
||||
config.baseURL ?? service.defaults.baseURL,
|
||||
);
|
||||
|
||||
if (typeof config.url === "string") {
|
||||
config.url = normalizePathForBase(config.url, normalizedBaseUrl);
|
||||
}
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
if (token) {
|
||||
config.headers.set("Authorization", `Bearer ${token}`);
|
||||
}
|
||||
|
||||
const locale = localStorage.getItem("astrbot-locale");
|
||||
if (locale) {
|
||||
config.headers.set("Accept-Language", locale);
|
||||
}
|
||||
|
||||
return config;
|
||||
});
|
||||
|
||||
export default service;
|
||||
export * from "axios";
|
||||
@@ -94,7 +94,21 @@
|
||||
|
||||
<template v-slot:item.title="{ item }">
|
||||
<div class="conversation-title-cell">
|
||||
<span class="conversation-title-text">{{ item.title || tm('status.noTitle') }}</span>
|
||||
<div class="conversation-title-row">
|
||||
<span class="conversation-title-text">{{ item.title || tm('status.noTitle') }}</span>
|
||||
<v-btn
|
||||
icon
|
||||
variant="plain"
|
||||
size="x-small"
|
||||
density="compact"
|
||||
:ripple="false"
|
||||
class="conversation-inline-edit"
|
||||
@click.stop="editConversation(item)"
|
||||
:disabled="loading"
|
||||
>
|
||||
<v-icon size="14">mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<span class="conversation-title-meta">{{ item.cid || tm('status.unknown') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -141,10 +155,6 @@
|
||||
@click="viewConversation(item)" :disabled="loading">
|
||||
<v-icon>mdi-eye</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon variant="plain" size="x-small" class="action-button"
|
||||
@click="editConversation(item)" :disabled="loading">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-btn>
|
||||
<v-btn icon color="error" variant="plain" size="x-small" class="action-button"
|
||||
@click="confirmDeleteConversation(item)" :disabled="loading">
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
@@ -1188,14 +1198,29 @@ export default {
|
||||
max-width: 145px;
|
||||
}
|
||||
|
||||
.conversation-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.conversation-title-text {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.conversation-inline-edit {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
min-width: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conversation-title-meta {
|
||||
display: block;
|
||||
color: rgba(var(--v-theme-on-surface), 0.58);
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
</v-tabs>
|
||||
|
||||
<!-- Chat Completion: 左侧列表 + 右侧上下卡片布局 -->
|
||||
<div v-if="selectedProviderType === 'chat_completion'" class="d-flex align-center justify-center">
|
||||
<v-row style="max-width: 1500px; ">
|
||||
<v-col cols="12" md="4" lg="3" class="pr-md-4">
|
||||
<div v-if="selectedProviderType === 'chat_completion'" class="provider-workbench">
|
||||
<v-row class="provider-workbench__shell">
|
||||
<v-col cols="12" md="4" lg="3" class="provider-workbench__sources">
|
||||
<ProviderSourcesPanel
|
||||
:displayed-provider-sources="displayedProviderSources"
|
||||
:selected-provider-source="selectedProviderSource"
|
||||
@@ -45,66 +45,77 @@
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="8" lg="9">
|
||||
<v-card class="provider-config-card h-100" elevation="0" style="overflow-y: auto;">
|
||||
<v-card-title class="d-flex align-center justify-space-between flex-wrap ga-3 pt-4 pl-5">
|
||||
<div class="d-flex align-center ga-3" v-if="selectedProviderSource">
|
||||
<div>
|
||||
<div class="text-h4 font-weight-bold">{{ selectedProviderSource.id }}</div>
|
||||
<div class="text-caption text-medium-emphasis">{{ selectedProviderSource.api_base || 'N/A' }}
|
||||
</div>
|
||||
<v-col cols="12" md="8" lg="9" class="provider-workbench__settings">
|
||||
<v-card class="provider-config-card provider-settings-panel h-100" elevation="0">
|
||||
<div v-if="selectedProviderSource" class="provider-config-header">
|
||||
<div class="provider-config-headline">
|
||||
<div class="provider-config-kicker">{{ tm('providers.settings') }}</div>
|
||||
<div class="provider-config-title">{{ selectedProviderSource.id }}</div>
|
||||
<div class="provider-config-subtitle">
|
||||
{{ selectedProviderSource.api_base || 'N/A' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-center ga-2" v-if="selectedProviderSource">
|
||||
<v-btn color="success" prepend-icon="mdi-check" :loading="savingSource"
|
||||
:disabled="!isSourceModified" @click="saveProviderSource" variant="flat">
|
||||
<div class="provider-config-actions">
|
||||
<v-btn
|
||||
color="primary"
|
||||
prepend-icon="mdi-content-save-outline"
|
||||
:loading="savingSource"
|
||||
:disabled="!isSourceModified"
|
||||
@click="saveProviderSource"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ tm('providerSources.save') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-title>
|
||||
</div>
|
||||
|
||||
<v-card-text>
|
||||
<v-card-text class="provider-config-body">
|
||||
<template v-if="selectedProviderSource">
|
||||
<div>
|
||||
<section class="provider-section">
|
||||
<div class="provider-section-head">
|
||||
<div class="provider-section-title">{{ tm('providers.settings') }}</div>
|
||||
</div>
|
||||
<AstrBotConfig v-if="basicSourceConfig" :iterable="basicSourceConfig" :metadata="providerSourceSchema"
|
||||
metadataKey="provider" :is-editing="true" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<v-expansion-panels variant="accordion" class="mb-2">
|
||||
<v-expansion-panel elevation="0" class="border rounded-lg">
|
||||
<v-expansion-panel-title>
|
||||
<span class="font-weight-medium">{{ tm('providerSources.advancedConfig') }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<AstrBotConfig v-if="advancedSourceConfig" :iterable="advancedSourceConfig"
|
||||
:metadata="providerSourceSchema" metadataKey="provider" :is-editing="true" />
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<section v-if="advancedSourceConfig" class="provider-section">
|
||||
<div class="provider-section-head">
|
||||
<div class="provider-section-title">{{ tm('providerSources.advancedConfig') }}</div>
|
||||
</div>
|
||||
<AstrBotConfig
|
||||
:iterable="advancedSourceConfig"
|
||||
:metadata="providerSourceSchema"
|
||||
metadataKey="provider"
|
||||
:is-editing="true"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<ProviderModelsPanel
|
||||
:entries="filteredMergedModelEntries"
|
||||
:available-count="availableModels.length"
|
||||
v-model:model-search="modelSearch"
|
||||
:loading-models="loadingModels"
|
||||
:is-source-modified="isSourceModified"
|
||||
:supports-image-input="supportsImageInput"
|
||||
:supports-tool-call="supportsToolCall"
|
||||
:supports-reasoning="supportsReasoning"
|
||||
:format-context-limit="formatContextLimit"
|
||||
:testing-providers="testingProviders"
|
||||
:tm="tm"
|
||||
@fetch-models="fetchAvailableModels"
|
||||
@open-manual-model="openManualModelDialog"
|
||||
@open-provider-edit="openProviderEdit"
|
||||
@toggle-provider-enable="toggleProviderEnable"
|
||||
@test-provider="testProvider"
|
||||
@delete-provider="deleteProvider"
|
||||
@add-model-provider="addModelProvider"
|
||||
/>
|
||||
<section class="provider-section provider-section--models">
|
||||
<ProviderModelsPanel
|
||||
:entries="filteredMergedModelEntries"
|
||||
:available-count="availableModels.length"
|
||||
v-model:model-search="modelSearch"
|
||||
:loading-models="loadingModels"
|
||||
:is-source-modified="isSourceModified"
|
||||
:supports-image-input="supportsImageInput"
|
||||
:supports-tool-call="supportsToolCall"
|
||||
:supports-reasoning="supportsReasoning"
|
||||
:format-context-limit="formatContextLimit"
|
||||
:testing-providers="testingProviders"
|
||||
:tm="tm"
|
||||
@fetch-models="fetchAvailableModels"
|
||||
@open-manual-model="openManualModelDialog"
|
||||
@open-provider-edit="openProviderEdit"
|
||||
@toggle-provider-enable="toggleProviderEnable"
|
||||
@test-provider="testProvider"
|
||||
@delete-provider="deleteProvider"
|
||||
@add-model-provider="addModelProvider"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
<div v-else class="text-center py-8 text-medium-emphasis">
|
||||
<div v-else class="provider-empty-state">
|
||||
<v-icon size="48" color="grey-lighten-1">mdi-cursor-default-click</v-icon>
|
||||
<p class="mt-2">{{ tm('providerSources.selectHint') }}</p>
|
||||
</div>
|
||||
@@ -665,18 +676,141 @@ function goToConfigPage() {
|
||||
|
||||
<style scoped>
|
||||
.provider-page {
|
||||
--provider-surface: rgb(var(--v-theme-surface));
|
||||
--provider-text: rgb(var(--v-theme-on-surface));
|
||||
--provider-muted: rgba(var(--v-theme-on-surface), 0.68);
|
||||
--provider-subtle: rgba(var(--v-theme-on-surface), 0.56);
|
||||
--provider-border: rgba(var(--v-theme-on-surface), 0.1);
|
||||
--provider-border-strong: rgba(var(--v-theme-on-surface), 0.14);
|
||||
--provider-soft: rgba(var(--v-theme-primary), 0.08);
|
||||
padding: 20px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.provider-workbench {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.provider-workbench__shell {
|
||||
width: 100%;
|
||||
max-width: 1500px;
|
||||
}
|
||||
|
||||
.provider-workbench__sources,
|
||||
.provider-workbench__settings {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.provider-config-card {
|
||||
min-height: 280px;
|
||||
border: 1px solid var(--provider-border);
|
||||
border-radius: 16px;
|
||||
background: var(--provider-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.provider-settings-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.provider-config-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid var(--provider-border);
|
||||
}
|
||||
|
||||
.provider-config-headline {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.provider-config-kicker {
|
||||
color: var(--provider-subtle);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.provider-config-title {
|
||||
margin-top: 8px;
|
||||
font-size: 22px;
|
||||
line-height: 1.1;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.03em;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--provider-text);
|
||||
}
|
||||
|
||||
.provider-config-subtitle {
|
||||
margin-top: 8px;
|
||||
color: var(--provider-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.provider-config-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.provider-config-body {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 18px 20px 20px;
|
||||
}
|
||||
|
||||
.provider-section {
|
||||
border: 1px solid var(--provider-border);
|
||||
border-radius: 14px;
|
||||
background: rgba(var(--v-theme-primary), 0.02);
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.provider-section--models {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.provider-section-head {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.provider-section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
line-height: 1.4;
|
||||
color: var(--provider-text);
|
||||
}
|
||||
|
||||
.provider-empty-state {
|
||||
min-height: 420px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--provider-muted);
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.provider-config-card {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.provider-config-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.provider-config-body {
|
||||
padding: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,10 +22,15 @@ function toggleTheme() {
|
||||
theme.global.name.value = newTheme;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// 检查用户是否已登录,如果已登录则重定向
|
||||
if (authStore.has_token()) {
|
||||
router.push('/welcome');
|
||||
const onboardingCompleted = await authStore.checkOnboardingCompleted();
|
||||
if (onboardingCompleted) {
|
||||
router.push('/dashboard/default');
|
||||
} else {
|
||||
router.push('/welcome');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -27,14 +27,27 @@
|
||||
</div>
|
||||
|
||||
<div v-else-if="kbList.length > 0" class="kb-grid">
|
||||
<v-card v-for="kb in kbList" :key="kb.kb_id" class="kb-card" elevation="2" hover
|
||||
@click="navigateToDetail(kb.kb_id)">
|
||||
<div class="kb-card-content">
|
||||
<v-card v-for="kb in kbList" :key="kb.kb_id" class="kb-card" elevation="2" :hover="!kb.init_error"
|
||||
:class="{ 'kb-card-error': kb.init_error }"
|
||||
@click="!kb.init_error && navigateToDetail(kb.kb_id)">
|
||||
<!-- Error badge -->
|
||||
<v-badge v-if="kb.init_error" color="error" icon="mdi-alert-circle"
|
||||
class="kb-error-badge position-absolute" style="top: 0; right: 0; transform: translate(34%, -34%);" />
|
||||
<div class="kb-card-content" :class="{ 'kb-card-content-error': kb.init_error }">
|
||||
<div class="kb-emoji">{{ kb.emoji || '📚' }}</div>
|
||||
<h3 class="kb-name">{{ kb.kb_name }}</h3>
|
||||
<p class="kb-description text-medium-emphasis">{{ kb.description || '暂无描述' }}</p>
|
||||
<p v-if="!kb.init_error" class="kb-description text-medium-emphasis">{{ kb.description || '暂无描述' }}</p>
|
||||
|
||||
<div class="kb-stats mt-4">
|
||||
<!-- Error message display -->
|
||||
<div v-if="kb.init_error" class="kb-error-panel mt-3 mb-2">
|
||||
<div class="kb-error-title">
|
||||
<v-icon size="16" color="error">mdi-close-circle</v-icon>
|
||||
<span>{{ t('list.initError') }}</span>
|
||||
</div>
|
||||
<div class="kb-error-detail" :title="kb.init_error">{{ kb.init_error }}</div>
|
||||
</div>
|
||||
|
||||
<div class="kb-stats mt-4" v-if="!kb.init_error">
|
||||
<div class="stat-item">
|
||||
<v-icon size="small" color="primary">mdi-file-document</v-icon>
|
||||
<span>{{ kb.doc_count || 0 }} {{ t('list.documents') }}</span>
|
||||
@@ -45,8 +58,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kb-actions">
|
||||
<v-btn icon="mdi-pencil" size="small" variant="text" color="info" @click.stop="editKB(kb)" />
|
||||
<div class="kb-actions" :class="{ 'error-actions': kb.init_error }">
|
||||
<v-btn v-if="!kb.init_error" icon="mdi-pencil" size="small" variant="text" color="info" @click.stop="editKB(kb)" />
|
||||
<v-btn icon="mdi-delete" size="small" variant="text" color="error" @click.stop="confirmDelete(kb)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -478,6 +491,34 @@ onMounted(() => {
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
/* Error state card styles */
|
||||
.kb-card-error {
|
||||
cursor: not-allowed;
|
||||
border: 1px solid rgba(var(--v-theme-error), 0.3);
|
||||
background-color: rgba(var(--v-theme-error), 0.02) !important;
|
||||
overflow: visible; /* Allow badge to overflow */
|
||||
}
|
||||
|
||||
.kb-card-error:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 4px 12px rgba(var(--v-theme-error), 0.1) !important;
|
||||
border-color: rgba(var(--v-theme-error), 0.5);
|
||||
}
|
||||
|
||||
.kb-card-error .kb-emoji {
|
||||
opacity: 0.7;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
.kb-card-error .kb-name {
|
||||
color: rgba(var(--v-theme-on-surface), 0.7);
|
||||
}
|
||||
|
||||
.kb-error-badge {
|
||||
z-index: 10;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.kb-card-content {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
@@ -488,6 +529,11 @@ onMounted(() => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.kb-card-content-error {
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kb-emoji {
|
||||
font-size: 56px;
|
||||
margin-bottom: 8px;
|
||||
@@ -518,6 +564,36 @@ onMounted(() => {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.kb-error-panel {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: rgba(var(--v-theme-error), 0.08);
|
||||
border: 1px solid rgba(var(--v-theme-error), 0.18);
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.kb-error-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: rgb(var(--v-theme-error));
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.kb-error-detail {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
color: rgba(var(--v-theme-on-surface), 0.82);
|
||||
word-break: break-word;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { fileURLToPath, URL } from 'url';
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import vuetify from 'vite-plugin-vuetify';
|
||||
import webfontDl from 'vite-plugin-webfont-dl';
|
||||
import { fileURLToPath, URL } from "url";
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import vuetify from "vite-plugin-vuetify";
|
||||
import webfontDl from "vite-plugin-webfont-dl";
|
||||
// @ts-ignore — .mjs not in TS project scope; Vite resolves this at runtime
|
||||
import { runMdiSubset } from './scripts/subset-mdi-font.mjs';
|
||||
import { runMdiSubset } from "./scripts/subset-mdi-font.mjs";
|
||||
|
||||
// Vite plugin: run MDI icon font subsetting (build only)
|
||||
function mdiSubset() {
|
||||
return {
|
||||
name: 'vite-plugin-mdi-subset',
|
||||
name: "vite-plugin-mdi-subset",
|
||||
async buildStart() {
|
||||
console.log('\n🔧 Running MDI icon font subsetting...');
|
||||
console.log("\n🔧 Running MDI icon font subsetting...");
|
||||
await runMdiSubset();
|
||||
},
|
||||
};
|
||||
@@ -21,47 +21,50 @@ function mdiSubset() {
|
||||
export default defineConfig(({ command }) => ({
|
||||
plugins: [
|
||||
// Only run MDI subsetting during production builds, skip in dev server
|
||||
...(command === 'build' ? [mdiSubset()] : []),
|
||||
...(command === "build" ? [mdiSubset()] : []),
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions: {
|
||||
isCustomElement: (tag) => ['v-list-recognize-title'].includes(tag)
|
||||
}
|
||||
}
|
||||
isCustomElement: (tag) => ["v-list-recognize-title"].includes(tag),
|
||||
},
|
||||
},
|
||||
}),
|
||||
vuetify({
|
||||
autoImport: true
|
||||
autoImport: true,
|
||||
}),
|
||||
webfontDl()
|
||||
webfontDl(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
mermaid: 'mermaid/dist/mermaid.js',
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
mermaid: "mermaid/dist/mermaid.js",
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {}
|
||||
}
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
silenceDeprecations: ["import", "global-builtin"],
|
||||
},
|
||||
},
|
||||
},
|
||||
build: {
|
||||
sourcemap: false,
|
||||
chunkSizeWarningLimit: 1024 * 1024 // Set the limit to 1 MB
|
||||
chunkSizeWarningLimit: 1024 * 1024, // Set the limit to 1 MB
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['vuetify'],
|
||||
entries: ['./src/**/*.vue']
|
||||
exclude: ["vuetify"],
|
||||
entries: ["./src/**/*.vue"],
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
host: "0.0.0.0",
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:6185/',
|
||||
"/api": {
|
||||
target: "http://127.0.0.1:6185/",
|
||||
changeOrigin: true,
|
||||
ws: true
|
||||
}
|
||||
}
|
||||
}
|
||||
ws: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -87,4 +87,4 @@
|
||||
|
||||
## 🎉 大功告成
|
||||
|
||||
此时,你的 AstrBot 和 NapCatQQ 应该已经连接成功。使用 `私聊` 的方式在 QQ 对机器人发送 `/help` 以检查是否连接成功。
|
||||
此时,你的 AstrBot 应该已经成功连接 QQ 官方接口。使用 `私聊` 的方式在 QQ 对机器人发送 `/help` 以检查是否连接成功。
|
||||
|
||||
@@ -66,6 +66,7 @@ dependencies = [
|
||||
"shipyard-python-sdk>=0.2.4",
|
||||
"shipyard-neo-sdk>=0.2.0",
|
||||
"python-socks>=2.8.0",
|
||||
"pysocks>=1.7.1",
|
||||
"packaging>=24.2",
|
||||
]
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ pydub>=0.25.1
|
||||
pyjwt>=2.10.1
|
||||
python-telegram-bot>=22.6
|
||||
qq-botpy>=1.2.1
|
||||
python-socks>=2.8.0
|
||||
quart>=0.20.0
|
||||
readability-lxml>=0.8.4.1
|
||||
silk-python>=0.2.6
|
||||
@@ -45,6 +46,7 @@ wechatpy>=1.8.18
|
||||
audioop-lts ; python_full_version >= '3.13'
|
||||
click>=8.2.1
|
||||
pypdf>=6.1.1
|
||||
pysocks>=1.7.1
|
||||
aiofiles>=25.1.0
|
||||
rank-bm25>=0.2.2
|
||||
jieba>=0.42.1
|
||||
|
||||
@@ -1173,3 +1173,360 @@ async def test_parse_openai_completion_raises_empty_model_output_error():
|
||||
await provider._parse_openai_completion(completion, tools=None)
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_filters_empty_assistant_message_without_tool_calls(monkeypatch):
|
||||
"""Test that empty assistant messages without tool_calls are filtered out."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": ""}, # Should be filtered
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# The empty assistant message should be filtered out
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 2
|
||||
assert messages[0] == {"role": "user", "content": "hello"}
|
||||
assert messages[1] == {"role": "user", "content": "world"}
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_filters_null_content_assistant_message_without_tool_calls(
|
||||
monkeypatch,
|
||||
):
|
||||
"""Test that assistant messages with null content and no tool_calls are filtered."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": None}, # Should be filtered
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# The null content assistant message should be filtered out
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 2
|
||||
assert messages[0] == {"role": "user", "content": "hello"}
|
||||
assert messages[1] == {"role": "user", "content": "world"}
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_converts_empty_content_to_none_with_tool_calls(monkeypatch):
|
||||
"""Test that empty content with tool_calls is converted to None (OpenAI spec)."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call-123",
|
||||
"type": "function",
|
||||
"function": {"name": "test", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# The assistant message with tool_calls should be kept but content set to None
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 3
|
||||
assert messages[1]["role"] == "assistant"
|
||||
assert messages[1]["content"] is None
|
||||
assert messages[1]["tool_calls"] is not None
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_keeps_valid_assistant_message_with_content(monkeypatch):
|
||||
"""Test that valid assistant messages with content are kept."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "response"},
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# All messages should be kept
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 3
|
||||
assert messages[1] == {"role": "assistant", "content": "response"}
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_keeps_assistant_message_with_tool_calls_and_none_content(
|
||||
monkeypatch,
|
||||
):
|
||||
"""Test that assistant messages with tool_calls and None content are kept."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call-123",
|
||||
"type": "function",
|
||||
"function": {"name": "test", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": "world"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# The assistant message with tool_calls should be kept
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 3
|
||||
assert messages[1]["role"] == "assistant"
|
||||
assert messages[1]["content"] is None
|
||||
assert messages[1]["tool_calls"] is not None
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_does_not_filter_user_or_system_messages(monkeypatch):
|
||||
"""Test that user and system messages are not affected by the filter."""
|
||||
provider = _make_provider()
|
||||
try:
|
||||
captured_kwargs = {}
|
||||
|
||||
async def fake_create(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return ChatCompletion.model_validate(
|
||||
{
|
||||
"id": "chatcmpl-test",
|
||||
"object": "chat.completion",
|
||||
"created": 0,
|
||||
"model": "gpt-4o-mini",
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "ok",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 1,
|
||||
"completion_tokens": 1,
|
||||
"total_tokens": 2,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
monkeypatch.setattr(provider.client.chat.completions, "create", fake_create)
|
||||
|
||||
payloads = {
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": [
|
||||
{"role": "system", "content": ""}, # Empty system message
|
||||
{"role": "user", "content": ""}, # Empty user message
|
||||
{"role": "assistant", "content": ""}, # Should be filtered
|
||||
{"role": "user", "content": "hello"},
|
||||
],
|
||||
}
|
||||
|
||||
await provider._query(payloads=payloads, tools=None)
|
||||
|
||||
# Only assistant message should be filtered
|
||||
messages = captured_kwargs["messages"]
|
||||
assert len(messages) == 3
|
||||
assert messages[0] == {"role": "system", "content": ""}
|
||||
assert messages[1] == {"role": "user", "content": ""}
|
||||
assert messages[2] == {"role": "user", "content": "hello"}
|
||||
finally:
|
||||
await provider.terminate()
|
||||
|
||||
305
tests/unit/test_kb_manager_resilience.py
Normal file
305
tests/unit/test_kb_manager_resilience.py
Normal file
@@ -0,0 +1,305 @@
|
||||
"""
|
||||
Unit tests for knowledge base manager resilience behavior.
|
||||
|
||||
Tests the following scenarios:
|
||||
1. update_kb preserves old instance when re-initialization fails
|
||||
2. update_kb switches instance only after new instance initializes successfully
|
||||
3. _ensure_vec_db clears stale init_error after successful initialization
|
||||
|
||||
These tests use lazy imports and mocks to avoid circular import issues
|
||||
in the astrbot core module chain.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import types
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stub_provider_manager_module():
|
||||
"""Stub provider manager module to avoid circular imports in unit tests."""
|
||||
original_module = sys.modules.get("astrbot.core.provider.manager")
|
||||
stub_module = types.ModuleType("astrbot.core.provider.manager")
|
||||
|
||||
class ProviderManager: ...
|
||||
|
||||
setattr(stub_module, "ProviderManager", ProviderManager)
|
||||
sys.modules["astrbot.core.provider.manager"] = stub_module
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if original_module is not None:
|
||||
sys.modules["astrbot.core.provider.manager"] = original_module
|
||||
else:
|
||||
sys.modules.pop("astrbot.core.provider.manager", None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_provider_manager():
|
||||
"""Create a mock ProviderManager."""
|
||||
manager = MagicMock()
|
||||
manager.get_provider_by_id = AsyncMock()
|
||||
manager.acm = MagicMock()
|
||||
manager.acm.default_conf = {}
|
||||
return manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_kb_db():
|
||||
"""Create a mock KBSQLiteDatabase."""
|
||||
db = MagicMock()
|
||||
db.get_db = MagicMock()
|
||||
db.list_kbs = AsyncMock(return_value=[])
|
||||
db.get_kb_by_id = AsyncMock()
|
||||
return db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_knowledge_base():
|
||||
"""Create a mock KnowledgeBase instance."""
|
||||
# Use lazy import to avoid circular import
|
||||
from astrbot.core.knowledge_base.models import KnowledgeBase
|
||||
|
||||
kb = KnowledgeBase(
|
||||
kb_name="test_kb",
|
||||
description="Test knowledge base",
|
||||
emoji="📚",
|
||||
embedding_provider_id="test-embedding-provider",
|
||||
rerank_provider_id=None,
|
||||
chunk_size=512,
|
||||
chunk_overlap=50,
|
||||
top_k_dense=50,
|
||||
top_k_sparse=50,
|
||||
top_m_final=5,
|
||||
)
|
||||
return kb
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_embedding_provider():
|
||||
"""Create a mock EmbeddingProvider."""
|
||||
provider = MagicMock()
|
||||
provider.get_embeddings_batch = AsyncMock(return_value=[[0.1, 0.2, 0.3]])
|
||||
return provider
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_kb_preserves_old_instance_when_reinit_fails(
|
||||
stub_provider_manager_module,
|
||||
mock_provider_manager,
|
||||
mock_kb_db,
|
||||
mock_knowledge_base,
|
||||
mock_embedding_provider,
|
||||
):
|
||||
"""
|
||||
Test that update_kb preserves the old KBHelper instance when
|
||||
re-initialization fails, ensuring the knowledge base remains available.
|
||||
"""
|
||||
# Lazy import to avoid circular import
|
||||
from astrbot.core.knowledge_base.kb_helper import KBHelper
|
||||
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
|
||||
|
||||
# Setup: create an existing KBHelper with working vec_db
|
||||
mock_provider_manager.get_provider_by_id.return_value = mock_embedding_provider
|
||||
|
||||
# Create KBHelper using __new__ to avoid __init__ side effects
|
||||
old_helper = KBHelper.__new__(KBHelper)
|
||||
old_helper.kb = mock_knowledge_base
|
||||
old_helper.prov_mgr = mock_provider_manager
|
||||
old_helper.kb_db = mock_kb_db
|
||||
old_helper.kb_root_dir = "/tmp/test_kb"
|
||||
old_helper.chunker = MagicMock()
|
||||
old_helper.init_error = None
|
||||
old_helper.vec_db = MagicMock() # Simulate existing working vec_db
|
||||
old_helper.terminate = AsyncMock()
|
||||
|
||||
# Create KBManager and inject the existing helper
|
||||
kb_mgr = KnowledgeBaseManager.__new__(KnowledgeBaseManager)
|
||||
kb_mgr.provider_manager = mock_provider_manager
|
||||
kb_mgr.kb_db = mock_kb_db
|
||||
kb_mgr.kb_insts = {mock_knowledge_base.kb_id: old_helper}
|
||||
kb_mgr.retrieval_manager = MagicMock()
|
||||
|
||||
# Mock KBHelper creation to simulate initialization failure
|
||||
with patch.object(KBHelper, "initialize", new_callable=AsyncMock) as mock_init:
|
||||
# First call (for new_helper) should fail
|
||||
mock_init.side_effect = Exception("Embedding provider unavailable")
|
||||
|
||||
# Execute update_kb with a different embedding provider
|
||||
result = await kb_mgr.update_kb(
|
||||
kb_id=mock_knowledge_base.kb_id,
|
||||
kb_name="updated_kb",
|
||||
embedding_provider_id="new-embedding-provider",
|
||||
)
|
||||
|
||||
# Verify: the old helper should be returned, not a new one
|
||||
assert result is not None
|
||||
assert result is old_helper
|
||||
assert kb_mgr.kb_insts[mock_knowledge_base.kb_id] is old_helper
|
||||
|
||||
# Verify: old helper's vec_db should still be available
|
||||
assert hasattr(result, "vec_db")
|
||||
assert result.vec_db is not None
|
||||
|
||||
# Verify: failure does not replace the existing helper state
|
||||
assert result.init_error is None
|
||||
assert result.kb.kb_name == "test_kb"
|
||||
assert result.kb.embedding_provider_id == "test-embedding-provider"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_kb_switches_instance_only_after_new_reinit_success(
|
||||
stub_provider_manager_module,
|
||||
mock_provider_manager,
|
||||
mock_kb_db,
|
||||
mock_knowledge_base,
|
||||
mock_embedding_provider,
|
||||
):
|
||||
"""
|
||||
Test that update_kb only switches to the new KBHelper instance
|
||||
after the new instance successfully initializes.
|
||||
"""
|
||||
# Lazy import to avoid circular import
|
||||
from astrbot.core.knowledge_base.kb_helper import KBHelper
|
||||
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
|
||||
|
||||
# Setup: create an existing KBHelper
|
||||
mock_provider_manager.get_provider_by_id.return_value = mock_embedding_provider
|
||||
|
||||
old_helper = KBHelper.__new__(KBHelper)
|
||||
old_helper.kb = mock_knowledge_base
|
||||
old_helper.prov_mgr = mock_provider_manager
|
||||
old_helper.kb_db = mock_kb_db
|
||||
old_helper.kb_root_dir = "/tmp/test_kb"
|
||||
old_helper.chunker = MagicMock()
|
||||
old_helper.init_error = None
|
||||
old_helper.vec_db = MagicMock()
|
||||
old_helper.terminate = AsyncMock()
|
||||
|
||||
kb_mgr = KnowledgeBaseManager.__new__(KnowledgeBaseManager)
|
||||
kb_mgr.provider_manager = mock_provider_manager
|
||||
kb_mgr.kb_db = mock_kb_db
|
||||
kb_mgr.kb_insts = {mock_knowledge_base.kb_id: old_helper}
|
||||
kb_mgr.retrieval_manager = MagicMock()
|
||||
|
||||
# Mock session context for database operations
|
||||
mock_session = MagicMock()
|
||||
mock_session.add = MagicMock()
|
||||
mock_session.commit = AsyncMock()
|
||||
mock_session.refresh = AsyncMock()
|
||||
|
||||
mock_db_context = MagicMock()
|
||||
mock_db_context.__aenter__ = AsyncMock(return_value=mock_session)
|
||||
mock_db_context.__aexit__ = AsyncMock()
|
||||
mock_kb_db.get_db.return_value = mock_db_context
|
||||
|
||||
# Mock KBHelper.initialize to succeed
|
||||
with patch.object(KBHelper, "initialize", new_callable=AsyncMock) as mock_init:
|
||||
mock_init.return_value = None
|
||||
|
||||
# Execute update_kb
|
||||
result = await kb_mgr.update_kb(
|
||||
kb_id=mock_knowledge_base.kb_id,
|
||||
kb_name="updated_kb",
|
||||
embedding_provider_id="new-embedding-provider",
|
||||
)
|
||||
|
||||
# Verify: a new helper should be returned
|
||||
assert result is not None
|
||||
assert result is not old_helper
|
||||
assert result.init_error is None
|
||||
assert kb_mgr.kb_insts[mock_knowledge_base.kb_id] is result
|
||||
|
||||
# Verify: old helper should be terminated
|
||||
old_helper.terminate.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_vec_db_clears_stale_init_error(
|
||||
stub_provider_manager_module,
|
||||
mock_provider_manager,
|
||||
mock_kb_db,
|
||||
mock_knowledge_base,
|
||||
mock_embedding_provider,
|
||||
):
|
||||
"""
|
||||
Test that _ensure_vec_db clears the init_error attribute
|
||||
after successful initialization, removing stale error state.
|
||||
"""
|
||||
# Lazy import to avoid circular import
|
||||
from astrbot.core.knowledge_base.kb_helper import KBHelper
|
||||
|
||||
# Setup: create KBHelper with stale init_error
|
||||
mock_provider_manager.get_provider_by_id.return_value = mock_embedding_provider
|
||||
|
||||
helper = KBHelper.__new__(KBHelper)
|
||||
helper.kb = mock_knowledge_base
|
||||
helper.prov_mgr = mock_provider_manager
|
||||
helper.kb_db = mock_kb_db
|
||||
helper.kb_root_dir = "/tmp/test_kb"
|
||||
helper.chunker = MagicMock()
|
||||
helper.init_error = "Previous initialization failed"
|
||||
helper.kb_dir = Path("/tmp/test_kb") / mock_knowledge_base.kb_id
|
||||
helper.kb_medias_dir = helper.kb_dir / "medias" / mock_knowledge_base.kb_id
|
||||
helper.kb_files_dir = helper.kb_dir / "files" / mock_knowledge_base.kb_id
|
||||
|
||||
# Mock FaissVecDB initialization
|
||||
mock_vec_db = MagicMock()
|
||||
mock_vec_db.initialize = AsyncMock()
|
||||
mock_vec_db.close = AsyncMock()
|
||||
|
||||
with patch(
|
||||
"astrbot.core.knowledge_base.kb_helper.FaissVecDB",
|
||||
return_value=mock_vec_db,
|
||||
):
|
||||
# Execute _ensure_vec_db
|
||||
await helper._ensure_vec_db()
|
||||
|
||||
# Verify: init_error should be cleared
|
||||
assert helper.init_error is None
|
||||
assert helper.vec_db is mock_vec_db
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ensure_vec_db_sets_init_error_on_failure(
|
||||
stub_provider_manager_module,
|
||||
mock_provider_manager,
|
||||
mock_kb_db,
|
||||
mock_knowledge_base,
|
||||
):
|
||||
"""
|
||||
Test that _ensure_vec_db does NOT clear init_error when
|
||||
initialization fails, preserving the error state.
|
||||
"""
|
||||
# Lazy import to avoid circular import
|
||||
from astrbot.core.knowledge_base.kb_helper import KBHelper
|
||||
|
||||
# Setup: provider unavailable
|
||||
mock_provider_manager.get_provider_by_id.return_value = None
|
||||
|
||||
helper = KBHelper.__new__(KBHelper)
|
||||
helper.kb = mock_knowledge_base
|
||||
helper.prov_mgr = mock_provider_manager
|
||||
helper.kb_db = mock_kb_db
|
||||
helper.kb_root_dir = "/tmp/test_kb"
|
||||
helper.chunker = MagicMock()
|
||||
helper.init_error = "Previous initialization failed"
|
||||
helper.kb_dir = Path("/tmp/test_kb") / mock_knowledge_base.kb_id
|
||||
helper.kb_medias_dir = helper.kb_dir / "medias" / mock_knowledge_base.kb_id
|
||||
helper.kb_files_dir = helper.kb_dir / "files" / mock_knowledge_base.kb_id
|
||||
|
||||
# Execute _ensure_vec_db - should raise exception
|
||||
try:
|
||||
await helper._ensure_vec_db()
|
||||
pytest.fail("Expected exception but none was raised")
|
||||
except ValueError as e:
|
||||
# Verify: exception should be raised
|
||||
assert "无法找到" in str(e) or "未配置" in str(e)
|
||||
|
||||
# Verify: init_error should NOT be cleared (still has previous error)
|
||||
# Note: _ensure_vec_db doesn't set init_error; that's done by the caller
|
||||
assert helper.init_error is not None
|
||||
Reference in New Issue
Block a user