Merge origin/develop_web into feature/20251219_yjp

- Resolved conflict in web/src/components/RbModal/index.tsx
- Combined className and maskClosable properties
This commit is contained in:
yujiangping
2025-12-22 17:34:53 +08:00
150 changed files with 588251 additions and 1653 deletions

9
.gitignore vendored
View File

@@ -5,7 +5,7 @@ build/
dist/
wheels/
logs/
res/
api/res/
*.egg-info
# Virtual environments
@@ -16,17 +16,16 @@ examples/
# Environment variables
.env
.kiro
.vscode/settings.json
.vscode
.idea
# Temporary outputs
app/core/memory/agent/.DS_Store
app/core/memory/src/utils/.DS_Store
.DS_Store
time.log
celerybeat-schedule.db
search_results.json
migrations/versions
api/migrations/versions
tmp
files

301
README.md
View File

@@ -1,184 +1,215 @@
# MemoryBear 让AI拥有如同人类一样的记忆
### [安装教程](#memorybear安装教程)
## 项目简介
MemoryBear是红熊AI自主研发的新一代AI记忆系统其核心突破在于跳出传统知识“静态存储”的局限以生物大脑认知机制为原型构建了具备“感知-提炼-关联-遗忘”全生命周期的智能知识处理体系。该系统致力于让机器摆脱“信息堆砌”的困境,实现对知识的深度理解与自主进化,成为人类认知协作的核心伙伴。
<img width="2346" height="1310" alt="image" src="https://github.com/user-attachments/assets/bc73a64d-cd1e-4d22-be3e-04ce40423a20" />
## MemoryBear是从解决这些问题来的
### 一、单模型知识遗忘的核心原因</br>
上下文窗口限制:主流大模型上下文窗口通常为 8k-32k tokens长对话中早期信息会被 “挤出”,导致后续回复脱离历史语境:如用户第 1 轮说 “我对海鲜过敏”,第 5 轮问 “推荐今晚的菜品” 时模型可能遗忘过敏信息。</br>
静态知识库与动态数据割裂:大模型训练时的静态知识库如截止 2023 年数据,无法实时吸收用户对话中的个性化信息如用户偏好、历史订单,需依赖外部记忆模块补充。</br>
模型注意力机制缺陷Transformer 的自注意力对长距离依赖的捕捉能力随序列长度下降,出现 “近因效应”更关注最新输入,忽略早期关键信息。</br>
# MemoryBear empowers AI with human-like memory capabilities
### 二、多 Agent 协作的记忆断层问题</br>
Agent 数据孤岛:不同 Agent如咨询 Agent、售后 Agent、推荐 Agent各自维护独立记忆未建立跨模块的共享机制导致用户重复提供信息如用户向咨询 Agent 说明地址后,售后 Agent 仍需再次询问。</br>
对话状态不一致:多轮交互中 Agent 切换时,对话状态如用户当前意图、历史问题标签传递不完整,引发服务断层如用户从 “产品咨询” 转 “投诉” 时,新 Agent 未继承前期投诉细节。</br>
决策冲突:不同 Agent 基于局部记忆做出的响应可能矛盾如推荐 Agent 推荐用户过敏的产品,因未获取健康禁忌的历史记录。</br>
[中文](./README_CN.md) | English
### 三、模型推理过程中的 “语义歧义” 引发理解偏差</br>
用户对话中的个性化信息如行业术语、口语化表达、上下文指代未被准确编码,导致模型对记忆内容的语义解析失真,比如对用户历史对话中的模糊表述如 “上次说的那个方案”无法准确定位具体内容。</br>
多语言、方言场景中,跨语种记忆关联失效如用户混用中英描述需求时,模型无法整合多语言信息。</br>
典型案例:用户说之前客服说可以‘加急处理’现在进度如何?模型因未记录 “加急” 对应的具体服务等级,回复笼统模糊。</br>
### [Installation Guide](#memorybear-installation-guide)
### Paper: <a href="https://memorybear.ai/pdf/memoryBear" target="_blank" rel="noopener noreferrer">《Memory Bear AI: A Breakthrough from Memory to Cognition》</a>
## Project Overview
MemoryBear is a next-generation AI memory system independently developed by RedBear AI. Its core breakthrough lies in moving beyond the limitations of traditional "static knowledge storage". Inspired by the cognitive mechanisms of biological brains, MemoryBear builds an intelligent knowledge-processing framework that spans the full lifecycle of perception, refinement, association, and forgetting.The system is designed to free machines from the trap of mere "information accumulation", enabling deep knowledge understanding, autonomous evolution, and ultimately becoming a key partner in human-AI cognitive collaboration.
## MemoryBear核心定位
与传统记忆管理工具将知识视为“待检索的静态数据”不同MemoryBear以“模拟人类大脑知识处理逻辑”为核心目标构建了从知识摄入到智能输出的闭环体系。系统通过复刻大脑海马体的记忆编码、新皮层的知识固化及突触修剪的遗忘机制让知识具备动态演化的“生命特征”彻底重构了知识与使用者之间的交互关系——从“被动查询”升级为“主动辅助记忆认知”
## MemoryBear was created to address these challenges
### 1. Core causes of knowledge forgetting in single models</br>
Context window limitations: Mainstream large language models typically have context windows of 8k-32k tokens. In long conversations, earlier messages are pushed out of the window, causing later responses to lose their historical context.For example, a user says in turn 1, "I'm allergic to seafood", but by turn 5 when they ask, "What should I have for dinner tonight?" the model may have already forgotten the allergy information.</br>
## MemoryBear核心哲学
MemoryBear的设计哲学源于对人类认知本质的深刻洞察知识的价值不在于存量积累而在于动态流转中的价值升华。传统系统中知识一旦存储便陷入“静止状态”难以形成跨领域关联更无法主动适配使用者的认知需求而MemoryBear坚信只有让知识经历“原始信息提炼为结构化规则、孤立规则关联为知识网络、冗余信息智能遗忘”的完整过程才能实现从“信息记忆”到“认知理解”的跨越最终涌现出真正的智能。
Gap between static knowledge bases and dynamic data: The model's training corpus is a static snapshot (e.g., data up to 2023) and cannot continuously absorb personalized information from user interactions, such as preferences or order history. External memory modules are required to supplement and maintain this dynamic, user-specific knowledge.</br>
## MemoryBear核心特性
MemoryBear作为模仿生物大脑认知过程的智能记忆管理系统其核心特性围绕“记忆知识全生命周期管理”与“智能认知进化”两大维度构建覆盖记忆从摄入提炼到存储检索、动态优化的完整链路同时通过标准化服务架构实现高效集成与调用。
Limitations of the attention mechanism: In Transformer architectures, self-attention becomes less effective at capturing long-range dependencies as the sequence grows. This leads to a recency bias, where the model overweights the latest input and ignores crucial information that appeared earlier in the conversation.</br>
### 一、记忆萃取引擎:多维度结构化提炼,夯实认知基础</br>
记忆萃取是MemoryBear实现“认知化管理”的起点区别于传统数据提取的“机械转换”其核心优势在于对非结构化信息的“语义级解析”与“多格式标准化输出”精准适配后续图谱构建与智能检索需求。具体能力包括</br>
多类型信息精准解析:可自动识别并提取文本中的陈述句核心信息,剥离冗余修饰成分,保留“主体-行为-对象”核心逻辑同时精准抽取三元组数据如“MemoryBear-核心功能-知识萃取”),为图谱存储提供基础数据单元,保障知识关联的准确性。</br>
时序信息锚定:针对含有时效性的知识(如事件记录、政策文件、实验数据),自动提取并标记时间戳信息,支持“时间维度”的知识追溯与关联,解决传统知识管理中“时序混乱”导致的认知偏差问题。</br>
智能剪枝生成:基于上下文语义理解,生成“关键信息全覆盖+逻辑连贯性强”的摘要内容支持自定义摘要长度50-500字与侧重点如技术型、业务型适配不同场景的知识快速获取需求。例如对10页技术文档处理时可在3秒内生成含核心参数、实现逻辑与应用场景的精简摘要。</br>
### 2. Memory gaps in multi-agent collaboration</br>
Data silos between agents: Different agents-such as a consulting agent, after-sales agent, and recommendation agent-often maintain their own isolated memories without a shared layer. As a result, users have to repeat information. For instance, after providing their address to the consulting agent, the user may be asked for it again by the after-sales agent.</br>
### 二、图谱存储对接Neo4j构建可视化知识网络</br>
存储层采用“图数据库优先”的架构设计通过对接业界成熟的Neo4j图数据库实现知识实体与关系的高效管理突破传统关系型数据库“关联弱、查询繁”的局限契合生物大脑“神经元关联”的认知模式。</br>
该特性核心价值体现在一是支持海量实体与多元关系的灵活存储可管理百万级知识实体及千万级关联关系涵盖“上下位、因果、时序、逻辑”等12种核心关系类型适配多领域知识场景二是与知识萃取模块深度联动萃取的三元组数据可直接同步至Neo4j自动构建初始知识图谱无需人工二次映射三是支持图谱可视化交互用户可直观查看实体关联路径手动调整关系权重实现“机器构建+人工优化”的协同管理。</br>
Inconsistent dialogue state: When switching between agents in multi-turn interactions, key dialogue state-such as the user's current intent or past issue labels-may not be passed along completely. This causes service discontinuities. For example,a user transitions from "product inquiry" to "complaint", but the new agent does not inherit the complaint details discussed earlier.</br>
### 三、混合搜索:关键词+语义向量,兼顾精准与智能</br>
为解决传统搜索“要么精准但僵化要么模糊但失准”的痛点MemoryBear采用“关键词检索+语义向量检索”的混合搜索架构,实现“精准匹配”与“意图理解”的双重目标。</br>
其中关键词检索基于Lucene引擎优化针对知识中的核心实体、关键参数等结构化信息实现毫秒级精准定位保障“明确需求”下的高效检索语义向量检索则通过BERT模型对查询语句进行语义编码将其转化为高维向量后与知识库中的向量数据比对可识别同义词、近义词及隐含意图例如用户查询“如何优化记忆衰减效率”时系统可关联到“遗忘机制参数调整”“记忆强度评估方法”等相关知识。两种检索方式智能融合先通过语义检索扩大候选范围再通过关键词检索精准筛选使检索准确率提升至92%较单一检索方式平均提升35%。</br>
Conflicting decisions: Agents that only see partial memory can generate contradictory responses. For example, a recommendation agent might suggest products that the user is allergic to, simply because it does not have access to the user's recorded health constraints.</br>
### 四、记忆遗忘引擎:基于强度与时效的动态衰减,模拟生物记忆特性</br>
遗忘是MemoryBear区别于传统静态知识管理工具的核心特性之一其灵感源于生物大脑“突触修剪”机制通过“记忆强度+时效”双维度模型实现知识的逐步衰减,避免冗余知识占用资源,保障核心知识的“认知优先级”。</br>
具体实现逻辑为:系统为每条知识分配“初始记忆强度”(由萃取质量、人工标注重要性决定),并结合“调用频率、关联活跃度”实时更新强度值;同时设定“时效衰减周期”,根据知识类型(如核心规则、临时数据)差异化配置衰减速率。当知识强度低于阈值且超过设定时效后,将进入“休眠-衰减-清除”三阶段流程休眠阶段保留数据但降低检索优先级衰减阶段逐步压缩存储体积清除阶段则彻底删除并备份至冷存储。该机制使系统冗余知识占比控制在8%以内较传统无遗忘机制系统降低60%以上。</br>
### 3. Semantic ambiguity during model reasoning distorted understanding of personalized context</br>
Personalized signals in user conversations-such as domain-specific jargon, colloquial expressions, or context-dependent references-are often not encoded accurately, leading to semantic drift in how the model interprets memory. For instance, when the user refers to "that plan we discussed last time", the model may be unable to reliably locate the specific plan in previous conversations. Broken cross-lingual and dialect memory links in multilingual or dialect-rich scenarios, cross-language associations in memory may fail. When a user mixes Chinese and English in their requests, the model may struggle to integrate information expressed across languages.</br>
### 五、自我反思引擎:定期回顾优化,实现记忆自主进化</br>
自我反思机制是MemoryBear实现“智能升级”的关键通过定期对已有记忆进行回顾、校验与优化模拟人类“复盘总结”的认知行为持续提升知识体系的准确性与有效性。</br>
系统默认每日凌晨触发自动反思流程,核心动作包括:一是“一致性校验”,对比关联知识间的逻辑冲突(如同一实体的矛盾属性),标记可疑知识并推送人工审核;二是“价值评估”,统计知识的调用频次、关联贡献度,将高价值知识强化记忆强度,低价值知识加速衰减;三是“关联优化”,基于近期检索与使用行为,调整知识间的关联权重,强化高频关联路径。此外,支持人工触发专项反思(如新增核心知识后),并提供反思报告可视化展示优化结果,实现“自主进化+人工监督”的双重保障。</br>
Typical example: A user says: "Last time customer support told me it could be processed 'as an urgent case'. What's the status now?" If the system never encoded what "urgent" corresponds to in terms of a concrete service level, the model can only respond with vague, unhelpful answers.</br>
### 六、FastAPI服务标准化API输出实现高效集成与管理</br>
为保障系统与外部业务场景的高效对接MemoryBear采用FastAPI构建统一服务架构实现管理端与服务端API的集中暴露具备“高性能、易集成、强规范”的核心优势。服务端API涵盖知识萃取、图谱操作、搜索查询、遗忘控制等全功能模块支持JSON/XML多格式数据交互响应延迟平均低于50ms单实例可支撑1000QPS并发请求管理端API则提供系统配置、权限管理、日志查询等运维功能支持通过API实现批量知识导入导出、反思周期调整等操作。同时系统自动生成Swagger API文档包含接口参数说明、请求示例与返回格式定义开发者可快速完成集成调试。该架构已适配企业级微服务体系支持Docker容器化部署可灵活对接CRM、OA、研发管理等各类业务系统。</br>
## Core Positioning of MemoryBear
Unlike traditional memory management tools that treat knowledge as static data to be retrieved, MemoryBear is designed around the goal of simulating the knowledge-processing logic of the human brain. It builds a closed-loop system that spans the entire lifecycle-from knowledge intake to intelligent output. By emulating the hippocampus's memory encoding, the neocortex's knowledge consolidation, and synaptic pruning-based forgetting mechanisms, MemoryBear enables knowledge to dynamically evolve with "life-like" properties. This fundamentally redefines the relationship between knowledge and its users-shifting from passive lookup to proactive cognitive assistance.</br>
## MemoryBear架构总览
## Core Philosophy of MemoryBear
MemoryBear's design philosophy is rooted in deep insight into the essence of human cognition: the value of knowledge does not lie in its accumulation, but in the continuous transformation and refinement that occurs as it flows.
In traditional systems, once stored, knowledge becomes static-hard to associate across domains and incapable of adapting to users' cognitive needs. MemoryBear, by contrast, is built on the belief that true intelligence emerges only when knowledge undergoes a full evolutionary process: raw information distilled into structured rules, isolated rules connected into a semantic network, redundant information intelligently forgotten. Through this progression, knowledge shifts from mere informational memory to genuine cognitive understanding, enabling the emergence of real intelligence.</br>
## Core Features of MemoryBear
As an intelligent memory management system inspired by biological cognitive processes, MemoryBear centers its capabilities on two dimensions: full-lifecycle knowledge memory management and intelligent cognitive evolution. It covers the complete chain-from memory ingestion and refinement to storage, retrieval, and dynamic optimization-while providing a standardized service architecture that ensures efficient integration and invocation across applications.</br>
### 1. Memory Extraction Engine: Multi-dimensional Structured Refinement as the Foundation of Cognition</br>
Memory extraction is the starting point of MemoryBear's cognitive-oriented knowledge management. Unlike traditional data extraction, which performs "mechanical transformation", MemoryBear focuses on semantic-level parsing of unstructured information and standardized multi-format outputs, ensuring precise compatibility with downstream graph construction and intelligent retrieval. Core capabilities include:</br>
Accurate parsing of diverse information types: The engine automatically identifies and extracts core information from declarative sentences, removing redundant modifiers while preserving the essential subject-action-object logic. It also extracts structured triples (e.g., "MemoryBear-core functionality-knowledge extraction"), providing atomic data units for graph storage and ensuring high-accuracy knowledge association.</br>
Temporal information anchoring: For time-sensitive knowledge-such as event logs, policy documents, or experimental data-the engine automatically extracts timestamps and associates them with the content. This enables time-based reasoning and resolves the "temporal confusion" found in traditional knowledge systems.</br>
Intelligent pruning summarization: Based on contextual semantic understanding, the engine generates summaries that cover all key information with strong logical coherence. Users may customize summary length (50-500 words) and emphasis (technical, business, etc.), enabling fast knowledge acquisition across scenarios.Example: For a 10-page technical document, MemoryBear can produce a concise summary including core parameters, implementation logic, and application scenarios in under 3 seconds.</br>
### 2. Graph Storage: Neo4j-Powered Visual Knowledge Networks</br>
The storage layer adopts a graph-first architecture, integrating with the mature Neo4j graph database to manage knowledge entities and relationships efficiently. This overcomes limitations of traditional relational databases-such as weak relational modeling and slow complex queries-and mirrors the biological "neuron-synapse" cognition model.</br>
Key advantages include:
Scalable, flexible storage: supportting millions of entities and tens of millions of relational edges, covering 12 core relationship types (hierarchical, causal, temporal, logical, etc.) to fit multi-domain knowledge applications. Seamless integration with the extraction module: Extracting triples synchronize directly into Neo4j, automatically constructing the initial knowledge graph with zero manual mapping. Interactive graph visualization: users can intuitively explore entity connection paths, adjust relationship weights, and perform hybrid "machine-generated + human-optimized" graph management.</br>
### 3. Hybrid Search: Keyword + Semantic Vector for Precision and Intelligence</br>
To overcome the classic tradeoff-precision but rigidity vs. fuzziness but inaccuracy-MemoryBear implements a hybrid retrieval framework combining keyword search and semantic vector search.</br>
Keyword search: Optimized with Lucene, enabling millisecond-level exact matching of structured Semantic vector search:Powered by BERT embeddings, transforming queries into high-dimensional vectors for deep semantic comparison. This allows recognition of synonyms, near-synonyms, and implicit intent.For example, the query "How to optimize memory decay efficiency?" may surface related knowledge such as "forgetting-mechanism parameter tuning" or "memory strength evaluation methods".
Intelligent fusion strategy:Semantic retrieval expands the candidate space; keyword retrieval then performs precise filtering.This dual-stage process increases retrieval accuracy to 92%, improving by 35% compared with single-mode retrieval.</br>
### 4. Memory Forgetting Engine: Dynamic Decay Based on Strength & Timeliness</br>
Forgetting is one of MemoryBear's defining features-setting it apart from static knowledge systems. Inspired by the brain's synaptic pruning mechanism, MemoryBear models forgetting using a dual-dimension approach based on memory strength and time decay, ensuring redundant knowledge is removed while key knowledge retains cognitive priority.</br>
Implementation details:Each knowledge item is assigned an initial memory strength (determined by extraction quality and manual importance labels). Strength is updated dynamically according to usage frequency and association activity; A configurable time-decay cycle defines how different knowledge types (core rules vs. temporary data) lose strength over time. When knowledge falls below the strength threshold and exceeds its validity period, it enters a three-stage lifecycle: Dormancy-retained but with lower retrieval priority. Decay-gradually compressed to reduce storage cost. Clearance -permanently removed and archived into cold storage. This mechanism maintains redundant knowledge under 8%, reducing waste by over 60% compared with systems lacking forgetting capabilities.</br>
### 5. Self-Reflection Engine: Periodic Optimization for Autonomous Memory Evolution</br>
The self-reflection mechanism is key to MemoryBear's "intelligent self-improvement'. It periodically revisits, validates, and optimizes existing knowledge, mimicking the human behavior of review and retrospection.</br>
A scheduled reflection process runs automatically at midnight each day, performing:
1. Consistency checks, Detects logical conflicts across related knowledge (e.g., contradictory attributes for the same entity), flags suspicious records, and routes them for human verification;
2. Value assessment, Evaluates invocation frequency and contribution to associations. High-value knowledge is reinforced; low-value knowledge experiences accelerated decay;
3. Association optimization, Adjusts relationship weights based on recent usage and retrieval behavior, strengthening high-frequency association paths.</br>
### 6. FastAPI Services: Standardized API Layer for Efficient Integration & Management</br>
To support seamless integration with external business systems, MemoryBear uses FastAPI to build a unified service architecture that exposes both management and service APIs with high performance, easy integration, and strong consistency. Service-side APIs cover knowledge extraction, graph operations, search queries, forgetting management, and more. Support JSON/XML formats, with average latency below 50 ms, and a single instance sustaining 1000 QPS concurrency. Management-side APIs provide configuration, permissions, log queries, batch knowledge import/export, reflection cycle adjustments, and other operational capabilities. Swagger API documentation is auto-generated, including parameter descriptions, request samples, and response schemas, enabling rapid integration and testing. The architecture is compatible with enterprise microservice ecosystems, supports Docker-based deployment, and integrates easily with CRM, OA, R&D management, and various business applications.</br>
## MemoryBear Architecture Overview
<img width="2294" height="1154" alt="image" src="https://github.com/user-attachments/assets/3afd3b49-20ea-4847-b9ed-38b646a4ad89" />
</br>
- 记忆萃取引擎(Extraction Engine):预处理、去重、结构化提取</br>
- 记忆遗忘引擎(Forgetting Engine):记忆强度模型与衰减策略</br>
- 记忆自我反思引擎(Reflection Engine):评价与重写记忆</br>
- 检索服务:关键词、语义与混合检索</br>
- Agent MCP:提供多工具协作的智能体能力</br>
- Memory Extraction Engine: Preprocessing, deduplication, and structured knowledge extraction</br>
- Memory Forgetting Engine: Memory strength modeling and decay strategies</br>
- Memory Reflection Engine: Evaluation and rewriting of stored memories</br>
- Retrieval Services: Keyword search, semantic search, and hybrid retrieval</br>
- Agent & MCP Integration: Multi-tool collaborative agent capabilities</br>
## Metrics
We evaluate MemoryBear across multiple datasets covering different types of tasks, comparing its performance with other memory-enabled systems. The evaluation metrics include F1 score (F1), BLEU-1 (B1), and LLM-as-a-Judge score (J)-where higher values indicate better performance. MemoryBear achieves state-of-the-art results across all task categories:
In single-hop scenarios, MemoryBear leads in precision, answer matching quality, and task specificity.
In multi-hop reasoning, it demonstrates stronger information coherence and higher reasoning accuracy.
In open generalization tasks, it exhibits superior capability in handling diverse, unbounded information and maintaining high-quality generalization.
In temporal reasoning tasks, it excels at aligning and processing time-sensitive information.
Across the core metrics of all four task types, MemoryBear consistently outperforms other competing systems in the industry, including Mem O, Zep, and LangMem, demonstrating significantly stronger overall performance.
## 实验室指标
我们采用不同问题的数据集中通过具备记忆功能的系统进行性能对比。评估指标包括F1分数F1、BLEU-1B1以及LLM-as-a-Judge分数J数值越高表示表现越好性能更高。
MemoryBear 在 “单跳场景” 的精准度、结果匹配度与任务特异性表现上,均处于领先,“多跳”更强的信息连贯性与推理准确性,“开放泛化”对多样,无边界信息的处理质量与泛化能力更优,“时序”对时效性信息的匹配与处理表现更出色,四大任务的核心指标中,均优于 行业内的其他海外竞争对手Mem O、Zep、Lang Mem 等现有方法,整体性能更突出。
<img width="2256" height="890" alt="image" src="https://github.com/user-attachments/assets/5ff86c1f-53ac-4816-976d-95b48a4a10c0" />
Memory Bear 基于向量的知识记忆非图谱版本成功在保持高准确性的同时极大地优化了检索效率。该方法在总体准确性上的表现已明显高于现有最高全文检索方法72.90 ± 0.19%)。更重要的是,它在关键的延迟指标(包括 Search Latency Total Latency 的 p50/p95上也保持了较低水平充分体现出 “性能更优且延迟更高效” 的特点,解决了全文检索方法的高准确性伴随的高延迟瓶颈。
MemoryBear's vector-based knowledge memory (non-graph version) achieves substantial improvements in retrieval efficiency while maintaining high accuracy. Its overall accuracy surpasses the best existing full-text retrieval methods (72.90 ± 0.19%). More importantly, it maintains low latency across critical metrics-including Search Latency and Total Latency at both p50 and p95-demonstrating the characteristics of higher performance with greater latency efficiency. This effectively resolves the common bottleneck in full-text retrieval systems, where high accuracy typically comes at the cost of significantly increased latency.
<img width="2248" height="498" alt="image" src="https://github.com/user-attachments/assets/2759ea19-0b71-4082-8366-e8023e3b28fe" />
Memory Bear 通过集成知识图谱架构,在需要复杂推理和关系感知的任务上进一步释放了潜力。虽然图谱的遍历和推理可能会引入轻微的检索开销,但该版本通过优化图检索策略和决策流,成功将延迟控制在高效范围。更关键的是,基于图谱的 Memory Bear 将总体准确性推至新的高度75.00 ± 0.20%),在保持准确性的同时,整体指标显著优于其他所有方法,证明了“结构化记忆带来的性能决定性优势”。
MemoryBear further unlocks its potential in tasks requiring complex reasoning and relationship awareness through the integration of a knowledge-graph architecture. Although graph traversal and reasoning introduce a slight retrieval overhead, this version effectively keeps latency within an efficient range by optimizing graph-query strategies and decision flows. More importantly, the graph-based MemoryBear pushes overall accuracy to a new benchmark (75.00 ± 0.20%). While maintaining high accuracy, it delivers performance metrics that significantly surpass all other methods, demonstrating the decisive advantage of structured memory systems.
<img width="2238" height="342" alt="image" src="https://github.com/user-attachments/assets/c928e094-45a2-414b-831a-6990b711ed07" />
# MemoryBear安装教程
## 一、前期准备
# MemoryBear Installation Guide
## 1. Prerequisites
### 1.环境要求
### 1.1 Environment Requirements
* Node.js 20.19+ 22.12+ 前端运行环境
* Node.js 20.19+ or 22.12+- Required for running the frontend
* Python 3.12 后端运行环境
* Python 3.12- Backend runtime environment
* PostgreSQL 13+ 主数据库
* PostgreSQL 13+- Primary relational database
* Neo4j 4.4+ 图数据库(存储知识图谱)
* Neo4j 4.4+- Graph database (used for storing the knowledge graph)
* Redis 6.0+ 缓存和消息队列
* Redis 6.0+- Cache layer and message queue
## 二、项目获取
## 2. Getting the Project
### 1.获取方式
### 1. Download Method
Git克隆推荐
Clone via Git (recommended):
```plain&#x20;text
git clone https://github.com/SuanmoSuanyangTechnology/MemoryBear.git
```
### 2.目录说明
### 2. Directory Structure Explanation
<img width="5238" height="1626" alt="diagram" src="https://github.com/user-attachments/assets/416d6079-3f34-40c3-9bcf-8760d186741a" />
## 三、安装步骤
## Installation Steps
### 1.后端API服务启动
### 1. Start the Backend API Service
#### 1.1 安装python依赖
#### 1.1 Install Python Dependencies
```python
# 0.安装依赖管理工具uv
# 0. Install the dependency management tool: uv
pip install uv
# 1.终端切换API目录
# 1. Switch to the API directory
cd api
# 2.安装依赖
# 2. Install dependencies
uv sync
# 3.激活虚拟环境 (Windows)
.venv\Scripts\Activate.ps1 powershell在api目录下
api\.venv\Scripts\activate powershell在根目录下
.venv\Scripts\activate.bat cmd在api目录下
# 3. Activate the Virtual Environment (Windows)
.venv\Scripts\Activate.ps1 # run inside /api directory
api\.venv\Scripts\activate # run inside project root directory
.venv\Scripts\activate.bat # run inside /api directory
```
#### 1.2 安装必备基础服务docker镜像
#### 1.2 Install Required Base Services (Docker Images)
使用docker desktop安装所需的docker镜像
Use Docker Desktop to install the necessary service images.
* **docker desktop安装地址:**&#x68;ttps://www.docker.com/products/docker-desktop/
* **Docker Desktop download page:** &#x68;ttps://www.docker.com/products/docker-desktop/
* **PostgreSQL**
**拉取镜像**
**Pull the Image**
search——select——pull
search-select-pull
<img width="1280" height="731" alt="image-9" src="https://github.com/user-attachments/assets/0609eb5f-e259-4f24-8a7b-e354da6bae4d" />
**创建容器**
**Create the Container**
<img width="1280" height="731" alt="image-8" src="https://github.com/user-attachments/assets/d57b3206-1df1-42a4-80fd-e71f37201a25" />
**服务启动成功**
**Service Started Successfully**
<img width="1280" height="731" alt="image" src="https://github.com/user-attachments/assets/76e04c54-7a36-46ec-a68e-241ad268e427" />
* **Neo4j**
**拉取镜像**与PostgreSQL一样从docker desktop中拉取镜像
**Pull the Image** from Docker Desktop, the same way as with PostgreSQL.
**创建容器**Neo4j 默认需要映射**2 个关键端口**7474 对应 Browser7687 对应 Bolt 协议),同时需设置初始密码
**Create the Neo4j Container** ensure that you map **the two required ports** 7474 - Neo4j Browser, 7687 - Bolt protocol. Additionally, you must set an initial password for the Neo4j database during container creation.
<img width="1280" height="731" alt="image-1" src="https://github.com/user-attachments/assets/6bfb0c27-74e8-45f7-b381-189325d516bd" />
**服务成功启动**
**Service Started Successfully**
<img width="1280" height="731" alt="image-2" src="https://github.com/user-attachments/assets/0d28b4fa-e8ed-4c05-8983-7a47f0a892d1" />
* **Redis**
同上
The same as above
#### 1.3 配置环境变量
#### 1.3 Configure environment variables
复制 env.example .env 并填写配置
Copy env.example as.env and fill in the configuration
```bash
# Neo4j 图数据库
# Neo4j Graph Database
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your-password
# Neo4j Browser访问地址
# Neo4j Browser Access URL (optional documentation)
# PostgreSQL 数据库
# PostgreSQL Database
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=postgres
@@ -187,39 +218,38 @@ DB_NAME=redbear-mem
# Database Migration Configuration
# Set to true to automatically upgrade database schema on startup
DB_AUTO_UPGRADE=true # 首次启动设为true自动迁移数据库 在空白数据库创建表结构
DB_AUTO_UPGRADE=true # For the first startup, keep this as true to create the schema in an empty database.
# Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=1
# Celery (使用Redis作为broker)
# Celery (Using Redis as broker)
BROKER_URL=redis://127.0.0.1:6379/0
RESULT_BACKEND=redis://127.0.0.1:6379/0
# JWT密钥 (生成方式: openssl rand -hex 32)
# JWT Secret Key (Formation method: openssl rand -hex 32)
SECRET_KEY=your-secret-key-here
```
#### 1.4 PostgreSQL数据库建立
#### 1.4 Initialize the PostgreSQL Database
通过项目中已有的 alembic 数据库迁移文件,为全新创建的空白 PostgreSQL 数据库创建对应的表结构。
MemoryBear uses Alembic migration files included in the project to create the required table structures in a newly created, empty PostgreSQL database.
**1配置数据库连接**
**(1) Configure the Database Connection**
确认项目中`alembic.ini`文件的`sqlalchemy.url`配置指向你的空白 PostgreSQL 数据库,格式示例:
Ensure that the sqlalchemy.url value in the project's alembic.ini file points to your empty PostgreSQL database. Example format:
```toml
sqlalchemy.url = postgresql://用户名:密码@数据库地址:端口/空白数据库名
```bash
sqlalchemy.url = postgresql://<username>:<password>@<host>:<port>/<database_name>
```
同时检查 migrations`/env.py`中`target_metadata`是否正确关联到 ORM 模型的`metadata`(确保迁移脚本和模型一致)
Also verify that target_metadata in migrations/env.py is correctly linked to the ORM model's metadata object.
**2执行迁移文件**
在API目录执行以下命令alembic 会自动识别空白数据库,并执行所有未应用的迁移脚本,创建完整表结构:
**(2) Apply the Migration Files**
Run the following command inside the API directory. Alembic will automatically detect the empty database and apply all outstanding migrations to create the full schema:
```bash
alembic upgrade head
```
@@ -227,57 +257,57 @@ alembic upgrade head
<img width="1076" height="341" alt="image-3" src="https://github.com/user-attachments/assets/9edda79d-4637-46e3-bee3-2eec39975d59" />
通过Navicat查看迁移创建的数据库表结构
Use Navicat to inspect the database tables created by the Alembic migration process.
<img width="1280" height="680" alt="image-4" src="https://github.com/user-attachments/assets/aa5c1d98-bdc3-4d25-acb2-5c8cf6ecd3f5" />
#### API服务启动
#### Start the API Service
```python
uv run -m app.main
```
访问 API 文档:http://localhost:8000/docs
Access the API documentation at http://localhost:8000/docs
<img width="1280" height="675" alt="image-5" src="https://github.com/user-attachments/assets/68fa62b4-2c4f-4cf0-896c-41d59aa7d712" />
### 2.前端web应用启动
### 2. Start the Frontend Web Application
#### 2.1安装依赖
#### 2.1 Install Dependencies
```python
# 切换web目录下
# Switch to the web directory
cd web
# 下载依赖
# Install dependencies
npm install
```
#### 2.2 修改API代理配置
#### 2.2 Update the API Proxy Configuration
编辑 web/vite.config.ts,将代理目标改为后端地址
Edit web/vite.config.ts and update the proxy target to point to your backend API service:
```python
proxy: {
'/api': {
target: 'http://127.0.0.1:8000', // 改为后端地址win用户127.0.0.1 mac用户0.0.0.0
target: 'http://127.0.0.1:8000', // Change to the backend address, windows users 127.0.0.1 macOS users 0.0.0.0
changeOrigin: true,
},
}
```
#### 2.3 启动服务
#### 2.3 Start the Frontend Service
```python
# 启动web服务
# Start the web service
npm run dev
```
服务启动会输出可访问的前端界面
After the service starts, the console will output the URL for accessing the frontend interface.
<img width="935" height="311" alt="image-6" src="https://github.com/user-attachments/assets/cba1074a-440c-4866-8a94-7b6d1c911a93" />
@@ -285,33 +315,26 @@ npm run dev
<img width="1280" height="652" alt="image-7" src="https://github.com/user-attachments/assets/a719dc0a-cbdd-4ba1-9b21-123d5eac32eb" />
## 四、用户操作
## 4. User Guide
step1:项目获取
step1: Retrieve the Project.
step2后端API服务启动
step2: Start the Backend API Service.
step3前端web应用启动
step3: Start the Frontend Web Application.
step4 终端输入 curl.exe -X POST http://127.0.0.1:8000/api/setup ,访问接口初始化数据库获得超级管理员账号
step4: Enter curl.exe -X POST http://127.0.0.1:8000/api/setup in the terminal to access the interface, initialize the database, and obtain the super administrator account.
step5:超级管理员&#x20;
step5: Super Administrator Credentials
Account: admin@example.com
Password: admin_password
账号admin@example.com
step6: Log In to the Frontend Interface.
密码admin\_password
## License
This project is licensed under the Apache License 2.0. For details, see the LICENSE file.
step6登陆前端页面
## 许可证
本项目采用 Apache License 2.0 开源协议,详情见 `LICENSE`。
## 致谢与交流
- 问题反馈与讨论:请提交 Issue 到代码仓库
- 欢迎贡献:提交 PR 前请先创建功能分支并遵循常规提交信息格式
- 如感兴趣需要联络tianyou_hubm@redbearai.com
## Acknowledgements & Community
- Feedback & Issues: Please submit an Issue in the repository for bug reports or discussions.
- Contributions Welcome: When submitting a Pull Request, please create a feature branch and follow conventional commit message guidelines.
- Contact: If you are interested in contributing or collaborating, feel free to reach out at tianyou_hubm@redbearai.com

323
README_CN.md Normal file
View File

@@ -0,0 +1,323 @@
<img width="2346" height="1310" alt="image" src="https://github.com/user-attachments/assets/bc73a64d-cd1e-4d22-be3e-04ce40423a20" />
# MemoryBear 让AI拥有如同人类一样的记忆
中文 | [English](./README.md)
### [安装教程](#memorybear安装教程)
### 论文:<a href="https://memorybear.ai/pdf/memoryBear" target="_blank" rel="noopener noreferrer">《Memory Bear AI: 从记忆到认知的突破》</a>
## 项目简介
MemoryBear是红熊AI自主研发的新一代AI记忆系统其核心突破在于跳出传统知识“静态存储”的局限以生物大脑认知机制为原型构建了具备“感知-提炼-关联-遗忘”全生命周期的智能知识处理体系。该系统致力于让机器摆脱“信息堆砌”的困境,实现对知识的深度理解与自主进化,成为人类认知协作的核心伙伴。
## MemoryBear是从解决这些问题来的
### 一、单模型知识遗忘的核心原因</br>
上下文窗口限制:主流大模型上下文窗口通常为 8k-32k tokens长对话中早期信息会被 “挤出”,导致后续回复脱离历史语境:如用户第 1 轮说 “我对海鲜过敏”,第 5 轮问 “推荐今晚的菜品” 时模型可能遗忘过敏信息。</br>
静态知识库与动态数据割裂:大模型训练时的静态知识库如截止 2023 年数据,无法实时吸收用户对话中的个性化信息如用户偏好、历史订单,需依赖外部记忆模块补充。</br>
模型注意力机制缺陷Transformer 的自注意力对长距离依赖的捕捉能力随序列长度下降,出现 “近因效应”更关注最新输入,忽略早期关键信息。</br>
### 二、多 Agent 协作的记忆断层问题</br>
Agent 数据孤岛:不同 Agent如咨询 Agent、售后 Agent、推荐 Agent各自维护独立记忆未建立跨模块的共享机制导致用户重复提供信息如用户向咨询 Agent 说明地址后,售后 Agent 仍需再次询问。</br>
对话状态不一致:多轮交互中 Agent 切换时,对话状态如用户当前意图、历史问题标签传递不完整,引发服务断层如用户从 “产品咨询” 转 “投诉” 时,新 Agent 未继承前期投诉细节。</br>
决策冲突:不同 Agent 基于局部记忆做出的响应可能矛盾如推荐 Agent 推荐用户过敏的产品,因未获取健康禁忌的历史记录。</br>
### 三、模型推理过程中的 “语义歧义” 引发理解偏差</br>
用户对话中的个性化信息如行业术语、口语化表达、上下文指代未被准确编码,导致模型对记忆内容的语义解析失真,比如对用户历史对话中的模糊表述如 “上次说的那个方案”无法准确定位具体内容。</br>
多语言、方言场景中,跨语种记忆关联失效如用户混用中英描述需求时,模型无法整合多语言信息。</br>
典型案例:用户说之前客服说可以‘加急处理’现在进度如何?模型因未记录 “加急” 对应的具体服务等级,回复笼统模糊。</br>
## MemoryBear核心定位
与传统记忆管理工具将知识视为“待检索的静态数据”不同MemoryBear以“模拟人类大脑知识处理逻辑”为核心目标构建了从知识摄入到智能输出的闭环体系。系统通过复刻大脑海马体的记忆编码、新皮层的知识固化及突触修剪的遗忘机制让知识具备动态演化的“生命特征”彻底重构了知识与使用者之间的交互关系——从“被动查询”升级为“主动辅助记忆认知”
## MemoryBear核心哲学
MemoryBear的设计哲学源于对人类认知本质的深刻洞察知识的价值不在于存量积累而在于动态流转中的价值升华。传统系统中知识一旦存储便陷入“静止状态”难以形成跨领域关联更无法主动适配使用者的认知需求而MemoryBear坚信只有让知识经历“原始信息提炼为结构化规则、孤立规则关联为知识网络、冗余信息智能遗忘”的完整过程才能实现从“信息记忆”到“认知理解”的跨越最终涌现出真正的智能。
## MemoryBear核心特性
MemoryBear作为模仿生物大脑认知过程的智能记忆管理系统其核心特性围绕“记忆知识全生命周期管理”与“智能认知进化”两大维度构建覆盖记忆从摄入提炼到存储检索、动态优化的完整链路同时通过标准化服务架构实现高效集成与调用。
### 一、记忆萃取引擎:多维度结构化提炼,夯实认知基础</br>
记忆萃取是MemoryBear实现“认知化管理”的起点区别于传统数据提取的“机械转换”其核心优势在于对非结构化信息的“语义级解析”与“多格式标准化输出”精准适配后续图谱构建与智能检索需求。具体能力包括</br>
多类型信息精准解析:可自动识别并提取文本中的陈述句核心信息,剥离冗余修饰成分,保留“主体-行为-对象”核心逻辑同时精准抽取三元组数据如“MemoryBear-核心功能-知识萃取”),为图谱存储提供基础数据单元,保障知识关联的准确性。</br>
时序信息锚定:针对含有时效性的知识(如事件记录、政策文件、实验数据),自动提取并标记时间戳信息,支持“时间维度”的知识追溯与关联,解决传统知识管理中“时序混乱”导致的认知偏差问题。</br>
智能剪枝生成:基于上下文语义理解,生成“关键信息全覆盖+逻辑连贯性强”的摘要内容支持自定义摘要长度50-500字与侧重点如技术型、业务型适配不同场景的知识快速获取需求。例如对10页技术文档处理时可在3秒内生成含核心参数、实现逻辑与应用场景的精简摘要。</br>
### 二、图谱存储对接Neo4j构建可视化知识网络</br>
存储层采用“图数据库优先”的架构设计通过对接业界成熟的Neo4j图数据库实现知识实体与关系的高效管理突破传统关系型数据库“关联弱、查询繁”的局限契合生物大脑“神经元关联”的认知模式。</br>
该特性核心价值体现在一是支持海量实体与多元关系的灵活存储可管理百万级知识实体及千万级关联关系涵盖“上下位、因果、时序、逻辑”等12种核心关系类型适配多领域知识场景二是与知识萃取模块深度联动萃取的三元组数据可直接同步至Neo4j自动构建初始知识图谱无需人工二次映射三是支持图谱可视化交互用户可直观查看实体关联路径手动调整关系权重实现“机器构建+人工优化”的协同管理。</br>
### 三、混合搜索:关键词+语义向量,兼顾精准与智能</br>
为解决传统搜索“要么精准但僵化要么模糊但失准”的痛点MemoryBear采用“关键词检索+语义向量检索”的混合搜索架构,实现“精准匹配”与“意图理解”的双重目标。</br>
其中关键词检索基于Lucene引擎优化针对知识中的核心实体、关键参数等结构化信息实现毫秒级精准定位保障“明确需求”下的高效检索语义向量检索则通过BERT模型对查询语句进行语义编码将其转化为高维向量后与知识库中的向量数据比对可识别同义词、近义词及隐含意图例如用户查询“如何优化记忆衰减效率”时系统可关联到“遗忘机制参数调整”“记忆强度评估方法”等相关知识。两种检索方式智能融合先通过语义检索扩大候选范围再通过关键词检索精准筛选使检索准确率提升至92%较单一检索方式平均提升35%。</br>
### 四、记忆遗忘引擎:基于强度与时效的动态衰减,模拟生物记忆特性</br>
遗忘是MemoryBear区别于传统静态知识管理工具的核心特性之一其灵感源于生物大脑“突触修剪”机制通过“记忆强度+时效”双维度模型实现知识的逐步衰减,避免冗余知识占用资源,保障核心知识的“认知优先级”。</br>
具体实现逻辑为:系统为每条知识分配“初始记忆强度”(由萃取质量、人工标注重要性决定),并结合“调用频率、关联活跃度”实时更新强度值;同时设定“时效衰减周期”,根据知识类型(如核心规则、临时数据)差异化配置衰减速率。当知识强度低于阈值且超过设定时效后,将进入“休眠-衰减-清除”三阶段流程休眠阶段保留数据但降低检索优先级衰减阶段逐步压缩存储体积清除阶段则彻底删除并备份至冷存储。该机制使系统冗余知识占比控制在8%以内较传统无遗忘机制系统降低60%以上。</br>
### 五、自我反思引擎:定期回顾优化,实现记忆自主进化</br>
自我反思机制是MemoryBear实现“智能升级”的关键通过定期对已有记忆进行回顾、校验与优化模拟人类“复盘总结”的认知行为持续提升知识体系的准确性与有效性。</br>
系统默认每日凌晨触发自动反思流程,核心动作包括:一是“一致性校验”,对比关联知识间的逻辑冲突(如同一实体的矛盾属性),标记可疑知识并推送人工审核;二是“价值评估”,统计知识的调用频次、关联贡献度,将高价值知识强化记忆强度,低价值知识加速衰减;三是“关联优化”,基于近期检索与使用行为,调整知识间的关联权重,强化高频关联路径。此外,支持人工触发专项反思(如新增核心知识后),并提供反思报告可视化展示优化结果,实现“自主进化+人工监督”的双重保障。</br>
### 六、FastAPI服务标准化API输出实现高效集成与管理</br>
为保障系统与外部业务场景的高效对接MemoryBear采用FastAPI构建统一服务架构实现管理端与服务端API的集中暴露具备“高性能、易集成、强规范”的核心优势。服务端API涵盖知识萃取、图谱操作、搜索查询、遗忘控制等全功能模块支持JSON/XML多格式数据交互响应延迟平均低于50ms单实例可支撑1000QPS并发请求管理端API则提供系统配置、权限管理、日志查询等运维功能支持通过API实现批量知识导入导出、反思周期调整等操作。同时系统自动生成Swagger API文档包含接口参数说明、请求示例与返回格式定义开发者可快速完成集成调试。该架构已适配企业级微服务体系支持Docker容器化部署可灵活对接CRM、OA、研发管理等各类业务系统。</br>
## MemoryBear架构总览
<img width="2294" height="1154" alt="image" src="https://github.com/user-attachments/assets/3afd3b49-20ea-4847-b9ed-38b646a4ad89" />
</br>
- 记忆萃取引擎Extraction Engine预处理、去重、结构化提取</br>
- 记忆遗忘引擎Forgetting Engine记忆强度模型与衰减策略</br>
- 记忆自我反思引擎Reflection Engine评价与重写记忆</br>
- 检索服务:关键词、语义与混合检索</br>
- Agent 与 MCP提供多工具协作的智能体能力</br>
## 实验室指标
我们采用不同问题的数据集中通过具备记忆功能的系统进行性能对比。评估指标包括F1分数F1、BLEU-1B1以及LLM-as-a-Judge分数J数值越高表示表现越好性能更高。
MemoryBear 在 “单跳场景” 的精准度、结果匹配度与任务特异性表现上,均处于领先,“多跳”更强的信息连贯性与推理准确性,“开放泛化”对多样,无边界信息的处理质量与泛化能力更优,“时序”对时效性信息的匹配与处理表现更出色,四大任务的核心指标中,均优于 行业内的其他海外竞争对手Mem O、Zep、Lang Mem 等现有方法,整体性能更突出。
<img width="2256" height="890" alt="image" src="https://github.com/user-attachments/assets/5ff86c1f-53ac-4816-976d-95b48a4a10c0" />
Memory Bear 基于向量的知识记忆非图谱版本成功在保持高准确性的同时极大地优化了检索效率。该方法在总体准确性上的表现已明显高于现有最高全文检索方法72.90 ± 0.19%)。更重要的是,它在关键的延迟指标(包括 Search Latency 和 Total Latency 的 p50/p95上也保持了较低水平充分体现出 “性能更优且延迟更高效” 的特点,解决了全文检索方法的高准确性伴随的高延迟瓶颈。
<img width="2248" height="498" alt="image" src="https://github.com/user-attachments/assets/2759ea19-0b71-4082-8366-e8023e3b28fe" />
Memory Bear 通过集成知识图谱架构,在需要复杂推理和关系感知的任务上进一步释放了潜力。虽然图谱的遍历和推理可能会引入轻微的检索开销,但该版本通过优化图检索策略和决策流,成功将延迟控制在高效范围。更关键的是,基于图谱的 Memory Bear 将总体准确性推至新的高度75.00 ± 0.20%),在保持准确性的同时,整体指标显著优于其他所有方法,证明了“结构化记忆带来的性能决定性优势”。
<img width="2238" height="342" alt="image" src="https://github.com/user-attachments/assets/c928e094-45a2-414b-831a-6990b711ed07" />
# MemoryBear安装教程
## 一、前期准备
### 1.环境要求
* Node.js 20.19+ 或 22.12+ 前端运行环境
* Python 3.12 后端运行环境
* PostgreSQL 13+ 主数据库
* Neo4j 4.4+ 图数据库(存储知识图谱)
* Redis 6.0+ 缓存和消息队列
## 二、项目获取
### 1.获取方式
Git克隆推荐
```plain&#x20;text
git clone https://github.com/SuanmoSuanyangTechnology/MemoryBear.git
```
### 2.目录说明
<img width="5238" height="1626" alt="diagram" src="https://github.com/user-attachments/assets/416d6079-3f34-40c3-9bcf-8760d186741a" />
## 三、安装步骤
### 1.后端API服务启动
#### 1.1 安装python依赖
```python
# 0.安装依赖管理工具uv
pip install uv
# 1.终端切换API目录
cd api
# 2.安装依赖
uv sync
# 3.激活虚拟环境 (Windows)
.venv\Scripts\Activate.ps1 powershell在api目录下
api\.venv\Scripts\activate powershell在根目录下
.venv\Scripts\activate.bat cmd在api目录下
```
#### 1.2 安装必备基础服务docker镜像
使用docker desktop安装所需的docker镜像
* **docker desktop安装地址**&#x68;ttps://www.docker.com/products/docker-desktop/
* **PostgreSQL**
**拉取镜像**
search——select——pull
<img width="1280" height="731" alt="image-9" src="https://github.com/user-attachments/assets/0609eb5f-e259-4f24-8a7b-e354da6bae4d" />
**创建容器**
<img width="1280" height="731" alt="image-8" src="https://github.com/user-attachments/assets/d57b3206-1df1-42a4-80fd-e71f37201a25" />
**服务启动成功**
<img width="1280" height="731" alt="image" src="https://github.com/user-attachments/assets/76e04c54-7a36-46ec-a68e-241ad268e427" />
* **Neo4j**
**拉取镜像**与PostgreSQL一样从docker desktop中拉取镜像
**创建容器**Neo4j 默认需要映射**2 个关键端口**7474 对应 Browser7687 对应 Bolt 协议),同时需设置初始密码
<img width="1280" height="731" alt="image-1" src="https://github.com/user-attachments/assets/6bfb0c27-74e8-45f7-b381-189325d516bd" />
**服务成功启动**
<img width="1280" height="731" alt="image-2" src="https://github.com/user-attachments/assets/0d28b4fa-e8ed-4c05-8983-7a47f0a892d1" />
* **Redis**
同上
#### 1.3 配置环境变量
复制 env.example 为 .env 并填写配置
```bash
# Neo4j 图数据库
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your-password
# Neo4j Browser访问地址
# PostgreSQL 数据库
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=your-password
DB_NAME=redbear-mem
# Database Migration Configuration
# Set to true to automatically upgrade database schema on startup
DB_AUTO_UPGRADE=true # 首次启动设为true自动迁移数据库 在空白数据库创建表结构
# Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_DB=1
# Celery (使用Redis作为broker)
BROKER_URL=redis://127.0.0.1:6379/0
RESULT_BACKEND=redis://127.0.0.1:6379/0
# JWT密钥 (生成方式: openssl rand -hex 32)
SECRET_KEY=your-secret-key-here
```
#### 1.4 PostgreSQL数据库建立
通过项目中已有的 alembic 数据库迁移文件,为全新创建的空白 PostgreSQL 数据库创建对应的表结构。
**1配置数据库连接**
确认项目中`alembic.ini`文件的`sqlalchemy.url`配置指向你的空白 PostgreSQL 数据库,格式示例:
```bash
sqlalchemy.url = postgresql://用户名:密码@数据库地址:端口/空白数据库名
```
同时检查 migrations`/env.py`中`target_metadata`是否正确关联到 ORM 模型的`metadata`(确保迁移脚本和模型一致)
**2执行迁移文件**
在API目录执行以下命令alembic 会自动识别空白数据库,并执行所有未应用的迁移脚本,创建完整表结构:
```bash
alembic upgrade head
```
<img width="1076" height="341" alt="image-3" src="https://github.com/user-attachments/assets/9edda79d-4637-46e3-bee3-2eec39975d59" />
通过Navicat查看迁移创建的数据库表结构
<img width="1280" height="680" alt="image-4" src="https://github.com/user-attachments/assets/aa5c1d98-bdc3-4d25-acb2-5c8cf6ecd3f5" />
#### API服务启动
```python
uv run -m app.main
```
访问 API 文档http://localhost:8000/docs
<img width="1280" height="675" alt="image-5" src="https://github.com/user-attachments/assets/68fa62b4-2c4f-4cf0-896c-41d59aa7d712" />
### 2.前端web应用启动
#### 2.1安装依赖
```python
# 切换web目录下
cd web
# 下载依赖
npm install
```
#### 2.2 修改API代理配置
编辑 web/vite.config.ts将代理目标改为后端地址
```python
proxy: {
'/api': {
target: 'http://127.0.0.1:8000', // 改为后端地址win用户127.0.0.1 mac用户0.0.0.0
changeOrigin: true,
},
}
```
#### 2.3 启动服务
```python
# 启动web服务
npm run dev
```
服务启动会输出可访问的前端界面
<img width="935" height="311" alt="image-6" src="https://github.com/user-attachments/assets/cba1074a-440c-4866-8a94-7b6d1c911a93" />
<img width="1280" height="652" alt="image-7" src="https://github.com/user-attachments/assets/a719dc0a-cbdd-4ba1-9b21-123d5eac32eb" />
## 四、用户操作
step1项目获取
step2后端API服务启动
step3前端web应用启动
step4 终端输入 curl.exe -X POST http://127.0.0.1:8000/api/setup ,访问接口初始化数据库获得超级管理员账号
step5超级管理员&#x20;
账号admin@example.com
密码admin\_password
step6登陆前端页面
## 许可证
本项目采用 Apache License 2.0 开源协议,详情见 `LICENSE`。
## 致谢与交流
- 问题反馈与讨论:请提交 Issue 到代码仓库
- 欢迎贡献:提交 PR 前请先创建功能分支并遵循常规提交信息格式
- 如感兴趣需要联络tianyou_hubm@redbearai.com

View File

@@ -28,6 +28,7 @@ from . import (
public_share_controller,
multi_agent_controller,
workflow_controller,
prompt_optimizer_controller
)
# 创建管理端 API 路由器
@@ -58,5 +59,6 @@ manager_router.include_router(public_share_controller.router) # 公开路由(
manager_router.include_router(memory_dashboard_controller.router)
manager_router.include_router(multi_agent_controller.router)
manager_router.include_router(workflow_controller.router)
manager_router.include_router(prompt_optimizer_controller.router)
__all__ = ["manager_router"]

View File

@@ -0,0 +1,170 @@
import uuid
from fastapi import APIRouter, Depends, Path
from sqlalchemy.orm import Session
from app.core.logging_config import get_api_logger
from app.core.response_utils import success
from app.dependencies import get_current_user, get_db
from app.models.prompt_optimizer_model import RoleType
from app.schemas.prompt_optimizer_schema import PromptOptMessage, PromptOptModelSet, CreateSessionResponse, \
OptimizePromptResponse, SessionHistoryResponse, SessionMessage
from app.schemas.response_schema import ApiResponse
from app.services.prompt_optimizer_service import PromptOptimizerService
router = APIRouter(prefix="/prompt", tags=["Prompts-Optimization"])
logger = get_api_logger()
@router.post(
"/sessions",
summary="Create a new prompt optimization session",
response_model=ApiResponse
)
def create_prompt_session(
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""
Create a new prompt optimization session for the current user.
Returns:
ApiResponse: Contains the newly generated session ID.
"""
service = PromptOptimizerService(db)
# create new session
session = service.create_session(current_user.tenant_id, current_user.id)
result_schema = CreateSessionResponse.model_validate(session)
return success(data=result_schema)
@router.get(
"/sessions/{session_id}",
summary="获取 prompt 优化历史对话",
response_model=ApiResponse
)
def get_prompt_session(
session_id: uuid.UUID = Path(..., description="Session ID"),
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""
Retrieve all messages from a specified prompt optimization session.
Args:
session_id (UUID): The ID of the session to retrieve
db (Session): Database session
current_user: Current logged-in user
Returns:
ApiResponse: Contains the session ID and the list of messages.
"""
service = PromptOptimizerService(db)
history = service.get_session_message_history(
session_id=session_id,
user_id=current_user.id
)
messages = [
SessionMessage(role=role, content=content)
for role, content in history
]
result = SessionHistoryResponse(
session_id=session_id,
messages=messages
)
return success(data=result)
@router.post(
"/sessions/{session_id}/messages",
summary="Get prompt optimization",
response_model=ApiResponse
)
async def get_prompt_opt(
session_id: uuid.UUID = Path(..., description="Session ID"),
data: PromptOptMessage = ...,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""
Send a user message in the specified session and return the optimized prompt
along with its description and variables.
Args:
session_id (UUID): The session ID
data (PromptOptMessage): Contains the user message, model ID, and current prompt
db (Session): Database session
current_user: Current user information
Returns:
ApiResponse: Contains the optimized prompt, description, and a list of variables.
"""
service = PromptOptimizerService(db)
service.create_message(
tenant_id=current_user.tenant_id,
session_id=session_id,
user_id=current_user.id,
role=RoleType.USER,
content=data.message
)
opt_result = await service.optimize_prompt(
tenant_id=current_user.tenant_id,
model_id=data.model_id,
session_id=session_id,
user_id=current_user.id,
current_prompt=data.current_prompt,
message=data.message
)
service.create_message(
tenant_id=current_user.tenant_id,
session_id=session_id,
user_id=current_user.id,
role=RoleType.ASSISTANT,
content=opt_result.desc
)
variables = service.parser_prompt_variables(opt_result.prompt)
result = {
"prompt": opt_result.prompt,
"desc": opt_result.desc,
"variables": variables
}
result_schema = OptimizePromptResponse.model_validate(result)
return success(data=result_schema)
@router.put(
"/model",
summary="Create or update prompt model config",
response_model=ApiResponse
)
def set_system_prompt(
data: PromptOptModelSet = ...,
db: Session = Depends(get_db),
current_user=Depends(get_current_user),
):
"""
Create or update a system prompt model configuration for the tenant.
Args:
data (PromptOptModelSet): Model configuration data including model ID,
system prompt, and optional configuration ID
db (Session): Database session
current_user: Current user information
Returns:
UUID: The ID of the created or updated model configuration.
"""
if data.id is None:
data.id = uuid.uuid4()
model_config = PromptOptimizerService(db).create_update_model_config(
current_user.tenant_id,
data.id,
data.system_prompt
)
return success(data=model_config.id)

555629
api/app/core/rag/res/huqie.txt Normal file

File diff suppressed because it is too large Load Diff

10880
api/app/core/rag/res/ner.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@ from .data_config_model import DataConfig
from .multi_agent_model import MultiAgentConfig, AgentInvocation
from .workflow_model import WorkflowConfig, WorkflowExecution, WorkflowNodeExecution
from .retrieval_info import RetrievalInfo
from .prompt_optimizer_model import PromptOptimizerModelConfig, PromptOptimizerSession, PromptOptimizerSessionHistory
__all__ = [
"Tenants",
@@ -54,5 +55,8 @@ __all__ = [
"WorkflowConfig",
"WorkflowExecution",
"WorkflowNodeExecution",
"RetrievalInfo"
"RetrievalInfo",
"PromptOptimizerModelConfig",
"PromptOptimizerSession",
"PromptOptimizerSessionHistory"
]

View File

@@ -15,6 +15,25 @@ class ModelType(StrEnum):
EMBEDDING = "embedding"
RERANK = "rerank"
@classmethod
def from_str(cls, value: str) -> "ModelType":
"""
Get a ModelType enum instance from a string value.
Args:
value (str): The string representation of the model type.
Returns:
ModelType: The corresponding ModelType enum object.
Raises:
ValueError: If the given value does not match any ModelType.
"""
try:
return cls(value)
except ValueError:
raise ValueError(f"Invalid ModelType: {value}")
class ModelProvider(StrEnum):
"""模型提供商枚举"""

View File

@@ -0,0 +1,173 @@
import datetime
import uuid
from enum import StrEnum
from sqlalchemy import Column, ForeignKey, Text, DateTime, String, Index
from sqlalchemy.dialects.postgresql import UUID
from app.db import Base
class RoleType(StrEnum):
"""
Enumeration of message roles used in prompt optimization conversations.
This enum standardizes the role identifiers for messages stored in the
prompt optimization session history, ensuring consistency across
system-generated messages, user inputs, and assistant responses.
Attributes:
SYSTEM (str): Represents system-level instructions or prompts that
define the behavior or constraints of the assistant.
USER (str): Represents messages originating from the end user.
ASSISTANT (str): Represents messages generated by the AI assistant.
"""
SYSTEM = "system"
USER = "user"
ASSISTANT = "assistant"
class PromptOptimizerModelConfig(Base):
"""
Prompt Optimization Model Configuration.
This table stores system-level prompt configurations for each tenant.
The configuration defines the base system prompt used during prompt
optimization sessions and serves as a foundational instruction set
for the optimization process.
Each tenant may have one or more model configurations depending on
business requirements.
Table Name:
prompt_model_config
Columns:
id (UUID):
Primary key. Unique identifier for the prompt model configuration.
tenant_id (UUID):
Foreign key referencing `tenants.id`.
Identifies the tenant that owns this configuration.
system_prompt (Text):
The system-level prompt used to guide prompt optimization logic.
created_at (DateTime):
Timestamp indicating when the configuration was created.
updated_at (DateTime):
Timestamp indicating the last update time of the configuration.
Usage:
- Loaded when initializing a prompt optimization session
- Acts as the root system instruction for all subsequent prompts
"""
__tablename__ = "prompt_model_config"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, comment="Tenant ID")
# model_id = Column(UUID(as_uuid=True), nullable=False, comment="Model ID")
system_prompt = Column(Text, nullable=False, comment="System Prompt")
created_at = Column(DateTime, default=datetime.datetime.now, comment="Creation Time")
updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment="Update Time")
class PromptOptimizerSession(Base):
"""
Prompt Optimization Session Registry.
This table records high-level metadata for prompt optimization sessions.
Each record represents a single logical session initiated by a user
under a specific tenant.
The session acts as a container for multiple conversation messages
stored in the session history table.
Table Name:
prompt_opt_session_list
Columns:
id (UUID):
Public-facing session identifier used to group conversation history.
tenant_id (UUID):
Foreign key referencing `tenants.id`.
Identifies the tenant under which the session is created.
user_id (UUID):
Foreign key referencing `users.id`.
Identifies the user who initiated the session.
created_at (DateTime):
Timestamp indicating when the session was created.
Design Notes:
- This table intentionally does not store message content
- Message-level data is stored in `prompt_opt_session_history`
- Enables efficient session listing and pagination
"""
__tablename__ = "prompt_opt_session_list"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True, comment="Session ID")
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, comment="Tenant ID")
# app_id = Column(UUID(as_uuid=True), ForeignKey("apps.id"), nullable=False, comment="Application ID")
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, comment="User ID")
created_at = Column(DateTime, default=datetime.datetime.now, comment="Creation Time", index=True)
class PromptOptimizerSessionHistory(Base):
"""
Prompt Optimization Session Message History.
This table stores the complete conversational history of a prompt
optimization session, including system prompts, user inputs, and
assistant responses.
Each record represents a single message within a session, preserving
the chronological order of interactions.
Table Name:
prompt_opt_session_history
Columns:
id (UUID):
Primary key. Unique identifier for the message record.
tenant_id (UUID):
Foreign key referencing `tenants.id`.
Identifies the tenant under which the session operates.
session_id (UUID):
Logical session identifier linking messages to a session.
user_id (UUID):
Foreign key referencing `users.id`.
Identifies the user associated with the session.
message_role (Text):
Role of the message sender (e.g., system, user, assistant).
message_content (Text):
Raw message content generated or provided during the session.
prompt (Text):
The prompt snapshot used at the time of message generation.
created_at (DateTime):
Timestamp indicating when the message was created.
Design Notes:
- Supports full conversation replay and audit
- Enables prompt evolution tracking over time
- Indexed by creation time for efficient chronological queries
"""
__tablename__ = "prompt_opt_session_history"
__table_args__ = (
Index(
"ix_prompt_opt_session_history_session_user_created",
"session_id",
"user_id",
"created_at"
),
)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True)
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, comment="Tenant ID")
# app_id = Column(UUID(as_uuid=True), ForeignKey("apps.id"), nullable=False, comment="Application ID")
session_id = Column(UUID(as_uuid=True), ForeignKey("prompt_opt_session_list.id"),nullable=False, comment="Session ID")
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False, comment="User ID")
role = Column(String, nullable=False, comment="Message Role")
content = Column(Text, nullable=False, comment="Message Content")
# prompt = Column(Text, nullable=False, comment="Prompt")
created_at = Column(DateTime, default=datetime.datetime.now, comment="Creation Time", index=True)

View File

@@ -0,0 +1,229 @@
import uuid
from typing import Optional
from sqlalchemy.orm import Session
from app.core.logging_config import get_db_logger
from app.models.prompt_optimizer_model import (
PromptOptimizerModelConfig,
PromptOptimizerSession, PromptOptimizerSessionHistory, RoleType
)
db_logger = get_db_logger()
class PromptOptimizerModelConfigRepository:
"""Repository for managing prompt optimizer model configurations."""
def __init__(self, db: Session):
self.db = db
def get_by_tenant_id(self, tenant_id: uuid.UUID) -> Optional[PromptOptimizerModelConfig]:
"""
Retrieve the prompt optimizer model configuration for a specific tenant.
Args:
tenant_id (uuid.UUID): The unique identifier of the tenant.
Returns:
Optional[PromptOptimizerModelConfig]: The model configuration if found, else None.
"""
db_logger.debug(f"Get prompt optimization model configuration: tenant_id={tenant_id}")
try:
config = self.db.query(PromptOptimizerModelConfig).filter(
PromptOptimizerModelConfig.tenant_id == tenant_id,
# PromptOptimizerModelConfig.model_id == model_id
).first()
if config:
db_logger.debug(f"Prompt optimization model configuration found: (ID: {config.id})")
else:
db_logger.debug(f"Prompt optimization model configuration not found: tenant_id={tenant_id}")
return config
except Exception as e:
db_logger.error(
f"Error retrieving prompt optimization model configuration: tenant_id={tenant_id} - {str(e)}")
raise
def get_by_config_id(self, tenant_id: uuid.UUID, config_id: uuid.UUID) -> Optional[PromptOptimizerModelConfig]:
"""
Retrieve a specific prompt optimizer model configuration by config ID and tenant ID.
Args:
tenant_id (uuid.UUID): The unique identifier of the tenant.
config_id (uuid.UUID): The unique identifier of the model configuration.
Returns:
Optional[PromptOptimizerModelConfig]: The model configuration if found, else None.
"""
db_logger.debug(f"Get prompt optimization model configuration: config_id={config_id}, tenant_id={tenant_id}")
try:
model = self.db.query(PromptOptimizerModelConfig).filter(
PromptOptimizerModelConfig.tenant_id == tenant_id,
PromptOptimizerModelConfig.id == config_id
).first()
if model:
db_logger.debug(f"Prompt optimization model configuration found: (ID: {model.id})")
else:
db_logger.debug(f"Prompt optimization model configuration not found: config_id={config_id}")
return model
except Exception as e:
db_logger.error(
f"Error retrieving prompt optimization model configuration: model_id={config_id} - {str(e)}")
raise
def create_or_update(
self,
config_id: uuid.UUID,
tenant_id: uuid.UUID,
system_prompt: str,
) -> Optional[PromptOptimizerModelConfig]:
"""
Create a new or update an existing prompt optimizer model configuration.
If a configuration with the given config_id exists, it updates its system_prompt.
Otherwise, it creates a new configuration record.
Args:
config_id (uuid.UUID): The unique identifier for the configuration.
tenant_id (uuid.UUID): The tenant's unique identifier.
system_prompt (str): The system prompt content for prompt optimization.
Returns:
Optional[PromptOptimizerModelConfig]: The created or updated model configuration.
"""
db_logger.debug(f"Create/Update prompt optimization model configuration: tenant_id={tenant_id}")
existing_config = self.get_by_config_id(tenant_id, config_id)
if existing_config:
existing_config.system_prompt = system_prompt
self.db.commit()
self.db.refresh(existing_config)
db_logger.debug(f"Prompt optimization model configuration update: ID:{config_id}")
return existing_config
else:
config = PromptOptimizerModelConfig(
id=config_id,
# model_id=model_id,
tenant_id=tenant_id,
system_prompt=system_prompt
)
self.db.add(config)
self.db.commit()
self.db.refresh(config)
db_logger.debug(f"Prompt optimization model configuration created: ID:{config.id}")
return config
class PromptOptimizerSessionRepository:
"""Repository for managing prompt optimization sessions and session history."""
def __init__(self, db: Session):
self.db = db
def create_session(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID
) -> PromptOptimizerSession:
"""
Create a new prompt optimization session for a user and app.
Args:
tenant_id (uuid.UUID): The unique identifier of the tenant.
user_id (uuid.UUID): The unique identifier of the user.
Returns:
PromptOptimizerSession: The newly created session object.
"""
db_logger.debug(f"Create prompt optimization session: tenant_id={tenant_id}, user_id={user_id}")
try:
session = PromptOptimizerSession(
tenant_id=tenant_id,
user_id=user_id,
)
self.db.add(session)
self.db.commit()
self.db.refresh(session)
db_logger.debug(f"Prompt optimization session created: ID:{session.id}")
return session
except Exception as e:
db_logger.error(f"Error creating prompt optimization session: user_id={user_id} - {str(e)}")
raise
def get_session_history(
self,
session_id: uuid.UUID,
user_id: uuid.UUID
) -> list[type[PromptOptimizerSessionHistory]]:
"""
Retrieve all message history of a specific prompt optimization session.
Args:
session_id (uuid.UUID): The unique identifier of the session.
user_id (uuid.UUID): The unique identifier of the user.
Returns:
list[PromptOptimizerSessionHistory]: A list of session history records
ordered by creation time ascending.
"""
db_logger.debug(f"Get prompt optimization session history: "
f"user_id={user_id}, session_id={session_id}")
try:
# First get the internal session ID from the session list table
session = self.db.query(PromptOptimizerSession).filter(
PromptOptimizerSession.id == session_id,
PromptOptimizerSession.user_id == user_id
).first()
if not session:
return []
history = self.db.query(PromptOptimizerSessionHistory).filter(
PromptOptimizerSessionHistory.session_id == session.id,
PromptOptimizerSessionHistory.user_id == user_id
).order_by(PromptOptimizerSessionHistory.created_at.asc()).all()
return history
except Exception as e:
db_logger.error(f"Error retrieving prompt optimization session history: session_id={session_id} - {str(e)}")
raise
def create_message(
self,
tenant_id: uuid.UUID,
session_id: uuid.UUID,
user_id: uuid.UUID,
role: RoleType,
content: str,
) -> PromptOptimizerSessionHistory:
"""
Create a new message in the session history.
This method is a placeholder for future implementation.
"""
try:
# Get the session to ensure it exists and belongs to the user
session = self.db.query(PromptOptimizerSession).filter(
PromptOptimizerSession.id == session_id,
PromptOptimizerSession.user_id == user_id,
PromptOptimizerSession.tenant_id == tenant_id
).first()
if not session:
db_logger.error(f"Session {session_id} not found for user {user_id}")
raise ValueError(f"Session {session_id} not found for user {user_id}")
message = PromptOptimizerSessionHistory(
tenant_id=tenant_id,
session_id=session.id,
user_id=user_id,
role=role.value,
content=content,
)
self.db.add(message)
self.db.commit()
return message
except Exception as e:
db_logger.error(f"Error creating prompt optimization session history: session_id={session_id} - {str(e)}")
raise

View File

@@ -0,0 +1,99 @@
from pydantic import BaseModel, Field
from uuid import UUID
# =========================================
# API Request Schemas
# =========================================
class PromptOptMessage(BaseModel):
model_id: UUID = Field(
...,
description="Model ID"
)
message: str = Field(
...,
min_length=1,
description="User's input message"
)
current_prompt: str = Field(
default="",
description="currently optimized prompt"
)
class PromptOptModelSet(BaseModel):
id: UUID | None = Field(
default=None,
description="Configuration ID"
)
system_prompt: str = Field(
...,
description="System Prompt"
)
# =========================================
# Service Layer Results
# =========================================
class OptimizePromptResult(BaseModel):
prompt: str = Field(
...,
description="Optimized Prompt"
)
desc: str = Field(
...,
description="Description"
)
# =========================================
# API Response Schemas
# =========================================
class CreateSessionResponse(BaseModel):
model_config = {"from_attributes": True}
id: UUID = Field(
...,
description="Session ID"
)
class OptimizePromptResponse(BaseModel):
model_config = {"from_attributes": True}
prompt: str = Field(
...,
description="Optimized Prompt"
)
desc: str = Field(
...,
description="Description"
)
variables: list = Field(
...,
description="Variables"
)
class SessionMessage(BaseModel):
role: str = Field(
...,
description="Message role (user/assistant)"
)
content: str = Field(
...,
description="Message content"
)
class SessionHistoryResponse(BaseModel):
session_id: UUID = Field(
...,
description="Session ID"
)
messages: list[SessionMessage] = Field(
...,
description="List of messages in the session"
)

View File

@@ -0,0 +1,280 @@
import json
import re
import uuid
from langchain_core.prompts import ChatPromptTemplate
from sqlalchemy.orm import Session
from app.core.error_codes import BizCode
from app.core.exceptions import BusinessException
from app.core.logging_config import get_business_logger
from app.core.models import RedBearModelConfig
from app.core.models.llm import RedBearLLM
from app.models import ModelConfig, ModelApiKey, ModelType, PromptOptimizerSessionHistory
from app.models.prompt_optimizer_model import (
PromptOptimizerModelConfig,
PromptOptimizerSession,
RoleType
)
from app.repositories.model_repository import ModelConfigRepository
from app.repositories.prompt_optimizer_repository import (
PromptOptimizerModelConfigRepository,
PromptOptimizerSessionRepository
)
from app.schemas.prompt_optimizer_schema import OptimizePromptResult
logger = get_business_logger()
class PromptOptimizerService:
def __init__(self, db: Session):
self.db = db
def get_model_config(
self,
tenant_id: uuid.UUID,
model_id: uuid.UUID
) -> tuple[PromptOptimizerModelConfig, ModelConfig]:
"""
Retrieve the prompt optimizer model configuration and model configuration.
This method retrieves the prompt optimizer model configuration associated
with the specified model ID and tenant. It also fetches the corresponding
model configuration.
Args:
tenant_id (uuid.UUID): The unique identifier of the tenant.
model_id (uuid.UUID): The unique identifier of the prompt optimization model.
Returns:
tuple[PromptOptimzerModelConfig, ModelConfig]:
A tuple containing the prompt optimizer model configuration
and the corresponding model configuration.
Raises:
BusinessException: If the prompt optimizer model configuration does not exist.
BusinessException: If the model configuration does not exist.
"""
prompt_config = PromptOptimizerModelConfigRepository(self.db).get_by_tenant_id(
tenant_id
)
if not prompt_config:
raise BusinessException("提示词模型配置不存在", BizCode.NOT_FOUND)
model = ModelConfigRepository.get_by_id(
self.db, model_id, tenant_id=tenant_id
)
if not model:
raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND)
return prompt_config, model
def create_update_model_config(
self,
tenant_id: uuid.UUID,
config_id: uuid.UUID,
system_prompt: str,
) -> PromptOptimizerModelConfig:
"""
Create or update a prompt optimizer model configuration.
This method creates a new prompt optimizer model configuration or updates
an existing one identified by the given configuration ID. The configuration
defines the system prompt used for prompt optimization.
Args:
tenant_id (uuid.UUID): The unique identifier of the tenant.
config_id (uuid.UUID): The unique identifier of the configuration to create or update.
system_prompt (str): The system prompt content used for prompt optimization.
Returns:
PromptOptimzerModelConfig: The created or updated prompt optimizer model configuration.
"""
prompt_config = PromptOptimizerModelConfigRepository(self.db).create_or_update(
config_id=config_id,
tenant_id=tenant_id,
system_prompt=system_prompt,
)
return prompt_config
def create_session(
self,
tenant_id: uuid.UUID,
user_id: uuid.UUID
) -> PromptOptimizerSession:
"""
Create a new prompt optimization session.
This method initializes a new prompt optimization session for the specified
tenant, application, and user, and persists it to the database.
Args:
tenant_id (uuid.UUID): The unique identifier of the tenant.
user_id (uuid.UUID): The unique identifier of the user.
Returns:
PromptOptimzerSession: The newly created prompt optimization session.
"""
session = PromptOptimizerSessionRepository(self.db).create_session(
tenant_id=tenant_id,
user_id=user_id
)
return session
def get_session_message_history(
self,
session_id: uuid.UUID,
user_id: uuid.UUID
) -> list[tuple[str, str]]:
"""
Retrieve the chronological message history for a prompt optimization session.
This method queries the database to fetch all messages associated with a
specific prompt optimization session for a given user. Messages are returned
in chronological order and typically include both user inputs and
model-generated responses.
Args:
session_id (uuid.UUID): The unique identifier of the prompt optimization session.
user_id (uuid.UUID): The unique identifier of the user associated with the session.
Returns:
list[tuple[str, str]]: A list of tuples representing messages. Each tuple contains:
- role (str): The role of the message sender, e.g., 'system', 'user', or 'assistant'.
- content (str): The content of the message.
"""
history = PromptOptimizerSessionRepository(self.db).get_session_history(
session_id=session_id,
user_id=user_id
)
messages = []
for message in history:
messages.append((message.role, message.content))
return messages
async def optimize_prompt(
self,
tenant_id: uuid.UUID,
model_id: uuid.UUID,
session_id: uuid.UUID,
user_id: uuid.UUID,
current_prompt: str,
message: str
) -> OptimizePromptResult:
"""
Optimize a prompt using a prompt optimizer LLM.
This method uses a configured prompt optimizer model to refine an existing
prompt based on the user's requirements. The optimized prompt is generated
according to predefined system rules, including Jinja2 variable syntax and
a strict JSON output format.
Args:
tenant_id (uuid.UUID): The unique identifier of the tenant.
model_id (uuid.UUID): The unique identifier of the prompt optimizer model.
session_id (uuid.UUID): The unique identifier of the prompt optimization session.
user_id (uuid.UUID): The unique identifier of the user associated with the session.
current_prompt (str): The original prompt to be optimized.
message (str): The user's requirements or modification instructions.
Returns:
dict: A dictionary containing the optimized prompt and the description
of changes, in the following format:
{
"prompt": "<optimized_prompt>",
"desc": "<change_description>"
}
Raises:
BusinessException: If the model response cannot be parsed as valid JSON
or does not conform to the expected output format.
"""
prompt_config, model_config = self.get_model_config(tenant_id, model_id)
session_history = self.get_session_message_history(session_id=session_id, user_id=user_id)
# Create LLM instance
api_config: ModelApiKey = model_config.api_keys[0]
llm = RedBearLLM(RedBearModelConfig(
model_name=api_config.model_name,
provider=api_config.provider,
api_key=api_config.api_key,
base_url=api_config.api_base
), type=ModelType.from_str(model_config.type))
# build message
messages = [
# init system_prompt
(RoleType.SYSTEM.value, prompt_config.system_prompt),
# base model limit
(RoleType.SYSTEM.value,
"Optimization Rules:\n"
"1. Fully adjust the prompt content according to the user's requirements.\n"
"2. When the user requests the insertion of variables, you must use Jinja2 syntax {{variable_name}} "
"(the variable name should be determined based on the user's requirement).\n"
"3. Keep the prompt logic clear and instructions explicit.\n"
"4. Ensure that the modified prompt can be directly used.\n\n"
"Output Requirements:\n"
"Provide the result in JSON format, containing exactly two fields:\n"
" - prompt: The modified prompt (string).\n"
" - desc: A response addressing the user's optimization request (string).")
]
messages.extend(session_history[:-1]) # last message is current message
user_message_template = ChatPromptTemplate.from_messages([
(RoleType.USER.value, "[current_prompt]\n{current_prompt}\n[user_require]\n{message}")
])
formatted_user_message = user_message_template.format(current_prompt=current_prompt, message=message)
messages.extend([(RoleType.USER.value, formatted_user_message)])
logger.info(f"Prompt optimization message: {messages}")
result = await llm.ainvoke(messages)
try:
data_dict = json.loads(result.content)
model_resp = OptimizePromptResult.model_validate(data_dict)
except Exception as e:
logger.error(f"Failed to parse model reponse to json - Error: {str(e)}", exc_info=True)
raise BusinessException("Failed to parse model response", BizCode.PARSER_NOT_SUPPORTED)
return model_resp
@staticmethod
def parser_prompt_variables(prompt: str):
try:
pattern = r'\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
matches = re.findall(pattern, prompt)
variables = list(set(matches))
return variables
except Exception as e:
logger.error(f"Failed to parse prompt variables - Error: {str(e)}", exc_info=True)
raise BusinessException("Failed to parse prompt variables", BizCode.PARSER_NOT_SUPPORTED)
@staticmethod
def fill_prompt_variables(prompt: str, variables: dict[str, str]):
try:
pattern = r'\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
def replace_var(match):
var_name = match.group(1)
return variables.get(var_name, match.group(0))
result = re.sub(pattern, replace_var, prompt)
return result
except Exception as e:
logger.error(f"Failed to fill prompt variables - Error: {str(e)}", exc_info=True)
raise BusinessException("Failed to fill prompt variables", BizCode.PARSER_NOT_SUPPORTED)
def create_message(
self,
tenant_id: uuid.UUID,
session_id: uuid.UUID,
user_id: uuid.UUID,
role: RoleType,
content: str
) -> PromptOptimizerSessionHistory:
"""Insert Message to Session History"""
message = PromptOptimizerSessionRepository(self.db).create_message(
tenant_id=tenant_id,
session_id=session_id,
user_id=user_id,
role=role,
content=content
)
return message

View File

@@ -0,0 +1,132 @@
"""202512151530
Revision ID: 2a46f1e9a590
Revises: 94a98e279951
Create Date: 2025-12-15 15:27:33.975790
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '2a46f1e9a590'
down_revision: Union[str, None] = '94a98e279951'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def table_exists(table_name: str) -> bool:
"""检查表是否存在"""
bind = op.get_bind()
inspector = inspect(bind)
return table_name in inspector.get_table_names()
def column_exists(table_name: str, column_name: str) -> bool:
"""检查列是否存在"""
bind = op.get_bind()
inspector = inspect(bind)
if not table_exists(table_name):
return False
columns = [col['name'] for col in inspector.get_columns(table_name)]
return column_name in columns
def index_exists(table_name: str, index_name: str) -> bool:
"""检查索引是否存在"""
bind = op.get_bind()
inspector = inspect(bind)
if not table_exists(table_name):
return False
indexes = [idx['name'] for idx in inspector.get_indexes(table_name)]
return index_name in indexes
def constraint_exists(table_name: str, constraint_name: str) -> bool:
"""检查约束是否存在(外键、唯一约束等)"""
bind = op.get_bind()
inspector = inspect(bind)
if not table_exists(table_name):
return False
# 检查外键约束
foreign_keys = [fk['name'] for fk in inspector.get_foreign_keys(table_name) if fk['name']]
if constraint_name in foreign_keys:
return True
# 检查唯一约束
unique_constraints = [uc['name'] for uc in inspector.get_unique_constraints(table_name) if uc['name']]
if constraint_name in unique_constraints:
return True
# 检查检查约束
check_constraints = [cc['name'] for cc in inspector.get_check_constraints(table_name) if cc['name']]
if constraint_name in check_constraints:
return True
return False
def trigger_exists(trigger_name: str) -> bool:
"""检查触发器是否存在PostgreSQL"""
bind = op.get_bind()
result = bind.execute(sa.text(
"SELECT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = :trigger_name)"
), {"trigger_name": trigger_name})
return result.scalar()
def sequence_exists(sequence_name: str) -> bool:
"""检查序列是否存在PostgreSQL"""
bind = op.get_bind()
result = bind.execute(sa.text(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = :sequence_name)"
), {"sequence_name": sequence_name})
return result.scalar()
def enum_exists(enum_name: str) -> bool:
"""检查枚举类型是否存在PostgreSQL"""
bind = op.get_bind()
result = bind.execute(sa.text(
"SELECT EXISTS (SELECT 1 FROM pg_type WHERE typname = :enum_name)"
), {"enum_name": enum_name})
return result.scalar()
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('api_keys', schema=None) as batch_op:
batch_op.add_column(sa.Column('api_key', sa.String(length=255), nullable=False, comment='API Key 明文'))
batch_op.alter_column('scopes',
existing_type=postgresql.JSONB(astext_type=sa.Text()),
nullable=True,
existing_comment='权限范围列表')
batch_op.drop_index(batch_op.f('ix_api_keys_key_hash'))
batch_op.create_index(batch_op.f('ix_api_keys_api_key'), ['api_key'], unique=True)
batch_op.drop_column('key_hash')
batch_op.drop_column('resource_type')
batch_op.drop_column('key_prefix')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('api_keys', schema=None) as batch_op:
batch_op.add_column(sa.Column('key_prefix', sa.VARCHAR(length=20), autoincrement=False, nullable=False, comment='Key 前缀'))
batch_op.add_column(sa.Column('resource_type', sa.VARCHAR(length=50), autoincrement=False, nullable=True, comment='资源类型'))
batch_op.add_column(sa.Column('key_hash', sa.VARCHAR(length=255), autoincrement=False, nullable=False, comment='Key 哈希值'))
batch_op.drop_index(batch_op.f('ix_api_keys_api_key'))
batch_op.create_index(batch_op.f('ix_api_keys_key_hash'), ['key_hash'], unique=True)
batch_op.alter_column('scopes',
existing_type=postgresql.JSONB(astext_type=sa.Text()),
nullable=False,
existing_comment='权限范围列表')
batch_op.drop_column('api_key')
# ### end Alembic commands ###

View File

@@ -0,0 +1,212 @@
"""202512151837
Revision ID: 64ddbf3c3bcc
Revises: 2a46f1e9a590
Create Date: 2025-12-15 18:37:42.649720
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy import inspect
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '64ddbf3c3bcc'
down_revision: Union[str, None] = '2a46f1e9a590'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def table_exists(table_name: str) -> bool:
"""检查表是否存在"""
bind = op.get_bind()
inspector = inspect(bind)
return table_name in inspector.get_table_names()
def column_exists(table_name: str, column_name: str) -> bool:
"""检查列是否存在"""
bind = op.get_bind()
inspector = inspect(bind)
if not table_exists(table_name):
return False
columns = [col['name'] for col in inspector.get_columns(table_name)]
return column_name in columns
def index_exists(table_name: str, index_name: str) -> bool:
"""检查索引是否存在"""
bind = op.get_bind()
inspector = inspect(bind)
if not table_exists(table_name):
return False
indexes = [idx['name'] for idx in inspector.get_indexes(table_name)]
return index_name in indexes
def constraint_exists(table_name: str, constraint_name: str) -> bool:
"""检查约束是否存在(外键、唯一约束等)"""
bind = op.get_bind()
inspector = inspect(bind)
if not table_exists(table_name):
return False
# 检查外键约束
foreign_keys = [fk['name'] for fk in inspector.get_foreign_keys(table_name) if fk['name']]
if constraint_name in foreign_keys:
return True
# 检查唯一约束
unique_constraints = [uc['name'] for uc in inspector.get_unique_constraints(table_name) if uc['name']]
if constraint_name in unique_constraints:
return True
# 检查检查约束
check_constraints = [cc['name'] for cc in inspector.get_check_constraints(table_name) if cc['name']]
if constraint_name in check_constraints:
return True
return False
def trigger_exists(trigger_name: str) -> bool:
"""检查触发器是否存在PostgreSQL"""
bind = op.get_bind()
result = bind.execute(sa.text(
"SELECT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = :trigger_name)"
), {"trigger_name": trigger_name})
return result.scalar()
def sequence_exists(sequence_name: str) -> bool:
"""检查序列是否存在PostgreSQL"""
bind = op.get_bind()
result = bind.execute(sa.text(
"SELECT EXISTS (SELECT 1 FROM pg_class WHERE relkind = 'S' AND relname = :sequence_name)"
), {"sequence_name": sequence_name})
return result.scalar()
def enum_exists(enum_name: str) -> bool:
"""检查枚举类型是否存在PostgreSQL"""
bind = op.get_bind()
result = bind.execute(sa.text(
"SELECT EXISTS (SELECT 1 FROM pg_type WHERE typname = :enum_name)"
), {"enum_name": enum_name})
return result.scalar()
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('workflow_configs',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('app_id', sa.UUID(), nullable=False),
sa.Column('nodes', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('edges', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('variables', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('execution_config', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('triggers', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['app_id'], ['apps.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('workflow_configs', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_workflow_configs_app_id'), ['app_id'], unique=True)
batch_op.create_index(batch_op.f('ix_workflow_configs_id'), ['id'], unique=False)
op.create_table('workflow_executions',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('workflow_config_id', sa.UUID(), nullable=False),
sa.Column('app_id', sa.UUID(), nullable=False),
sa.Column('conversation_id', sa.UUID(), nullable=True),
sa.Column('execution_id', sa.String(length=100), nullable=False),
sa.Column('trigger_type', sa.String(length=20), nullable=False),
sa.Column('triggered_by', sa.UUID(), nullable=True),
sa.Column('input_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('output_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('context', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('error_node_id', sa.String(length=100), nullable=True),
sa.Column('started_at', sa.DateTime(), nullable=False),
sa.Column('completed_at', sa.DateTime(), nullable=True),
sa.Column('elapsed_time', sa.Float(), nullable=True),
sa.Column('token_usage', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('meta_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['app_id'], ['apps.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['conversation_id'], ['conversations.id'], ondelete='SET NULL'),
sa.ForeignKeyConstraint(['triggered_by'], ['users.id'], ),
sa.ForeignKeyConstraint(['workflow_config_id'], ['workflow_configs.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('workflow_executions', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_workflow_executions_app_id'), ['app_id'], unique=False)
batch_op.create_index(batch_op.f('ix_workflow_executions_conversation_id'), ['conversation_id'], unique=False)
batch_op.create_index(batch_op.f('ix_workflow_executions_execution_id'), ['execution_id'], unique=True)
batch_op.create_index(batch_op.f('ix_workflow_executions_id'), ['id'], unique=False)
batch_op.create_index(batch_op.f('ix_workflow_executions_started_at'), ['started_at'], unique=False)
batch_op.create_index(batch_op.f('ix_workflow_executions_status'), ['status'], unique=False)
batch_op.create_index(batch_op.f('ix_workflow_executions_workflow_config_id'), ['workflow_config_id'], unique=False)
op.create_table('workflow_node_executions',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('execution_id', sa.UUID(), nullable=False),
sa.Column('node_id', sa.String(length=100), nullable=False),
sa.Column('node_type', sa.String(length=20), nullable=False),
sa.Column('node_name', sa.String(length=100), nullable=True),
sa.Column('execution_order', sa.Integer(), nullable=False),
sa.Column('retry_count', sa.Integer(), nullable=False),
sa.Column('input_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('output_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('started_at', sa.DateTime(), nullable=False),
sa.Column('completed_at', sa.DateTime(), nullable=True),
sa.Column('elapsed_time', sa.Float(), nullable=True),
sa.Column('token_usage', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('cache_hit', sa.Boolean(), nullable=True),
sa.Column('cache_key', sa.String(length=255), nullable=True),
sa.Column('meta_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['execution_id'], ['workflow_executions.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('workflow_node_executions', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_workflow_node_executions_execution_id'), ['execution_id'], unique=False)
batch_op.create_index(batch_op.f('ix_workflow_node_executions_id'), ['id'], unique=False)
batch_op.create_index(batch_op.f('ix_workflow_node_executions_node_id'), ['node_id'], unique=False)
batch_op.create_index(batch_op.f('ix_workflow_node_executions_status'), ['status'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('workflow_node_executions', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_workflow_node_executions_status'))
batch_op.drop_index(batch_op.f('ix_workflow_node_executions_node_id'))
batch_op.drop_index(batch_op.f('ix_workflow_node_executions_id'))
batch_op.drop_index(batch_op.f('ix_workflow_node_executions_execution_id'))
op.drop_table('workflow_node_executions')
with op.batch_alter_table('workflow_executions', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_workflow_executions_workflow_config_id'))
batch_op.drop_index(batch_op.f('ix_workflow_executions_status'))
batch_op.drop_index(batch_op.f('ix_workflow_executions_started_at'))
batch_op.drop_index(batch_op.f('ix_workflow_executions_id'))
batch_op.drop_index(batch_op.f('ix_workflow_executions_execution_id'))
batch_op.drop_index(batch_op.f('ix_workflow_executions_conversation_id'))
batch_op.drop_index(batch_op.f('ix_workflow_executions_app_id'))
op.drop_table('workflow_executions')
with op.batch_alter_table('workflow_configs', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_workflow_configs_id'))
batch_op.drop_index(batch_op.f('ix_workflow_configs_app_id'))
op.drop_table('workflow_configs')
# ### end Alembic commands ###

View File

@@ -0,0 +1,74 @@
"""202512171846
Revision ID: 87a6537b4074
Revises: 64ddbf3c3bcc
Create Date: 2025-12-17 18:45:16.574812
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '87a6537b4074'
down_revision: Union[str, None] = '64ddbf3c3bcc'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('prompt_model_config',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False, comment='Tenant ID'),
sa.Column('system_prompt', sa.Text(), nullable=False, comment='System Prompt'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='Creation Time'),
sa.Column('updated_at', sa.DateTime(), nullable=True, comment='Update Time'),
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_prompt_model_config_id'), 'prompt_model_config', ['id'], unique=False)
op.create_table('prompt_opt_session_list',
sa.Column('id', sa.UUID(), nullable=False, comment='Session ID'),
sa.Column('tenant_id', sa.UUID(), nullable=False, comment='Tenant ID'),
sa.Column('user_id', sa.UUID(), nullable=False, comment='User ID'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='Creation Time'),
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_prompt_opt_session_list_created_at'), 'prompt_opt_session_list', ['created_at'], unique=False)
op.create_index(op.f('ix_prompt_opt_session_list_id'), 'prompt_opt_session_list', ['id'], unique=False)
op.create_table('prompt_opt_session_history',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('tenant_id', sa.UUID(), nullable=False, comment='Tenant ID'),
sa.Column('session_id', sa.UUID(), nullable=False, comment='Session ID'),
sa.Column('user_id', sa.UUID(), nullable=False, comment='User ID'),
sa.Column('role', sa.String(), nullable=False, comment='Message Role'),
sa.Column('content', sa.Text(), nullable=False, comment='Message Content'),
sa.Column('created_at', sa.DateTime(), nullable=True, comment='Creation Time'),
sa.ForeignKeyConstraint(['session_id'], ['prompt_opt_session_list.id'], ),
sa.ForeignKeyConstraint(['tenant_id'], ['tenants.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_prompt_opt_session_history_created_at'), 'prompt_opt_session_history', ['created_at'], unique=False)
op.create_index(op.f('ix_prompt_opt_session_history_id'), 'prompt_opt_session_history', ['id'], unique=False)
op.create_index('ix_prompt_opt_session_history_session_user_created', 'prompt_opt_session_history', ['session_id', 'user_id', 'created_at'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_prompt_opt_session_history_session_user_created', table_name='prompt_opt_session_history')
op.drop_index(op.f('ix_prompt_opt_session_history_id'), table_name='prompt_opt_session_history')
op.drop_index(op.f('ix_prompt_opt_session_history_created_at'), table_name='prompt_opt_session_history')
op.drop_table('prompt_opt_session_history')
op.drop_index(op.f('ix_prompt_opt_session_list_id'), table_name='prompt_opt_session_list')
op.drop_index(op.f('ix_prompt_opt_session_list_created_at'), table_name='prompt_opt_session_list')
op.drop_table('prompt_opt_session_list')
op.drop_index(op.f('ix_prompt_model_config_id'), table_name='prompt_model_config')
op.drop_table('prompt_model_config')
# ### end Alembic commands ###

6
web/.gitignore vendored
View File

@@ -24,9 +24,3 @@ dist-ssr
*.sw?
package-lock.json
# 文档和截图(不上传到仓库)
操作说明.md
记忆熊系统功能使用说明.md
截图清单.md
images/

View File

@@ -10,10 +10,14 @@
"preview": "vite preview"
},
"dependencies": {
"@antv/layout": "^1.2.14-beta.8",
"@antv/x6": "^3.0.1",
"@antv/x6-react-shape": "^3.0.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@lexical/react": "^0.39.0",
"antd": "^5.27.4",
"axios": "^1.12.2",
"clsx": "^2.1.1",
@@ -23,6 +27,8 @@
"echarts": "^5.6.0",
"echarts-for-react": "^3.0.2",
"i18next": "^25.6.0",
"js-yaml": "^4.1.1",
"lexical": "^0.39.0",
"mermaid": "^11.12.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -31,7 +37,6 @@
"react-markdown": "^10.1.0",
"react-router-dom": "^6.22.0",
"react-syntax-highlighter": "^16.1.0",
"reactflow": "^11.11.4",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"remark-breaks": "^4.0.0",
@@ -46,6 +51,7 @@
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.14",
"@types/crypto-js": "^4.2.2",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.6.0",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",

View File

@@ -31,6 +31,8 @@ import { cookieUtils } from './utils/request';
function App() {
const { t } = useTranslation();
const { locale, language, timeZone } = useI18n()

33
web/src/api/apiKey.ts Normal file
View File

@@ -0,0 +1,33 @@
import { request } from '@/utils/request'
import type { ApiKey } from '@/views/ApiKeyManagement/types'
// API Key列表
export const getApiKeyListUrl = '/apikeys'
export const getApiKeyList = (data: Record<string, unknown>) => {
return request.get(getApiKeyListUrl, data)
}
// API Key详情
export const getApiKey = (id: string) => {
return request.get(`/apikeys/${id}`)
}
// 创建API Key
export const createApiKey = (values: ApiKey) => {
return request.post('/apikeys', values)
}
// 更新API Key
export const updateApiKey = (id: string, values: ApiKey) => {
return request.put(`/apikeys/${id}`, values)
}
// 删除 API Key
export const deleteApiKey = (id: string) => {
return request.delete(`/apikeys/${id}`)
}
// 使用统计
export const getApiKeyStats = (app_key_id: string) => {
return request.get(`/apikeys/${app_key_id}/stats`)
}

View File

@@ -1,7 +1,9 @@
import { request } from '@/utils/request'
import type { Application } from '@/views/ApplicationManagement/types'
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
import type { Config } from '@/views/ApplicationConfig/types'
import { handleSSE } from '@/utils/stream'
import { handleSSE, type SSEMessage } from '@/utils/stream'
import type { QueryParams } from '@/views/Conversation/types'
import type { WorkflowConfig } from '@/views/Workflow/types'
// 应用列表
export const getApplicationListUrl = '/apps'
@@ -12,20 +14,24 @@ export const getApplicationList = (data: Record<string, unknown>) => {
export const getApplicationConfig = (id: string) => {
return request.get(`/apps/${id}/config`)
}
// 获取集群应配置
// 获取集群应配置
export const getMultiAgentConfig = (id: string) => {
return request.get(`/apps/${id}/multi-agent`)
}
// 获取 workflow应用配置
export const getWorkflowConfig = (id: string) => {
return request.get(`/apps/${id}/workflow`)
}
// 应用详情
export const getApplication = (id: string) => {
return request.get(`/apps/${id}`)
}
// 更新应用
export const updateApplication = (id: string, values: Application) => {
export const updateApplication = (id: string, values: ApplicationModalData) => {
return request.put(`/apps/${id}`, values)
}
// 创建应用
export const addApplication = (values: Application) => {
export const addApplication = (values: ApplicationModalData) => {
return request.post('/apps', values)
}
// 保存Agent配置
@@ -36,11 +42,15 @@ export const saveAgentConfig = (app_id: string, values: Config) => {
export const saveMultiAgentConfig = (app_id: string, values: Config) => {
return request.put(`/apps/${app_id}/multi-agent`, values)
}
// 保存workflow配置
export const saveWorkflowConfig = (app_id: string, values: WorkflowConfig) => {
return request.put(`/apps/${app_id}/workflow`, values)
}
// 模型比对试运行
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: string) => void) => {
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage)
}
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: string) => void) => {
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage)
}
// 删除应用
@@ -76,18 +86,7 @@ export const getConversationHistory = (share_token: string, data: { page: number
})
}
// 发送体验对话
export const sendConversation = (share_token: string, values: {
message: string;
web_search: boolean;
memory: boolean;
stream: boolean;
conversation_id: string | null;
}, onMessage, shareToken: string) => {
// return request.post(`/public/share/chat`, values, {
// headers: {
// 'Authorization': `Bearer ${localStorage.getItem(`shareToken_${share_token}`)}`
// }
// })
export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string) => {
return handleSSE(`/public/share/chat`, values, onMessage, {
headers: {
'Authorization': `Bearer ${shareToken}`

View File

@@ -8,7 +8,14 @@ import type {
import type {
ConfigForm as ExtractionConfigForm
} from '@/views/MemoryExtractionEngine/types'
import type {
ConfigForm as EmotionConfig
} from '@/views/EmotionEngine/types'
import type {
ConfigForm as SelfReflectionEngineConfig
} from '@/views/SelfReflectionEngine/types'
import type { TestParams } from '@/views/MemoryConversation'
import { handleSSE, type SSEMessage } from '@/utils/stream'
// 记忆对话
export const readService = (query: TestParams) => {
@@ -95,6 +102,23 @@ export const getChunkInsight = (end_user_id: string) => {
export const getRagContent = (end_user_id: string) => {
return request.get(`/dashboard/rag_content`, { end_user_id, limit: 20 })
}
// 情感分布分析
export const getWordCloud = (group_id: string) => {
return request.post(`/memory/emotion/wordcloud`, { group_id, limit: 20 })
}
// 高频情绪关键词
export const getEmotionTags = (group_id: string) => {
return request.post(`/memory/emotion/tags`, { group_id, limit: 20 })
}
// 情绪健康指数
export const getEmotionHealth = (group_id: string) => {
return request.post(`/memory/emotion/health`, { group_id, limit: 20 })
}
// 个性化建议
export const getEmotionSuggestions = (group_id: string) => {
return request.post(`/memory/emotion/suggestions`, { group_id, limit: 20 })
}
/*************** end 用户记忆 相关接口 ******************************/
/****************** 记忆管理 相关接口 *******************************/
@@ -132,9 +156,30 @@ export const updateMemoryExtractionConfig = (values: ExtractionConfigForm) => {
return request.post('/memory-storage/update_config_extracted', values)
}
// 记忆萃取引擎-试运行
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string }) => {
return request.post('/memory-storage/pilot_run', values)
export const pilotRunMemoryExtractionConfig = (values: { config_id: number | string; dialogue_text: string; }, onMessage?: (data: SSEMessage[]) => void) => {
return handleSSE('/memory-storage/pilot_run', values, onMessage)
}
// 情绪引擎-获取配置
export const getMemoryEmotionConfig = (config_id: number | string) => {
return request.get('/memory/emotion/read_config', { config_id: config_id })
}
// 情绪引擎-更新配置
export const updateMemoryEmotionConfig = (values: EmotionConfig) => {
return request.post('/memory/emotion/updated_config', values)
}
// 反思引擎-获取配置
export const getMemoryReflectionConfig = (config_id: number | string) => {
return request.get('/memory/reflection/configs', { config_id: config_id })
}
// 反思引擎-更新配置
export const updateMemoryReflectionConfig = (values: SelfReflectionEngineConfig) => {
return request.post('/memory/reflection/save', values)
}
// 反思引擎-试运行
export const pilotRunMemoryReflectionConfig = (values: { config_id: number | string; dialogue_text: string; }) => {
return request.get('/memory/reflection/run', values)
}
/*************** end 记忆管理 相关接口 ******************************/

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 775 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 792 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

View File

@@ -34,12 +34,12 @@ const ButtonCheckbox: FC<ButtonCheckboxProps> = ({
}
return (
<div className={clsx("rb:flex rb:items-center rb:border rb:rounded-[8px] rb:px-[8px] rb:text-[12px] rb:h-[24px] rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
<div className={clsx("rb:flex rb:items-center rb:border rb:rounded-lg rb:px-2 rb:text-[12px] rb:h-6 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
"rb:bg-[rgba(21,94,239,0.06)] rb:border-[#155EEF] rb:text-[#155EEF]": checked,
"rb:border-[#DFE4ED] rb:text-[#212332]": !checked,
})} onClick={handleChange}>
{icon && !checked && <img src={icon} className="rb:w-[16px] rb:h-[16px] rb:mr-[4px]" />}
{checkedIcon && checked && <img src={checkedIcon} className="rb:w-[16px] rb:h-[16px] rb:mr-[4px]" />}
{icon && !checked && <img src={icon} className="rb:w-4 rb:h-4 rb:mr-1" />}
{checkedIcon && checked && <img src={checkedIcon} className="rb:w-4 rb:h-4 rb:mr-1" />}
{children}
</div>
);

View File

@@ -0,0 +1,84 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:17
* @Last Modified by: ZhaoYing
* @Last Modified time: 2025-12-11 13:40:18
*/
import { type FC, useRef, useEffect } from 'react'
import clsx from 'clsx'
import Markdown from '@/components/Markdown'
import type { ChatContentProps } from './types'
/**
* 聊天内容显示组件
* 负责渲染聊天消息列表,支持不同角色的消息样式和自动滚动
*/
const ChatContent: FC<ChatContentProps> = ({
classNames,
contentClassNames,
data = [],
streamLoading = false,
empty,
labelPosition = 'bottom',
labelFormat,
errorDesc
}) => {
// 滚动容器引用,用于控制自动滚动到底部
const scrollContainerRef = useRef<(HTMLDivElement | null)>(null)
// 当数据变化时,自动滚动到底部显示最新消息
useEffect(() => {
setTimeout(() => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
}
}, 0);
}, [data])
return (
<div ref={scrollContainerRef} className={clsx("rb:relative rb:overflow-y-auto", classNames)}>
{data.length === 0
? empty // 显示空状态
: data.map((item, index) => (
<div key={index} className={clsx("rb:relative", {
'rb:mt-6': index !== 0, // 非第一条消息添加上边距
'rb:right-0 rb:text-right': item.role === 'user', // 用户消息右对齐
'rb:left-0 rb:text-left': item.role === 'assistant', // 助手消息左对齐
})}>
{/* 流式加载时且内容为空则不显示 */}
{streamLoading && item.content === ''
? null
: <>
{/* 顶部标签(如时间戳、用户名等) */}
{labelPosition === 'top' &&
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:font-regular">
{labelFormat(item)}
</div>
}
{/* 消息气泡框 */}
<div className={clsx('rb:border rb:text-left rb:rounded-lg rb:mt-1.5 rb:leading-4.5 rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-100', contentClassNames, {
// 错误消息样式内容为null且非助手消息
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': errorDesc && item.role === 'assistant' && item.content === null,
// 助手消息样式
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': item.role === 'user',
// 用户消息样式
'rb:bg-[#FFFFFF] rb:border-[#EBEBEB]': item.role === 'assistant' && (item.content || item.content === ''),
})}>
{/* 使用Markdown组件渲染消息内容 */}
<Markdown content={item.content ?? errorDesc ?? ''} />
</div>
{/* 底部标签(如时间戳、用户名等) */}
{labelPosition === 'bottom' &&
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-4 rb:font-regular">
{labelFormat(item)}
</div>
}
</>
}
</div>
))
}
</div>
)
}
export default ChatContent

View File

@@ -0,0 +1,80 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:14
* @Last Modified by: ZhaoYing
* @Last Modified time: 2025-12-20 15:38:40
*/
import { useEffect } from 'react'
import { Flex, Input, Form } from 'antd'
import SendIcon from '@/assets/images/conversation/send.svg'
import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg'
import LoadingIcon from '@/assets/images/conversation/loading.svg'
import type { ChatInputProps } from './types'
/**
* 聊天输入框组件
* 提供消息输入、发送功能,支持键盘快捷键和加载状态显示
*/
const ChatInput = ({ message, onChange, onSend, loading, children }: ChatInputProps) => {
const [form] = Form.useForm()
// 监听表单值变化,用于控制发送按钮状态
const values = Form.useWatch([], form);
// 当外部message为空时清空表单
useEffect(() => {
if (!message) {
form.setFieldsValue({
message: undefined,
})
}
}, [form, message])
// 当加载状态时,清空输入框
useEffect(() => {
if (loading) {
form.setFieldsValue({
message: undefined,
})
}
}, [loading])
return (
<div className="rb:absolute rb:bottom-3 rb:left-0 rb:right-0">
<Flex vertical justify="space-between" className="rb:border rb:border-[#DFE4ED] rb:rounded-xl rb:min-h-30">
{/* 消息输入表单 */}
<Form form={form} layout="vertical">
<Form.Item name="message" noStyle>
<Input.TextArea
className="rb:m-[10px_12px_10px_12px]! rb:p-0! rb:w-[calc(100%-24px)]! rb:flex-[1_1_auto]"
variant="borderless"
autoSize={{ minRows: 2, maxRows: 2 }}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => {
// Enter键发送Shift+Enter换行
if (e.key === 'Enter' && !e.shiftKey && (e.target as HTMLTextAreaElement).value?.trim() !== '' && !loading) {
e.preventDefault();
onSend();
}
}}
/>
</Form.Item>
</Form>
{/* 底部操作区域 */}
<Flex align="center" justify="space-between" className="rb:m-[0_10px_10px_10px]!">
{/* 子组件内容(如按钮等) */}
{children}
{/* 发送按钮 - 根据状态显示不同图标 */}
{loading
? <img src={LoadingIcon} className="rb:w-5.5 rb:h-5.5 rb:cursor-pointer" />
: !values || !values?.message || values?.message?.trim() === ''
? <img src={SendDisabledIcon} className="rb:w-5.5 rb:h-5.5 rb:cursor-pointer" />
: <img src={SendIcon} className="rb:w-5.5 rb:h-5.5 rb:cursor-pointer" onClick={onSend} />
}
</Flex>
</Flex>
</div>
)
}
export default ChatInput

View File

@@ -0,0 +1,47 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:46:09
* @Last Modified by: ZhaoYing
* @Last Modified time: 2025-12-11 13:43:51
*/
import { type FC } from 'react'
import ChatInput from './ChatInput'
import type { ChatProps } from './types'
import ChatContent from './ChatContent'
/**
* 聊天组件 - 主要组件,由内容区域和输入框组成
* 提供完整的聊天界面功能,包括消息显示和输入交互
*/
const Chat: FC<ChatProps> = ({
empty,
data,
onChange,
onSend,
streamLoading = false,
loading,
contentClassName = '',
children,
labelFormat,
errorDesc
}) => {
return (
<div className="rb:h-full rb:relative rb:pt-2">
{/* 聊天内容显示区域 */}
<ChatContent
classNames={contentClassName}
data={data}
streamLoading={streamLoading}
empty={empty}
labelFormat={labelFormat}
errorDesc={errorDesc}
/>
{/* 聊天输入框区域 */}
<ChatInput onChange={onChange} onSend={onSend} loading={loading}>
{children}
</ChatInput>
</div>
)
}
export default Chat

View File

@@ -0,0 +1,84 @@
/*
* @Author: ZhaoYing
* @Date: 2025-12-10 16:45:54
* @Last Modified by: ZhaoYing
* @Last Modified time: 2025-12-11 13:43:52
*/
import { type ReactNode } from 'react'
/**
* 聊天消息项接口
*/
export interface ChatItem {
/** 消息唯一标识 */
id?: string;
/** 会话ID */
conversation_id?: string | null;
/** 消息角色:用户或助手 */
role?: 'user' | 'assistant';
/** 消息内容 */
content?: string | null;
/** 创建时间 */
created_at?: number | string
}
/**
* 聊天组件主要属性接口
*/
export interface ChatProps {
/** 空状态显示内容 */
empty?: ReactNode;
/** 聊天数据列表 */
data: ChatItem[];
/** 输入内容变化回调 */
onChange: (message: string) => void;
/** 发送消息回调 */
onSend: () => void;
/** 流式加载状态 */
streamLoading?: boolean;
/** 加载状态 */
loading: boolean;
/** 内容区域自定义样式类名 */
contentClassName?: string;
/** 子组件内容 */
children?: ReactNode;
/** 标签格式化函数 */
labelFormat: (item: ChatItem) => any;
errorDesc?: string;
}
/**
* 聊天输入框组件属性接口
*/
export interface ChatInputProps {
/** 当前输入消息 */
message?: string;
/** 输入内容变化回调 */
onChange: (message: string) => void;
/** 发送消息回调 */
onSend: () => void;
/** 加载状态 */
loading: boolean;
/** 子组件内容 */
children?: ReactNode;
}
/**
* 聊天内容区域组件属性接口
*/
export interface ChatContentProps {
/** 自定义样式类名 */
classNames?: string | Record<string, boolean>;
contentClassNames?: string | Record<string, boolean>;
/** 聊天数据列表 */
data: ChatItem[];
/** 流式加载状态 */
streamLoading: boolean;
/** 空状态显示内容 */
empty?: ReactNode;
/** 标签位置:顶部或底部 */
labelPosition?: 'top' | 'bottom';
/** 标签格式化函数 */
labelFormat: (item: ChatItem) => any;
errorDesc?: string;
}

View File

@@ -9,7 +9,7 @@ interface ApiResponse<T> {
items?: T[];
}
interface CustomSelectProps {
interface CustomSelectProps extends Omit<SelectProps, 'filterOption'> {
url: string;
params?: Record<string, unknown>;
valueKey?: string;

View File

@@ -6,6 +6,7 @@ interface EmptyProps {
url?: string;
size?: number | number[];
title?: string;
isNeedSubTitle?: boolean;
subTitle?: string;
className?: string;
}
@@ -13,6 +14,7 @@ const Empty: FC<EmptyProps> = ({
url,
size = 200,
title,
isNeedSubTitle = true,
subTitle,
className = '',
}) => {
@@ -20,12 +22,12 @@ const Empty: FC<EmptyProps> = ({
const width = Array.isArray(size) ? size[0] : size ? size : url ? 200 : 88;
const height = Array.isArray(size) ? size[1] : size ? size : url ? 200 : 88;
subTitle = subTitle || t('empty.tableEmpty');
const curSubTitle = isNeedSubTitle ? (subTitle || t('empty.tableEmpty')) : null;
return (
<div className={`rb:flex rb:items-center rb:justify-center rb:flex-col ${className}`}>
<img src={url || emptyIcon} alt="404" style={{ width: `${width}px`, height: `${height}px` }} />
{title && <div className="rb:mt-[8px] rb:leading-[20px]">{title}</div>}
{subTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-[16px] rb:text-[#5B6167]`}>{subTitle}</div>}
{title && <div className="rb:mt-2 rb:leading-5">{title}</div>}
{curSubTitle && <div className={`rb:mt-[${url ? 8 : 5}px] rb:leading-4 rb:text-[12px] rb:text-[#A8A9AA]`}>{subTitle}</div>}
</div>
);
}

