fix: 修复 5 项确定 bug + Provider UX 重做 + 文档统一

Bug fixes:
- fix(dao): AsyncSession.delete 补齐漏掉的 await(provider/user/individual 共 4 处)
- fix(worker): result.data.output → result.output.output(pydantic-ai 1.x API 适配)
- fix(api): 删除 create_worker_from_template 死端点(ORM 字段不匹配必崩)
- fix(api): /provider/test 按 provider_type 分支适配 Anthropic/Gemini/OpenAI 三种协议
- fix(chat): SSE 流式聊天在 distributed 模式 fallback 到非流式,避免 asyncio.Queue 序列化崩溃

Features (previously unstaged):
- feat(provider): Provider 管理页重做(品牌图标、5 种类型、Test Connection、编辑模式)
- feat(provider): 新增 Gemini provider_type 支持
- feat(workflow): Finalize 节点输出 blackboard 摘要 + 失败原因;步骤完成/失败实时推送 SSE
- feat(i18n): regulatory_node 提示词从路由模式改为直接对话模式(中英双语)
- feat(consciousness): dynamic_prompt 支持 locale 国际化
- feat(logs): SystemLogsView 自动刷新 + 暂停按钮

Docs:
- docs: README/README-EN 统一为"开源通用多 Agent 协作平台"口径
- docs: ROADMAP 按 v0.1.x / v0.2.x / v0.3.x 重组
- docs: project.md 重写为结构化项目介绍

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 08:49:38 +00:00
parent c0fcbe2849
commit 9b73ae4db4
27 changed files with 858 additions and 214 deletions
+40 -13
View File
@@ -2,7 +2,7 @@
# KiloStar
A distributed multi-agent collaboration system built with Python
An open-source general-purpose multi-agent collaboration platform
[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/)
[![Ray](https://img.shields.io/badge/Distributed-Ray-0288d1.svg)](https://docs.ray.io/)
@@ -15,31 +15,58 @@ A distributed multi-agent collaboration system built with Python
---
**KiloStar** is a next-generation distributed multi-agent collaboration system powered by **Ray**. It adopts a "central oversight + edge execution" heterogeneous cluster model, leveraging large MoE models for high-level reasoning while coordinating fine-tuned lightweight models for efficient task execution. Built on **Pydantic-AI** with strong typing and a FastAPI async gateway, KiloStar delivers end-to-end automation from requirement decomposition to resource scheduling and execution.
## Overview
**KiloStar** is an open-source general-purpose multi-agent collaboration platform that provides a complete capability stack covering model integration, agent orchestration, workflow execution, and plugin extension. The system uses **Ray** for distributed execution, **Pydantic-AI** for type-safe agent development, and exposes a unified API surface through **FastAPI**.
The platform supports both cloud API models and locally fine-tuned models, ships with built-in core nodes for multi-agent collaboration (Regulatory, Consciousness, Control, Growth), and provides a **heavy plugin** mechanism that lets users reshape the platform into purpose-built agent applications.
> **Current version**: `v0.1.1-alpha`
## Highlights
- **Local fine-tuned models as first-class citizens**: Built-in vLLM adapter; locally fine-tuned models are interchangeable with cloud API models at the call site, allowing different agent nodes to bind different local models.
- **Heavy plugin mechanism**: Plugins can ship their own frontend pages, tool sets, and API endpoints — turning KiloStar into specialized agent applications such as coding assistants, learning helpers, or data analysis tools.
- **Multi-agent collaboration core**: Four system node types (Regulatory / Consciousness / Control / Growth) plus dynamically spawned Worker individuals, with task decomposition, scheduling, and supervision built in.
- **standalone / distributed dual mode**: Zero-dependency single-machine startup; horizontal scaling on demand. Business code is identical across both modes.
- **Private deployment friendly**: Every component runs inside the user's own environment without mandatory third-party dependencies.
---
## ✨ Key Features
## ✨ Core Capabilities
### 🧠 Heterogeneous Agent Architecture
- **Multi-agent cluster**: Built-in Regulatory, Consciousness, Control, and Growth core nodes
- **Dynamic Worker spawning**: On-demand creation of Ordinary or Skill-type Worker Individuals
### 🧠 Multi-Agent Collaboration
- **System node specialization**: Regulatory, Consciousness, Control, and Growth nodes each cover a distinct responsibility
- **Worker dynamic spawning**: On-demand creation of Ordinary / Skill / Special Worker individuals
- **Strongly-typed communication**: Pydantic-AI constrains LLM output to structured data, eliminating the unstructured-text black box in multi-agent flows
### 🚀 Distributed Performance
- **Ray-powered**: Cross-process, cross-machine Actor communication for high-concurrency workloads
- **Local-first**: Deep vLLM integration for private model deployment
### 🚀 Distributed Execution
- **Ray Actor model**: Cross-process and cross-machine collaboration for high-concurrency workloads
- **Heterogeneous resource labels**: `kilostar_node_cpu` / `core` / `gpu` route Workers to the right physical nodes
- **Standalone mode**: Zero external dependencies for single-machine startup; shares the same business code as distributed mode
### 🔄 Workflow Engine
- **pydantic-graph based**: Directed-graph workflow orchestration with conditional branching
- **Cross-process persistence**: PostgreSQL state snapshots enabling workflow resume after interruption
- **pydantic-graph driven**: Directed-graph workflow orchestration with conditional branching and loops
- **Cross-process persistence**: PostgreSQL state snapshots enable workflow resume after interruption
- **Human-in-the-Loop (HITL)**: Built-in HumanApproval node with idempotent resume semantics
### 🧩 Plugin System
- **Tool plugins**: Standard tool calls; MCP protocol support for third-party services
- **Skill (compatible with Anthropic Agent Skills spec)**: Installed and parsed via [viceroy](https://github.com/zhaoxi826/viceroy), loaded on demand at runtime
- **Heavy plugins (planned)**: Vertical application packages with dedicated UI that reshape KiloStar into specialized platforms
### 🛡️ Security
- **JWT authentication**: All API endpoints (including SSE streams) require Bearer Token auth
- **Ownership enforcement**: Workflow/chat resources are user-bound; cross-user access returns 403
- **fetch-based SSE**: Token transmitted via Authorization header, never exposed in URLs
- **Ownership enforcement**: Workflow / chat resources are user-bound; cross-user access returns 403
- **fetch-based SSE**: Token is transmitted via the `Authorization` header, never exposed in URLs
### 📦 Companion Subprojects
| Project | Codename | Purpose | Status |
|:--|:--|:--|:--|
| [kilostar-viceroy](https://github.com/zhaoxi826/viceroy) | Viceroy | Skill installation and cluster-wide distribution | ✅ Released |
| [kilostar-stardomain](./subprojects/stardomain) | Stardomain | Sandbox execution for Skill / plugin scripts | In progress |
| [kilostar-thought](https://github.com/zhaoxi826/thought) | Thought | Augmented memory system for agents | In progress |
---
+29 -9
View File
@@ -2,7 +2,7 @@
# KiloStar (千星)
一款基于 Python 的分布式多 Agent 协作系统
开源通用多 Agent 协作平台
[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/)
[![Ray](https://img.shields.io/badge/Distributed-Ray-0288d1.svg)](https://docs.ray.io/)
@@ -15,27 +15,46 @@
---
**KiloStar** 是一款基于 **Ray** 构建的下一代分布式多 Agent 协作系统。项目采用"中心监管 + 边缘执行"的异构集群模式,通过大参数 MoE 模型进行高层逻辑推理,并协同微调后的轻量化模型高效完成具体任务。借助 **Pydantic-AI** 提供的强类型约束与 FastAPI 异步网关,KiloStar 实现了任务从需求拆解、资源调度到自动化执行的全链路闭环。
## 简介
**KiloStar** 是一个开源的通用多 Agent 协作平台,提供从模型接入、Agent 编排、工作流执行到插件扩展的完整能力栈。系统基于 **Ray** 实现分布式执行,基于 **Pydantic-AI** 提供类型安全的 Agent 开发框架,并通过 **FastAPI** 网关对外暴露统一接口。
平台同时支持云端 API 模型与本地微调模型,内置多 Agent 协作的核心节点(监管、意识、控制、生长),并通过**重型插件**机制允许使用者把平台改造成面向具体场景的专用 Agent 应用。
> **当前版本**`v0.1.1-alpha`
## 项目特色
- **本地微调小模型一等公民**:内置 vLLM 适配,支持将本地微调模型部署为系统中的 Agent 节点,与云端 API 模型在调用层面对等
- **重型插件机制**:插件可附带独立前端页面、工具组与 API 接口,将 KiloStar 改造为编程辅助、学习助手、数据分析等专用 Agent 应用
- **多 Agent 协作内核**:监管 / 意识 / 控制 / 生长四类系统节点 + 动态派生的 Worker 个体,原生支持任务拆解、调度、监督的分工模式
- **分布式与单机统一**standalone 与 distributed 双模式共享同一套代码,单机零依赖起步,集群按需横向扩展
- **私有化部署友好**:所有组件可在用户自有环境内运行,不强制依赖任何第三方服务
---
## ✨ 核心特性
## ✨ 核心能力
### 🧠 异构协作体系
- **多智能体集群**:内置监控 (Regulatory)、意识 (Consciousness)、控制 (Control)、生长 (Growth) 核心节点
- **Worker 动态派生**:根据任务需求动态拉起 Ordinary Skill 类型的 Worker Individual
### 🧠 多 Agent 协作
- **核心节点分工**:监管 (Regulatory)、意识 (Consciousness)、控制 (Control)、生长 (Growth) 四类系统节点
- **Worker 动态派生**:根据任务需求拉起 Ordinary / Skill / Special 三种 Worker 个体
- **强类型通信**:基于 Pydantic-AI 将 LLM 输出约束为结构化数据,避免多 Agent 协作中的非结构化文本黑盒
### 🚀 分布式性能保障
- **Ray 驱动**:跨进程、跨机器的 Actor 通讯,轻松应对高并发任务流
- **本地化优先**:深度适配 vLLM,支持本地私有化模型部署
### 🚀 分布式执行
- **Ray Actor 模型**:跨进程、跨机器协作,支持高并发任务流
- **异构资源标签**`kilostar_node_cpu` / `core` / `gpu` 调度不同 Worker 到合适节点
- **standalone 模式**:单机零依赖起步,与分布式模式共享同一套业务代码
### 🔄 工作流引擎
- **pydantic-graph 驱动**:基于有向图的工作流编排,支持条件分支与循环
- **跨进程持久化**:PostgreSQL 状态快照,支持 workflow 中断后恢复(resume
- **人工介入 (HITL)**:内置 HumanApproval 节点,支持审批挂起与幂等恢复
### 🧩 插件体系
- **工具插件**:标准 Tool 调用,支持 MCP 协议接入第三方服务
- **Skill(兼容 Anthropic Agent Skills 标准)**:通过 [viceroy](https://github.com/zhaoxi826/viceroy) 安装解析,运行时按需加载
- **重型插件(规划中)**:带独立 UI 的垂直应用包,把 KiloStar 改造成专用 Agent 平台
### 🛡️ 安全设计
- **JWT 鉴权**:所有 API 端点(含 SSE 事件流)均走 Bearer Token 认证
- **归属校验**workflow / chat 资源严格绑定 user_id,跨用户访问返回 403
@@ -46,6 +65,7 @@
| 项目 | 代号 | 功能 | 状态 |
|:--|:--|:--|:--|
| [kilostar-viceroy](https://github.com/zhaoxi826/viceroy) | 总督 | Skill 动态安装与全集群分发 | ✅ 已发布 |
| [kilostar-stardomain](./subprojects/stardomain) | 星域 | Skill / 插件脚本沙箱执行 | 开发中 |
| [kilostar-thought](https://github.com/zhaoxi826/thought) | 思绪 | Agent 增强记忆系统 | 开发中 |
---
+67 -13
View File
@@ -1,16 +1,70 @@
# ROADMAP
KiloStar 各阶段的方向规划。已完成项归入 [CHANGELOG](./CHANGELOG.md)。
---
## [v0.1.0Alpha] - 2026/4/28
### 未来展望:
#### 功能增加
- [ ] **完善系统插件**: 如 **RAG(检索增强生成)****沙箱** **联网搜索** ,使agent拥有更多的能力适应多样化任务需求
- [ ] **增加MCP功能**: 增加MCP,使得agent可以调用通用工具
- [ ] **完善special_individual** 使得`regulatory_node`等可以调用实现语言生成图像生成等功能
- [ ] **完善regulatory_node**: 实现`regulatory_node`对于工作流状态的访问,实现更方便的检测
- [ ] **对消息平台的对接**: 完善platform,实现对于更多消息平台的对接(如:钉钉微信等),实现在社交软件对`regulatory_node`下达命令
#### 系统优化
- [ ] **优化workflow逻辑**: 通过**graph**等设计实现更优秀的工作流调度
- [ ] **优化GSM设计**: 对于 **GSMglobal_state_machine全局状态机)** 进行重构,实现更高的并发
- [ ] **工具及skill优化**: 完善前端获取工具或skill的逻辑,实现对于skill或者tool的配置改写以及详细信息获取
- [ ] **前端优化**: 完善前端设置逻辑(如:调节语言等),以及使前端更加灵活智能
## v0.1.x 系列(当前)
主线目标:补齐 v0.1.0 骨架之上的工程化与可用性短板,让平台进入"装上能用"的状态。
### 已完成(截至 v0.1.1-alpha
- **多 Agent 编排能力线**:人设模板、节点调度标签、Worker 动态派生、调控节点对话模式重构
- **工具系统重构**:以 toolset 为单位组织工具,系统预置工具集自动补种,Agent 工具集多选绑定
- **MCP 完整接入**:前端 CRUD、Dockerfile Node.js、后端 API 全链路落地,可作为标准 MCP 客户端调用第三方 MCP 服务器
- **Provider UX 重做**5 种 Provider 类型、品牌图标、默认 URL、Test Connection、API key 脱敏
- **沙箱执行子项目**stardomain 落地(local + Docker 双模式)
- **基础安全**:JWT 鉴权、资源归属校验、fetch-based SSE、生产密钥强制校验
### 计划中
#### 平台体验
- **重型插件机制**:定义包格式(manifest + frontend + backend + tools + agent 配置)、挂载协议、生命周期管理;为后续生态铺路
- **Skill 工程化**:兼容 Anthropic Agent Skills 标准的同时,补充 KiloStar 自己的 SkillManifest 抽象(依赖识别、文件分类、执行模式声明)
- **Provider 模型调用参数体系**:贯通 temperature / top_p / 自定义 headers / 超时 等模型调用参数,统一前后端
- **前端 Tauri 桌面端**:把当前 Web 前端打包为 Tauri 桌面客户端,承载需要本地能力的功能
#### 模型与 Agent 能力
- **本地微调模型集成**:vLLM 适配深化,支持把本地微调小模型部署为 Agent 节点
- **special_individual 完善**embedding / TTS / 图像生成等特殊 Agent 标准化接入
- **regulatory_node 工作流可见性**:监管节点对工作流执行状态的访问与干预能力
#### 系统性优化
- **GSM 写入路径优化**:单 Actor 写串行化的瓶颈处理,方向上倾向"PG 为真相之源 + GSM 退化为热缓存"
- **workflow 引擎深化**:基于 pydantic-graph 的更复杂调度模式(嵌套子流、并行分支汇聚等)
- **可观测性**:跨节点 trace 串联、workflow 执行可视化、日志检索体验
---
## v0.2.x 系列(中期)
围绕"通用 Agent 平台"的关键缺口。
- **重型插件生态**:第一批官方重型插件示例 + 第三方插件开发文档
- **Skill 分发与缓存层**:跨节点的 Skill 同步策略,按需拉取 + 本地缓存 + 哈希去重
- **多镜像部署**:拆分 core / worker / gpu / standalone 等多镜像,按场景组合
- **消息平台对接**:完善 platform 模块,支持钉钉 / 微信 / Slack 等平台的接入
- **persona 外键化**:人设统一为外键引用,消除 system_prompt 的数据冗余
---
## v0.3.x 系列及之后(远期)
- **特殊 Agent 生态**embedding / 多模态 / 语音 / 图像生成等专项 Agent 的标准化接入
- **生长机制**growth_node 真正实现集群与子个体的自适应扩张
- **微调模型工具链**:与 unsloth / axolotl 等微调框架的集成路径,把"训练 → 部署 → 接入"做成顺滑流程
- **多用户多租户**:从单实例多用户演进到真正的多租户隔离
- **联邦化部署**:跨组织的 Agent 协作与资源借用机制
---
## 不在路线图中
KiloStar 不计划自己做:
- 任何具体垂直场景的 Agent 产品(编程助手、英语学习、数据分析等都应通过重型插件实现)
- 闭源模型的深度定制
- 自研推理引擎(继续依托 vLLM / llama.cpp 等成熟方案)
> 路线图按版本节奏组织,但实际推进顺序会根据使用反馈调整。重大方向变化会在此文档留痕。
+94 -26
View File
@@ -1,34 +1,102 @@
## kilostar项目
# KiloStar 项目介绍
#### 简介
**kilostar**是一款python开发,实现将小模型进行微调后整理为一个大型集群,从而实现低算力情况下高复杂度任务的实现。
系统模型分为以下部分:
- **监管节点**:负责基本交流和简单任务执行;
- **管控节点**:负责调度系统资源;
- **意识节点**:负责复杂任务的处理;
- **生长节点**:负责扩张集群和子个体;
- **特殊子个体**:与外界交互的模型,如embedding模型,tts模型等;
- **专家子个体**:携带有专业skill的agent对象;
- **基础子个体**:普通的agent对象;
---
#### 项目介绍
**kilostar** 是一款基于分布式计算平台 **Ray** 和 agent开发框架**pydantic-AI** 开发的多智能体协作平台,通过多智能体的协作和任务拆解,实现复杂任务的高质量完成。
## 简介
**kilostar**使用 **python**著名的高性能后端框架 **Fastapi** 来作为整个系统对用户暴露接口的网关。在**kilostar**运行过程中,用户通过发送请求至fastapi从而包装为 `kilostarEvent`对象,并且发往`regulatory_node`,由**regulatory_node**进行简单的意图判断,如果判断用户只是简单交流比如聊天等,**regulatory_node**会直接对用户进行回复结束事件
,如果判断用户想要完成复杂的任务,**regulatory_node**会选择将从`workflow_template(工作流模板)`中选择一个或者不选择,然后将event挂到`全局状态机`实现追溯方便并发往`Workflow_Running_Engine`的异步队列,被协程对象取走后,由**consciousness_node**创建为`kilostarWorkflow`对象,挂载到实例化的`WorkflowEngine`进行执行。完成任务后返回给用户。
**KiloStar(千星)** 是一个开源的通用多 Agent 协作平台,提供从模型接入、Agent 编排、工作流执行到插件扩展的完整能力栈。系统基于 [Ray](https://www.ray.io/) 实现分布式执行,基于 [Pydantic-AI](https://ai.pydantic.dev/) 提供类型安全的 Agent 开发框架,并通过 [FastAPI](https://fastapi.tiangolo.com/) 网关对外暴露统一接口
平台同时支持云端 API 模型与本地微调模型,内置多 Agent 协作的核心系统节点,并通过**重型插件**机制允许使用者把平台改造成面向具体场景的专用 Agent 应用。
---
#### 技术架构背景
- 分布式大脑:利用 Ray 框架实现 Actor 模型,将不同的智能体节点(Node)部署为独立运行的分布式 Actor,具备跨节点通信和动态调度的能力。
- 强类型通信协议:引入 PydanticAI 作为智能体开发框架,核心目的在于将大语言模型(LLM)产生的非结构化文本,通过 Pydantic 模型转化为强类型的结构化数据(JSON),确保多智能体协作时数据传输的工业级稳定性。
- 推理驱动路由:系统针对最新的**deepseek-v4**系列进行了适配,实现灵活调用
## 项目特色
1. **本地微调小模型一等公民**:内置 vLLM 适配,本地微调模型在调用层与云端 API 模型对等,使用者可以为不同 Agent 节点绑定不同的本地模型。
2. **重型插件机制**:插件可附带独立前端页面、工具组与 API 接口,将 KiloStar 改造为编程辅助、学习助手、数据分析等专用 Agent 应用。
3. **多 Agent 协作内核**:监管 / 意识 / 控制 / 生长四类系统节点 + 动态派生的 Worker 个体,原生支持任务拆解、调度、监督的分工模式。
4. **standalone / distributed 双模式**:单机零依赖起步,集群按需横向扩展,业务代码在两种模式下完全一致。
5. **私有化部署友好**:所有组件可在用户自有环境内运行,不强制依赖任何第三方服务。
---
#### 项目背景
###### 1.多智能体架构的需求
随着任务复杂度的提升,单一**Agent**一定程度上以及满足不了人们对于人工智能完成复杂任务的需求。模仿人类社会中的团队合作,kilostar以**Ray**作为底座,从而实现一种多智能体协作的设计。
###### 2.对于大语言模型输出内容约束的需求
LLM 输出的非结构化文本在多智能体交互中极易崩溃。所以,**kilostar**没有选择如**LangChain**这种老牌智能体开发框架,而是选择了新兴的**pydanticAI**这种强约束框架,使得多智能体协作避免黑盒化。
**PydanticAI**是一款基于**Pydantic**的智能体开发框架,**Pydantic**是**python**中著名的数据类型约束库,**Pydantic**官方通过**Pydantic**的强约束,实现了对于LLM的生成约束。
## 系统组成
### 节点体系
| 节点类型 | 职责 |
|:--|:--|
| **Regulatory Node** | 监管节点。承担用户对话入口、意图判断、对话模式的工具调用,以及对工作流执行结果的监督 |
| **Consciousness Node** | 意识节点。负责复杂任务的拆解、规划与工作流构建 |
| **Control Node** | 控制节点。负责工作流执行过程中的路由调度与状态监控 |
| **Growth Node** | 生长节点。负责集群与子个体的扩张 |
| **Worker Individual** | Worker 个体。包括 Ordinary(基础 Agent)、Skill(携带专业技能的 Agent)、Special(与外部世界交互的特殊 Agent,例如 embedding / TTS |
### 模块布局
```
KiloStar/
├── kilostar/
│ ├── api/ # FastAPI 路由层
│ ├── core/
│ │ ├── individual/ # 各类系统节点实现
│ │ ├── work/ # 工作流 / 对话 / 任务执行层
│ │ ├── global_state_machine/ # 全局状态机(Provider / Config 等)
│ │ ├── global_workflow_manager/ # 工作流消息队列 Actor
│ │ └── postgres_database/ # PostgreSQL DAO 层
│ ├── adapter/ # 模型适配器
│ ├── plugin/ # 工具插件
│ ├── worker_cluster/ # Worker 集群管理
│ ├── worker_individual/ # Worker 个体生命周期
│ └── utils/ # 鉴权 / Ray 句柄 / 配置等
├── frontend/ # React + Vite 前端
├── subprojects/ # Rust 子项目(viceroy / stardomain
├── alembic/ # 数据库迁移
├── tests/ # 单元 + 集成测试
└── docs/ # 设计文档
```
---
## 技术栈
| 层级 | 选型 | 说明 |
|:--|:--|:--|
| 分布式运行时 | **Ray** | Actor 模型、跨进程跨机器通信、自定义资源调度 |
| Agent 框架 | **Pydantic-AI** | LLM 输出强类型约束,避免多 Agent 协作中的非结构化文本黑盒 |
| Web 网关 | **FastAPI + Ray Serve** | 异步 HTTP 网关与 SSE 事件流 |
| 工作流引擎 | **pydantic-graph** | 有向图工作流编排,支持条件分支、循环、人工介入 |
| 状态持久化 | **PostgreSQL + Alembic** | workflow 中断恢复、Agent 配置、Provider 注册等 |
| 前端 | **React 19 + Vite + Tailwind + Zustand** | TypeScript 单页应用 |
| 子项目 | **Rust** | viceroySkill 安装)、stardomain(沙箱执行) |
---
## 运行模式
### standalone 单机模式
通过 `KILOSTAR_MODE=standalone` 启动,所有 Actor 退化为普通 Python 异步实例,无需安装 Ray,适合个人使用与开发调试。
### distributed 分布式模式
默认模式。Ray 启动后将系统节点部署为命名 ActorWorkerCluster 按 `kilostar_node_cpu` / `core` / `gpu` 自定义资源调度到对应物理节点,支持跨机器横向扩展。
两种模式共享同一套业务代码,通过 `kilostar.utils.standalone_proxy` 在 Actor Handle 层做透明适配。
---
## 安全设计
- **JWT 鉴权**:所有 API 端点(含 SSE 事件流)均走 Bearer Token 认证
- **资源归属校验**workflow / chat 等用户资源严格绑定 user_id,跨用户访问返回 403
- **fetch-based SSE**Token 走 `Authorization` header,不暴露在 URL 中
- **生产模式密钥校验**:未提供有效 SECRET_KEY 时拒绝以 production 模式启动
---
## 生态子项目
| 项目 | 代号 | 功能 | 状态 |
|:--|:--|:--|:--|
| [kilostar-viceroy](https://github.com/zhaoxi826/viceroy) | 总督 | Skill 动态安装与全集群分发 | ✅ 已发布 |
| [kilostar-stardomain](../subprojects/stardomain) | 星域 | Skill / 插件脚本沙箱执行 | 开发中 |
| [kilostar-thought](https://github.com/zhaoxi826/thought) | 思绪 | Agent 增强记忆系统 | 开发中 |
@@ -0,0 +1,82 @@
interface ProviderIconProps {
type: string;
size?: number;
className?: string;
}
export function ProviderIcon({ type, size = 18, className = '' }: ProviderIconProps) {
const props = {
width: size,
height: size,
viewBox: '0 0 24 24',
fill: 'currentColor',
className,
};
switch (type?.toLowerCase()) {
case 'openai':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855l-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135l-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08L8.704 5.46a.795.795 0 0 0-.393.681zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z"/>
</svg>
);
case 'claude':
case 'anthropic':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M4.709 15.955l4.72-2.647.079-.23-.079-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.328h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312z"/>
</svg>
);
case 'deepseek':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M23.748 4.482c-.254-.124-.364.113-.512.234-.051.039-.094.09-.137.136-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.156-.708-.311-.955-.65-.172-.241-.219-.51-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.093.172.187.129.323-.082.28-.18.552-.266.833-.055.179-.137.217-.329.14a5.526 5.526 0 01-1.736-1.18c-.857-.828-1.631-1.742-2.597-2.458a11.365 11.365 0 00-.689-.471c-.985-.957.13-1.743.388-1.836.27-.098.093-.432-.779-.428-.872.004-1.67.295-2.687.684a3.055 3.055 0 01-.465.137 9.597 9.597 0 00-2.883-.102c-1.885.21-3.39 1.102-4.497 2.623C.082 8.606-.231 10.684.152 12.85c.403 2.284 1.569 4.175 3.36 5.653 1.858 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.133-.284 4.994-1.86.47.234.962.327 1.78.397.63.059 1.236-.03 1.705-.128.735-.156.684-.837.419-.961-2.155-1.004-1.682-.595-2.113-.926 1.096-1.296 2.746-2.642 3.392-7.003.05-.347.007-.565 0-.846-.004-.17.035-.237.23-.256a4.173 4.173 0 001.545-.475c1.396-.763 1.96-2.015 2.093-3.517.02-.23-.004-.467-.247-.588zM11.581 18c-2.089-1.642-3.102-2.183-3.52-2.16-.392.024-.321.471-.235.763.09.288.207.486.371.739.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.167-1.361-.802-2.5-1.86-3.301-3.307-.774-1.393-1.224-2.887-1.298-4.482-.02-.386.093-.522.477-.592a4.696 4.696 0 011.529-.039c2.132.312 3.946 1.265 5.468 2.774.868.86 1.525 1.887 2.202 2.891.72 1.066 1.494 2.082 2.48 2.914.348.292.625.514.891.677-.802.09-2.14.11-3.054-.614zm1-6.44a.306.306 0 01.415-.287.302.302 0 01.2.288.306.306 0 01-.31.307.303.303 0 01-.304-.308zm3.11 1.596c-.2.081-.399.151-.59.16a1.245 1.245 0 01-.798-.254c-.274-.23-.47-.358-.552-.758a1.73 1.73 0 01.016-.588c.07-.327-.008-.537-.239-.727-.187-.156-.426-.199-.688-.199a.559.559 0 01-.254-.078c-.11-.054-.2-.19-.114-.358.028-.054.16-.186.192-.21.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.391.451.462.576.685.914.176.265.336.537.445.848.067.195-.019.354-.25.452z"/>
</svg>
);
case 'gemini':
case 'google':
return (
<svg {...props} viewBox="0 0 24 24">
<path d="M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12"/>
</svg>
);
case 'local':
case 'ollama':
return (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="4" width="18" height="12" rx="2" />
<path d="M7 20h10M9 16v4M15 16v4" />
<circle cx="8" cy="10" r="1" fill="currentColor" />
<circle cx="12" cy="10" r="1" fill="currentColor" />
<circle cx="16" cy="10" r="1" fill="currentColor" />
</svg>
);
default:
return (
<svg {...props} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="3" />
<path d="M9 9h6v6H9z" />
</svg>
);
}
}
const PROVIDER_BRAND_COLORS: Record<string, string> = {
openai: '#10a37f',
claude: '#d97757',
anthropic: '#d97757',
deepseek: '#4d6bfe',
gemini: '#4285f4',
google: '#4285f4',
local: '#8a8a8a',
ollama: '#8a8a8a',
};
export function getProviderBrandColor(type: string): string {
return PROVIDER_BRAND_COLORS[type?.toLowerCase()] || '#8a8a8a';
}
@@ -1,17 +1,51 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Plus, X, Server, Loader2, Boxes } from 'lucide-react';
import { Plus, X, Loader2, Boxes, Zap, ChevronDown, ChevronRight, Settings as SettingsIcon } from 'lucide-react';
import type { Provider } from '../../types';
import apiClient from '../../api/client';
import { ProviderIcon, getProviderBrandColor } from './ProviderIcon';
interface ProviderTypeOption {
id: string;
iconKey: string;
backendType: 'openai' | 'claude' | 'deepseek' | 'gemini';
defaultUrl: string;
descKey: string;
nameKey: string;
}
const PROVIDER_TYPES: ProviderTypeOption[] = [
{ id: 'openai', iconKey: 'openai', backendType: 'openai', defaultUrl: 'https://api.openai.com/v1', nameKey: 'agent.providerTypeOpenai', descKey: 'agent.providerTypeOpenaiDesc' },
{ id: 'openai_compat', iconKey: 'openai', backendType: 'openai', defaultUrl: '', nameKey: 'agent.providerTypeOpenaiCompat', descKey: 'agent.providerTypeOpenaiCompatDesc' },
{ id: 'anthropic', iconKey: 'claude', backendType: 'claude', defaultUrl: 'https://api.anthropic.com', nameKey: 'agent.providerTypeAnthropic', descKey: 'agent.providerTypeAnthropicDesc' },
{ id: 'gemini', iconKey: 'gemini', backendType: 'gemini', defaultUrl: 'https://generativelanguage.googleapis.com/v1beta', nameKey: 'agent.providerTypeGemini', descKey: 'agent.providerTypeGeminiDesc' },
{ id: 'deepseek', iconKey: 'deepseek', backendType: 'deepseek', defaultUrl: 'https://api.deepseek.com/v1', nameKey: 'agent.providerTypeDeepseek', descKey: 'agent.providerTypeDeepseekDesc' },
];
function detectTypeFromProvider(p: Provider): string {
if (p.provider_type === 'openai') {
return p.provider_url?.includes('api.openai.com') ? 'openai' : 'openai_compat';
}
if (p.provider_type === 'claude') return 'anthropic';
return p.provider_type || 'openai';
}
export function ProvidersSettings() {
const { t } = useTranslation();
const [providers, setProviders] = useState<Provider[]>([]);
const [loading, setLoading] = useState(true);
const [isModalOpen, setIsModalOpen] = useState(false);
const [formData, setFormData] = useState({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' });
const [editingProvider, setEditingProvider] = useState<string | null>(null);
const [selectedTypeId, setSelectedTypeId] = useState<string>('openai');
const [formData, setFormData] = useState({ provider_title: '', provider_url: '', provider_apikey: '', custom_models: '' });
const [showAdvanced, setShowAdvanced] = useState(false);
const [submitLoading, setSubmitLoading] = useState(false);
const [testLoading, setTestLoading] = useState(false);
const [testResult, setTestResult] = useState<{ success: boolean; error?: string; model_count?: number } | null>(null);
const [error, setError] = useState('');
const [expandedProvider, setExpandedProvider] = useState<string | null>(null);
const selectedType = PROVIDER_TYPES.find((p) => p.id === selectedTypeId) || PROVIDER_TYPES[0];
const fetchProviders = async () => {
setLoading(true);
@@ -19,7 +53,7 @@ export function ProvidersSettings() {
const response = await apiClient.get('/api/v1/provider/list');
setProviders(Object.values(response.data.provider_list || {}));
} catch (error) {
console.error("Failed to fetch providers", error);
console.error('Failed to fetch providers', error);
setProviders([]);
} finally {
setLoading(false);
@@ -28,6 +62,72 @@ export function ProvidersSettings() {
useEffect(() => { fetchProviders(); }, []);
const openAddModal = () => {
setEditingProvider(null);
setSelectedTypeId('openai');
setFormData({ provider_title: '', provider_url: PROVIDER_TYPES[0].defaultUrl, provider_apikey: '', custom_models: '' });
setError('');
setTestResult(null);
setShowAdvanced(false);
setIsModalOpen(true);
};
const openEditModal = (provider: Provider) => {
const typeId = detectTypeFromProvider(provider);
setEditingProvider(provider.provider_title);
setSelectedTypeId(typeId);
setFormData({
provider_title: provider.provider_title,
provider_url: provider.provider_url || '',
provider_apikey: '',
custom_models: '',
});
setError('');
setTestResult(null);
setShowAdvanced(false);
setIsModalOpen(true);
};
const handleSelectType = (typeId: string) => {
setSelectedTypeId(typeId);
if (!editingProvider) {
const tp = PROVIDER_TYPES.find((p) => p.id === typeId);
if (tp) setFormData((prev) => ({ ...prev, provider_url: tp.defaultUrl }));
}
setTestResult(null);
};
const buildPayload = () => {
const customModels = formData.custom_models
.split(',').map((s) => s.trim()).filter(Boolean);
const payload: any = {
provider_type: selectedType.backendType,
provider_title: formData.provider_title,
provider_url: formData.provider_url,
provider_apikey: formData.provider_apikey,
};
if (customModels.length > 0) payload.custom_models = customModels;
return payload;
};
const handleTestConnection = async () => {
if (!formData.provider_url || !formData.provider_apikey) {
setError(t('agent.providerFillUrlAndKey'));
return;
}
setTestLoading(true);
setTestResult(null);
setError('');
try {
const resp = await apiClient.post('/api/v1/provider/test', buildPayload());
setTestResult(resp.data);
} catch {
setTestResult({ success: false, error: 'Request failed' });
} finally {
setTestLoading(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.provider_title || !formData.provider_url || !formData.provider_apikey) {
@@ -37,9 +137,13 @@ export function ProvidersSettings() {
setSubmitLoading(true);
setError('');
try {
await apiClient.post('/api/v1/provider', formData);
if (editingProvider) {
await apiClient.delete(`/api/v1/provider/${editingProvider}`);
}
await apiClient.post('/api/v1/provider', buildPayload());
await fetchProviders();
setIsModalOpen(false);
setEditingProvider(null);
} catch (err) {
setError(t('agent.providerAddFailed'));
} finally {
@@ -54,7 +158,7 @@ export function ProvidersSettings() {
<h3 className="text-lg font-bold text-text-primary">{t('agent.providerManagement')}</h3>
<p className="text-sm text-text-muted mt-0.5">{t('agent.providerDesc')}</p>
</div>
<button onClick={() => { setFormData({ provider_type: 'openai', provider_title: '', provider_url: '', provider_apikey: '' }); setError(''); setIsModalOpen(true); }}
<button onClick={openAddModal}
className="flex items-center gap-2 px-4 py-2.5 bg-accent text-white rounded-xl hover:bg-accent-hover transition-all shadow-lg shadow-accent/15 text-sm font-medium">
<Plus size={14} /> {t('agent.addProvider')}
</button>
@@ -76,25 +180,43 @@ export function ProvidersSettings() {
<div key={i} className="bg-bg-card border border-border-primary rounded-2xl p-5 card-hover">
<div className="flex justify-between items-start mb-4">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center">
<Box size={18} className="text-text-secondary" />
<div className="w-9 h-9 rounded-xl bg-bg-secondary border border-border-secondary flex items-center justify-center" style={{ color: getProviderBrandColor(provider.provider_type) }}>
<ProviderIcon type={provider.provider_type} size={20} />
</div>
<div>
<h4 className="font-semibold text-sm text-text-primary">{provider.provider_title}</h4>
<span className="text-[10px] text-text-muted font-mono uppercase">{provider.provider_type}</span>
</div>
</div>
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${provider.status === 'Connected' ? 'bg-success-bg text-success border-success/20' : 'bg-bg-secondary text-text-muted border-border-primary'}`}>
{provider.status === 'Connected' && <span className="w-1 h-1 rounded-full bg-success" />}
{provider.status || t('common.unknown')}
<span className={`flex items-center gap-1 text-[10px] font-medium px-2 py-1 rounded-lg border ${provider.status === 'Connected' || provider.provider_status === 'up' ? 'bg-success-bg text-success border-success/20' : 'bg-bg-secondary text-text-muted border-border-primary'}`}>
{(provider.status === 'Connected' || provider.provider_status === 'up') && <span className="w-1 h-1 rounded-full bg-success" />}
{provider.provider_status === 'up' ? 'Connected' : provider.status || t('common.unknown')}
</span>
</div>
<div className="bg-bg-secondary rounded-lg px-3 py-2 mb-4">
<div className="bg-bg-secondary rounded-lg px-3 py-2 mb-3">
<p className="text-[10px] text-text-muted mb-0.5">{t('agent.endpoint')}</p>
<p className="text-xs font-mono text-text-secondary truncate">{provider.provider_url || t('common.default')}</p>
</div>
{provider.provider_models && provider.provider_models.length > 0 && (
<div className="mb-3">
<button
onClick={() => setExpandedProvider(expandedProvider === provider.provider_title ? null : provider.provider_title)}
className="flex items-center gap-1.5 text-[11px] text-text-muted hover:text-accent transition-colors"
>
{expandedProvider === provider.provider_title ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
{provider.provider_models.length} {t('agent.providerModels')}
</button>
{expandedProvider === provider.provider_title && (
<div className="mt-2 bg-bg-secondary rounded-lg p-2 max-h-32 overflow-y-auto">
{provider.provider_models.map((model: string) => (
<div key={model} className="text-[11px] font-mono text-text-secondary py-0.5 px-1.5">{model}</div>
))}
</div>
)}
</div>
)}
<div className="flex justify-end gap-2">
<button className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">{t('common.edit')}</button>
<button onClick={() => openEditModal(provider)} className="px-3 py-1.5 text-xs font-medium text-text-secondary bg-bg-secondary hover:bg-bg-hover rounded-lg transition-colors border border-border-primary">{t('common.edit')}</button>
<button onClick={async () => {
if (!confirm(t('agent.deleteProviderConfirm'))) return;
try { await apiClient.delete(`/api/v1/provider/${provider.provider_title}`); fetchProviders(); } catch { alert(t('common.deleteFailed')); }
@@ -105,46 +227,171 @@ export function ProvidersSettings() {
</div>
)}
{/* Modal */}
{/* Modal — 2 column layout */}
{isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm">
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-border-primary animate-fade-in-scale">
<div className="flex justify-between items-center p-5 border-b border-border-primary">
<div className="flex items-center gap-2">
<Server size={16} className="text-accent" />
<h3 className="text-base font-bold text-text-primary">{t('agent.addNewProvider')}</h3>
</div>
<button onClick={() => setIsModalOpen(false)} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors"><X size={18} /></button>
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 backdrop-blur-sm p-4">
<div className="bg-bg-card rounded-2xl shadow-2xl w-full max-w-3xl overflow-hidden border border-border-primary animate-fade-in-scale flex flex-col max-h-[90vh]">
<div className="flex justify-between items-center px-5 py-4 border-b border-border-primary shrink-0">
<h3 className="text-base font-bold text-text-primary">
{editingProvider ? t('agent.editProvider') : t('agent.addNewProvider')}
</h3>
<button onClick={() => { setIsModalOpen(false); setEditingProvider(null); }} className="p-1 text-text-muted hover:text-text-primary rounded-lg transition-colors">
<X size={18} />
</button>
</div>
<form onSubmit={handleSubmit} className="p-5 space-y-4">
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>}
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">{t('agent.providerType')}</label>
<select name="provider_type" value={formData.provider_type} onChange={(e) => setFormData({...formData, provider_type: e.target.value})}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary">
<option value="openai">OpenAI</option>
<option value="deepseek">DeepSeek</option>
<option value="claude">Claude</option>
<option value="local">Local</option>
</select>
</div>
{['provider_title', 'provider_url', 'provider_apikey'].map((field) => (
<div key={field}>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{field === 'provider_title' ? t('agent.providerTitle') : field === 'provider_url' ? t('agent.baseUrl') : t('agent.apiKey')}
</label>
<input type={field === 'provider_apikey' ? 'password' : field === 'provider_url' ? 'url' : 'text'}
name={field} value={(formData as any)[field]}
onChange={(e) => setFormData({...formData, [field]: e.target.value})}
placeholder={field === 'provider_title' ? t('agent.providerTitlePlaceholder') : field === 'provider_url' ? t('agent.baseUrlPlaceholder') : t('agent.apiKeyPlaceholder')}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono" />
<form onSubmit={handleSubmit} className="flex flex-1 overflow-hidden">
{/* Left: provider type list */}
<div className="w-56 shrink-0 border-r border-border-primary bg-bg-secondary/40 overflow-y-auto p-3 space-y-1">
<div className="text-[10px] font-bold text-text-muted uppercase tracking-wider px-2 py-1.5">
{t('agent.providerType')}
</div>
{PROVIDER_TYPES.map((type) => {
const active = selectedTypeId === type.id;
return (
<button
key={type.id}
type="button"
onClick={() => handleSelectType(type.id)}
disabled={!!editingProvider}
className={`w-full flex items-center gap-3 px-2.5 py-2.5 rounded-lg text-left transition-all ${
active
? 'bg-bg-card border border-accent/40 shadow-sm'
: 'border border-transparent hover:bg-bg-card/60'
} disabled:opacity-50 disabled:cursor-not-allowed`}
>
<div
className="w-8 h-8 rounded-lg bg-bg-card border border-border-primary flex items-center justify-center shrink-0"
style={{ color: getProviderBrandColor(type.iconKey) }}
>
<ProviderIcon type={type.iconKey} size={18} />
</div>
<div className="flex-1 min-w-0">
<div className={`text-xs font-semibold truncate ${active ? 'text-accent' : 'text-text-primary'}`}>
{t(type.nameKey)}
</div>
<div className="text-[10px] text-text-muted truncate">{t(type.descKey)}</div>
</div>
</button>
);
})}
</div>
{/* Right: form */}
<div className="flex-1 overflow-y-auto p-5 space-y-4">
{error && <div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>}
{testResult && (
<div className={`p-3 text-sm rounded-xl border ${testResult.success ? 'bg-success-bg text-success border-success/20' : 'bg-danger-bg text-danger border-danger/20'}`}>
{testResult.success
? `${t('agent.providerTestSuccess')} · ${testResult.model_count} ${t('agent.providerModels')}`
: `${t('agent.providerTestFailed')}: ${testResult.error}`}
</div>
)}
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.providerTitle')}
</label>
<input
type="text"
value={formData.provider_title}
onChange={(e) => setFormData({ ...formData, provider_title: e.target.value })}
placeholder={t('agent.providerTitlePlaceholder')}
disabled={!!editingProvider}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 disabled:opacity-50"
/>
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.baseUrl')}
</label>
<input
type="text"
value={formData.provider_url}
onChange={(e) => setFormData({ ...formData, provider_url: e.target.value })}
placeholder={selectedType.defaultUrl || t('agent.baseUrlPlaceholder')}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono"
/>
{selectedType.defaultUrl && !editingProvider && (
<p className="text-[10px] text-text-muted mt-1">
{t('agent.baseUrlHint')}: <span className="font-mono">{selectedType.defaultUrl}</span>
</p>
)}
</div>
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.apiKey')}
</label>
<input
type="password"
value={formData.provider_apikey}
onChange={(e) => setFormData({ ...formData, provider_apikey: e.target.value })}
placeholder={editingProvider ? t('agent.apiKeyEditPlaceholder') : t('agent.apiKeyPlaceholder')}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono"
/>
</div>
{/* 参数设置 — collapsible */}
<div className="border border-border-primary rounded-xl overflow-hidden">
<button
type="button"
onClick={() => setShowAdvanced(!showAdvanced)}
className="w-full flex items-center justify-between px-3.5 py-2.5 bg-bg-secondary/50 hover:bg-bg-secondary transition-colors"
>
<div className="flex items-center gap-2 text-xs font-semibold text-text-secondary">
<SettingsIcon size={13} />
{t('agent.providerAdvanced')}
</div>
{showAdvanced ? <ChevronDown size={14} className="text-text-muted" /> : <ChevronRight size={14} className="text-text-muted" />}
</button>
{showAdvanced && (
<div className="p-4 space-y-3 bg-bg-card">
<div>
<label className="block text-xs font-semibold text-text-secondary mb-1.5 uppercase tracking-wider">
{t('agent.providerCustomModels')}
</label>
<textarea
value={formData.custom_models}
onChange={(e) => setFormData({ ...formData, custom_models: e.target.value })}
placeholder={t('agent.providerCustomModelsPlaceholder')}
rows={3}
className="w-full bg-bg-input border border-border-primary text-sm rounded-xl px-3.5 py-2.5 focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent text-text-primary placeholder:text-text-muted/50 font-mono resize-none"
/>
<p className="text-[10px] text-text-muted mt-1">{t('agent.providerCustomModelsHint')}</p>
</div>
</div>
)}
</div>
))}
<div className="pt-2 flex justify-end gap-2">
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">{t('common.cancel')}</button>
<button type="submit" disabled={submitLoading} className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50">{submitLoading ? t('common.saving') : t('agent.addProvider')}</button>
</div>
</form>
{/* Footer */}
<div className="px-5 py-4 border-t border-border-primary bg-bg-secondary/30 flex items-center justify-between shrink-0">
<button
type="button"
onClick={handleTestConnection}
disabled={testLoading}
className="flex items-center gap-1.5 px-3 py-2 text-xs font-medium text-accent bg-accent-light hover:bg-accent/20 rounded-xl transition-colors disabled:opacity-50"
>
{testLoading ? <Loader2 size={12} className="animate-spin" /> : <Zap size={12} />}
{t('agent.testConnection')}
</button>
<div className="flex gap-2">
<button type="button" onClick={() => { setIsModalOpen(false); setEditingProvider(null); }} className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-bg-hover rounded-xl transition-colors">
{t('common.cancel')}
</button>
<button
type="button"
onClick={handleSubmit}
disabled={submitLoading}
className="px-4 py-2 text-sm font-medium text-white bg-accent rounded-xl hover:bg-accent-hover transition-colors disabled:opacity-50"
>
{submitLoading ? t('common.saving') : editingProvider ? t('common.save') : t('agent.addProvider')}
</button>
</div>
</div>
</div>
</div>
)}
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { RefreshCw, Server, GitBranch, ChevronDown, ChevronRight } from 'lucide-react';
import { RefreshCw, Server, GitBranch, ChevronDown, ChevronRight, Pause, Play } from 'lucide-react';
import apiClient from '../../api/client';
interface EventLog {
@@ -47,6 +47,7 @@ const STATUS_STYLES: Record<string, { bg: string; text: string }> = {
export function SystemLogsView() {
const { t } = useTranslation();
const [tab, setTab] = useState<'system' | 'workflow'>('system');
const [autoRefresh, setAutoRefresh] = useState(true);
// System logs state
const [logs, setLogs] = useState<EventLog[]>([]);
@@ -63,7 +64,7 @@ export function SystemLogsView() {
const [wfLoading, setWfLoading] = useState(false);
const [expandedStep, setExpandedStep] = useState<number | null>(null);
const fetchLogs = async () => {
const fetchLogs = useCallback(async () => {
setLoading(true);
try {
const params = new URLSearchParams();
@@ -78,7 +79,7 @@ export function SystemLogsView() {
} finally {
setLoading(false);
}
};
}, [traceFilter, typeFilter, levelFilter]);
const fetchWorkflows = async () => {
try {
@@ -106,7 +107,13 @@ export function SystemLogsView() {
useEffect(() => {
if (tab === 'system') fetchLogs();
else fetchWorkflows();
}, [tab]);
}, [tab, fetchLogs]);
useEffect(() => {
if (tab !== 'system' || !autoRefresh) return;
const interval = setInterval(fetchLogs, 5000);
return () => clearInterval(interval);
}, [tab, autoRefresh, fetchLogs]);
useEffect(() => {
if (selectedTrace) fetchWorkflowDetail(selectedTrace);
@@ -143,6 +150,15 @@ export function SystemLogsView() {
{t('agent.workflowLogs')}
</button>
<div className="flex-1" />
{tab === 'system' && (
<button
onClick={() => setAutoRefresh(!autoRefresh)}
className={`p-2 rounded-lg transition-all ${autoRefresh ? 'text-accent bg-accent-light' : 'text-text-muted hover:text-accent hover:bg-accent-light'}`}
title={autoRefresh ? t('agent.logPauseRefresh') : t('agent.logResumeRefresh')}
>
{autoRefresh ? <Pause size={14} /> : <Play size={14} />}
</button>
)}
<button
onClick={() => tab === 'system' ? fetchLogs() : fetchWorkflows()}
disabled={loading || wfLoading}
+25 -1
View File
@@ -151,8 +151,15 @@
"addProvider": "Add Provider",
"noProviders": "No providers configured yet",
"providerFillAll": "Please fill in all fields.",
"providerFillUrlAndKey": "Please fill in URL and API Key first.",
"providerAddFailed": "Failed to add provider. Please check your inputs and try again.",
"deleteProviderConfirm": "Delete this provider?",
"testConnection": "Test Connection",
"providerTestSuccess": "Connection successful",
"providerTestFailed": "Connection failed",
"providerModels": "models",
"editProvider": "Edit Provider",
"apiKeyEditPlaceholder": "Enter new API key (leave blank to keep current)",
"providerType": "Type",
"providerTitle": "Title",
"baseUrl": "Base URL",
@@ -184,6 +191,8 @@
"logFilterAllTypes": "All event types",
"logFilterAllLevels": "All levels",
"logSearch": "Search",
"logPauseRefresh": "Pause auto-refresh",
"logResumeRefresh": "Resume auto-refresh",
"logLevel": "Level",
"logType": "Type",
"logNode": "Node",
@@ -224,7 +233,22 @@
"customPrompt": "Custom Prompt",
"customPromptPlaceholder": "Additional content appended after the default system prompt...",
"displayName": "Display Name",
"persona": "Persona"
"persona": "Persona",
"providerTypeOpenai": "OpenAI",
"providerTypeOpenaiDesc": "Official OpenAI API",
"providerTypeOpenaiCompat": "OpenAI-Compatible",
"providerTypeOpenaiCompatDesc": "Self-hosted or third-party compatible",
"providerTypeAnthropic": "Anthropic",
"providerTypeAnthropicDesc": "Claude family models",
"providerTypeGemini": "Gemini",
"providerTypeGeminiDesc": "Google AI models",
"providerTypeDeepseek": "DeepSeek",
"providerTypeDeepseekDesc": "DeepSeek official API",
"baseUrlHint": "Default",
"providerAdvanced": "Parameter Settings",
"providerCustomModels": "Custom Model List",
"providerCustomModelsPlaceholder": "Comma separated, e.g. gpt-4o, gpt-4o-mini",
"providerCustomModelsHint": "Optional. Leave empty to auto-fetch model list from provider."
},
"plugin": {
"toolManagement": "Toolset Center",
+25 -1
View File
@@ -151,8 +151,15 @@
"addProvider": "添加供应商",
"noProviders": "暂无已配置的供应商",
"providerFillAll": "请填写所有字段。",
"providerFillUrlAndKey": "请先填写 URL 和 API 密钥。",
"providerAddFailed": "添加供应商失败,请检查输入后重试。",
"deleteProviderConfirm": "确定要删除此供应商吗?",
"testConnection": "测试连接",
"providerTestSuccess": "连接成功",
"providerTestFailed": "连接失败",
"providerModels": "个模型",
"editProvider": "编辑供应商",
"apiKeyEditPlaceholder": "输入新密钥(留空保持现有密钥)",
"providerType": "类型",
"providerTitle": "名称",
"baseUrl": "基础 URL",
@@ -184,6 +191,8 @@
"logFilterAllTypes": "所有事件类型",
"logFilterAllLevels": "所有级别",
"logSearch": "查询",
"logPauseRefresh": "暂停自动刷新",
"logResumeRefresh": "恢复自动刷新",
"logLevel": "级别",
"logType": "类型",
"logNode": "节点",
@@ -224,7 +233,22 @@
"customPrompt": "附加人设",
"customPromptPlaceholder": "在默认系统提示词之后追加的自定义内容...",
"displayName": "显示名称",
"persona": "人设"
"persona": "人设",
"providerTypeOpenai": "OpenAI",
"providerTypeOpenaiDesc": "官方 OpenAI API",
"providerTypeOpenaiCompat": "OpenAI 兼容",
"providerTypeOpenaiCompatDesc": "自托管或第三方兼容服务",
"providerTypeAnthropic": "Anthropic",
"providerTypeAnthropicDesc": "Claude 系列模型",
"providerTypeGemini": "Gemini",
"providerTypeGeminiDesc": "Google AI 模型",
"providerTypeDeepseek": "DeepSeek",
"providerTypeDeepseekDesc": "DeepSeek 官方 API",
"baseUrlHint": "默认地址",
"providerAdvanced": "参数设置",
"providerCustomModels": "自定义模型列表",
"providerCustomModelsPlaceholder": "用逗号分隔,如:gpt-4o, gpt-4o-mini",
"providerCustomModelsHint": "可选,留空则自动从供应商拉取模型清单"
},
"plugin": {
"toolManagement": "工具集中心",
+2
View File
@@ -19,6 +19,7 @@ export interface Provider {
provider_url?: string;
provider_owner?: string;
provider_models?: string[];
provider_status?: string;
status?: string;
model?: string;
}
@@ -84,6 +85,7 @@ export interface WorkflowStep {
action: string;
desc: string;
status: string;
output?: string;
agent_id?: string;
}
+17 -6
View File
@@ -163,15 +163,15 @@ async def stream_chat_message(
request: Request,
token_data: TokenData = Depends(Accessor.get_current_user),
):
"""SSE 流式聊天端点:通过 regulatory_node agent 流式输出,支持工具调用"""
"""SSE 流式聊天端点:standalone 模式下逐 token 流式输出distributed 模式 fallback 到整段回复"""
from kilostar.utils.standalone_proxy import _STANDALONE
postgres_database = ray_actor_hook("postgres_database").postgres_database
# 存用户消息
await postgres_database.add_chat_message.remote(
chat_id=chat_id, message=request_body.message, message_owner="user"
)
# 构造 MessageRequest payload
payload = MessageRequest(
platform="client",
user_name=token_data.user_id,
@@ -180,9 +180,21 @@ async def stream_chat_message(
)
regulatory_node = ray_actor_hook("regulatory_node").regulatory_node
token_queue = asyncio.Queue()
# stream_working.remote() returns an asyncio.Task in standalone mode
if not _STANDALONE:
async def fallback_generator():
resp = await regulatory_node.working.remote(payload)
full_response = resp.reply_message if resp else ""
if full_response:
await postgres_database.add_chat_message.remote(
chat_id=chat_id, message=full_response, message_owner="regulatory_node"
)
yield f"data: {json.dumps({'token': full_response})}\n\n"
yield f"data: {json.dumps({'done': True, 'full_message': full_response})}\n\n"
return StreamingResponse(fallback_generator(), media_type="text/event-stream")
token_queue = asyncio.Queue()
stream_task = regulatory_node.stream_working.remote(payload, token_queue)
async def event_generator():
@@ -207,7 +219,6 @@ async def stream_chat_message(
full_response = "抱歉,生成回复时出错。"
yield f"data: {json.dumps({'token': full_response})}\n\n"
# 流结束,存入数据库
if full_response:
await postgres_database.add_chat_message.remote(
chat_id=chat_id,
+58 -1
View File
@@ -27,7 +27,7 @@ provider_router = APIRouter(prefix="/api/v1/provider", tags=["provider"])
class ProviderRegister(BaseModel):
"""``POST /provider`` 入参:注册一个模型 Provider 的最小字段集。"""
provider_type: Literal["openai", "claude", "deepseek"]
provider_type: Literal["openai", "claude", "deepseek", "gemini"]
provider_title: str
provider_url: str
provider_apikey: str
@@ -72,6 +72,63 @@ async def get_provider_list(
return {"provider_list": masked}
@provider_router.post("/test")
async def test_provider_connection(
provider_register: ProviderRegister,
_: TokenData = Depends(Accessor.get_current_user),
) -> Dict[str, Any]:
"""测试 Provider 连接:按 provider_type 选择对应协议拉取模型列表。"""
import httpx
ptype = provider_register.provider_type
url = provider_register.provider_url
apikey = provider_register.provider_apikey
try:
async with httpx.AsyncClient(timeout=10.0) as client:
if ptype == "claude":
endpoint = f"{url}/v1/models"
headers = {
"x-api-key": apikey,
"anthropic-version": "2023-06-01",
}
response = await client.get(endpoint, headers=headers)
if response.status_code == 200:
data = response.json()
models = [m["id"] for m in data.get("data", [])]
return {"success": True, "models": sorted(models), "model_count": len(models)}
return {"success": False, "error": f"HTTP {response.status_code}", "models": []}
elif ptype == "gemini":
endpoint = f"{url}/models"
params = {"key": apikey}
response = await client.get(endpoint, params=params)
if response.status_code == 200:
data = response.json()
models = [m.get("name", "").removeprefix("models/") for m in data.get("models", [])]
return {"success": True, "models": sorted(models), "model_count": len(models)}
return {"success": False, "error": f"HTTP {response.status_code}", "models": []}
else:
if "/v1" not in url:
endpoint = f"{url}/v1/models"
else:
endpoint = f"{url}/models"
headers = {
"Authorization": f"Bearer {apikey}",
"Content-Type": "application/json",
}
response = await client.get(endpoint, headers=headers)
if response.status_code == 200:
data = response.json()
models = [m["id"] for m in data.get("data", [])]
return {"success": True, "models": sorted(models), "model_count": len(models)}
return {"success": False, "error": f"HTTP {response.status_code}", "models": []}
except Exception as e:
return {"success": False, "error": str(e), "models": []}
@provider_router.delete("/{provider_title}")
async def delete_provider(
provider_title: str,
@@ -39,6 +39,7 @@ class ConsciousnessNode:
self.logger = get_logger("consciousness_node")
self.agent: None | Agent = None
self.locale: str = "zh"
async def create_agent(
self,
@@ -51,6 +52,7 @@ class ConsciousnessNode:
custom_system_prompt: str | None = None,
) -> None:
system_prompt: str = agent_prompt("consciousness_node", locale=locale, custom_system_prompt=custom_system_prompt)
self.locale = locale or "zh"
output_type = Union[ForregulatoryNode, ForWorkflow, ForWorkflowEngine]
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
@@ -74,23 +76,43 @@ class ConsciousnessNode:
@self.agent.system_prompt
async def dynamic_prompt(ctx: RunContext[ConsciousnessNodeDeps]):
locale = ctx.deps.locale
prompt = system_prompt + "\n\n"
prompt += (
f"=== 当前任务上下文 ===\n"
f"- 当前指令 (Command): {ctx.deps.command}\n"
f"- 原始用户命令 (Original Command): {ctx.deps.original_command}\n"
)
if ctx.deps.available_skills:
prompt += "\n=== 当前可用 Skill Individual ===\n"
prompt += "你可以直接将以下 Skill Individual 安排进工作流的步骤中(设置 node 为 skill_individual,并将 agent_id 设置为对应 Skill Individual 的真实 agent_id,不要用名称!),作为可调用的工具。\n"
for skill in ctx.deps.available_skills:
prompt += f"- 真实 agent_id: {skill.get('agent_id')}\n 名称: {skill['name']}\n 描述: {skill['description']}\n"
if locale == "en":
prompt += (
f"=== Current Task Context ===\n"
f"- Command: {ctx.deps.command}\n"
f"- Original User Command: {ctx.deps.original_command}\n"
)
if ctx.deps.available_skills:
prompt += "\n=== Available Skill Individuals ===\n"
prompt += "You may assign the following Skill Individuals to workflow steps (set node to skill_individual, and set agent_id to the real agent_id below — never use the name!).\n"
for skill in ctx.deps.available_skills:
prompt += f"- agent_id: {skill.get('agent_id')}\n Name: {skill['name']}\n Description: {skill['description']}\n"
else:
prompt += "\n=== IMPORTANT: No Worker Individuals Available ===\n"
prompt += "No Worker Individuals are registered. When generating a workflow, you have exactly two options:\n"
prompt += "1. Assign the step to consciousness_node itself (set node to consciousness_node, agent_id to null).\n"
prompt += "2. If the task truly requires specialized tools, refuse and explain that a Worker must be created first.\n"
prompt += "NEVER fabricate non-existent agent_ids!\n"
else:
prompt += "\n=== 重要:当前无可用 Worker Individual ===\n"
prompt += "系统中当前没有注册任何 Worker Individual。在生成工作流时,你有且仅有以下两种选择:\n"
prompt += "1. 将步骤分配给 consciousness_node 自己完成(设置 node 为 consciousness_nodeagent_id 为 null)。\n"
prompt += "2. 如果任务确实需要专用工具或技能才能完成,则拒绝执行并在输出中说明需要先创建对应的 Worker。\n"
prompt += "绝对禁止编造不存在的 agent_id!\n"
prompt += (
f"=== 当前任务上下文 ===\n"
f"- 当前指令: {ctx.deps.command}\n"
f"- 原始用户命令: {ctx.deps.original_command}\n"
)
if ctx.deps.available_skills:
prompt += "\n=== 当前可用 Skill Individual ===\n"
prompt += "你可以直接将以下 Skill Individual 安排进工作流的步骤中(设置 node 为 skill_individual,并将 agent_id 设置为对应 Skill Individual 的真实 agent_id,不要用名称!)。\n"
for skill in ctx.deps.available_skills:
prompt += f"- 真实 agent_id: {skill.get('agent_id')}\n 名称: {skill['name']}\n 描述: {skill['description']}\n"
else:
prompt += "\n=== 重要:当前无可用 Worker Individual ===\n"
prompt += "系统中当前没有注册任何 Worker Individual。在生成工作流时,你有且仅有以下两种选择:\n"
prompt += "1. 将步骤分配给 consciousness_node 自己完成(设置 node 为 consciousness_nodeagent_id 为 null)。\n"
prompt += "2. 如果任务确实需要专用工具或技能才能完成,则拒绝执行并在输出中说明需要先创建对应的 Worker。\n"
prompt += "绝对禁止编造不存在的 agent_id!\n"
return prompt
@@ -127,7 +149,6 @@ class ConsciousnessNode:
original_command=command, available_skills=available_skills
)
# 通知 SSE 正在生成图结构(pending 队列:节点端写入 → API SSE 读取,单向下行)
global_workflow_manager = ray_actor_hook(
"global_workflow_manager"
).global_workflow_manager
@@ -135,7 +156,6 @@ class ConsciousnessNode:
trace_id, "正在为您构建并规划工作流任务节点,请稍候..."
)
# 实际构建过程
result = await self.working(payload)
if result and isinstance(result, ForWorkflowEngine):
@@ -197,6 +217,7 @@ class ConsciousnessNode:
original_command=payload.original_command,
command="自主分析并拆解原始命令,生成严密可执行的工作流",
available_skills=payload.available_skills,
locale=self.locale,
)
self.logger.debug("ConsciousnessNode: 开始生成工作流 (原生重试开启)")
prompt = "根据original_command制定严密的可执行workflow"
@@ -207,6 +228,7 @@ class ConsciousnessNode:
deps = ConsciousnessNodeDeps(
original_command=payload.original_command,
command="完成workflow step中分配给意识节点的特定任务或指导",
locale=self.locale,
)
self.logger.debug(
"ConsciousnessNode: 开始处理工作流节点任务 (原生重试开启)"
@@ -221,6 +243,7 @@ class ConsciousnessNode:
deps = ConsciousnessNodeDeps(
original_command=payload.original_command,
command="对于工作流整体执行结果进行检查,并且生成一份专业的技术性总结报告",
locale=self.locale,
)
self.logger.debug(
"ConsciousnessNode: 开始生成技术总结报告 (原生重试开启)"
@@ -28,7 +28,8 @@ class ConsciousnessNodeDeps(DepsModel):
"""ConsciousnessNode 在 pydantic-ai Agent 中使用的依赖:原始指令、当前指令以及可用 Skill 列表。"""
original_command: str
command: str
available_skills: Optional[List[str]] = None
available_skills: Optional[List[dict]] = None
locale: str = "zh"
class ConsciousnessNodeInput(RequestModel):
"""ConsciousnessNode 各类入参的共同基类,仅用于打 schema 标签。"""
@@ -30,10 +30,9 @@ from kilostar.utils.i18n import agent_prompt
@actor_class
class RegulatoryNode:
"""RegulatoryNode(监管节点):用户请求的入口路由 Actor
"""RegulatoryNode(监管节点):用户请求的直接对话节点
负责对消息做意图识别:闲聊 → 直接回 ``ForUser``;复杂任务 → 走
``ForConsciousnessNode`` 移交给意识节点;工作流回执 → 转译成对用户的总结回复。
负责理解用户需求并提供回复;如果收到工作流执行报告则转化为用户友好的总结。
"""
def __init__(self) -> None:
@@ -100,12 +99,9 @@ class RegulatoryNode:
f"- 用户名 (User): {ctx.deps.user_name}\n"
f"- 当前时间 (Time): {ctx.deps.time}\n"
)
# 修改 system_prompt 变量
prompt += (
"\n\n注意:你必须调用且只能调用一个函数(工具)来输出结果"
"如果你想直接回复用户,请调用 ForUser;"
"如果你想移交给工作流,请调用 ForConsciousnessNode。"
"严禁返回纯文本,必须使用工具格式!"
"\n\n注意:请基于上下文信息为用户提供准确、专业的回复"
"如果你有可用工具,可在需要时主动调用。"
)
if ctx.deps.error_history:
prompt += (
@@ -130,7 +126,7 @@ class RegulatoryNode:
"规则:\n"
"1. 直接、详细地回答用户问题,像一个专业且友好的助手。\n"
"2. 如果你有可用工具,可以调用工具来辅助回答(如搜索、读文件等)。\n"
"3. 不要输出内部思考过程,不要做路由判断,不要提及 ForUser/ForConsciousnessNode 等格式\n"
"3. 不要输出内部思考过程,直接给出回复内容\n"
"4. 回复应当完整、有帮助,避免过于简短。\n"
)
@@ -104,7 +104,7 @@ class IndividualDatabase:
individual = results.scalar_one_or_none()
if not individual:
return False
session.delete(individual)
await session.delete(individual)
await session.commit()
return True
@@ -90,7 +90,7 @@ class ProviderDatabase:
async with self.async_session_maker() as session:
provider = await session.get(ProviderModel, provider_id)
if provider is not None:
session.delete(provider)
await session.delete(provider)
await session.commit()
@database_exception
@@ -78,7 +78,7 @@ class AuthDatabase:
user = results.scalar_one_or_none()
if user is None:
raise UserNotExistError()
session.delete(user)
await session.delete(user)
await session.commit()
@database_exception
@@ -88,7 +88,7 @@ class AuthDatabase:
user = await session.get(User, user_id)
if user is None:
raise UserNotExistError()
session.delete(user)
await session.delete(user)
await session.commit()
@database_exception
+31 -6
View File
@@ -223,11 +223,28 @@ class Finalize(BaseNode[WorkflowGraphState, WorkflowDeps, str]):
) -> End[str]:
ctx.state.final_status = self.status
await ctx.deps.update_workflow_status(ctx.state.trace_id, self.status)
msg = (
"工作流执行完成!"
if self.status == WorkflowStatus.COMPLETED.value
else "工作流执行失败。"
)
if self.status == WorkflowStatus.COMPLETED.value:
summary_parts = []
for key, val in ctx.state.blackboard.items():
text = str(val)[:200]
summary_parts.append(f"{key}: {text}")
summary = "\n".join(summary_parts) if summary_parts else ""
msg = f"工作流执行完成!\n{summary}" if summary else "工作流执行完成!"
else:
failed_logs = [
entry for entry in ctx.state.logs
if any(
isinstance(v, (list, tuple)) and len(v) >= 2 and v[1] == "failed"
for v in (entry.values() if isinstance(entry, dict) else [])
)
]
msg = "工作流执行失败。"
if failed_logs:
last = list(failed_logs[-1].values())[0]
if isinstance(last, (list, tuple)) and len(last) >= 3:
msg += f"\n失败原因: {last[2][:300]}"
await ctx.deps.put_pending(ctx.state.trace_id, msg)
return End(self.status)
@@ -295,9 +312,13 @@ async def _execute_step(
state.logs[-1][str(state.current_step_index)] = [
str(datetime.datetime.now()),
"completed",
f"成功: {step_data.get('action', '')}",
output_text,
]
await _persist_context(ctx, status=WorkflowStatus.RUNNING.value)
await ctx.deps.put_pending(
state.trace_id,
f"✅ 步骤 {state.current_step_index + 1} ({step_data.get('name', '')}) 完成:\n{output_text[:500]}",
)
logic_gate = step_data.get("logic_gate") or {}
if logic_gate.get("if_pass") == "exit":
@@ -314,6 +335,10 @@ async def _execute_step(
"failed",
output_text,
]
await ctx.deps.put_pending(
state.trace_id,
f"❌ 步骤 {state.current_step_index + 1} ({step_data.get('name', '')}) 失败:\n{output_text[:300]}",
)
logic_gate = step_data.get("logic_gate") or {}
fail_target = logic_gate.get("if_fail")
if fail_target and "jump_to_step_" in fail_target:
+15 -15
View File
@@ -36,24 +36,24 @@ _DEFAULT_LOCALE: str = get_settings().kilostar_lang
_PROMPTS: Dict[str, Dict[str, str]] = {
"regulatory_node": {
"zh": (
"你叫kilostar,是一个多智能体AI助手系统中的【监节点 (regulatory Node)】。\n"
"你是系统'前台接待''大脑皮层',负责接收用户的初始请求或工作流的最终报告。\n"
"你的核心职责是进行【意图识别与路由】。请仔细阅读用户的请求\n"
"1. 如果用户只是进行简单的问候、闲聊或查询非常基础的信息,请直接生成友好的回复,使用 ForUser 格式\n"
"2. 如果用户提出的是复杂任务(如需要编写代码、多步骤规划、数据处理等),请务必将其判定为需要工作流处理的任务,"
" 并使用 ForConsciousnessNode 格式将其移交意识节点处理\n"
"3. 如果你收到的是 TerminationMessage(代表工作流已完成并生成了报告),请将报告内容转化为友好的面向用户的回复,使用 ForUser 格式\n"
"请保持冷静、专业,并严格遵循上述路由规则"
"你叫kilostar,是一个多智能体AI助手系统中的【监节点 (Regulatory Node)】。\n"
"你是系统中直接面向用户的对话节点,负责理解用户需求并提供高质量的回复。\n\n"
"你的核心职责:\n"
"1. 准确理解用户的意图,提供专业、友好且有帮助的回复\n"
"2. 如果你有可用工具,可以主动调用工具来辅助回答(如搜索、文件操作等)。\n"
"3. 如果你收到工作流的执行报告,请将其转化为面向用户的清晰总结\n"
"4. 保持回复简洁、有结构,避免冗余信息\n"
"请保持专业、友好的沟通风格"
),
"en": (
"You are kilostar, the [Regulatory Node] in a multi-agent AI assistant system.\n"
"You are the system's 'front desk' and 'cerebral cortex', responsible for receiving user requests and final workflow reports.\n"
"Your core duty is [intent recognition and routing]. Please read the user's request carefully:\n"
"1. If the user is simply greeting, chatting, or asking very basic questions, generate a friendly reply directly in the ForUser format.\n"
"2. If the user presents a complex task (e.g., writing code, multi-step planning, data processing), you must classify it as a workflow-requiring task "
" and hand it over to the Consciousness Node using the ForConsciousnessNode format.\n"
"3. If you receive a TerminationMessage (indicating the workflow is complete and a report has been generated), convert the report into a user-friendly reply in the ForUser format.\n"
"Please remain calm, professional, and strictly follow the routing rules above."
"You are the user-facing conversational node, responsible for understanding user needs and providing high-quality responses.\n\n"
"Your core responsibilities:\n"
"1. Accurately understand user intent and provide professional, friendly, and helpful replies.\n"
"2. If tools are available, proactively use them to assist your responses (e.g., search, file operations).\n"
"3. If you receive a workflow execution report, convert it into a clear user-facing summary.\n"
"4. Keep responses concise, well-structured, and free of redundancy.\n"
"Maintain a professional and friendly communication style."
),
},
"consciousness_node": {
@@ -41,7 +41,7 @@ class OrdinaryIndividual(BaseIndividual):
self.agent.retries = 3
try:
result = await self.agent.run(f"请执行以下任务:\n{task_event}", deps=deps)
return {"output": result.data.output}
return {"output": result.output.output}
except Exception as e:
logger.exception(f"OrdinaryIndividual {self.agent_id} 执行失败: {e}")
raise
@@ -123,7 +123,7 @@ class SkillIndividual(BaseIndividual):
deps=deps,
tools=tools if tools else None,
)
return {"output": result.data.output}
return {"output": result.output.output}
except Exception as e:
logger.exception(f"SkillIndividual {self.agent_id} 执行失败: {e}")
raise
@@ -41,7 +41,7 @@ class SpecialIndividual(BaseIndividual):
self.agent.retries = 3
try:
result = await self.agent.run(f"请执行以下任务:\n{task_event}", deps=deps)
return {"output": result.data.output}
return {"output": result.output.output}
except Exception as e:
logger.exception(f"SpecialIndividual {self.agent_id} 执行失败: {e}")
raise
-36
View File
@@ -118,17 +118,6 @@ async def test_delete_template_not_found(app, fake_actors):
assert r.status_code == 404
@pytest.mark.asyncio
async def test_delete_builtin_template_forbidden(app, fake_actors):
pg = types.SimpleNamespace(
get_template=types.SimpleNamespace(remote=AsyncMock(return_value=_tpl(is_builtin=True)))
)
fake_actors.register("postgres_database", pg)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
r = await c.delete("/api/v1/agent/template/tpl1")
assert r.status_code == 403
@pytest.mark.asyncio
async def test_delete_other_users_template_forbidden(app, fake_actors):
pg = types.SimpleNamespace(
@@ -138,28 +127,3 @@ async def test_delete_other_users_template_forbidden(app, fake_actors):
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
r = await c.delete("/api/v1/agent/template/tpl1")
assert r.status_code == 403
@pytest.mark.asyncio
async def test_create_worker_from_template(app, fake_actors):
worker = types.SimpleNamespace(agent_id="w1")
pg = types.SimpleNamespace(
get_template=types.SimpleNamespace(remote=AsyncMock(return_value=_tpl())),
add_worker_individual=types.SimpleNamespace(remote=AsyncMock(return_value=worker)),
)
fake_actors.register("postgres_database", pg)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
r = await c.post("/api/v1/agent/worker/from-template/tpl1")
assert r.status_code == 200
assert r.json()["agent_id"] == "w1"
@pytest.mark.asyncio
async def test_create_worker_from_missing_template(app, fake_actors):
pg = types.SimpleNamespace(
get_template=types.SimpleNamespace(remote=AsyncMock(return_value=None))
)
fake_actors.register("postgres_database", pg)
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://t") as c:
r = await c.post("/api/v1/agent/worker/from-template/nope")
assert r.status_code == 404
+1 -1
View File
@@ -79,7 +79,7 @@ def test_rebuild_custom_toolsets(manager: GlobalToolManager):
manager.rebuild_custom_toolsets(custom_defs)
assert "grp1" in manager._custom_toolsets
sets = manager.get_toolsets_for_scope("default")
custom_ids = [getattr(ts, "id", "") for ts in sets if "custom::" in getattr(ts, "id", "")]
custom_ids = [getattr(ts, "id", "") for ts in sets if "toolset::" in getattr(ts, "id", "")]
assert any("grp1" in cid for cid in custom_ids)
manager.rebuild_custom_toolsets({})
assert manager._custom_toolsets == {}
+1
View File
@@ -144,6 +144,7 @@ def consciousness_instance():
from kilostar.utils.logger import get_logger
obj.logger = get_logger("consciousness_node")
obj.agent = None
obj.locale = "zh"
return obj
Generated
+2
View File
@@ -2147,6 +2147,7 @@ dependencies = [
{ name = "pretor-viceroy" },
{ name = "pwdlib", extra = ["argon2", "bcrypt"] },
{ name = "pydantic-ai" },
{ name = "pydantic-settings" },
{ name = "pyfiglet" },
{ name = "pyjwt" },
{ name = "python-ulid" },
@@ -2178,6 +2179,7 @@ requires-dist = [
{ name = "pretor-viceroy", specifier = ">=0.2.0" },
{ name = "pwdlib", extras = ["argon2", "bcrypt"], specifier = ">=0.3.0" },
{ name = "pydantic-ai", specifier = ">=1.73.0" },
{ name = "pydantic-settings", specifier = ">=2.0" },
{ name = "pyfiglet", specifier = ">=1.0.4" },
{ name = "pyjwt", specifier = ">=2.12.1" },
{ name = "python-ulid", specifier = ">=3.1.0" },