View File

@@ -6,6 +6,7 @@ import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs';
import AppHeader from '@/components/Header';
import Sider from '@/components/SiderMenu'
import { useUser } from '@/store/user';
import { cookieUtils } from '@/utils/request';
const { Content } = Layout;
@@ -18,7 +19,12 @@ const AuthLayout: FC = () => {
// 自动更新面包屑导航
useNavigationBreadcrumbs('manage');
useEffect(() => {
getUserInfo()
const authToken = cookieUtils.get('authToken')
if (!authToken && !window.location.hash.includes('#/login')) {
window.location.href = `/#/login`;
} else {
getUserInfo()
}
}, []);
return (

View File

@@ -6,6 +6,7 @@ import { useNavigationBreadcrumbs } from '@/hooks/useNavigationBreadcrumbs';
import AppHeader from '@/components/Header';
import Sider from '@/components/SiderMenu';
import { useUser } from '@/store/user';
import { cookieUtils } from '@/utils/request';
const { Content } = Layout;
@@ -18,8 +19,13 @@ const AuthSpaceLayout: FC = () => {
// 自动更新面包屑导航
useNavigationBreadcrumbs('space');
useEffect(() => {
getUserInfo()
getStorageType()
const authToken = cookieUtils.get('authToken')
if (!authToken && !window.location.hash.includes('#/login')) {
window.location.href = `/#/login`;
} else {
getUserInfo()
getStorageType()
}
}, []);
return (

View File

@@ -29,7 +29,7 @@ interface PageScrollListProps {
const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
renderItem,
query = {},
query,
url,
column = 4,
className = '',
@@ -51,11 +51,11 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
request.get(url, {
page: page,
pagesize: PAGE_SIZE,
...query,
...(query||{}),
})
.then((res) => {
const response = res as ApiResponse;
const results = Array.isArray(response.items) ? response.items : Array.isArray(response.hosts) ? response.hosts : Array.isArray(response) ? response : [];
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response : [];
if (flag) {
setData(results);
} else {

View File

@@ -3,7 +3,7 @@ import { type FC, type ReactNode } from 'react'
interface RbAlertProps {
color?: 'blue' | 'green' | 'orange' | 'purple',
children: ReactNode | string;
icon: ReactNode;
icon?: ReactNode;
className?: string;
}
@@ -16,8 +16,8 @@ const colors = {
const RbAlert: FC<RbAlertProps> = ({ color = 'blue', icon, className, children }) => {
return (
<div className={`${colors[color]} ${className} rb:p-[6px_9px] rb:flex rb:items-center rb:text-[12px] rb:font-regular rb:leading-[16px] rb:border-[1px] rb:rounded-[6px]`}>
{icon && <span className="rb:text-[16px] rb:mr-[9px]">{icon}</span>}
<div className={`${colors[color]} ${className} rb:p-[6px_9px] rb:flex rb:items-center rb:text-[12px] rb:font-regular rb:leading-4 rb:border rb:rounded-md`}>
{icon && <span className="rb:text-[16px] rb:mr-2.25">{icon}</span>}
{children}
</div>
)

View File

@@ -52,7 +52,7 @@ const RbCard: FC<RbCardProps> = ({
title={typeof title === 'function' ? title() : title ?
<div className="rb:flex rb:items-center">
{avatarUrl
? <img src={avatarUrl} className="rb:mr-[13px] rb:w-[48px] rb:h-[48px] rb:rounded-[8px]" />
? <img src={avatarUrl} className="rb:mr-3.25 rb:w-12 rb:h-12 rb:rounded-lg" />
: avatar ? avatar : null
}
<div className={

View File

@@ -25,9 +25,10 @@ const RbModal: FC<ModalProps> = ({
onOk={onOk}
destroyOnHidden={true}
className={`rb-modal ${className || ''}`}
maskClosable={false}
{...props}
>
<div className='rb:max-h-[550px] rb:overflow-y-auto rb:overflow-x-hidden'>
<div className='rb:max-h-137.5 rb:overflow-y-auto rb:overflow-x-hidden'>
{children}
</div>
</Modal>

View File

@@ -21,11 +21,11 @@ import modelActiveIcon from '@/assets/images/menu/model_active.svg';
import memoryIcon from '@/assets/images/menu/memory.svg';
import memoryActiveIcon from '@/assets/images/menu/memory_active.svg';
import spaceIcon from '@/assets/images/menu/space.svg';
import spaceActiveIcon from '@/assets/images/menu/space_acitve.svg';
import spaceActiveIcon from '@/assets/images/menu/space_active.svg';
import userIcon from '@/assets/images/menu/user.svg';
import userActiveIcon from '@/assets/images/menu/user_active.svg';
import userMemoryIcon from '@/assets/images/menu/userMemory.svg';
import userMemoryActiveIcon from '@/assets/images/menu/userMemory_acitve.svg';
import userMemoryActiveIcon from '@/assets/images/menu/userMemory_active.svg';
import applicationIcon from '@/assets/images/menu/application.svg';
import applicationActiveIcon from '@/assets/images/menu/application_active.svg';
import knowledgeIcon from '@/assets/images/menu/knowledge.svg';
@@ -34,6 +34,10 @@ import memoryConversationIcon from '@/assets/images/menu/memoryConversation.svg'
import memoryConversationActiveIcon from '@/assets/images/menu/memoryConversation_active.svg';
import memberIcon from '@/assets/images/menu/member.svg';
import memberActiveIcon from '@/assets/images/menu/member_active.svg';
import toolIcon from '@/assets/images/menu/tool.png';
import toolActiveIcon from '@/assets/images/menu/tool_active.png';
import apiKeyIcon from '@/assets/images/menu/apiKey.png';
import apiKeyActiveIcon from '@/assets/images/menu/apiKey_active.png';
// 图标路径映射表
const iconPathMap: Record<string, string> = {
@@ -57,6 +61,10 @@ const iconPathMap: Record<string, string> = {
'memoryConversationActive': memoryConversationActiveIcon,
'member': memberIcon,
'memberActive': memberActiveIcon,
'tool': toolIcon,
'toolActive': toolActiveIcon,
'apiKey': apiKeyIcon,
'apiKeyActive': apiKeyActiveIcon,
};
const { Sider } = Layout;

View File

@@ -1,6 +1,6 @@
import { type FC, type ReactNode } from 'react'
interface TagProps {
export interface TagProps {
color?: 'processing' | 'error' | 'success' | 'warning' | 'default',
children: ReactNode;
className?: string;
@@ -16,7 +16,7 @@ const colors = {
const Tag: FC<TagProps> = ({ color = 'processing', children, className }) => {
return (
<span className={`rb:inline-block rb:px-[4px] rb:py-[2px] rb:rounded-[4px] rb:text-[12px] rb:font-regular! rb:leading-[16px] rb:border-[1px] ${colors[color]} ${className || ''}`}>
<span className={`rb:inline-block rb:px-1 rb:py-0.5 rb:rounded-sm rb:text-[12px] rb:font-regular! rb:leading-4 rb:border ${colors[color]} ${className || ''}`}>
{children}
</span>
)

View File

@@ -21,7 +21,7 @@ export const en = {
userMemory: 'User Memory',
memberManagement: 'Member Management',
memorySummary: 'Memory Summary',
memoryConversation: 'Memory Verification',
memoryConversation: 'Memory Validation',
memorySummaryHandlers: 'Memory Summary Handlers',
createMemorySummary: 'Create Memory Summary',
memoryManagement: 'Memory Management',
@@ -33,6 +33,11 @@ export const en = {
knowledgeCreateDataset: 'Create Dataset',
knowledgeDocumentDetails: 'Document Details',
userMemoryDetail: 'UserMemory Detail',
apiKeyManagement: 'API KEY Management',
toolManagement: 'Tool Management',
emotionEngine: 'Emotion Engine',
emotionDetail: 'Emotion Memory',
selfReflectionEngine: 'Self Reflection Engine',
},
dashboard: {
totalMemoryCapacity: 'Total Memory Capacity',
@@ -57,13 +62,13 @@ export const en = {
forgettingExecutionRate: 'Forgetting Execution Rate',
memoryClassificationDistribution: 'Memory classification distribution',
knowledgeBaseTypeDistribution: 'Distribution of knowledge base types',
memoryGrowthTrend: 'Memory growth trend',
knowledgeBaseTypeDistribution: 'Distribution of Knowledge Base Types',
memoryGrowthTrend: 'Memory Growth Trend',
corporateMemory: 'Corporate Memory',
recentMemoryActivities: 'Recent memory activities',
recentMemoryActivities: 'Recent Memory Activities',
apiCallTrend: 'API call trend',
quickOperation: 'Quick Operation',
popularMemoryTags: 'Popular memory tags',
popularMemoryTags: 'Popular Memory Tags',
title: 'Real-time Monitoring of Your AI Memory Core and Agent Status',
loading: 'Loading...',
@@ -115,9 +120,9 @@ export const en = {
statements_count_desc: 'Manage {{count}} knowledge statements',
triplet_count: 'Entity Relation Extraction',
triplet_count_desc: 'Build {{entities_count}} entity nodes and {{relations_count}} relation connections',
temporal_count: 'Time extraction',
temporal_count: 'Time Extraction',
temporal_count_desc: 'Record {{count}} time series information',
dialogue: 'Dialogue',
chunk: 'Chunk',
statement: 'Statement',
@@ -262,6 +267,7 @@ export const en = {
exportList: 'Export List',
selectPlaceholder: 'Please select {{title}}',
inputPlaceholder: 'Please enter {{title}}',
enterPlaceholder: 'Enter {{title}}',
saveSuccess: 'Save Success',
saveFailure: 'Save Failure',
pleaseSelect: 'Please select',
@@ -288,8 +294,8 @@ export const en = {
addOption: 'Add Option',
viewDetail: 'View Detail',
deleteSuccess: 'Delete successfully',
foldUp: 'Fold Up',
expanded: 'Expanded',
foldUp: 'Collapse',
expanded: 'Expand',
clickUploadIcon: 'click on the upload icon',
export: 'Export',
active: 'Active',
@@ -318,10 +324,7 @@ export const en = {
loginApiCannotRefreshToken: 'Login API cannot refresh token',
logoutApiCannotRefreshToken: 'Logout API cannot refresh token',
publicApiCannotRefreshToken: 'Public API cannot refresh token',
refreshTokenNotExist: 'Refresh token does not exist',
reset: 'Reset',
statusEnabled: 'Enabled',
statusDisabled: 'Disabled',
refreshTokenNotExist: 'Refresh token does not exist'
},
model: {
searchPlaceholder: 'search model…',
@@ -329,7 +332,7 @@ export const en = {
provider: 'Provider',
status: 'Status',
created: 'Created',
configureBtn: 'Click to Configure',
configureBtn: 'Run Configuration',
name: 'Name',
displayName: 'Display Name',
nameRequired: 'Please enter model name',
@@ -401,11 +404,19 @@ export const en = {
saveConfig: 'Save Config',
apiKeyName: 'API Key Name',
llm: 'LLM',
chat: 'Chat',
embedding: 'Embedding',
rerank: 'Rerank',
openai: "Openai",
dashscope: "Dashscope",
ollama: "Ollama",
xinference: "Xinference",
gpustack: "Gpustack",
bedrock: "Bedrock"
},
knowledgeBase: {
home: 'Home',
selectSpace: 'Please select a workspace.',
preview:'Preview',
pleaseUploadFileFirst: 'Please upload file first',
shareSuccess: 'Share successfully',
shareFailed: 'Share failed',
@@ -439,7 +450,7 @@ export const en = {
recallTestDescription:'Input test questions to evaluate the recall effectiveness and relevance of the knowledge base',
similarityThreshold: 'Similarity Threshold',
startTesting: 'Start Testing',
semanticSimilarity: 'Semantic similarity',
semanticSimilarity: 'Semantic Similarity',
recallResult: 'Result',
setting: 'Setting',
similarity: 'Similarity',
@@ -501,7 +512,7 @@ export const en = {
delete: 'Delete',
rechunking: 'Rechunking',
download: 'Download',
selectSource:'Please select the source',
selectSource:'Please select a source',
confirmDelete: 'Are you sure you want to delete this document?',
knowledgeBaseSettings: 'Knowledge Base Settings',
modelConfiguration: 'Model Configuration',
@@ -547,7 +558,6 @@ export const en = {
fileName: 'File Name',
fileList: 'File List',
blockPreview: 'Block Preview',
processingDocuments: 'Processing documents, please wait...',
chunkContent: 'Chunk Content',
sampleChunk: 'Sample Chunk Content',
noFilesSelected: 'No files',
@@ -557,10 +567,6 @@ export const en = {
waiting: 'Waiting',
startUpload: 'Total {{count}} files | Start Upload',
startUploading: 'Start Uploading',
startUploadConfirmTitle: 'Start Processing Documents',
startUploadConfirmContent: 'Document processing will run in the background. You can choose to return to the list page immediately or stay on this page to view the processing progress.',
returnToList: 'Return to List',
stayOnPage: 'Stay on Page',
uploadSuccess: 'Upload Success',
datasetName: 'Dataset Name',
pleaseEnterDatasetName: 'Please enter dataset name',
@@ -652,6 +658,8 @@ export const en = {
active: 'Active',
inactive: 'Inactive',
configurationName: 'Configuration Name',
emotionEngine: 'Emotion Engine',
reflectionEngine: 'Self-Reflection Engine'
},
member: {
username: 'Username',
@@ -659,16 +667,14 @@ export const en = {
role: 'Role',
lastLoginTime: 'Last Login Time',
editMember: 'Edit Member',
createMember: 'Create Member',
createMember: 'Add Member',
email: 'Email',
inviteToMember: 'Invite to Member',
inviteToMember: 'Member Role',
member: 'Member',
memberDesc: 'Can only use the application, cannot create the application',
admin: 'Admin',
adminDesc: 'Can create applications and manage team settings',
sendInvitation: 'Send Invitation',
manager: 'Admin',
managerDesc: 'Can create applications and manage team settings',
managerDesc: 'Can access applications, but cannot create or manage them',
inviteLinkDesc: 'Invite link 【{{inviteLink}}】, please copy and send to the member',
inviteLinkTip: 'Please copy the invite link and send it to the user to complete the invitation',
},
@@ -745,10 +751,10 @@ export const en = {
workflowDesc: 'To be opened, please stay tuned',
editApplication: 'Edit Application Info',
currentModel: 'Current Model',
modelConfig: 'Model Config',
parameterConfig: 'Parameter Config',
parameterConfig: 'Parameter Configuration',
apply: 'Apply',
resetDefault: 'Reset Default',
@@ -792,7 +798,7 @@ export const en = {
promptConfiguration: 'Prompt Configuration',
configurationDesc: 'Define the role, capabilities, and behavioral guidelines of the Agent',
aiPrompt: 'AI Prompt',
promptPlaceholder: 'You are a professional AI assistant, and your responsibilities are ..',
promptPlaceholder: 'You are a professional AI assistant, and your responsibility is to help users solve problems.',
knowledgeBaseAssociation: 'Knowledge base association',
associatedKnowledgeBase: 'Associated Knowledge Base',
addKnowledgeBase: 'Add Knowledge Base',
@@ -905,7 +911,7 @@ export const en = {
frequency_penalty_desc: 'Frequency penalty',
presence_penalty: 'Presence Penalty',
presence_penalty_desc: 'Presence Penalty',
n: 'Number of replies generated (n)',
n: 'Number of Replies Generated (n)',
n_desc: 'Number of replies generated',
contains: 'Contains {{include_count}} documents',
@@ -918,7 +924,7 @@ export const en = {
versionNumber: 'Version Number',
versionNumberTip: 'Version number format: v[major version number].[next version number].[revision number] (e.g. v1.3.0)',
versionDescription: 'Version Description',
versionDescriptionTip: 'Suggest explaining the feature updates, bug fixes, and optimization items for this release',
versionDescriptionTip: 'Please describe the feature updates, bug fixes, and optimizations included in this release.',
releasePreview: 'Release Preview',
globalConfig: 'Global Config',
globalConfigDesc: 'The global configuration will be applied to all associated knowledge bases as the default configuration. The configuration of a single knowledge base will override the global configuration.',
@@ -939,7 +945,7 @@ export const en = {
similarity_threshold: 'Semantic similarity threshold',
similarity_threshold_desc: 'Only return results with semantic similarity higher than this threshold',
similarity_threshold_desc1: 'The minimum similarity threshold for semantic retrieval',
vector_similarity_weight: 'Vector Similarity Weight',
vector_similarity_weight_desc: 'Only return results with BM25 scores above this threshold',
vector_similarity_weight_desc1: 'The minimum BM25 score threshold for word segmentation retrieval',
@@ -957,7 +963,7 @@ export const en = {
versionNameTip: 'Version number format: v[major version number].[next version number].[revision number] (e.g. v1.3.0)',
agentName: 'Agent Name',
roleType: 'Role Type',
coordinator: 'Coordinator',
analyzer: 'Analyzer',
executor: 'Executor',
@@ -967,8 +973,28 @@ export const en = {
capabilities: 'Capabilities',
subAgent: 'Sub Agent',
maxChatCount: 'Add up to 4 models',
addApiKey: 'Add API Key',
ReplyException: 'Reply exception'
ReplyException: 'Reply exception',
endpointConfigurationSubTitle: 'Configure API access address and supported HTTP methods',
apiKeys: 'API Keys Management',
apiKeySubTitle: 'Manage API keys, view usage and traffic statistics for each key',
addApiKey: 'Add New API Key',
apiKeyName: 'Key Name',
apiKeyNamePlaceholder: 'e.g.: Production, Testing, Development',
apiKeyDescPlaceholder: 'Describe the purpose of this Key',
apiKeyTotal: 'Total Keys',
apiKeyRequestTotal: 'Total Requests',
qps: 'Average QPS',
qpsLimit: 'QPS Limit',
qpsLimitTip: '(Requests per second)',
apiLimitConfig: 'Rate Limiting Configuration',
qpsLimitDesc: 'Limit the maximum number of requests this Key can make per second',
dailyUsageLimit: 'Daily Usage Limit',
dailyUsageLimitDesc: 'Limit the maximum total number of requests this Key can make per day',
dailyUsageLimitUnit: 'times/day',
apiKeyDeleteContent: 'Once deleted, it cannot be recovered, and applications using this Key will not be able to access the API',
currentValue: 'Current Value',
qpsLimitUnit: 'times/second',
},
userMemory: {
userMemory: 'User Memory',
@@ -984,7 +1010,7 @@ export const en = {
memoryInsight: 'Memory Insight',
relationshipNetwork: 'Relationship Network',
aboutMe: 'About Me',
foldUp: 'Fold Up',
foldUp: 'Collapse',
interestDistribution: 'Interest Distribution',
memoryDetails: 'Memory Details',
importantMomentsInLife: 'Important Moments in Life',
@@ -995,7 +1021,7 @@ export const en = {
memoryDetailEmpty: 'Please select a memory node',
memoryDetailEmptyDesc: 'Click on any node in the above view to view detailed information',
totalNumOfMemories: 'Total number of memories',
totalNumOfMemories: 'Total Number of Memories',
footprintCity: 'Footprint City',
totalNumOfPhotos: 'Total number of photos',
importantRelationships: 'Important Relationships',
@@ -1005,7 +1031,7 @@ export const en = {
emotions: 'Emotions',
occupation: 'Occupation',
memories: 'memories',
expanded: 'Expanded',
expanded: 'Expand',
description: 'Description',
entityType: 'Entity Type',
conversationMemory: 'Conversation Storage Content',
@@ -1021,32 +1047,32 @@ export const en = {
associated: 'Associated',
notAssociated: 'Not Associated',
storageType: 'Storage Type',
rag: 'RAG storage',
rag: 'RAG Storage',
ragDesc: 'Based on vector retrieval, suitable for document Q&A and semantic search',
neo4j: 'Graph storage',
neo4j: 'Graph Storage',
neo4jDesc: 'Based on knowledge graph, suitable for relational reasoning and path query',
llmModel: 'LLM Model',
embeddingModel: 'Embedding Model',
rerankModel: 'Rerank Model'
},
memoryExtractionEngine: {
title: 'Memory Engine module configuration center',
title: 'Memory Engine Module Configuration Center',
subTitle: 'Configure the parameters of six core modules, and view in real-time the impact on the memory processing conclusions of the "sample memory text (insights from the technology conference)". Any parameter changes will be instantly reflected in the results area on the right.',
example: 'Example memory text',
storageLayerModule: 'Storage layer module',
example: 'Example Memory Text',
storageLayerModule: 'Storage Layer Module',
enableLlmDedupBlockwise: 'Entity de-duplication (LLM decision-making)',
enableLlmDisambiguation: 'Memory disambiguation function (LLM decision)',
tNameStrict: 'Name matching threshold',
tTypeStrict: 'Type matching threshold',
tOverall: 'Comprehensive matching threshold',
enableLlmDedupBlockwise: 'Entity De-duplication (LLM decision-making)',
enableLlmDisambiguation: 'Memory Disambiguation Function (LLM decision)',
tNameStrict: 'Name Matching Threshold',
tTypeStrict: 'Type Matching Threshold',
tOverall: 'Comprehensive Matching Threshold',
arrangementLayerModule: 'Arrangement layer module',
queryMode: 'Query mode',
arrangementLayerModule: 'Arrangement Layer Module',
queryMode: 'Query Mode',
queryModeSubTitle: 'Control whether to activate deeper search functions',
deepRetrieval: 'Deep Retrieval',
deepRetrievalMeaning: 'Control whether to initiate deep memory retrieval (true/false).',
dataPreprocessing: 'Data preprocessing',
dataPreprocessing: 'Data Preprocessing',
dataPreprocessingSubTitle: 'Through reflection and refinement, transform episodic memory into deeper semantic memory.',
entityDeduplicationModuleThreshold: 'Entity de-duplication - name matching threshold',
@@ -1055,20 +1081,19 @@ export const en = {
control: 'Control',
button: 'button',
inputNumber: 'progress value',
slider: 'progress value',
slider: 'Slider',
select: 'select',
location: 'Location',
CurrentValue: 'Current Value',
type: 'Type',
Meaning: 'Meaning',
exampleMemoryExtractionResults: 'Example memory extraction results',
exampleMemoryExtractionResults: 'Example Memory Extraction Results',
exampleMemoryExtractionResultsSubTitle: '(from a technology conference)',
warning: 'When you modify the configuration items on the left, the extraction conclusion will be updated in real-time here',
extractTheNumberOfEntities: 'Extract the number of entities',
extractTheNumberOfEntitiesDesc: 'Merge after deduplication: {{num}} (exact: {{exact}}, fuzzy: {{fuzzy}}, LLM: {{llm}})',
numberOfEntityDisambiguation: 'Number of entity disambiguation',
numberOfEntityDisambiguationDesc: 'Total {{num}} times (blocking: {{block_count}})',
@@ -1093,26 +1118,26 @@ export const en = {
lateChunker: 'Late Chunker',
debug: 'Debug',
model: 'Model',
chunkerStrategy: 'Chunker strategy',
chunkerStrategy: 'Chunker Strategy',
chunkerStrategyDesc: 'Choose a partitioning strategy.',
intelligentSemanticPruning: 'Intelligent semantic pruning',
intelligentSemanticPruning: 'Intelligent Semantic Pruning',
intelligentSemanticPruningSubTitle: 'Whether to activate the intelligent semantic pruning function, select pruning scenarios, and set thresholds.',
intelligentSemanticPruningFunction: 'Intelligent semantic pruning function',
intelligentSemanticPruningFunction: 'Intelligent Semantic Pruning Function',
intelligentSemanticPruningFunctionDesc: 'Whether to activate intelligent semantic pruning (true/false).',
intelligentSemanticPruningScene: 'Intelligent semantic pruning scene',
intelligentSemanticPruningScene: 'Intelligent Semantic Pruning Scene',
intelligentSemanticPruningSceneDesc: 'Select intelligent semantic pruning scene (education, online_service, outbound).',
intelligentSemanticPruningThreshold: 'Intelligent semantic pruning threshold',
intelligentSemanticPruningThreshold: 'Intelligent Semantic Pruning Threshold',
intelligentSemanticPruningThresholdDesc: 'Set intelligent semantic pruning threshold (0-0.9).',
selfReflexionEngine: 'Self-reflexion engine',
reflectionEngine: 'Self-Reflexion Engine',
selfReflexionEngineSubTitle: 'Through reflection and refinement, transform episodic memory into deeper semantic memory.',
enableSelfReflexion: 'Enable self-reflexion',
iterationPeriod: 'Iteration period',
iterationPeriod: 'Iteration Period',
iterationPeriodDesc: 'Set the iteration period for self-reflexion (hourly, 3_hours, 6_hours, 12_hours, daily).',
reflexionRange: 'Reflexion range',
reflexionRange: 'Reflexion Range',
reflexionRangeDesc: "When selecting 'Database', the iteration cycle is non configurable and fixed at daily",
retrieval: 'Retrieval',
database: 'Database',
reflectOnTheBaseline: 'Reflect on the baseline',
reflectOnTheBaseline: 'Reflect on the Baseline',
basedOnTime: 'Based on time',
basedOnFacts: 'Based on facts',
basedOnFactsAndTime: 'Based on facts and time',
@@ -1124,15 +1149,15 @@ export const en = {
education: 'Education',
online_service: 'Online service',
outbound: 'Outbound',
entityDeduplicationDisambiguation: 'Entity de-duplication disambiguation',
entityDeduplicationDisambiguation: 'Entity De-duplication Disambiguation',
entityDeduplicationDisambiguationSubTitle: 'Control the LLM decision-making function for memory deduplication and disambiguation, set various matching thresholds, and affect the accuracy of memory deduplication.',
semanticAnchorAnnotationModule: 'Semantic anchor annotation module',
semanticAnchorAnnotationModule: 'Semantic Anchor Annotation Module',
semanticAnchorAnnotationModuleSubTitle: 'Control the granularity of statement extraction and whether to include dialog context.',
statementGranularity: 'Statement granularity',
statementGranularity: 'Statement Granularity',
statementGranularityDesc: 'Statement extraction granularity (1-3): 1 represents breaking down sentences into different statements, 2 represents sentence level, and 3 represents merging sentences into paragraphs.',
includeDialogueContext: 'Include dialogue context',
includeDialogueContext: 'Include Dialogue Context',
includeDialogueContextDesc: 'Control whether the complete dialogue context is included in the extraction process (true/false).',
maxDialogueContextChars: 'Max dialogue context chars',
maxDialogueContextChars: 'Max Dialogue Context Chars',
maxDialogueContextCharsDesc: 'The maximum number of characters included in the dialogue context (to avoid character limit issues) (greater than 100).',
coreEntitiesAfterDedup: 'Core entities after deduplication',
extractRelationalTriples: 'Extracted relational triples (partial)',
@@ -1155,7 +1180,28 @@ Memory Bear: Qin succeeded for several reasons: Shang Yangs reforms were thor
Student: Then switching to Tang history: after the An Lushan Rebellion, the central government began reforms, so why did regional warlordism (the fanzhen problem) actually get worse?
Memory Bear: After the rebellion, regional warlordism intensified for several reasons: military governors (jiedushi) held the power to recruit troops, control local finances, and command military forces, effectively becoming regional warlords; the central governments finances declined due to the breakdown of the equal-field system and the collapse of the tax-labor system, making it increasingly unable to support the army, which pushed military forces to rely on the jiedushi; the recruitment-based military system made soldiers loyal to individual commanders rather than the state; eunuchs controlled the imperial guards, the civil bureaucracy lost influence, and the central governments ability to balance regional power weakened.
`
`,
warning: 'When you modify the configuration items on the left, click [Debug], and the extraction conclusions will be updated in real time here',
processing: 'Configuration updated, re-extracting sample memory...',
success: 'Memory extraction completed!',
overallProgress: 'Overall Progress',
text_preprocessing: 'Text Preprocessing',
fragment: 'Fragment',
knowledge_extraction: 'Knowledge Extraction',
creating_nodes_edges: 'Creating Entity Relationships',
deduplication: 'Deduplication and Disambiguation',
status: {
pending: 'Pending',
processing: 'Processing',
completed: 'Completed',
failed: 'Failed'
},
time: 'Time: ',
text_preprocessing_desc: 'Text split into {{count}} semantic fragments',
knowledge_extraction_desc: 'Knowledge extraction completed, identified {{entities}} entities, {{statements}} statements, {{temporal_ranges_count}} temporal extractions, {{triplets}} triplets',
creating_nodes_edges_desc: 'Entity relationship creation completed, {{num}} relationships in total',
deduplication_desc: 'Deduplication and disambiguation completed, {{count}} unique entities in total'
},
memoryConversation: {
searchPlaceholder: 'Input user ID...',
@@ -1237,6 +1283,417 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
tableEmpty: 'There are currently no data',
loadingEmpty: 'The content is loading…',
loadingEmptyDesc: 'Your content is on its way by rocket! It will soon land on your screen'
},
apiKey: {
name: 'Project Name',
createApiKey: 'Create API Key',
updateApiKey: 'Edit API Key',
id: 'ID',
created_at: 'Created At',
description: 'Description',
memoryEngine: 'Memory Engine',
knowledgeBase: 'Knowledge Base',
advancedSettings: 'Advanced Settings',
expires_at: 'Expiration At',
apiKey: 'API Key',
status: 'Status',
createdAt: 'Created At',
expiresAt: 'Expires At',
requestsPerMinute: 'Requests/Minute',
viewDetail: 'View Details',
disable: 'Disable',
enable: 'Enable',
baseInfo: 'Basic Information',
permissionInfo: 'Permission Information',
is_expired: 'Status',
active: 'Active',
inactive: 'Expired'
},
tool: {
mcp: 'MCP Service',
inner: 'Built-in Tools',
custom: 'Custom Tools',
mcpSearchPlaceholder: 'Search MCP services...',
innerSearchPlaceholder: 'Search tools...',
customSearchPlaceholder: 'Search custom tools...',
addService: 'Add MCP Service',
addServiceSuccess: 'Service added successfully',
server_url: 'Service URL',
lastConnection: 'Last Connection',
responseTime: 'Response Time',
status: {
active: 'Active',
inactive: 'Inactive',
},
testConnectionSuccess: 'Connection test successful',
serviceEndpoint: 'Service Endpoint URL',
serviceEndpointPlaceholder: 'URL of the service endpoint',
serviceEndpointExtra: 'Complete access address of the MCP service',
nameAndIcon: 'Name and Icon',
namePlaceholder: 'Name your MCP service',
serverIdentifier: 'Server Identifier',
serverIdentifierPlaceholder: 'Unique server identifier, e.g. my-mcp-server',
serverIdentifierLength: 'Maximum 24 characters',
serverIdentifierPattern: 'Supports lowercase letters, numbers, underscores and hyphens',
description: 'Description',
auth: 'Authentication',
requestHeader: 'Request Headers',
config: 'Configuration',
authType: 'Authentication Type',
noAuth: 'No Authentication',
apiKey: 'API Key',
basicAuth: 'Basic Auth',
bearerToken: 'Bearer Token',
username: 'Username',
password: 'Password',
requestHeaderDesc: 'Additional HTTP request headers sent to MCP server',
addRequestHeader: 'Add Request Header',
editRequestHeader: 'Edit Request Header',
requestHeaderName: 'Request Header Name',
requestHeaderValue: 'Request Header Value',
timeout: 'Timeout (seconds)',
sseReadTimeout: 'SSE Read Timeout (seconds)',
saveAndTest: 'Save and Test',
timeFormat: 'Time Formatting',
timeZoneConversion: 'Time Zone Conversion',
timestampConversion: 'Timestamp Conversion',
timeCalculation: 'Time Calculation',
time_desc: 'Date and Time Processing',
DateTimeTool_features: 'Provides time format conversion, time zone conversion, timestamp calculation and other functions',
currentTime: 'Current Time',
timestamp: 'Timestamp',
localTime: 'Local Time',
utcTime: 'UTC Time',
secondsTimestamp: 'Timestamp (seconds)',
millisecondsTimestamp: 'Timestamp (milliseconds)',
enterTimestamp: 'Enter Timestamp',
conversion: 'Conversion',
conversionResult: 'Conversion Result',
chooseFormatType: 'Choose Format',
JsonTool_desc: 'Data Format Conversion',
JsonTool_features: 'JSON formatting, compression, validation and conversion functions',
jsonFormat: 'JSON Formatting',
jsonGzip: 'JSON Compression',
jsonCheck: 'JSON Validation',
jsonConversion: 'Format Conversion',
jsonEg: 'Example JSON',
enterJson: 'Enter JSON',
jsonPlaceholder: 'Enter JSON data, e.g.: {"name": "test", "value": 123}',
clear: 'Clear',
parse: 'Paste',
format: 'Format',
minify: 'Minify',
validate: 'Validate',
convert: 'Escape',
outputResult: 'Output Result',
validJosn: 'JSON format is correct, validation passed!',
BaiduSearchTool_desc: 'Search Engine Service',
BaiduSearchTool_features: 'Integrated Baidu Search API, providing web search, news search and other functions',
webSearch: 'Web Search',
newsSearch: 'News Search',
imageSearch: 'Image Search',
realTimeResults: 'Real-time Results',
configStatus: 'Configuration Status',
hasApiKey: 'API configured and enabled',
needApiKey: 'Need to configure API Key',
MinerUTool_desc: 'PDF Parsing Tool',
MinerUTool_features: 'High-precision PDF document parsing tool, supports text, table, and image extraction',
pdfParser: 'PDF Parser',
tableExtraction: 'Table Extraction',
imageRecognition: 'Image Recognition',
textExtraction: 'Text Extraction',
TextInTool_desc: 'OCR Text Recognition',
TextInTool_features: 'Intelligent OCR text recognition service, supports multi-language and handwriting recognition',
universalOCR: 'Universal OCR',
handwritingRecognition: 'Handwriting Recognition',
multilingualSupport: 'Multi-language Support',
highPrecisionRecognition: 'High Precision Recognition',
configDesc: 'Configuration Description',
BaiduSearchTool_config_desc: 'To use Baidu Search API, you need to apply for API Key and Secret Key on Baidu Open Platform first.',
MinerUTool_config_desc: 'MinerU is a high-precision PDF document parsing tool that requires an API Key to use.',
TextInTool_config_desc: 'TextIn provides intelligent OCR text recognition service with multi-language support.',
link: 'Application URL',
api_key: 'API Key',
BaiduSearchTool_api_key_desc: 'API Key obtained from Baidu Open Platform',
MinerUTool_api_key_desc: 'API Key obtained from MinerU platform',
secret_key: 'Secret Key',
BaiduSearchTool_secret_key_desc: 'Secret Key obtained from Baidu Open Platform',
TextInTool_secret_key_desc: 'Secret Key obtained from TextIn platform',
type: 'Search Type',
pagesize: 'Results Per Page',
pagesize_desc: 'Number of results returned per search ({{count1}}-{{count2}})',
BaiduSearchTool_enable: 'Enable Baidu Search',
BaiduSearchTool_safe_enable: 'Enable Safe Search',
BaiduSearchTool_safe_enable_desc: 'Filter inappropriate content',
api_address: 'API Address',
MinerUTool_api_address_desc: 'Uses official API address by default, can be modified if privately deployed',
TextInTool_api_address_desc: 'Uses official API address by default',
parsing_mode: 'Parsing Mode',
auto_recognition: 'Auto Recognition',
pure_text_mode: 'Pure Text Mode',
table_priority: 'Table Priority',
image_priority: 'Image Priority',
MinerUTool_timeout_desc: 'PDF parsing timeout (10-300 seconds)',
MinerUTool_enable: 'Enable MinerU',
MinerUTool_extract_images_enable: 'Extract Images',
MinerUTool_extract_images_enable_desc: 'Whether to extract image content from PDF',
app_id: 'APP ID',
TextInTool_app_id_desc: 'App ID obtained from TextIn platform',
language_identification: 'Recognition Language',
automatic_detection: 'Automatic Detection',
simplified_chinese: 'Simplified Chinese',
traditional_chinese: 'Traditional Chinese',
english: 'English',
japanese: 'Japanese',
korean_language: 'Korean',
pattern_recognition: 'Recognition Mode',
universal_identification: 'Universal Recognition',
high_precision_identification: 'High Precision Recognition',
handwriting_recognition: 'Handwriting Recognition',
formula_recognition: 'Formula Recognition',
TextInTool_enable: 'Enable TextIn',
return_text_position_enable: 'Return Text Position Info',
return_text_position_enable_desc: 'Whether to return coordinate positions of recognized text',
addCustom: 'Add Custom Tool',
editCustom: 'Edit Custom Tool',
schema: 'Schema',
schemaPlaceholder: 'Enter your OpenAPI schema here',
authentication: 'Authentication Method',
tag: 'Tag',
created_at: 'Created At',
headerName: 'Header Name',
null: 'None',
tagDesc: 'Multiple tags separated by commas',
availableTools: 'Available Tools',
name: 'Name',
desc: 'Description',
method: 'Method',
path: 'Path',
viewDetail: 'View Details'
},
workflow: {
coreNode: 'Core Nodes',
start: 'Start',
end: 'End',
answer: 'Answer',
aiAndCognitiveProcessing: 'AI & Cognitive Processing',
llm: 'Large Language Model (LLM)',
model_selection: 'Model Selection',
model_voting: 'Model Voting',
rag: 'Knowledge Retrieval (RAG)',
classification: 'Smart Classification',
parameter_extraction: 'Parameter Extraction',
flowControl: 'Flow Control',
condition: 'Conditional Branch',
iteration: 'Iteration',
loop: 'Loop',
parallel: 'Parallel Execution',
aggregator: 'Aggregator',
externalInteraction: 'External Interaction',
http_request: 'HTTP Request',
tools: 'Tools',
code_execution: 'Code Execution',
template_rendering: 'Template Rendering',
cognitiveUpgrading: 'Cognitive Upgrading (Innovation)',
task_planning: 'Task Planning',
reasoning_control: 'Reasoning Control',
self_reflection: 'Self Reflection',
memory_enhancement: 'Memory Enhancement',
agentCollaborationNode: 'Agent Collaboration Nodes',
agent_scheduling: 'Agent Scheduling',
agent_collaboration: 'Agent Collaboration',
agent_arbitration: 'Agent Arbitration',
safetyAndCompliance: 'Safety & Compliance',
sensitive_detection: 'Sensitive Detection',
output_audit: 'Output Audit',
evolutionAndGovernance: 'Evolution & Governance',
self_optimization: 'Self Optimization',
process_evolution: 'Process Evolution',
clickToConfigure: 'Click to configure node parameters',
nodeProperties: 'Node Properties',
empty: "Emmm... The box is empty, there's nothing here~",
nodeName: 'Node Name',
config: {
llm: {
model_id: 'Model',
temperature: 'Temperature',
max_tokens: 'Max Tokens',
},
start: {
variables: 'Input Fields',
string: 'Text',
number: 'Number',
boolean: 'Checkbox',
array: 'Dropdown Options',
object: 'Object',
addVariable: 'Add Variable',
editVariable: 'Edit Variable',
variableType: 'Variable Type',
variableName: 'Variable Name',
description: 'Display Name',
default: 'Default Value',
required: 'Required',
max_length: 'Max Length',
defaultChecked: 'Checked',
notDefaultChecked: 'Not Checked',
options: 'Options',
},
end: {
output: 'Reply'
}
},
clear: 'Clear',
run: 'Run',
save: 'Save',
export: 'Export',
variableConfig: 'Variable Configuration',
variableRequired: 'required',
},
emotionEngine: {
emotionEngineConfig: 'Emotion Engine Configuration',
emotion_enabled: 'Enable Emotion Engine',
emotion_enabled_desc: 'Automatically analyze emotional tendencies in conversations',
emotion_model_id: 'Emotion Analysis Model',
emotion_model_id_desc: 'Different models vary in accuracy and speed',
emotion_extract_keywords: 'Emotion Keyword Extraction',
emotion_extract_keywords_subTitle: 'Automatically extract emotion-related keywords from conversations',
emotion_extract_keywords_desc: 'Extract emotional keywords like "happy", "disappointed", "excited" to better understand user emotions',
emotion_min_intensity: 'Confidence Threshold',
emotion_min_intensity_desc: 'Higher confidence leads to more accurate recognition, but may miss some information',
emotion_enable_subject: 'Emotion Subject Classification',
emotion_enable_subject_subTitle: 'Identify emotion attribution (self/other/object)',
emotion_enable_subject_desc: 'Distinguish emotion subjects: self (I feel happy), other (he is angry), object (this product is great)',
currentValue: 'Current Value',
emotion_min_intensity_description: 'Confidence Threshold Description',
question: 'What is Confidence Threshold?',
answer: 'Confidence threshold is the "certainty level" standard for emotion engine to judge emotions. When the emotional confidence analyzed by AI is lower than the set threshold, the emotion will not be recorded.',
differentTitle: 'Impact of Different Thresholds',
advantage: 'Advantages',
shortcoming: 'Disadvantages',
scene: 'Applicable Scenarios',
low_title: 'Low Threshold (0.0 - 0.4)',
low_tag: 'Sensitive',
low_advantage: 'Can capture more subtle emotional changes without missing potential emotional signals',
low_shortcoming: 'May cause misjudgments, identifying neutral or unclear expressions as specific emotions',
low_scene: 'Scenarios requiring comprehensive understanding of user emotional fluctuations with low accuracy requirements',
middle_title: 'Medium Threshold (0.5 - 0.7)',
middle_tag: 'Recommended',
middle_advantage: 'Balances accuracy and coverage, can identify obvious emotions without being overly sensitive',
middle_shortcoming: 'May miss some less obvious emotional expressions',
middle_scene: 'Most daily conversation scenarios, suitable for general emotional analysis needs',
high_title: 'High Threshold (0.8 - 1.0)',
high_tag: 'Precise',
high_advantage: 'Only records very clear emotional expressions, extremely high accuracy with low misjudgment rate',
high_shortcoming: 'Will miss a large amount of less obvious emotional information, low data coverage',
high_scene: 'Scenarios requiring extremely high accuracy, such as emotional crisis warnings and important decision references',
configSuggest: 'Configuration Suggestions',
first: 'First Time Use',
first_desc: 'Recommend starting with medium threshold (0.6-0.7), observe for a period and adjust based on actual results',
customer_service: 'Customer Service Scenarios',
customer_service_desc: 'Recommend using lower threshold (0.4-0.6) to timely capture user dissatisfaction',
data_analysis: 'Data Analysis',
data_analysis_desc: 'Recommend using medium threshold (0.6-0.7) to ensure data quality while having sufficient sample size',
risk_warning: 'Risk Warning',
risk_warning_desc: 'Recommend using higher threshold (0.7-0.8) to ensure warning accuracy',
actual_case: 'Actual Case',
user_input: 'User Input',
user_input_message: '"This feature is okay, but there are some minor issues"',
neutral_emotion: 'Neutral Emotion',
neutral_emotion_tag: 'All thresholds will record',
minor_dissatisfaction: 'Minor Dissatisfaction',
minor_dissatisfaction_tag: 'Only low/medium thresholds will record',
expect_improvement: 'Expect Improvement',
expect_improvement_tag: 'Only low threshold will record',
confidence: 'Confidence'
},
emotionDetail: {
wordCloud: 'Emotion Distribution Analysis',
pieces: 'items',
emotionTags: 'High-Frequency Emotion Keywords',
joy: 'Joy',
anger: 'Anger',
sadness: 'Sadness',
fear: 'Fear',
neutral: 'Neutral',
surprise: 'Surprise',
health: 'Emotional Health Index',
positivity_rate: 'Positivity Rate',
stability: 'Stability',
resilience: 'Resilience',
suggestions: 'Personalized Suggestions',
},
reflectionEngine: {
reflectionEngineConfig: 'Reflection Engine Configuration',
reflection_enabled: 'Enable Reflection Engine',
reflection_enabled_desc: 'Transform episodic memory into semantic memory, forming long-term cognition',
reflection_model_id: 'Reflection Model',
reflection_model_id_desc: 'Different models vary in accuracy and speed',
reflection_period_in_hours: 'Iteration Period',
reflection_period_in_hours_desc: 'Determines how often the system performs memory reflection and refinement',
reflexion_range: 'Reflection Range',
partial: 'Partial Reflection (New memories only)',
all: 'Full Reflection (All historical memories)',
reflexion_range_desc: '',
baseline: 'Reflection Baseline',
baseline_desc: '',
TIME: 'Time-based (Temporal relationships)',
FACT: 'Fact-based (Knowledge points)',
HYBRID: 'Fact + Time (Comprehensive dimension)',
quality_assessment: 'Enable Quality Assessment',
quality_assessment_desc: 'Automatically evaluate memory accuracy, completeness and timeliness',
memory_verify: 'Enable Memory Verification',
memory_verify_desc: 'Detect sensitive information and filter inappropriate content',
oneHour: 'Every 1 hour',
threeHours: 'Every 3 hours',
sixHours: 'Every 6 hours',
twelveHours: 'Every 12 hours',
daily: 'Daily',
run: 'Run Debug',
example: 'Raw Data',
exampleText: 'I went to Beijing for work in the spring of 2023, and have basically been working in Beijing ever since, without changing cities much. However, due to company restructuring, I was transferred to Shanghai for about half a year in the first half of 2024, during which time I checked in at the Shanghai office every day. At that time, my employment records still used my previous identity information, with ID number 11010119950308123X and bank card 6222023847595898, which have never changed. By the way, I have actually been living in Beijing since 2023 and have never left Beijing for long periods. The Shanghai period was more like remote collaboration.',
runTitle: 'Reflection Test Run',
status: 'Status',
message: 'Message',
conflictDetection: 'Conflict Detection',
reason: 'Conflict Reason',
solution: 'Solution',
qualityAssessment: 'Quality Assessment',
qualityAssessmentObj: {
score: 'Quality Score',
summary: 'Assessment Summary',
},
privacyAudit: 'Privacy Audit',
privacyAuditObj: {
true: 'Yes',
false: 'No',
has_privacy: 'Contains Privacy Information',
privacy_types: 'Privacy Types',
summary: 'Audit Summary',
}
}
},
};

View File

@@ -28,16 +28,21 @@ export const zh = {
spaceManagement: '空间管理',
memoryExtractionEngine: '记忆提取引擎',
forgettingEngine: '遗忘引擎',
apiKeyManagement: 'API KEY管理',
knowledgePrivate: '详情',
knowledgeShare: '详情',
knowledgeCreateDataset: '新建数据集',
knowledgeDocumentDetails: '详情',
userMemoryDetail: '用户记忆详情',
toolManagement: '工具管理',
emotionEngine: '情感引擎',
emotionDetail: '情绪记忆',
selfReflectionEngine: '反思引擎',
},
knowledgeBase: {
home: '首页',
selectSpace: '请选择空间',
preview:'预览',
preview: '预览',
pleaseUploadFileFirst: '请先上传文件',
shareSuccess: '分享成功',
shareFailed: '分享失败',
@@ -178,7 +183,6 @@ export const zh = {
fileName: '文件名称',
fileList: '文件列表',
blockPreview: '分块预览',
processingDocuments: '正在处理文档,请稍候...',
chunkContent: '分块内容',
sampleChunk: '示例分块内容',
noFilesSelected: '暂无文件',
@@ -188,10 +192,6 @@ export const zh = {
waiting: '等待中',
startUpload: '共{{count}}个文件 | 开始上传',
startUploading: '开始上传',
startUploadConfirmTitle: '开始处理文档',
startUploadConfirmContent: '文档处理将在后台进行,您可以选择立即返回列表页或停留在此页面查看处理进度。',
returnToList: '返回列表页',
stayOnPage: '停留在此页',
uploadSuccess: '上传成功',
datasetName: '数据集名称',
pleaseEnterDatasetName: '请输入数据集名称',
@@ -297,7 +297,7 @@ export const zh = {
number: '数字',
checkbox: '复选框',
apiVariable: 'API变量',
displayName: '显示名称',
maxLength: '最大长度',
required: '必填',
@@ -317,7 +317,7 @@ export const zh = {
promptConfiguration: '提示词配置',
configurationDesc: '定义Agent的角色、能力和行为准则',
aiPrompt: 'AI提示词',
promptPlaceholder: '你是一个专业的AI助手你的职责是..',
promptPlaceholder: '你是一个专业的AI助手你的职责是帮助用户解决问题。',
knowledgeBaseAssociation: '知识库关联',
associatedKnowledgeBase: '关联知识库',
addKnowledgeBase: '添加知识库',
@@ -476,7 +476,7 @@ export const zh = {
similarity_threshold: '语义相似度阈值',
similarity_threshold_desc: '仅返回语义相似度高于此阈值的结果',
similarity_threshold_desc1: '语义检索的最小相似度阈值',
vector_similarity_weight: '向量相似度权重',
vector_similarity_weight_desc: '仅返回BM25分数高于此阈值的结果',
vector_similarity_weight_desc1: '分词检索的最小BM25分数阈值',
@@ -490,6 +490,27 @@ export const zh = {
chooseKnowledge: '选择知识库',
active: '活跃',
inactive: '不活跃',
endpointConfigurationSubTitle: '配置 API 访问地址和支持的 HTTP 方法',
apiKeys: 'API Keys 管理',
apiKeySubTitle: '管理 API 密钥,查看每个密钥的使用情况和流量统计',
addApiKey: '添加新 API Key',
apiKeyName: 'Key 名称',
apiKeyNamePlaceholder: '例如:生产环境、测试环境、开发环境',
apiKeyDescPlaceholder: '描述这个 Key 的用途',
apiKeyTotal: '总 Keys',
apiKeyRequestTotal: '总请求数',
qps: '平均 QPS',
qpsLimit: 'QPS 限制',
qpsLimitTip: '(每秒请求数)',
apiLimitConfig: '限流配置',
qpsLimitDesc: '限制此 Key 每秒最多可以发起的请求数',
dailyUsageLimit: '日调用量限制',
dailyUsageLimitDesc: '限制此 Key 每天最多可以发起的请求总数',
dailyUsageLimitUnit: '次/天',
apiKeyDeleteContent: '删除后将无法恢复使用此Key的应用将无法访问 API',
currentValue: '当前值',
qpsLimitUnit: '次/秒',
},
// 角色管理相关翻译
role: {
@@ -627,7 +648,7 @@ export const zh = {
triplet_count_desc: '构建{{entities_count}}个实体节点和{{relations_count}}个关系连接',
temporal_count: '时间提取',
temporal_count_desc: '记录{{count}}条时间序列信息',
dialogue: '对话',
chunk: '分块',
statement: '语句',
@@ -739,8 +760,8 @@ export const zh = {
copy: '复制',
copySuccess: '复制成功',
viewDetails: '查看详情',
enabled: '启用',
disabled: '停用',
enabled: '启用',
disabled: '停用',
updateWarning: '更新警告',
deleteWarning: '删除警告',
deleteWarningContent: '确定要删除此{{content}}吗?',
@@ -780,9 +801,7 @@ export const zh = {
logoutApiCannotRefreshToken: '退出登录接口不能刷新token',
publicApiCannotRefreshToken: '公共接口不能刷新token',
refreshTokenNotExist: '刷新token不存在',
reset: '重置',
statusEnabled: '已启用',
statusDisabled: '已禁用',
reset: '重置'
},
product: {
applicationManagement: '应用管理',
@@ -880,6 +899,17 @@ export const zh = {
saveConfig: '保存配置',
apiKeyName: 'API密钥名称',
llm: 'LLM',
chat: 'Chat',
embedding: 'Embedding',
rerank: 'Rerank',
openai: "Openai",
dashscope: "Dashscope",
ollama: "Ollama",
xinference: "Xinference",
gpustack: "Gpustack",
bedrock: "Bedrock"
},
timezones: {
'Asia/Shanghai': '中国标准时间 (UTC+8)',
@@ -976,6 +1006,8 @@ export const zh = {
active: '活跃',
inactive: '不活跃',
configurationName: '配置名称',
emotionEngine: '情感引擎',
reflectionEngine: '反思引擎'
},
member: {
username: '用户名',
@@ -988,8 +1020,6 @@ export const zh = {
inviteToMember: '邀请成员',
member: '成员',
memberDesc: '只能使用应用,不能创建应用',
admin: '管理员',
adminDesc: '可以创建应用和管理团队设置',
sendInvitation: '发送邀请',
manager: '管理员',
managerDesc: '可以创建应用和管理团队设置',
@@ -1036,10 +1066,10 @@ export const zh = {
minimumRetention: '时间遗忘率 (λ_time)',
minimumRetentionDesc: '控制记忆随时间的遗忘速度,值越高时间越短',
forgettingRate: '记忆遗忘率 (λ_mem)',
forgettingRate: '记忆遗忘率 (λ_mem)',
forgettingRateDesc: '控制记忆遗忘的速度,值越高遗忘越快',
offset: '最小保留度 (offset)',
offsetDesc: '控制记忆保留的最小保留阈值 遗忘这地方改个文字描述',
offset: '偏移量 (offset)',
offsetDesc: '最小保留度的偏移量',
CurrentValue: '当前值',
range: '范围',
forgettingEngineConfigParams: '遗忘引擎配置参数',
@@ -1095,11 +1125,8 @@ export const zh = {
storageType: '存储类型',
rag: 'RAG存储',
ragDesc: '基于向量检索,适合文档问答和语义搜索',
neo4j: '图存储',
neo4j: '图存储',
neo4jDesc: '基于知识图谱,适合关系推理和路径查询',
llmModel: 'LLM 模型',
embeddingModel: 'Embedding 模型',
rerankModel: 'Rerank 模型'
},
memoryExtractionEngine: {
title: '记忆引擎模块配置中心',
@@ -1136,11 +1163,10 @@ export const zh = {
exampleMemoryExtractionResults: '示例记忆提取结果',
exampleMemoryExtractionResultsSubTitle: '(来自技术会议)',
warning: '当您修改左侧的配置项时,提取结论将在此处实时更新',
extractTheNumberOfEntities: '提取实体数量',
extractTheNumberOfEntitiesDesc: '去重后合并:{{num}}(精确:{{exact}},模糊:{{fuzzy}}LLM{{llm}}',
numberOfEntityDisambiguation: '实体消歧数量',
numberOfEntityDisambiguationDesc: '总计{{num}}次(阻止:{{block_count}}',
@@ -1175,7 +1201,7 @@ export const zh = {
intelligentSemanticPruningSceneDesc: '选择智能语义修剪场景education、online_service、outbound。',
intelligentSemanticPruningThreshold: '智能语义修剪阈值',
intelligentSemanticPruningThresholdDesc: '设置智能语义修剪阈值0-0.9)。',
selfReflexionEngine: '自我反思引擎',
reflectionEngine: '自我反思引擎',
selfReflexionEngineSubTitle: '通过反思和精炼,将情节记忆转化为更深层的语义记忆。',
enableSelfReflexion: '启用自我反思',
iterationPeriod: '迭代周期',
@@ -1226,7 +1252,27 @@ export const zh = {
记忆熊:秦国统一的原因包括:商鞅变法彻底,建立法律、户籍和军功爵制度,提升国家组织能力;旧贵族势力弱,中央集权程度高;关中地理优越,资源丰富且易守难攻;从孝公到秦始皇政策连续性强。
学生:那我换到唐朝史:安史之乱后,中央已开始整顿,为何藩镇割据反而加剧?
记忆熊:安史之乱后藩镇割据加剧的原因包括:节度使掌握募兵权、财政调度权与军事指挥权,形成地方军阀;中央财政因均田制瓦解和租庸调失效而衰退,难以支撑军队,导致地方军事力量依附节度使;募兵制使士兵效忠个人而非国家;宦官掌控禁军,文官集团失势,中央制衡能力削弱。`
记忆熊:安史之乱后藩镇割据加剧的原因包括:节度使掌握募兵权、财政调度权与军事指挥权,形成地方军阀;中央财政因均田制瓦解和租庸调失效而衰退,难以支撑军队,导致地方军事力量依附节度使;募兵制使士兵效忠个人而非国家;宦官掌控禁军,文官集团失势,中央制衡能力削弱。`,
warning: '当您修改左侧的配置项后,点击【调试】,提取结论将在此处实时更新',
processing: '配置已更新,正在重新萃取示例记忆...',
success: '记忆萃取完成!',
overallProgress: '整体进度',
text_preprocessing: '文本预处理',
fragment: '片段',
knowledge_extraction: '知识抽取',
creating_nodes_edges: '创建实体关系',
deduplication: '去重消歧',
status: {
pending: '等待中',
processing: '处理中',
completed: '已完成',
failed: '失败'
},
time: '耗时: ',
text_preprocessing_desc: '文本切分为{{count}}个语义片段',
knowledge_extraction_desc: '知识抽取完成,共识别{{entities}}个实体,{{statements}}个句子, {{temporal_ranges_count}}个时间提取, {{triplets}}个三元组',
creating_nodes_edges_desc: '实体关系创建完成,共{{num}}条关系',
deduplication_desc: '去重消歧完成,最终{{count}}个唯一实体'
},
memoryConversation: {
searchPlaceholder: '输入用户ID...',
@@ -1321,28 +1367,421 @@ export const zh = {
websocketDemoCard: 'WebSocket 演示',
sseDemoCard: 'SSE演示'
},
workflow: {
title: '工作流编辑器',
description: '拖拽节点创建连接,构建您的工作流程。点击节点可进行配置。',
addNode: '添加节点',
deleteNode: '删除选中',
saveWorkflow: '保存工作流',
startNode: '触发节点',
conditionNode: '条件判断',
actionNode: '执行动作',
endNode: '结束节点',
newNode: '新节点',
node: '节点',
nodesCreated: '已创建节点',
loadingNodes: '正在加载节点 {{progress}}%',
loadingFailed: '加载节点失败',
create5kNodes: '创建5000节点',
create10kNodes: '创建10000节点'
},
notFound: {
title: '页面未找到',
description: '请求的页面不存在。',
backToHome: '返回首页'
},
apiKey: {
name: '项目名称',
createApiKey: '创建API Key',
updateApiKey: '编辑API Key',
id: 'ID',
created_at: '创建时间',
description: '描述',
memoryEngine: '记忆引擎',
knowledgeBase: '知识库',
advancedSettings: '高级设置',
expires_at: '过期时间',
apiKey: 'API Key',
status: '状态',
createdAt: '创建时间',
expiresAt: '过期时间',
requestsPerMinute: '次/分钟',
viewDetail: '查看详情',
disable: '禁用',
enable: '启用',
baseInfo: '基础信息',
permissionInfo: '授权信息',
is_expired: '状态',
active: '活跃',
inactive: '过期'
},
tool: {
mcp: 'MCP 服务',
inner: '内置工具',
custom: '自定义工具',
mcpSearchPlaceholder: '搜索MCP服务...',
innerSearchPlaceholder: '搜索工具...',
customSearchPlaceholder: '搜索自定义工具...',
addService: '添加MCP服务',
addServiceSuccess: '服务添加成功',
server_url: '服务地址',
lastConnection: '最后连接',
responseTime: '响应时间',
status: {
active: '活跃',
inactive: '不活跃',
},
testConnectionSuccess: '测试连接成功',
serviceEndpoint: '服务端点 URL',
serviceEndpointPlaceholder: '服务端点的 URL',
serviceEndpointExtra: 'MCP服务的完整访问地址',
nameAndIcon: '名称和图标',
namePlaceholder: '命名你的 MCP 服务',
serverIdentifier: '服务器标识符',
serverIdentifierPlaceholder: '服务器唯一标识,例如 my-mcp-server',
serverIdentifierLength: '最多 24 个字符',
serverIdentifierPattern: '支持小写字母、数字、下划线和连字符',
description: '描述信息',
auth: '认证',
requestHeader: '请求头',
config: '配置',
authType: '认证方式',
noAuth: '无需认证',
apiKey: 'API Key',
basicAuth: 'Basic Auth',
bearerToken: 'Bearer Token',
username: '用户名',
password: '密码',
requestHeaderDesc: '发送到 MCP 服务器的额外 HTTP 请求头',
addRequestHeader: '添加请求头',
editRequestHeader: '编辑请求头',
requestHeaderName: '请求头名称',
requestHeaderValue: '请求头值',
timeout: '超时时间(秒)',
sseReadTimeout: 'SSE 读取超时时间(秒)',
saveAndTest: '保存并测试',
timeFormat: '时间格式化',
timeZoneConversion: '时区转换',
timestampConversion: '时间戳转换',
timeCalculation: '时间计算',
time_desc: '日期时间处理',
DateTimeTool_features: '提供时间格式转换、时区转换、时间戳计算等功能',
currentTime: '当前时间',
timestamp: '时间戳',
localTime: '本地时间',
utcTime: 'UTC时间',
secondsTimestamp: '时间戳(秒)',
millisecondsTimestamp: '时间戳(毫秒)',
enterTimestamp: '输入时间戳',
conversion: '转换',
conversionResult: '转换结果',
chooseFormatType: '选择格式',
JsonTool_desc: '数据格式转换',
JsonTool_features: 'JSON格式化、压缩、验证和转换功能',
jsonFormat: 'JSON格式化',
jsonGzip: 'JSON压缩',
jsonCheck: 'JSON验证',
jsonConversion: '格式转换',
jsonEg: '示例JSON',
enterJson: '输入JSON',
jsonPlaceholder: '输入JSON数据例如{"name": "测试", "value": 123}',
clear: '清空',
parse: '粘贴',
format: '格式化',
minify: '压缩',
validate: '验证',
convert: '转义',
outputResult: '输出结果',
validJosn: 'JSON格式正确验证通过',
BaiduSearchTool_desc: '搜索引擎服务',
BaiduSearchTool_features: '集成百度搜索API提供网页搜索、新闻搜索等功能',
webSearch: '网页搜索',
newsSearch: '新闻搜索',
imageSearch: '图片搜索',
realTimeResults: '实时结果',
configStatus: '配置状态',
hasApiKey: 'API 已配置并启用',
needApiKey: '需要配置API Key',
MinerUTool_desc: 'PDF解析工具',
MinerUTool_features: '高精度PDF文档解析工具支持文字、表格、图片提取',
pdfParser: 'PDF解析',
tableExtraction: '表格提取',
imageRecognition: '图片识别',
textExtraction: '文本提取',
TextInTool_desc: 'OCR文字识别',
TextInTool_features: '智能OCR文字识别服务支持多语言、手写体识别',
universalOCR: '通用OCR',
handwritingRecognition: '手写识别',
multilingualSupport: '多语言支持',
highPrecisionRecognition: '高精度识别',
configDesc: '配置说明',
BaiduSearchTool_config_desc: '使用百度搜索API需要先在百度开放平台申请API Key和Secret Key。',
MinerUTool_config_desc: 'MinerU是高精度PDF文档解析工具需要API Key才能使用。',
TextInTool_config_desc: 'TextIn提供智能OCR文字识别服务支持多语言识别。',
link: '申请地址',
api_key: 'API Key',
BaiduSearchTool_api_key_desc: '从百度开放平台获取的API Key',
MinerUTool_api_key_desc: '从MinerU平台获取的API Key',
secret_key: 'Secret Key',
BaiduSearchTool_secret_key_desc: '从百度开放平台获取的Secret Key',
TextInTool_secret_key_desc: '从TextIn平台获取的Secret Key',
type: '搜索类型',
pagesize: '每页结果数',
pagesize_desc: '每次搜索返回的结果数量({{count1}}-{{count2}}',
BaiduSearchTool_enable: '启用百度搜索',
BaiduSearchTool_safe_enable: '启用安全搜索',
BaiduSearchTool_safe_enable_desc: '过滤不适宜内容',
api_address: 'API地址',
MinerUTool_api_address_desc: '默认使用官方API地址如有私有部署可修改',
TextInTool_api_address_desc: '默认使用官方API地址',
parsing_mode: '解析模式',
auto_recognition: '自动识别',
pure_text_mode: '纯文本模式',
table_priority: '表格优先',
image_priority: '图片优先',
MinerUTool_timeout_desc: 'PDF解析超时时间10-300秒',
MinerUTool_enable: '启用MinerU',
MinerUTool_extract_images_enable: '提取图片',
MinerUTool_extract_images_enable_desc: '是否提取PDF中的图片内容',
app_id: 'APP ID',
TextInTool_app_id_desc: '从TextIn平台获取的App ID',
language_identification: '识别语言',
automatic_detection: '自动检测',
simplified_chinese: '简体中文',
traditional_chinese: '繁体中文',
english: '英文',
japanese: '日文',
korean_language: '韩文',
pattern_recognition: '识别模式',
universal_identification: '通用识别',
high_precision_identification: '高精度识别',
handwriting_recognition: '手写体识别',
formula_recognition: '公式识别',
TextInTool_enable: '启用TextIn',
return_text_position_enable: '返回文本位置信息',
return_text_position_enable_desc: '是否返回识别文字的坐标位置',
addCustom: '添加自定义工具',
editCustom: '编辑自定义工具',
schema: 'Schema',
schemaPlaceholder: '在此处输入您的 OpenAPI schema',
authentication: '鉴权方式',
tag: '标签',
created_at: '创建时间',
headerName: 'Header 名称',
null: '无',
tagDesc: '多个标签用逗号分隔',
availableTools: '可用工具',
name: '名称',
desc: '描述',
method: '方法',
path: '路径',
viewDetail: '查看详情'
},
workflow: {
coreNode: '核心节点',
start: '开始Start',
end: '结束End',
answer: '回复Answer',
aiAndCognitiveProcessing: 'AI与认知处理',
llm: '大语言模型 (LLM)',
model_selection: '多模型选择',
model_voting: '多模型投票',
rag: '知识检索 (RAG)',
classification: '智能分类',
parameter_extraction: '参数提取',
flowControl: '流程控制',
condition: '条件分支',
iteration: '迭代 (Iteration)',
loop: '循环 (Loop)',
parallel: '并行执行',
aggregator: '聚合器',
externalInteraction: '外部交互',
http_request: 'HTTP请求',
tools: '工具 (Tools)',
code_execution: '代码执行',
template_rendering: '模板渲染',
cognitiveUpgrading: '认知升级(创新)',
task_planning: '任务规划',
reasoning_control: '推理控制',
self_reflection: '自我反思',
memory_enhancement: '记忆增强',
agentCollaborationNode: 'Agent 协作节点',
agent_scheduling: 'Agent 调度',
agent_collaboration: 'Agent 协同',
agent_arbitration: 'Agent 仲裁',
safetyAndCompliance: '安全与合规',
sensitive_detection: '敏感识别',
output_audit: '输出审计',
evolutionAndGovernance: '演化与治理',
self_optimization: '自我优化',
process_evolution: '流程演化',
clickToConfigure: '点击配置节点参数',
nodeProperties: '节点属性',
empty: "Emmm…盒子是空的这里什么都没有",
nodeName: '节点名称',
config: {
llm: {
model_id: '模型',
temperature: '温度',
max_tokens: '最大令牌数',
},
start: {
variables: '输入字段',
string: '文本',
number: '数字',
boolean: '复选框',
array: '下拉选项',
object: '对象',
addVariable: '添加变量',
editVariable: '编辑变量',
variableType: '变量类型',
variableName: '变量名称',
description: '显示名称',
default: '默认值',
required: '必填',
max_length: '最大长度',
defaultChecked: '选中',
notDefaultChecked: '不选中',
options: '选项',
},
end: {
output: '回复'
}
},
clear: '清空',
run: '运行',
save: '保存',
export: '导出',
variableConfig: '变量配置',
variableRequired: '必填',
},
emotionEngine: {
emotionEngineConfig: '情感引擎配置',
emotion_enabled: '启用情感引擎',
emotion_enabled_desc: '自动分析对话中的情感倾向',
emotion_model_id: '情感分析模型',
emotion_model_id_desc: '不同模型在准确度和速度上有所差异',
emotion_extract_keywords: '情绪关键词提取',
emotion_extract_keywords_subTitle: '自动提取对话中的情绪相关关键词',
emotion_extract_keywords_desc: '提取如"开心"、"失望"、"期待"等情绪关键词,帮助更好地理解用户情绪',
emotion_min_intensity: '置信度阈值',
emotion_min_intensity_desc: '置信度越高,识别越准确,但可能遗漏部分信息',
emotion_enable_subject: '情绪主体分类 ',
emotion_enable_subject_subTitle: '识别情绪归属(自己/他人/物体)',
emotion_enable_subject_desc: '区分情绪主体: self (我感到开心)、other (他很生气)、object (这个产品很棒)',
currentValue: '当前值',
emotion_min_intensity_description: '置信度阈值说明',
question: '什么是置信度阈值?',
answer: '置信度阈值是情感引擎判断情绪时的"确定程度"标准。当 AI 分析出的情感置信度低于设定阈值时,该情感将不会被记录。',
differentTitle: '不同阈值的影响',
advantage: '优点',
shortcoming: '缺点',
scene: '适用场景',
low_title: '低阈值 (0.0 - 0.4)',
low_tag: '灵敏',
low_advantage: '能捕捉到更多细微的情感变化,不会遗漏潜在的情绪信号',
low_shortcoming: '可能产生误判,将中性或不明确的表达识别为特定情感',
low_scene: '需要全面了解用户情绪波动,对准确度要求不高的场景',
middle_title: '中阈值 (0.5 - 0.7)',
middle_tag: '推荐',
middle_advantage: '平衡准确度和覆盖率,既能识别明显情感,也不会过度敏感',
middle_shortcoming: '可能遗漏一些不太明显的情感表达',
middle_scene: '大多数日常对话场景,适合一般性情感分析需求',
high_title: '高阈值 (0.8 - 1.0)',
high_tag: '精准',
high_advantage: '只记录非常明确的情感表达,准确度极高,误判率低',
high_shortcoming: '会遗漏大量不够明显的情感信息,数据覆盖率低',
high_scene: '对准确度要求极高的场景,如情感危机预警、重要决策参考',
configSuggest: '配置建议',
first: '初次使用',
first_desc: '建议从中等阈值0.6-0.7)开始,观察一段时间后根据实际效果调整',
customer_service: '客服场景',
customer_service_desc: '建议使用较低阈值0.4-0.6),及时捕捉用户的不满情绪',
data_analysis: '数据分析',
data_analysis_desc: '建议使用中等阈值0.6-0.7),保证数据质量的同时有足够样本量',
risk_warning: '风险预警',
risk_warning_desc: '建议使用较高阈值0.7-0.8),确保预警的准确性',
actual_case: '实际案例',
user_input: '用户输入',
user_input_message: '"这个功能还行吧,不过有点小问题"',
neutral_emotion: '中性情感',
neutral_emotion_tag: '所有阈值都会记录',
minor_dissatisfaction: '轻微不满',
minor_dissatisfaction_tag: '仅低/中阈值会记录',
expect_improvement: '期待改进',
expect_improvement_tag: '仅低阈值会记录',
confidence: '置信度'
},
emotionDetail: {
wordCloud: '情感分布分析',
pieces: '条',
emotionTags: '高频情绪关键词',
joy: '喜悦',
anger: '愤怒',
sadness: '悲伤',
fear: '恐惧',
neutral: '中性',
surprise: '惊讶',
health: '情绪健康指数',
positivity_rate: '积极率',
stability: '稳定性',
resilience: '恢复力',
suggestions: '个性化建议',
},
reflectionEngine: {
reflectionEngineConfig: '反思引擎配置',
reflection_enabled: '启用反思引擎',
reflection_enabled_desc: '将情节记忆转化为语义记忆,形成长期认知',
reflection_model_id: '反思模型',
reflection_model_id_desc: '不同模型在准确度和速度上有所差异',
reflection_period_in_hours: '迭代周期',
reflection_period_in_hours_desc: '决定系统多久进行一次记忆反思和提炼',
reflexion_range: '反思范围',
partial: '部分反思 (仅新增记忆)',
all: '全部反思 (所有历史记忆)',
reflexion_range_desc: '',
baseline: '反思基线',
baseline_desc: '',
TIME: '基于时间(时序关系)',
FACT: '基于事实(知识点)',
HYBRID: '事实+时间(综合维度)',
quality_assessment: '启用质量评估',
quality_assessment_desc: '自动评估记忆的准确性、完整性和时效性',
memory_verify: '启用记忆审核',
memory_verify_desc: '检测敏感信息并过滤违规内容',
oneHour: '每1个小时',
threeHours: '每3个小时',
sixHours: '每6个小时',
twelveHours: '每12个小时',
daily: '每天',
run: '运行调试',
example: '原始数据',
exampleText: '我是 2023 年春天去北京工作的后来基本一直都在北京上班也没怎么换过城市。不过后来公司调整2024 年上半年我被调到上海待了差不多半年,那段时间每天都是在上海办公室打卡。当时入职资料用的还是我之前的身份信息,身份证号是 11010119950308123X银行卡是 6222023847595898这些一直没变。对了其实我 从 2023 年开始就一直在北京生活,从来没有长期离开过北京,上海那段更多算是远程配合',
runTitle: '反思试运行',
status: '状态',
message: '消息',
conflictDetection: '冲突检测',
reason: '冲突原因',
solution: '解决方案',
qualityAssessment: '质量评估',
qualityAssessmentObj: {
score: '质量评分',
summary: '评估摘要',
},
privacyAudit: '隐私审核',
privacyAuditObj: {
true: '是',
false: '否',
has_privacy: '包含隐私信息',
privacy_types: '隐私类型',
summary: '审核摘要',
}
}
},
}

View File

@@ -10,19 +10,19 @@ import routesConfig from './routes.json';
// 递归收集所有路由中的element
function collectElements(routes: RouteConfig[]): Set<string> {
const elements = new Set<string>();
function traverse(routeList: RouteConfig[]) {
routeList.forEach(route => {
// 添加当前路由的element
elements.add(route.element);
// 递归处理子路由
if (route.children && route.children.length > 0) {
traverse(route.children);
}
});
}
traverse(routes);
return elements;
}
@@ -54,10 +54,14 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
UserManagement: lazy(() => import('@/views/UserManagement')),
ModelManagement: lazy(() => import('@/views/ModelManagement')),
SpaceManagement: lazy(() => import('@/views/SpaceManagement')),
ApiKeyManagement: lazy(() => import('@/views/ApiKeyManagement')),
EmotionEngine: lazy(() => import('@/views/EmotionEngine')),
EmotionDetail: lazy(() => import('@/views/UserMemoryDetail/pages/EmotionDetail')),
SelfReflectionEngine: lazy(() => import('@/views/SelfReflectionEngine')),
Login: lazy(() => import('@/views/Login')),
InviteRegister: lazy(() => import('@/views/InviteRegister')),
NoPermission: lazy(() => import('@/views/NoPermission')),
NotFound: lazy(() => import('@/views/NotFound')),
NotFound: lazy(() => import('@/views/NotFound'))
};
// 检查并报告缺失的组件
@@ -87,12 +91,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
// 获取组件
const componentKey = route.element as keyof typeof componentMap;
const Component = componentMap[componentKey];
if (!Component) {
console.error(`Component ${route.element} not found in componentMap`);
return null;
}
// 如果有子路由
if (route.children) {
return (
@@ -101,12 +105,12 @@ const generateRoutes = (routes: RouteConfig[]): ReactNode => {
</Route>
);
}
// 如果有path属性则为普通路由
if (route.path) {
return <Route key={index} path={route.path} element={<Component />} />;
}
return null;
});
};

View File

@@ -25,6 +25,10 @@
{ "path": "/knowledge-base/:knowledgeBaseId/share", "element": "Share" },
{ "path": "/knowledge-base/:knowledgeBaseId/create-dataset", "element": "CreateDataset" },
{ "path": "/knowledge-base/:knowledgeBaseId/DocumentDetails", "element": "DocumentDetails" },
{ "path": "/api-key", "element": "ApiKeyManagement" },
{ "path": "/emotion-engine/:id", "element": "EmotionEngine" },
{ "path": "/reflection-engine/:id", "element": "SelfReflectionEngine" },
{ "path": "/user-memory/emotion/:id", "element": "EmotionDetail" },
{ "path": "/no-permission", "element": "NoPermission" },
{ "path": "/*", "element": "NotFound" }
]

View File

@@ -26,6 +26,19 @@
"sort": 0,
"subs": []
},
{
"id": 4,
"parent": 0,
"code": "tool",
"label": "工具管理",
"i18nKey": "menu.toolManagement",
"path": "/tool",
"enable": true,
"display": true,
"level": 1,
"sort": 1,
"subs": []
},
{
"id": 3,
"parent": 0,
@@ -183,6 +196,32 @@
"level": 1,
"sort": 0,
"subs": null
},
{
"id": 72,
"parent": 7,
"code": "emotionEngine",
"label": "情感引擎",
"i18nKey": "menu.emotionEngine",
"path": "/emotion-engine/:id",
"enable": true,
"display": false,
"level": 1,
"sort": 0,
"subs": null
},
{
"id": 72,
"parent": 7,
"code": "selfReflectionEngine",
"label": "反思引擎",
"i18nKey": "menu.selfReflectionEngine",
"path": "/reflection-engine/:id",
"enable": true,
"display": false,
"level": 1,
"sort": 0,
"subs": null
}
]
},
@@ -210,7 +249,21 @@
"display": false,
"level": 2,
"sort": 0,
"subs": null
"subs": [
{
"id": 81,
"parent": 8,
"code": "emotionDetail",
"label": "记忆详情",
"i18nKey": "menu.emotionDetail",
"path": "/user-memory/emotion/:id",
"enable": true,
"display": false,
"level": 2,
"sort": 0,
"subs": null
}
]
}
]
},
@@ -243,6 +296,21 @@
"icon": null,
"iconActive": null,
"subs": null
},
{
"id": 11,
"parent": 0,
"code": "apiKey",
"label": "API KEY管理",
"i18nKey": "menu.apiKeyManagement",
"path": "/api-key",
"enable": true,
"display": true,
"level": 1,
"sort": 0,
"icon": null,
"iconActive": null,
"subs": null
}
]
}

View File

@@ -174,4 +174,10 @@ body {
}
.ant-breadcrumb a:hover {
background-color: transparent;
}
/* X6 节点样式 */
.x6-node foreignObject > body {
min-height: 100%;
max-height: 100%;
}

View File

@@ -0,0 +1,46 @@
/**
* API密钥替换工具
*/
const API_KEY_PATTERNS = {
service: /sk-service-[A-Za-z0-9_-]+/g,
agent: /sk-agent-[A-Za-z0-9_-]+/g,
multiAgent: /sk-multi_agent-[A-Za-z0-9_-]+/g,
workflow: /sk-workflow-[A-Za-z0-9_-]+/g
}
const API_KEY_PREFIX = {
service: 'sk-service-',
agent: 'sk-agent-',
multiAgent: 'sk-multi_agent-',
workflow: 'sk-workflow-'
}
/**
* 替换文本中的API密钥为*号
* @param text 原始文本
* @returns 替换后的文本
*/
export const maskApiKeys = (text: string): string => {
if (!text) return text
let result = text
Object.keys(API_KEY_PREFIX).map(type => {
const key = type as keyof typeof API_KEY_PREFIX
result = result.replace(API_KEY_PATTERNS[key as keyof typeof API_KEY_PREFIX], (match) => {
const prefixLength = API_KEY_PREFIX[key].length
const prefix = match.substring(0, prefixLength)
return prefix + '*'.repeat(match.length - prefixLength)
})
})
return result
}
/**
* 检测文本中是否包含API密钥
* @param text 待检测文本
* @returns 是否包含API密钥
*/
export const hasApiKeys = (text: string): boolean => {
return Object.values(API_KEY_PATTERNS).some(pattern => pattern.test(text))
}

View File

@@ -3,7 +3,47 @@ import i18n from '@/i18n'
import { cookieUtils } from './request'
const API_PREFIX = '/api'
export const handleSSE = async (url: string, data: any, onMessage?: (data: string) => void, config = {}) => {
export interface SSEMessage {
event?: string
data?: string | object
}
export function parseSSEToJSON(sseString: string) {
const events: SSEMessage[] = []
const lines = sseString.trim().split('\n')
let currentEvent: SSEMessage = {}
try {
for (const line of lines) {
if (line.startsWith('event:')) {
if (Object.keys(currentEvent).length > 0) {
events.push(currentEvent)
currentEvent = {}
}
currentEvent.event = line.substring(6).trim()
} else if (line.startsWith('data:')) {
const dataStr = line.substring(5).trim()
try {
currentEvent.data = JSON.parse(dataStr.replace(/"/g, '"'))
} catch {
currentEvent.data = dataStr
}
}
}
if (Object.keys(currentEvent).length > 0) {
events.push(currentEvent)
}
return events
} catch (error) {
console.error('Parse stream error:', error)
return []
}
}
export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }) => {
try {
const token = cookieUtils.get('authToken');
const response = await fetch(`${API_PREFIX}${url}`, {
@@ -37,7 +77,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: strin
const chunk = decoder.decode(value, { stream: true });
if (onMessage) {
onMessage(chunk);
onMessage(parseSSEToJSON(chunk) ?? {});
}
}
break;

View File

@@ -0,0 +1,102 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Switch, Button, Tooltip } from 'antd';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import type { ApiKey, ApiKeyModalRef } from '../types';
import RbModal from '@/components/RbModal'
import { getApiKey } from '@/api/apiKey';
import { formatDateTime } from '@/utils/format'
import Tag from '@/components/Tag'
import { maskApiKeys } from '@/utils/apiKeyReplacer';
const ApiKeyDetailModal = forwardRef<ApiKeyModalRef, { handleCopy: (content: string) => void }>(({ handleCopy }, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [data, setData] = useState<ApiKey>({} as ApiKey)
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
};
const handleOpen = (apiKey?: ApiKey) => {
if (apiKey?.id) {
getApiKey(apiKey.id)
.then((res) => {
setVisible(true);
setData(res as ApiKey)
})
}
};
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={t('apiKey.viewDetail')}
open={visible}
onCancel={handleClose}
>
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.baseInfo')}</div>
{['id', 'name', 'is_expired', 'created_at'].map((key, index) => (
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
'rb:mt-3': index !== 0
})}>
<span className="rb:text-[#5B6167]">{t(`apiKey.${key}`)}</span>
<span className="rb:text-right rb:flex-1 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap">
{ key === 'created_at'
? formatDateTime(data[key], 'YYYY-MM-DD HH:mm:ss')
: key === 'is_expired'
? <Tag color={data[key] ? 'error' : 'processing'}>{data[key] ? t('apiKey.inactive') : t('apiKey.active')}</Tag>
: <Tooltip title={String(data[key as keyof ApiKey])}>{String(data[key as keyof ApiKey])}</Tooltip>
}
</span>
</div>
))}
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
{maskApiKeys(data.api_key)}
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(data.api_key)}>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
</div>
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:my-4">{t('apiKey.permissionInfo')}</div>
<div className="rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px] rb:mt-3">
<span className="rb:text-[#5B6167]">{t(`apiKey.memoryEngine`)}</span>
<span>
<Switch checked={data.scopes?.includes('memory')} disabled />
</span>
</div>
<div className="rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px] rb:mt-3">
<span className="rb:text-[#5B6167]">{t(`apiKey.knowledgeBase`)}</span>
<span>
<Switch checked={data.scopes?.includes('rag')} disabled />
</span>
</div>
{/* 高级设置 */}
{data.expires_at && <>
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:my-4">{t('apiKey.advancedSettings')}</div>
<div className="rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px] rb:mt-3">
<span className="rb:text-[#5B6167]">{t(`apiKey.expires_at`)}</span>
<span>
{data.expires_at ? formatDateTime(data.expires_at as number, 'YYYY-MM-DD HH:mm:ss') : '-'}
</span>
</div>
</>}
</RbModal>
);
});
export default ApiKeyDetailModal;

View File

@@ -0,0 +1,153 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, Switch, App, DatePicker } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ApiKey, ApiKeyModalRef } from '../types';
import RbModal from '@/components/RbModal'
import dayjs from 'dayjs'
import { createApiKey, updateApiKey } from '@/api/apiKey';
const FormItem = Form.Item;
interface CreateModalProps {
refresh: () => void;
}
const ApiKeyModal = forwardRef<ApiKeyModalRef, CreateModalProps>(({
refresh,
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<ApiKey>();
const [loading, setLoading] = useState(false);
const [editVo, setEditVo] = useState<ApiKey | null>(null);
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false);
setEditVo(null);
};
const handleOpen = (apiKey?: ApiKey) => {
if (apiKey?.id) {
const { scopes = [], expires_at } = apiKey
// 编辑模式,填充表单
form.setFieldsValue({
name: apiKey.name,
description: apiKey.description,
memory: scopes.includes('memory'),
rag: scopes.includes('rag'),
expires_at: expires_at ? dayjs(expires_at) : undefined
});
setEditVo(apiKey);
}
setVisible(true);
};
// 封装保存方法,添加提交逻辑
const handleSave = async () => {
form.validateFields()
.then((values) => {
const { memory, rag, expires_at, ...rest } = values
let scopes = []
if (memory) {
scopes.push('memory')
}
if (rag) {
scopes.push('rag')
}
// 准备新的/更新的API Key数据
const apiKeyData = {
...rest,
scopes,
expires_at: expires_at ? dayjs(expires_at.valueOf()).endOf('day').valueOf() : null,
type: 'service'
};
setLoading(true)
const req = editVo?.id ? updateApiKey(editVo.id, apiKeyData as ApiKey) : createApiKey(apiKeyData as ApiKey)
req.then(() => {
refresh();
handleClose();
message.success(t(editVo ? 'common.updateSuccess' : 'common.createSuccess'));
})
.finally(() => setLoading(false))
})
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={editVo ? t('apiKey.updateApiKey') : t('apiKey.createApiKey')}
open={visible}
onCancel={handleClose}
okText={t('common.save')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
>
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.baseInfo')}</div>
<FormItem
name="name"
label={t('apiKey.name')}
rules={[{ required: true, message: t('common.pleaseEnter') }]}
>
<Input placeholder={t('common.enter')} />
</FormItem>
<FormItem
name="description"
label={t('apiKey.description')}
>
<Input.TextArea placeholder={t('common.pleaseEnter')} rows={3} />
</FormItem>
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.permissionInfo')}</div>
<FormItem
name="memory"
label={t('apiKey.memoryEngine')}
layout="horizontal"
valuePropName="checked"
>
<Switch />
</FormItem>
<FormItem
name="rag"
label={t('apiKey.knowledgeBase')}
layout="horizontal"
valuePropName="checked"
>
<Switch />
</FormItem>
{/* 高级设置 */}
<div className="rb:text-[#5B6167] rb:font-medium rb:leading-5 rb:mb-4">{t('apiKey.advancedSettings')}</div>
<FormItem
name="expires_at"
label={t('apiKey.expires_at')}
>
<DatePicker
className="rb:w-full"
disabledDate={(current) => current && current < dayjs().subtract(1, 'day').endOf('day')}
/>
</FormItem>
</Form>
</RbModal>
);
});
export default ApiKeyModal;

View File

@@ -0,0 +1,125 @@
import React, { useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, App, Space } from 'antd';
import clsx from 'clsx';
import { DeleteOutlined, EditOutlined, EyeOutlined } from '@ant-design/icons';
import type { ApiKey, ApiKeyModalRef } from './types';
import ApiKeyModal from './components/ApiKeyModal';
import ApiKeyDetailModal from './components/ApiKeyDetailModal';
import RbCard from '@/components/RbCard/Card'
import { getApiKeyListUrl, deleteApiKey } from '@/api/apiKey';
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
import { formatDateTime } from '@/utils/format';
import Tag from '@/components/Tag'
import copy from 'copy-to-clipboard'
import { maskApiKeys } from '@/utils/apiKeyReplacer';
const ApiKeyManagement: React.FC = () => {
const { t } = useTranslation();
const { modal, message } = App.useApp();
const apiKeyModalRef = useRef<ApiKeyModalRef>(null);
const apiKeyDetailModalRef = useRef<ApiKeyModalRef>(null)
const scrollListRef = useRef<PageScrollListRef>(null)
const refresh = () => {
scrollListRef.current?.refresh();
}
const handleEdit = (item?: ApiKey) => {
apiKeyModalRef.current?.handleOpen(item);
}
const handleView = (item: ApiKey) => {
apiKeyDetailModalRef.current?.handleOpen(item);
}
const handleDelete = (item: ApiKey) => {
modal.confirm({
title: t('common.confirmDeleteDesc', { name: item.name }),
okText: t('common.delete'),
okType: 'danger',
onOk: () => {
deleteApiKey(item.id)
.then(() => {
refresh();
message.success(t('common.deleteSuccess'))
})
}
})
}
const handleCopy = (content: string) => {
copy(content)
message.success(t('common.copySuccess'))
}
return (
<>
<div className="rb:flex rb:justify-end rb:mb-3 rb:p-4">
<Button type="primary" onClick={() => handleEdit()}>
{t('apiKey.createApiKey')}
</Button>
</div>
<PageScrollList
ref={scrollListRef}
url={getApiKeyListUrl}
query={{ is_active: true, type: 'service' }}
column={2}
renderItem={(item: Record<string, unknown>) => {
let apiKeyItem = item as unknown as ApiKey
return (
<RbCard
title={apiKeyItem.name}
>
{['id', 'is_expired', 'created_at'].map((key, index) => (
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
'rb:mt-3': index !== 0
})}>
<span className="rb:text-[#5B6167] rb:w-20">{t(`apiKey.${key}`)}</span>
<span className="rb:flex-1 rb:text-left rb:py-px rb:rounded rb:font-medium">
{ key === 'created_at'
? formatDateTime(apiKeyItem[key], 'YYYY-MM-DD HH:mm:ss')
: key === 'is_expired'
? <Tag color={apiKeyItem[key] ? 'error' : 'processing'}>{apiKeyItem[key] ? t('apiKey.inactive') : t('apiKey.active')}</Tag>
: String(apiKeyItem[key as keyof ApiKey])
}
</span>
</div>
))}
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
{maskApiKeys(apiKeyItem.api_key)}
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(apiKeyItem.api_key)}>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
</div>
<Space className="rb:pt-2 rb:min-h-6.25">
{apiKeyItem.scopes?.includes('memory') && <Tag>{t('apiKey.memoryEngine')}</Tag>}
{apiKeyItem.scopes?.includes('rag') && <Tag color="success">{t('apiKey.knowledgeBase')}</Tag>}
</Space>
<div className="rb:mt-5 rb:flex rb:justify-end rb:gap-2.5">
<Button icon={<EyeOutlined />} onClick={() => handleView(apiKeyItem)}></Button>
<Button icon={<EditOutlined />} onClick={() => handleEdit(apiKeyItem)}></Button>
<Button icon={<DeleteOutlined />} onClick={() => handleDelete(apiKeyItem)}></Button>
</div>
</RbCard>
);
}}
/>
<ApiKeyModal
ref={apiKeyModalRef}
refresh={refresh}
/>
<ApiKeyDetailModal
ref={apiKeyDetailModalRef}
handleCopy={handleCopy}
/>
</>
);
};
export default ApiKeyManagement;

View File

@@ -0,0 +1,40 @@
import type { Dayjs } from 'dayjs'
import { maskApiKeys } from '@/utils/apiKeyReplacer'
export interface ApiKey {
id: string;
name: string;
description?: string;
type: 'agent' | 'multi_agent' | 'workflow' | 'service';
scopes?: string[]; // 'memory' | 'rag' | 'app'
api_key: string;
is_active: boolean;
is_expired: boolean;
created_at: number;
expires_at?: number | Dayjs;
memory?: boolean;
rag?: boolean;
updated_at: string;
qps_limit?: number;
daily_request_limit?: number;
rate_limit?: number;
total_requests: number;
quota_used: number;
quota_limit: number;
}
export interface ApiKeyModalRef {
handleOpen: (apiKey?: ApiKey) => void;
handleClose: () => void;
}
/**
* 获取掩码后的API密钥
*/
export const getMaskedApiKey = (apiKey: string): string => {
return maskApiKeys(apiKey)
}

View File

@@ -239,6 +239,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
return [
...(prev || []).map(item => ({
...item,
conversation_id: undefined,
list: []
})),
newChatItem

View File

@@ -1,153 +1,194 @@
import { type FC, useState } from 'react';
import { type FC, useState, useRef, useEffect } from 'react';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import { Button, Space, App
// Slider, Input,
// Form,
// Checkbox
} from 'antd';
import { Button, Space, App, Statistic, Row, Col } from 'antd';
import copy from 'copy-to-clipboard'
import Card from './components/Card';
// import qpsRestrictions from '@/assets/images/application/qpsRestrictions.svg'
// import dailyAdjustmentDosage from '@/assets/images/application/dailyAdjustmentDosage.svg'
// import tokenCap from '@/assets/images/application/tokenCap.svg'
import type { Application } from '@/views/ApplicationManagement/types'
import type { ApiKeyModalRef, ApiKeyConfigModalRef } from './types'
import type { ApiKey } from '@/views/ApiKeyManagement/types'
import ApiKeyModal from './components/ApiKeyModal';
import ApiKeyConfigModal from './components/ApiKeyConfigModal';
import Tag from '@/components/Tag'
import { getApiKeyList, getApiKeyStats, deleteApiKey } from '@/api/apiKey';
import { maskApiKeys } from '@/utils/apiKeyReplacer'
// const limitList = [
// { key: 'qpsRestrictions', value: '10', icon: qpsRestrictions, unit: ' times/second' },
// { key: 'dailyAdjustmentDosage', value: '1000', icon: dailyAdjustmentDosage, unit: ' times/day' },
// { key: 'tokenCap', value: '10', icon: tokenCap, unit: 'M Tokens/day' },
// ]
// const sdkList = ['pythonSDK', 'nodejsSDK', 'goSDK', 'curlExample']
const Api: FC<{apiKeyList?: string[]}> = ({apiKeyList = []}) => {
const Api: FC<{ application: Application | null }> = ({ application }) => {
const { t } = useTranslation();
const [activeMethods, setActiveMethod] = useState(['GET']);
const { message } = App.useApp()
// const [form] = Form.useForm();
const activeMethods = ['GET'];
const { message, modal } = App.useApp()
const copyContent = window.location.origin + '/v1/chat'
const apiKeyModalRef = useRef<ApiKeyModalRef>(null);
const apiKeyConfigModalRef = useRef<ApiKeyConfigModalRef>(null);
const [apiKeyList, setApiKeyList] = useState<ApiKey[]>([])
const handleCopy = (content: string) => {
copy(content)
message.success(t('common.copySuccess'))
}
return (
<div className="rb:w-[1000px] rb:mt-[20px] rb:pb-[20px] rb:mx-auto">
{/* <Form form={form} layout="vertical"> */}
<Space size={20} direction="vertical" style={{width: '100%'}}>
<Card title={t('application.endpointConfiguration')}>
<div className="rb:p-[20px_20px_24px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]">
<Space size={8}>
{['GET', 'POST', 'PUT', 'DELETE'].map((method) => (
<Button key={method} type={activeMethods.includes(method) ? 'primary' : 'default'} onClick={() => setActiveMethod(prev => activeMethods.includes(method) ? prev.filter(m => m !== method) : [...prev, method])}>
{method}
</Button>
))}
</Space>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-[20px] rb:p-[20px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:leading-[20px]">
{copyContent}
<Button className="rb:px-[8px]! rb:h-[28px]! rb:group" onClick={() => handleCopy(copyContent)}>
useEffect(() => {
getApiList()
}, [])
const getApiList = () => {
if (!application) {
return
}
setApiKeyList([])
getApiKeyList({
type: application.type,
is_active: true,
resource_id: application.id,
page: 1,
pagesize: 10,
}).then(res => {
const response = res as { items: ApiKey[] }
const list = response.items ?? []
getAllStats([...list])
})
}
const getAllStats = (list: ApiKey[]) => {
const allList: ApiKey[] = []
list.forEach(async item => {
await getApiKeyStats(item.id)
.then(res => {
const response = res as { requests_today: number; total_requests: number; quota_limit: number; quota_used: number; }
allList.push({
...item,
...response,
})
setApiKeyList(prev => [...prev, {
...item,
...response,
}])
})
})
}
const handleAdd = () => {
apiKeyModalRef.current?.handleOpen()
}
const handleEdit = (vo: ApiKey) => {
apiKeyConfigModalRef.current?.handleOpen(vo)
}
const handleDelete = (vo: ApiKey) => {
modal.confirm({
title: t('common.confirmDeleteDesc', { name: vo.name }),
content: t('application.apiKeyDeleteContent'),
okText: t('common.delete'),
okType: 'danger',
onOk: () => {
deleteApiKey(vo.id)
.then(() => {
getApiList();
message.success(t('common.deleteSuccess'))
})
}
})
}
// 计算total_requests总数
const totalRequests = apiKeyList.reduce((total, item) => total + item.total_requests, 0);
return (
<div className="rb:w-250 rb:mt-5 rb:pb-5 rb:mx-auto">
<Space size={20} direction="vertical" style={{width: '100%'}}>
<Card
title={t('application.endpointConfiguration')}
>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.endpointConfigurationSubTitle')}</div>
<div className="rb:p-[20px_20px_24px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<Space size={8}>
{['GET', 'POST', 'PUT', 'DELETE'].map((method) => (
<div key={method} className={clsx("rb:w-20 rb:h-7 rb:leading-7 rb:text-center rb:rounded-md rb:text-regular", {
'rb:bg-[#155EEF] rb:text-white': activeMethods.includes(method),
'rb:bg-white': !activeMethods.includes(method),
})}>
{method}
</div>
))}
</Space>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[20px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
{copyContent}
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(copyContent)}>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
</div>
</div>
</Card>
<Card
title={t('application.apiKeys')}
extra={
<Button style={{padding: '0 8px', height: '24px'}} onClick={handleAdd}>+ {t('application.addApiKey')}</Button>
}
>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:mb-2">{t('application.apiKeySubTitle')}</div>
{/* 总览数据 */}
<Row>
<Col span={6}>
<Statistic title={t('application.apiKeyTotal')} value={apiKeyList.length} />
</Col>
<Col span={6}>
<Statistic title={t('application.apiKeyRequestTotal')} value={totalRequests} />
</Col>
</Row>
{/* API Key 列表 */}
{apiKeyList.sort((a, b) => b.created_at - a.created_at).map(item => (
<div key={item.id} className="rb:mt-4 rb:p-[10px_12px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
<div className="rb:flex rb:items-center rb:justify-between">
<div className="rb:flex rb:items-center rb:max-w-[calc(100%-92px)]">
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-1">{item.name}</div>
<Tag className="rb:ml-2">ID: {item.id}</Tag>
</div>
<Space>
<div
className="rb:w-[16px] rb:h-[16px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/editBorder.svg')] rb:hover:bg-[url('@/assets/images/editBg.svg')]"
onClick={() => handleEdit(item)}
></div>
<div
className="rb:w-6 rb:h-6 rb:cursor-pointer rb:bg-[url('@/assets/images/deleteBorder.svg')] rb:hover:bg-[url('@/assets/images/deleteBg.svg')]"
onClick={() => handleDelete(item)}
></div>
</Space>
</div>
<div className="rb:mb-3 rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-5 rb:p-[8px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-lg rb:leading-5">
{maskApiKeys(item.api_key)}
<Button className="rb:px-2! rb:h-7! rb:group" onClick={() => handleCopy(item.api_key)}>
<div
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
</div>
<Row gutter={12}>
<Col span={8}>
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.apiKeyRequestTotal')} value={item.total_requests} />
</Col>
<Col span={8}>
<Statistic valueStyle={{ fontSize: '18px' }} title={t('application.qpsLimit')} value={item.rate_limit} />
</Col>
</Row>
</div>
</Card>
<Card
title={t('application.authenticationMethod')}
// extra={
// <Button style={{padding: '0 8px', height: '24px'}} onClick={handleAdd}>+ {t('application.addApiKey')}</Button>
// }
>
<div className="rb:p-[10px_20px] rb:bg-[#F0F3F8] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:font-medium rb:text-center">
{t('application.apiKeyTitle')}
<p className="rb:mt-[6px] rb:text-[#5B6167] rb:text-[12px] rb:font-regular">{t('application.apiKeyDesc')}</p>
</div>
{apiKeyList.map((item, index) => (
<div key={index} className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:mt-[20px] rb:p-[12px_16px] rb:bg-[#FFFFFF] rb:border rb:border-[#DFE4ED] rb:rounded-[8px] rb:leading-[20px]">
{item}
))}
</Card>
</Space>
<Space>
<Button className="rb:px-[8px]! rb:h-[28px]! rb:group" onClick={() => handleCopy(item)}>
<div
className="rb:w-[16px] rb:h-[16px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/copy.svg')] rb:group-hover:bg-[url('@/assets/images/copy_active.svg')]"
></div>
{t('common.copy')}
</Button>
{/* <div
className="rb:w-[24px] rb:h-[24px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
onClick={() => handleDelete(index)}
></div> */}
</Space>
</div>
))}
</Card>
{/* <Card title={t('application.requestResponseExample')}>
<div className="rb:mb-[12px] rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:font-regular">
{t('application.requestExample')}
<Button>{t('application.downloadPostmanCollection')}</Button>
</div>
<div className="rb:p-[16px_20px] rb:bg-[#F0F3F8] rb:rounded-[8px] rb:text-[#5B6167] rb:leading-[18px]">
curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d
</div>
<div className="rb:mb-[12px] rb:mt-[24px] rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:font-regular">
{t('application.responseExample')}
</div>
<div className="rb:p-[16px_20px] rb:bg-[#F0F3F8] rb:rounded-[8px] rb:text-[#5B6167] rb:leading-[18px]">
curl -X POST https://api.example.com/v1/agent/execute \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d
</div>
</Card>
<Card title={t('application.rateLimitingStrategy')}>
<div className="rb:grid rb:grid-cols-3 rb:gap-[18px]">
{limitList.map(item => (
<div key={item.key} className="rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-[8px] rb:p-[16px_20px]">
<div className="rb:flex rb:justify-between">
<div className="rb:leading-[20px]">
{t(`application.${item.key}`)}
<div className="rb:text-[14px] rb:font-medium rb:text-[#155EEF] rb:mt-[8px]">{item.value}{item.unit}</div>
</div>
<img src={item.icon} className="rb:w-[24px] rb:h-[24px]" />
</div>
<Slider style={{ margin: '24px 0 0 0' }} value={item.value} />
</div>
))}
</div>
</Card>
<Card title={t('application.sdkDownload')}>
<div className="rb:grid rb:grid-cols-4 rb:gap-[16px]">
{sdkList.map(item => (
<div key={item} className="rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded-[8px] rb:p-[24px_20px] rb:text-center">
{t(`application.${item}`)}
</div>
))}
</div>
</Card>
<Card title={t('application.advancedSettings')}>
<Form.Item
name="WebhookReturnsTimeout"
label={<>{t('application.WebhookReturnsTimeout')}<span className="rb:text-[#5B6167] rb:text-[12px] rb:font-regular"> ({t('application.WebhookReturnsTimeoutDesc')})</span></>}
>
<Input disabled />
</Form.Item>
<Form.Item
name="whitelistIP"
label={<>{t('application.whitelistIP')}<span className="rb:text-[#5B6167] rb:text-[12px] rb:font-regular"> ({t('application.whitelistIPDesc')})</span></>}
>
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item
name="whitelistIP"
className="rb:mb-[0]!"
>
<Checkbox>{t('application.publicAPIDocumentation')}</Checkbox>
</Form.Item>
</Card> */}
</Space>
{/* </Form> */}
<ApiKeyModal
ref={apiKeyModalRef}
application={application}
refresh={getApiList}
/>
<ApiKeyConfigModal
ref={apiKeyConfigModalRef}
refresh={getApiList}
/>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { type FC, useEffect, useState, useRef, type Key } from 'react'
import { type FC, useEffect, useState, useRef, forwardRef, useImperativeHandle, type Key } from 'react'
import { useTranslation } from 'react-i18next'
import { useParams } from 'react-router-dom';
import Card from './components/Card'
@@ -11,17 +11,19 @@ import type {
Config,
SubAgentModalRef,
ChatData,
SubAgentItem
SubAgentItem,
ClusterRef
} from './types'
import Chat from './components/Chat'
import RbCard from '@/components/RbCard/Card'
import SubAgentModal from './components/SubAgentModal'
import Empty from '@/components/Empty'
import type { Application } from '@/views/ApplicationManagement/types'
const tagColors = ['processing', 'warning', 'default']
const MAX_LENGTH = 5;
const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
const Cluster = forwardRef<ClusterRef, { application: Application }>(({application}, ref) => {
const { t } = useTranslation()
const { message } = App.useApp()
const [form] = Form.useForm()
@@ -113,6 +115,9 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
form.setFieldsValue({ master_agent_name: option.children })
}
}
useImperativeHandle(ref, () => ({
handleSave
}))
return (
<Row className="rb:h-[calc(100vh-64px)]">
@@ -199,7 +204,7 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
chatList={chatList}
updateChatList={setChatList}
handleSave={handleSave}
source="cluster"
source="multi_agent"
/>
</RbCard>
</Col>
@@ -210,6 +215,6 @@ const Cluster: FC<{application: SubAgentItem}> = ({application}) => {
/>
</Row>
)
}
})
export default Cluster

View File

@@ -0,0 +1,131 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Slider } from 'antd';
import { useTranslation } from 'react-i18next';
import type { ApiKeyConfigModalRef } from '../types'
import RbModal from '@/components/RbModal'
import { updateApiKey } from '@/api/apiKey';
import type { ApiKey } from '@/views/ApiKeyManagement/types'
interface ApiKeyConfigModalProps {
refresh: () => void;
}
const ApiKeyConfigModal = forwardRef<ApiKeyConfigModalRef, ApiKeyConfigModalProps>(({
refresh
}, ref) => {
const { t } = useTranslation();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm<ApiKey>();
const [loading, setLoading] = useState(false)
const values = Form.useWatch<ApiKey>([], form)
const [editVo, setEditVo] = useState<ApiKey | null>(null)
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
form.resetFields();
setLoading(false)
setEditVo(null)
setVisible(false);
};
const handleOpen = (apiKey: ApiKey) => {
setVisible(true);
setEditVo(apiKey)
form.setFieldsValue({
daily_request_limit: apiKey.daily_request_limit,
rate_limit: apiKey.rate_limit
});
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
if (!editVo?.id) return
form.validateFields()
.then((values) => {
updateApiKey(editVo.id, {
...editVo,
...values
})
handleClose()
setTimeout(() => {
refresh()
}, 50)
})
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={t('application.apiLimitConfig')}
open={visible}
onCancel={handleClose}
okText={t('common.save')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
className="rb:px-2.5!"
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
>
{/* QPS 限制(每秒请求数) */}
<>
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mb-2">
{t(`application.qpsLimit`)}({t('application.qpsLimitTip')})
</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:mb-2">
{t('application.qpsLimitDesc')}
</div>
<div className="rb:pl-2">
<Form.Item
name="rate_limit"
>
<Slider
style={{ margin: '0' }}
min={1}
max={100}
step={1}
/>
</Form.Item>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
1
<span>{t('application.currentValue')}: {values?.rate_limit}{t('application.qpsLimitUnit')}</span>
</div>
</div>
</>
{/* 日调用量限制 */}
<>
<div className="rb:text-[14px] rb:font-medium rb:leading-5 rb:mt-6 rb:mb-2">
{t(`application.dailyUsageLimit`)}
</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:mb-2">
{t('application.dailyUsageLimitDesc')}
</div>
<div className="rb:pl-2">
<Form.Item
name="daily_request_limit"
>
<Slider
style={{ margin: '0' }}
min={100}
max={100000}
step={100}
/>
</Form.Item>
<div className="rb:flex rb:items-center rb:justify-between rb:text-[#5B6167] rb:leading-5 rb:-mt-6.5">
100
<span>{t('application.currentValue')}: {values?.daily_request_limit}{t('application.dailyUsageLimitUnit')}</span>
</div>
</div>
</>
</Form>
</RbModal>
);
});
export default ApiKeyConfigModal;

View File

@@ -0,0 +1,104 @@
import { forwardRef, useImperativeHandle, useState } from 'react';
import { Form, Input, App } from 'antd';
import { useTranslation } from 'react-i18next';
import type { Application } from '@/views/ApplicationManagement/types'
import type { ApiKeyModalRef } from '../types'
import { createApiKey } from '@/api/apiKey';
import RbModal from '@/components/RbModal'
const FormItem = Form.Item;
interface ApiKeyModalProps {
refresh: () => void;
application?: Application | null;
}
const ApiKeyModal = forwardRef<ApiKeyModalRef, ApiKeyModalProps>(({
refresh,
application
}, ref) => {
const { t } = useTranslation();
const { message } = App.useApp();
const [visible, setVisible] = useState(false);
const [form] = Form.useForm();
const [loading, setLoading] = useState(false)
// 封装取消方法,添加关闭弹窗逻辑
const handleClose = () => {
setVisible(false);
form.resetFields();
setLoading(false)
};
const handleOpen = () => {
setVisible(true);
form.resetFields();
};
// 封装保存方法,添加提交逻辑
const handleSave = () => {
if (!application) return
form.validateFields()
.then((values) => {
setLoading(true)
createApiKey({
...values,
type: application.type,
resource_id: application.id,
scopes: ['app']
})
.then(() => {
handleClose()
refresh()
message.success(t('common.createSuccess'))
})
.finally(() => {
setLoading(false)
})
})
}
// 暴露给父组件的方法
useImperativeHandle(ref, () => ({
handleOpen,
handleClose
}));
return (
<RbModal
title={t('application.addApiKey')}
open={visible}
onCancel={handleClose}
okText={t('common.create')}
onOk={handleSave}
confirmLoading={loading}
>
<Form
form={form}
layout="vertical"
scrollToFirstError={{ behavior: 'instant', block: 'end', focus: true }}
>
{/* Key 名称 */}
<FormItem
name="name"
label={t('application.apiKeyName')}
rules={[
{ required: true, message: t('common.pleaseEnter') },
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('application.invalidVariableName') },
]}
>
<Input placeholder={t('application.apiKeyNamePlaceholder')} />
</FormItem>
{/* 描述 */}
<FormItem
name="description"
label={t('application.description')}
>
<Input.TextArea placeholder={t('application.apiKeyDescPlaceholder')} />
</FormItem>
</Form>
</RbModal>
);
});
export default ApiKeyModal;

View File

@@ -1,46 +1,125 @@
import { type FC, useRef, useEffect, useState } from 'react';
import { type FC, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import clsx from 'clsx'
import { Input, Form } from 'antd'
import ChatIcon from '@/assets/images/application/chat.svg'
import ChatIcon from '@/assets/images/application/chat.png'
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.svg'
import type { ChatItem, ChatData, Config } from '../types'
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
import type { ChatData, Config } from '../types'
import { runCompare, draftRun } from '@/api/application'
import Empty from '@/components/Empty'
import Markdown from '@/components/Markdown'
import ChatContent from '@/components/Chat/ChatContent'
import type { ChatItem } from '@/components/Chat/types'
import { type SSEMessage } from '@/utils/stream'
interface ChatProps {
chatList: ChatData[];
data: Config;
updateChatList: (list: ChatData[]) => void;
updateChatList: React.Dispatch<React.SetStateAction<ChatData[]>>;
handleSave: (flag?: boolean) => Promise<any>;
source?: 'cluster' | 'agent';
source?: 'multi_agent' | 'agent';
}
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => {
const { t } = useTranslation();
const [form] = Form.useForm<{ message: string }>()
const scrollContainerRefs = useRef<(HTMLDivElement | null)[]>([])
const [loading, setLoading] = useState(false)
const [isCluster, setIsCluster] = useState(source === 'cluster')
const [isCluster, setIsCluster] = useState(source === 'multi_agent')
const [conversationId, setConversationId] = useState<string | null>(null)
const [compareLoading, setCompareLoading] = useState(false)
// 当聊天列表更新时,自动滚动到底部
useEffect(() => {
// 延迟一下确保DOM已经更新
setTimeout(() => {
scrollContainerRefs.current.forEach(container => {
if (container) {
container.scrollTop = container.scrollHeight;
}
});
}, 0);
}, [chatList]);
useEffect(() => {
setIsCluster(source === 'cluster')
setIsCluster(source === 'multi_agent')
}, [source])
const addUserMessage = (message: string) => {
const newUserMessage: ChatItem = {
role: 'user',
content: message,
created_at: Date.now(),
};
updateChatList(prev => prev.map(item => ({
...item,
list: [...(item.list || []), newUserMessage]
})))
}
const addAssistantMessage = () => {
const assistantMessage: ChatItem = {
role: 'assistant',
content: '',
created_at: Date.now(),
};
if (isCluster) {
updateChatList(prev => prev.map(item => ({
...item,
list: [...(item.list || []), assistantMessage]
})))
} else {
const assistantMessages: Record<string, ChatItem> = {}
chatList.forEach(item => {
assistantMessages[item.model_config_id as string] = assistantMessage
})
updateChatList(prev => prev.map(item => ({
...item,
list: [...(item.list || []), assistantMessages[item.model_config_id as string]]
})))
}
}
const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string) => {
if (!content || !model_config_id) return
updateChatList(prev => {
const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id);
if (targetIndex !== -1) {
const modelChatList = [...prev]
const curModelChat = modelChatList[targetIndex]
const curChatMsgList = curModelChat.list || []
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
if (lastMsg.role === 'assistant') {
modelChatList[targetIndex] = {
...modelChatList[targetIndex],
conversation_id: conversation_id,
list: [
...curChatMsgList.slice(0, curChatMsgList.length - 1),
{
...lastMsg,
content: lastMsg.content + content
}
]
}
}
return [...modelChatList]
}
return prev;
})
}
const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => {
if (message_length > 0 || !model_config_id) return
updateChatList(prev => {
const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id);
if (targetIndex > -1) {
const modelChatList = [...prev]
const curModelChat = modelChatList[targetIndex]
const curChatMsgList = curModelChat.list || []
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
if (lastMsg.role === 'assistant') {
modelChatList[targetIndex] = {
...modelChatList[targetIndex],
list: [
...curChatMsgList.slice(0, curChatMsgList.length - 1),
{
...lastMsg,
content: null
}
]
}
}
return [...modelChatList]
}
return prev
})
}
const handleSend = () => {
if (loading) return
setLoading(true)
@@ -48,182 +127,47 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
handleSave(false)
.then(() => {
const message = form.getFieldValue('message')
if (!message || message.trim() === '') return
const newUserMessage: ChatItem = {
role: 'question',
content: message,
time: Date.now(),
};
updateChatList((prev: ChatData[]) => {
return prev.map(item => ({
...item,
list: [
...(item.list || []),
newUserMessage
]
}))
})
if (!message?.trim()) return
addUserMessage(message)
form.setFieldsValue({ message: undefined })
// 添加空的助手消息用于流式更新
const assistantMessages: Record<string, ChatItem> = {};
if (isCluster) {
const assistantMessage: ChatItem = {
role: 'answer',
content: '',
time: Date.now(),
};
assistantMessages['cluster'] = assistantMessage;
updateChatList((prev: ChatData[]) => prev.map(item => ({
...item,
list: [...(item.list || []), assistantMessage]
})))
} else {
chatList.forEach(item => {
const assistantMessage: ChatItem = {
role: 'answer',
content: '',
time: Date.now(),
};
assistantMessages[item.model_config_id] = assistantMessage;
});
updateChatList((prev: ChatData[]) => prev.map(item => ({
...item,
list: [...(item.list || []), assistantMessages[item.model_config_id]]
})))
}
addAssistantMessage()
const handleStreamMessage = (data: string) => {
const handleStreamMessage = (data: SSEMessage[]) => {
setCompareLoading(false)
try {
const lines = data.split('\n');
let currentEvent = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.startsWith('event:')) {
currentEvent = line.substring(6).trim();
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_message')) {
const jsonData = line.substring(5).trim();
const parsed = JSON.parse(jsonData);
if (parsed.content && parsed.model_config_id) {
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
if (targetIndex !== -1) {
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
if (index === targetIndex) {
return {
...item,
conversation_id: parsed.conversation_id,
list: item.list?.map((msg, msgIndex) => {
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
return { ...msg, content: msg.content + parsed.content };
}
return msg;
}) || []
};
}
return item;
}))
}
}
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'message')) {
const jsonData = line.substring(5).trim();
const parsed = JSON.parse(jsonData);
if (parsed.content) {
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
if (index === 0) {
return {
...item,
list: item.list?.map((msg, msgIndex) => {
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
return { ...msg, content: (msg.content || '') + parsed.content };
}
return msg;
}) || []
};
}
return item;
}))
}
if (parsed.conversation_id) {
setConversationId(parsed.conversation_id);
}
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_end')) {
const jsonData = line.substring(5).trim();
const parsed = JSON.parse(jsonData);
if (parsed.message_length === 0 && parsed.model_config_id) {
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
if (targetIndex !== -1) {
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
if (index === targetIndex) {
return {
...item,
list: item.list?.map((msg, msgIndex) => {
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
return { ...msg, content: null };
}
return msg;
}) || []
};
}
return item;
}))
}
}
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'model_end')) {
const jsonData = line.substring(5).trim();
const parsed = JSON.parse(jsonData);
if (parsed.message_length === 0) {
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
if (index === 0) {
return {
...item,
list: item.list?.map((msg, msgIndex) => {
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
return { ...msg, content: null };
}
return msg;
}) || []
};
}
return item;
}))
}
if (parsed.conversation_id) {
setConversationId(parsed.conversation_id);
}
} else if (currentEvent === 'compare_end') {
data.map(item => {
const { model_config_id, conversation_id, content, message_length } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number };
switch(item.event) {
case 'model_message':
updateAssistantMessage(content, model_config_id, conversation_id)
break;
case 'model_end':
updateErrorAssistantMessage(message_length, model_config_id)
break;
case 'compare_end':
setLoading(false);
}
break;
}
} catch (e) {
console.error('Parse stream data error:', e);
}
})
};
setTimeout(() => {
if (isCluster) {
draftRun(data.app_id, { message, conversation_id: conversationId, stream: true }, handleStreamMessage)
.finally(() => setLoading(false))
} else {
runCompare(data.app_id, {
message,
models: chatList.map(item => ({
model_config_id: item.model_config_id,
label: item.label,
model_parameters: item.model_parameters,
conversation_id: item.conversation_id
})),
variables: {},
"parallel": true,
"stream": true,
"timeout": 60,
}, handleStreamMessage)
.finally(() => setLoading(false));
}
runCompare(data.app_id, {
message,
models: chatList.map(item => ({
model_config_id: item.model_config_id,
label: item.label,
model_parameters: item.model_parameters,
conversation_id: item.conversation_id
})),
variables: {},
"parallel": true,
"stream": true,
"timeout": 60,
}, handleStreamMessage)
.finally(() => setLoading(false));
}, 0)
})
.catch(() => {
@@ -231,6 +175,136 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
setCompareLoading(false)
})
}
const addClusterAssistantMessage = () => {
const assistantMessage: ChatItem = {
role: 'assistant',
content: '',
created_at: Date.now(),
};
updateChatList(prev => prev.map(item => ({
...item,
list: [...(item.list || []), assistantMessage]
})))
}
const updateClusterAssistantMessage = (content?: string) => {
if (!content) return
updateChatList(prev => {
const modelChatList = [...prev]
const curModelChat = modelChatList[0]
const curChatMsgList = curModelChat.list || []
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
if (lastMsg.role === 'assistant') {
modelChatList[0] = {
...modelChatList[0],
list: [
...curChatMsgList.slice(0, curChatMsgList.length - 1),
{
...lastMsg,
content: lastMsg.content + content
}
]
}
}
return [...modelChatList]
})
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
if (index === 0) {
return {
...item,
list: item.list?.map((msg, msgIndex) => {
if (msgIndex === item.list!.length - 1 && msg.role === 'assistant') {
return { ...msg, content: (msg.content || '') + content };
}
return msg;
}) || []
};
}
return item;
}))
}
const updateClusterErrorAssistantMessage = (message_length: number) => {
if (message_length > 0) return
updateChatList(prev => {
const modelChatList = [...prev]
const curModelChat = modelChatList[0]
const curChatMsgList = curModelChat.list || []
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
if (lastMsg.role === 'assistant') {
modelChatList[0] = {
...modelChatList[0],
list: [
...curChatMsgList.slice(0, curChatMsgList.length - 1),
{
...lastMsg,
content: null
}
]
}
}
return [...modelChatList]
})
}
const handleClusterSend = () => {
if (loading) return
setLoading(true)
setCompareLoading(true)
handleSave(false)
.then(() => {
const message = form.getFieldValue('message')
if (!message || message.trim() === '') return
addUserMessage(message)
form.setFieldsValue({ message: undefined })
addClusterAssistantMessage()
const handleStreamMessage = (data: SSEMessage[]) => {
setCompareLoading(false)
data.map(item => {
const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number };
switch(item.event) {
case 'start':
if (conversation_id && conversationId !== conversation_id) {
setConversationId(conversation_id);
}
break
case 'message':
updateClusterAssistantMessage(content)
if (conversation_id && conversationId !== conversation_id) {
setConversationId(conversation_id);
}
break;
case 'model_end':
updateClusterErrorAssistantMessage(message_length)
break;
case 'compare_end':
setLoading(false);
break;
}
})
};
setTimeout(() => {
draftRun(
data.app_id,
{
message,
conversation_id: conversationId,
stream: true
},
handleStreamMessage
)
.finally(() => setLoading(false))
}, 0)
})
.catch(() => {
setLoading(false)
setCompareLoading(false)
})
}
const handleDelete = (index: number) => {
updateChatList(chatList.filter((_, voIndex) => voIndex !== index))
}
@@ -240,6 +314,7 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
{chatList.length === 0
? <Empty
url={DebuggingEmpty}
size={[300, 200]}
title={t('application.debuggingEmpty')}
subTitle={t('application.debuggingEmptyDesc')}
className="rb:h-full"
@@ -257,69 +332,55 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
<div className={clsx(
"rb:grid rb:bg-[#F0F3F8] rb:text-center rb:flex-[0_0_auto]",
{
'rb:rounded-tr-[12px]': index === chatList.length - 1,
'rb:rounded-tl-[12px]': index === 0,
'rb:rounded-tr-xl': index === chatList.length - 1,
'rb:rounded-tl-xl': index === 0,
}
)}>
<div className='rb:relative rb:p-[10px_12px] rb:overflow-hidden'>
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:w-[calc(100%-24px)]">{chat.label}</div>
<div
className="rb:w-[16px] rb:h-[16px] rb:cursor-pointer rb:absolute rb:top-[12px] rb:right-[12px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:absolute rb:top-3 rb:right-3 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
onClick={() => handleDelete(index)}
></div>
</div>
</div>
}
{!chat.list || chat.list.length === 0
? <Empty url={ChatIcon} title={t('application.chatEmpty')} className="rb:h-full" />
: (
<div ref={el => scrollContainerRefs.current[index] = el} className={clsx(`rb:relative rb:overflow-y-auto rb:overflow-x-hidden`, {
'rb:h-[calc(100vh-186px)]': isCluster,
'rb:h-[calc(100vh-286px)]': !isCluster,
})}>
{chat.list?.map((vo, voIndex) => {
if (compareLoading && voIndex === chat.list?.length - 1) {
return null
}
return (
<div key={voIndex} className={clsx("rb:relative rb:mt-[24px]", {
'rb:right-[16px] rb:text-right': vo.role === 'question',
'rb:left-[16px] rb:text-left': vo.role !== 'question',
})}>
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-[16px] rb:font-regular">{vo.role === 'question' ? 'You' : chat.label}</div>
<div className={clsx('rb:border rb:text-left rb:rounded-[8px] rb:mt-[6px] rb:leading-[18px] rb:p-[10px_12px_2px_12px] rb:inline-block', {
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': vo.role !== 'question' && vo.content === null,
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': vo.role === 'question' && vo.content,
'rb:bg-[#ffffff] rb:border-[rgba(235,235,235,1)]': vo.role !== 'question' && (vo.content || vo.content === ''),
'rb:max-w-[400px]': chatList.length === 1,
'rb:max-w-[260px]': chatList.length === 2,
'rb:max-w-[150px]': chatList.length === 3,
'rb:max-w-[108px]': chatList.length === 4,
})}>
<Markdown content={vo.content === null ? t('application.ReplyException') : vo.content} />
</div>
</div>
)
})}
</div>
)
}
<ChatContent
classNames={{
'rb:mx-[16px] rb:pt-[24px]': true,
'rb:h-[calc(100vh-186px)]': isCluster,
'rb:h-[calc(100vh-286px)]': !isCluster,
}}
contentClassNames={{
'rb:max-w-[400px]!': chatList.length === 1,
'rb:max-w-[260px]!': chatList.length === 2,
'rb:max-w-[150px]!': chatList.length === 3,
'rb:max-w-[108px]!': chatList.length === 4,
}}
empty={<Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
data={chat.list || []}
streamLoading={compareLoading}
labelPosition="top"
labelFormat={(item) => item.role === 'user' ? 'You' : chat.label}
errorDesc={t('application.ReplyException')}
/>
</div>
))}
</div>
<div className="rb:flex rb:items-center rb:gap-[10px] rb:p-[16px]">
<div className="rb:flex rb:items-center rb:gap-2.5 rb:p-4">
<Form form={form} style={{width: 'calc(100% - 54px)'}}>
<Form.Item name="message" className="rb:mb-[0]!">
<Form.Item name="message" className="rb:mb-0!">
<Input
className="rb:h-[44px] rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
className="rb:h-11 rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
placeholder={t('application.chatPlaceholder')}
onPressEnter={handleSend}
onPressEnter={isCluster ? handleClusterSend : handleSend}
/>
</Form.Item>
</Form>
<img src={ChatSendIcon} className={clsx("rb:w-[44px] rb:h-[44px] rb:cursor-pointer", {
<img src={ChatSendIcon} className={clsx("rb:w-11 rb:h-11 rb:cursor-pointer", {
'rb:opacity-50': loading,
})} onClick={handleSend} />
})} onClick={isCluster ? handleClusterSend : handleSend} />
</div>
</>
}

View File

@@ -1,6 +1,6 @@
import { type FC, useRef } from 'react';
import { type FC, useEffect, useRef } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Layout, Tabs, Dropdown } from 'antd';
import { Layout, Tabs, Dropdown, Button } from 'antd';
import type { MenuProps } from 'antd';
import { useTranslation } from 'react-i18next';
import styles from '../index.module.css'
@@ -11,7 +11,7 @@ import exportIcon from '@/assets/images/export_hover.svg'
import deleteIcon from '@/assets/images/delete_hover.svg'
import type { Application, ApplicationModalRef } from '@/views/ApplicationManagement/types';
import ApplicationModal from '@/views/ApplicationManagement/components/ApplicationModal'
import type { CopyModalRef } from '../types'
import type { CopyModalRef, WorkflowRef } from '../types'
import { deleteApplication } from '@/api/application'
import CopyModal from './CopyModal'
@@ -29,8 +29,12 @@ interface ConfigHeaderProps {
activeTab: string;
handleChangeTab: (key: string) => void;
refresh: () => void;
workflowRef: React.RefObject<WorkflowRef>
}
const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleChangeTab, refresh }) => {
const ConfigHeader: FC<ConfigHeaderProps> = ({
application, activeTab, handleChangeTab, refresh,
workflowRef
}) => {
const { t } = useTranslation();
const navigate = useNavigate();
const { id } = useParams();
@@ -46,7 +50,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleCha
const formatMenuItems = () => {
const items = ['edit', 'copy', 'delete'].map(key => ({
key,
icon: <img src={menuIcons[key]} className="rb:w-[16px] rb:h-[16px] rb:mr-[8px]" />,
icon: <img src={menuIcons[key]} className="rb:w-4 rb:h-4 rb:mr-2" />,
label: t(`common.${key}`),
}))
return {
@@ -85,12 +89,23 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleCha
const goToApplication = () => {
navigate('/application', { replace: true })
}
const save = () => {
workflowRef.current?.handleSave()
}
const run = () => {
workflowRef.current?.handleSave(false)
.then(() => {
workflowRef.current?.handleRun()
})
}
const clear = () => {
workflowRef?.current?.graphRef?.current?.clearCells()
}
return (
<>
<Header className="rb:w-full rb:h-[64px] rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-[32px]">
<div className="rb:h-[32px] rb:flex rb:items-center rb:font-medium">
<div className="rb:w-[32px] rb:h-[32px] rb:rounded-[8px] rb:mr-[13px] rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[24px] rb:text-[#ffffff]">
<Header className="rb:w-full rb:h-16 rb:grid rb:grid-cols-3 rb:p-[16px_16px_16px_24px]! rb:border-b rb:border-[#EAECEE] rb:leading-8">
<div className="rb:h-8 rb:flex rb:items-center rb:font-medium">
<div className="rb:w-8 rb:h-8 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[24px] rb:text-[#ffffff]">
{application?.name[0]}
</div>
@@ -101,7 +116,7 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleCha
placement="bottomRight"
>
<div
className="rb:w-[20px] rb:h-[20px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
></div>
</Dropdown>
</div>
@@ -114,10 +129,19 @@ const ConfigHeader: FC<ConfigHeaderProps> = ({ application, activeTab, handleCha
className={styles.tabs}
/>
</div>
<div className="rb:h-[32px] rb:flex rb:items-center rb:justify-end rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
<img src={logoutIcon} className="rb:mr-[8px]" />
{application?.type === 'workflow'
? <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:gap-2.5">
<Button onClick={clear}>{t('workflow.clear')}</Button>
<Button onClick={run}>{t('workflow.run')}</Button>
<Button type="primary" onClick={save}>{t('workflow.save')}</Button>
{/* <Button type="primary">{t('workflow.export')}</Button> */}
<img src={logoutIcon} className="rb:w-4 rb:h-4 rb:cursor-pointer" onClick={goToApplication} />
</div>
: <div className="rb:h-8 rb:flex rb:items-center rb:justify-end rb:text-[12px] rb:text-[#5B6167] rb:font-regular rb:cursor-pointer" onClick={goToApplication}>
<img src={logoutIcon} className="rb:mr-2 rb:w-4 rb:h-4" />
{t('application.returnToApplicationList')}
</div>
}
</Header>
<ApplicationModal
ref={applicationModalRef}

Some files were not shown because too many files have changed in this diff Show More