Compare commits

..

10 Commits

Author SHA1 Message Date
Junyan Qin (Chin)
91cd8cf380 chore: release v4.0.1 (#1409) 2025-05-13 19:37:47 +08:00
Guanchao Wang
c3de3fa275 fix: wrong status when creating a WecomCS bot (#1408) 2025-05-13 19:33:32 +08:00
devin-ai-integration[bot]
039752419b Add User Card and Logout Button to Sidebar (#1405)
* feat: add user card and logout button to sidebar

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* feat: add test code to set dummy values in localStorage

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* style: fix formatting issues in HomeSidebar.tsx

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* style: fix whitespace in HomeSidebar.tsx

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* perf: styles of logout button

* fix: lint errors

* fix: lint errors

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-13 19:33:04 +08:00
Junyan Qin (Chin)
18c708da58 fix: windows path bug in importutil (#1404) 2025-05-13 16:52:16 +08:00
Junyan Qin (Chin)
8c08b8ee8a perf: no delay for model selector hover card (#1402) 2025-05-13 16:15:35 +08:00
Junyan Qin (Chin)
015be6008d fix: bugs in requesters (#1401) 2025-05-13 16:09:23 +08:00
Junyan Qin
da86384e58 doc(README): add Ask DeepWiki badge 2025-05-13 14:36:29 +08:00
devin-ai-integration[bot]
86ff6f5eb6 feat: plugin reordering (#1398)
* Add @dnd-kit/core and @dnd-kit/sortable dependencies for plugin sorting

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Add PluginSortDialog component with drag-and-drop functionality

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Integrate sorting button and dialog into PluginInstalledComponent

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Update HttpClient to use local backend URL for development

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Fix reorderPlugins method to use PUT and correct request format

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Update hover-card component using shadcn CLI

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Fix formatting issues in plugin sorting components

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* refactor: move plugin sorting button and dialog to page component

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* refactor: move PluginSortDialog component to plugins directory

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* chore: remove old PluginSortDialog component file

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* fix: api bug

* perf: desciption in plugin sorting dialog

* fix: lint errors

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-13 14:10:18 +08:00
devin-ai-integration[bot]
ae6979151f Fix #1391: Update bot's pipeline name when pipeline is renamed (#1397)
* Fix #1391: Update bot's pipeline name when pipeline is renamed

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Refactor: Use bot_service to update bot pipeline names

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-13 12:44:00 +08:00
devin-ai-integration[bot]
fd1b5d494e Add hover card to LLM model selector (#1393)
* Add hover card to LLM model selector to display detailed model information

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Fix formatting issues to resolve lint errors

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* Fix remaining formatting issue in DynamicFormItemComponent.tsx

Co-Authored-By: Junyan Qin <Chin> <rockchinq@gmail.com>

* perf(model preview): hover card style

* fix: wrong base url

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Junyan Qin <Chin> <rockchinq@gmail.com>
2025-05-13 12:25:32 +08:00
25 changed files with 562 additions and 75 deletions

View File

@@ -19,9 +19,9 @@
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![QQ Group](https://img.shields.io/badge/%E7%A4%BE%E5%8C%BAQQ%E7%BE%A4-966235608-blue)](https://qm.qq.com/q/JLi38whHum)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=%E4%BD%BF%E7%94%A8%E9%87%8F%EF%BC%887%E6%97%A5%EF%BC%89)
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">

View File

@@ -20,6 +20,7 @@
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days))
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">

View File

@@ -19,6 +19,7 @@
<br/>
[![Discord](https://img.shields.io/discord/1335141740050649118?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb)](https://discord.gg/wdNEHETs87)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/RockChinQ/LangBot)
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/RockChinQ/LangBot)](https://github.com/RockChinQ/LangBot/releases/latest)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.qchatgpt.rockchin.top%2Fapi%2Fv2%2Fview%2Frealtime%2Fcount_query%3Fminute%3D10080&query=%24.data.count&label=Usage(7days))
<img src="https://img.shields.io/badge/python-3.10 ~ 3.13 -blue.svg" alt="python">

View File

@@ -22,7 +22,6 @@ class WecomCSClient:
self.base_url = 'https://qyapi.weixin.qq.com/cgi-bin'
self.access_token = ''
self.app = Quart(__name__)
self.wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
self.app.add_url_rule(
'/callback/command', 'handle_callback', self.handle_callback_request, methods=['GET', 'POST']
)
@@ -198,17 +197,21 @@ class WecomCSClient:
msg_signature = request.args.get('msg_signature')
timestamp = request.args.get('timestamp')
nonce = request.args.get('nonce')
try:
wxcpt = WXBizMsgCrypt(self.token, self.aes, self.corpid)
except Exception as e:
raise Exception(f'初始化失败,错误码: {e}')
if request.method == 'GET':
echostr = request.args.get('echostr')
ret, reply_echo_str = self.wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
ret, reply_echo_str = wxcpt.VerifyURL(msg_signature, timestamp, nonce, echostr)
if ret != 0:
raise Exception(f'验证失败,错误码: {ret}')
return reply_echo_str
elif request.method == 'POST':
encrypt_msg = await request.data
ret, xml_msg = self.wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
ret, xml_msg = wxcpt.DecryptMsg(encrypt_msg, msg_signature, timestamp, nonce)
if ret != 0:
raise Exception(f'消息解密失败,错误码: {ret}')

View File

@@ -94,10 +94,24 @@ class PipelineService:
.values(**pipeline_data)
)
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
pipeline = await self.get_pipeline(pipeline_uuid)
if 'name' in pipeline_data:
from ....entity.persistence import bot as persistence_bot
result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(persistence_bot.Bot).where(
persistence_bot.Bot.use_pipeline_uuid == pipeline_uuid
)
)
bots = result.all()
for bot in bots:
bot_data = {'use_pipeline_name': pipeline_data['name']}
await self.ap.bot_service.update_bot(bot.uuid, bot_data)
await self.ap.pipeline_mgr.remove_pipeline(pipeline_uuid)
await self.ap.pipeline_mgr.load_pipeline(pipeline)
async def delete_pipeline(self, pipeline_uuid: str) -> None:

View File

@@ -79,7 +79,7 @@ class PluginManager:
await self.load_plugin_settings(self.plugin_containers)
# 按优先级倒序
self.plugin_containers.sort(key=lambda x: x.priority, reverse=True)
self.plugin_containers.sort(key=lambda x: x.priority, reverse=False)
self.ap.logger.debug(f'优先级排序后的插件列表 {self.plugin_containers}')
@@ -295,7 +295,7 @@ class PluginManager:
plugin.priority = plugin_priority
break
self.plugin_containers.sort(key=lambda x: x.priority, reverse=True)
self.plugin_containers.sort(key=lambda x: x.priority, reverse=False)
for plugin in self.plugin_containers:
await self.dump_plugin_container_setting(plugin)

View File

@@ -27,7 +27,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = extra_args.copy()
args = {}
args['model'] = use_model.model_entity.name
if use_funcs:
@@ -47,7 +47,7 @@ class DeepseekChatCompletions(chatcmpl.OpenAIChatCompletions):
args['messages'] = messages
# 发送请求
resp = await self._req(args, extra_body=self.requester_cfg['args'])
resp = await self._req(args, extra_body=extra_args)
if resp is None:
raise errors.RequesterError('接口返回为空,请确定模型提供商服务是否正常')

View File

@@ -28,7 +28,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = extra_args.copy()
args = {}
args['model'] = use_model.model_entity.name
if use_funcs:
@@ -44,7 +44,7 @@ class GiteeAIChatCompletions(chatcmpl.OpenAIChatCompletions):
args['messages'] = req_messages
resp = await self._req(args, extra_body=self.requester_cfg['args'])
resp = await self._req(args, extra_body=extra_args)
message = await self._make_msg(resp)

View File

@@ -9,7 +9,7 @@ import openai.types.chat.chat_completion_message_tool_call as chat_completion_me
import httpx
from .. import entities, errors, requester
from ....core import entities as core_entities, app
from ....core import entities as core_entities
from ... import entities as llm_entities
from ...tools import entities as tools_entities
@@ -19,12 +19,10 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['modelscope-chat-completions']
default_config: dict[str, typing.Any] = {
'base_url': 'https://api-inference.modelscope.cn/v1',
'timeout': 120,
}
async def initialize(self):
self.client = openai.AsyncClient(
@@ -37,6 +35,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
async def _req(
self,
args: dict,
extra_body: dict = {},
) -> chat_completion.ChatCompletion:
args['stream'] = True
@@ -46,7 +45,7 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
tool_calls = []
resp_gen: openai.AsyncStream = await self.client.chat.completions.create(**args)
resp_gen: openai.AsyncStream = await self.client.chat.completions.create(**args, extra_body=extra_body)
async for chunk in resp_gen:
# print(chunk)
@@ -107,7 +106,6 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
if chunk
else None
)
return await self.client.chat.completions.create(**args)
async def _make_msg(
self,
@@ -129,10 +127,11 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
req_messages: list[dict],
use_model: entities.LLMModelInfo,
use_funcs: list[tools_entities.LLMFunction] = None,
extra_args: dict[str, typing.Any] = {},
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = self.requester_cfg['args'].copy()
args = {}
args['model'] = use_model.name if use_model.model_name is None else use_model.model_name
if use_funcs:
@@ -156,19 +155,20 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
args['messages'] = messages
# 发送请求
resp = await self._req(args)
resp = await self._req(args, extra_body=extra_args)
# 处理请求结果
message = await self._make_msg(resp)
return message
async def call(
async def invoke_llm(
self,
query: core_entities.Query,
model: entities.LLMModelInfo,
messages: typing.List[llm_entities.Message],
funcs: typing.List[tools_entities.LLMFunction] = None,
extra_args: dict[str, typing.Any] = {},
) -> llm_entities.Message:
req_messages = [] # req_messages 仅用于类内,外部同步由 query.messages 进行
for m in messages:
@@ -182,7 +182,9 @@ class ModelScopeChatCompletions(requester.LLMAPIRequester):
req_messages.append(msg_dict)
try:
return await self._closure(query=query, req_messages=req_messages, use_model=model, use_funcs=funcs)
return await self._closure(
query=query, req_messages=req_messages, use_model=model, use_funcs=funcs, extra_args=extra_args
)
except asyncio.TimeoutError:
raise errors.RequesterError('请求超时')
except openai.BadRequestError as e:

View File

@@ -7,7 +7,7 @@ metadata:
zh_CN: 魔搭社区
spec:
config:
- name: base-url
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL

View File

@@ -28,7 +28,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
) -> llm_entities.Message:
self.client.api_key = use_model.token_mgr.get_token()
args = extra_args.copy()
args = {}
args['model'] = use_model.model_entity.name
if use_funcs:
@@ -51,7 +51,7 @@ class MoonshotChatCompletions(chatcmpl.OpenAIChatCompletions):
args['messages'] = messages
# 发送请求
resp = await self._req(args, extra_body=self.requester_cfg['args'])
resp = await self._req(args, extra_body=extra_args)
# 处理请求结果
message = await self._make_msg(resp)

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
import openai
import typing
from . import chatcmpl
from ....core import app
class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions):
@@ -11,9 +11,7 @@ class PPIOChatCompletions(chatcmpl.OpenAIChatCompletions):
client: openai.AsyncClient
requester_cfg: dict
def __init__(self, ap: app.Application):
self.ap = ap
self.requester_cfg = self.ap.provider_cfg.data['requester']['ppio-chat-completions']
default_config: dict[str, typing.Any] = {
'base_url': 'https://api.ppinfra.com/v3/openai',
'timeout': 120,
}

View File

@@ -7,7 +7,7 @@ metadata:
zh_CN: 派欧云
spec:
config:
- name: base-url
- name: base_url
label:
en_US: Base URL
zh_CN: 基础 URL

View File

@@ -1,4 +1,4 @@
semantic_version = 'v4.0.0'
semantic_version = 'v4.0.1'
required_database_version = 1
"""标记本版本所需要的数据库结构版本,用于判断数据库迁移"""

View File

@@ -32,6 +32,7 @@ def import_dir(path: str):
rel_path = full_path.replace(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), '')
rel_path = rel_path[1:]
rel_path = rel_path.replace('/', '.')[:-3]
rel_path = rel_path.replace('\\', '.')
importlib.import_module(rel_path)

103
web/package-lock.json generated
View File

@@ -8,9 +8,12 @@
"name": "web",
"version": "0.1.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",
@@ -46,7 +49,6 @@
"eslint-config-next": "15.2.4",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"prettier": "^3.5.3",
"tw-animate-css": "^1.2.9",
@@ -66,6 +68,59 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/core": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.3.1.tgz",
@@ -1115,6 +1170,37 @@
}
}
},
"node_modules/@radix-ui/react-hover-card": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.13.tgz",
"integrity": "sha512-Wtjvx0d/6Bgd/jAYS1mW6IPSUQ25y0hkUSOS1z5/4+U8+DJPwKroqJlM/AlVFl3LywGoruiPmcvB9Aks9mSOQw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.9",
"@radix-ui/react-popper": "1.2.6",
"@radix-ui/react-portal": "1.1.8",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -4298,21 +4384,6 @@
"node": ">=16.17.0"
}
},
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",

View File

@@ -16,9 +16,12 @@
]
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@hookform/resolvers": "^5.0.1",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-hover-card": "^1.1.13",
"@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-slot": "^1.2.2",

View File

@@ -18,6 +18,11 @@ import { useEffect, useState } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { LLMModel } from '@/app/infra/entities/api';
import { toast } from 'sonner';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@/components/ui/hover-card';
export default function DynamicFormItemComponent({
config,
@@ -133,9 +138,106 @@ export default function DynamicFormItemComponent({
<SelectContent>
<SelectGroup>
{llmModels.map((model) => (
<SelectItem key={model.uuid} value={model.uuid}>
{model.name}
</SelectItem>
<HoverCard key={model.uuid} openDelay={0} closeDelay={0}>
<HoverCardTrigger asChild>
<SelectItem value={model.uuid}>{model.name}</SelectItem>
</HoverCardTrigger>
<HoverCardContent
className="w-80 data-[state=open]:animate-none data-[state=closed]:animate-none"
align="end"
side="right"
sideOffset={10}
>
<div className="space-y-2">
<div className="flex items-center gap-2">
<img
src={httpClient.getProviderRequesterIconURL(
model.requester,
)}
alt="icon"
className="w-8 h-8 rounded-full"
/>
<h4 className="font-medium">{model.name}</h4>
</div>
<p className="text-sm text-muted-foreground">
{model.description}
</p>
{model.requester_config && (
<div className="flex items-center gap-1 text-xs">
<svg
className="w-4 h-4 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M13.0607 8.11097L14.4749 9.52518C17.2086 12.2589 17.2086 16.691 14.4749 19.4247L14.1214 19.7782C11.3877 22.5119 6.95555 22.5119 4.22188 19.7782C1.48821 17.0446 1.48821 12.6124 4.22188 9.87874L5.6361 11.293C3.68348 13.2456 3.68348 16.4114 5.6361 18.364C7.58872 20.3166 10.7545 20.3166 12.7072 18.364L13.0607 18.0105C15.0133 16.0578 15.0133 12.892 13.0607 10.9394L11.6465 9.52518L13.0607 8.11097ZM19.7782 14.1214L18.364 12.7072C20.3166 10.7545 20.3166 7.58872 18.364 5.6361C16.4114 3.68348 13.2456 3.68348 11.293 5.6361L10.9394 5.98965C8.98678 7.94227 8.98678 11.1081 10.9394 13.0607L12.3536 14.4749L10.9394 15.8891L9.52518 14.4749C6.79151 11.7413 6.79151 7.30911 9.52518 4.57544L9.87874 4.22188C12.6124 1.48821 17.0446 1.48821 19.7782 4.22188C22.5119 6.95555 22.5119 11.3877 19.7782 14.1214Z"></path>
</svg>
<span className="font-semibold">Base URL</span>
{model.requester_config.base_url}
</div>
)}
{model.abilities && model.abilities.length > 0 && (
<div className="flex flex-wrap gap-1">
{model.abilities.map((ability) => (
<div
key={ability}
className="flex items-center gap-1 px-2 py-1 text-xs rounded-full bg-blue-100 text-blue-600"
>
{ability === 'vision' && (
<svg
className="w-3 h-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2ZM12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4ZM12 7C14.7614 7 17 9.23858 17 12C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12C7 11.4872 7.07719 10.9925 7.22057 10.5268C7.61175 11.3954 8.48527 12 9.5 12C10.8807 12 12 10.8807 12 9.5C12 8.48527 11.3954 7.61175 10.5269 7.21995C10.9925 7.07719 11.4872 7 12 7Z"></path>
</svg>
)}
{ability === 'func_call' && (
<svg
className="w-3 h-3"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M5.32943 3.27158C6.56252 2.8332 7.9923 3.10749 8.97927 4.09446C10.1002 5.21537 10.3019 6.90741 9.5843 8.23385L20.293 18.9437L18.8788 20.3579L8.16982 9.64875C6.84325 10.3669 5.15069 10.1654 4.02952 9.04421C3.04227 8.05696 2.7681 6.62665 3.20701 5.39332L5.44373 7.63C6.02952 8.21578 6.97927 8.21578 7.56505 7.63C8.15084 7.04421 8.15084 6.09446 7.56505 5.50868L5.32943 3.27158ZM15.6968 5.15512L18.8788 3.38736L20.293 4.80157L18.5252 7.98355L16.7574 8.3371L14.6361 10.4584L13.2219 9.04421L15.3432 6.92289L15.6968 5.15512ZM8.97927 13.2868L10.3935 14.7011L5.09018 20.0044C4.69966 20.3949 4.06649 20.3949 3.67597 20.0044C3.31334 19.6417 3.28744 19.0699 3.59826 18.6774L3.67597 18.5902L8.97927 13.2868Z"></path>
</svg>
)}
<span>
{ability === 'vision'
? '视觉能力'
: ability === 'func_call'
? '函数调用'
: ability}
</span>
</div>
))}
</div>
)}
{model.extra_args &&
Object.keys(model.extra_args).length > 0 && (
<div className="text-xs">
<div className="font-semibold mb-1"></div>
<div className="space-y-1">
{Object.entries(
model.extra_args as Record<string, unknown>,
).map(([key, value]) => (
<div
key={key}
className="flex items-center gap-1"
>
<span className="text-gray-500">{key}</span>
<span className="break-all">
{JSON.stringify(value)}
</span>
</div>
))}
</div>
</div>
)}
</div>
</HoverCardContent>
</HoverCard>
))}
</SelectGroup>
</SelectContent>

View File

@@ -8,6 +8,7 @@
align-items: flex-start;
justify-content: space-between;
padding-block: 1rem;
padding-left: 0.4rem;
user-select: none;
/* box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); */
}
@@ -54,13 +55,18 @@
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
gap: 0.8rem;
}
.sidebarItemsContainer {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.sidebarChildContainer {
width: 9rem;
height: 3rem;
margin: 0.8rem 0;
padding-left: 1.6rem;
font-size: 1rem;
border-radius: 12px;
@@ -70,6 +76,7 @@
justify-content: flex-start;
cursor: pointer;
gap: 0.5rem;
/* background-color: aqua; */
}
.sidebarSelected {
@@ -90,11 +97,12 @@
.sidebarBottomContainer {
width: 100%;
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: auto;
padding-bottom: 1rem;
}
.sidebarBottomChildContainer {

View File

@@ -30,6 +30,10 @@ export default function HomeSidebar({
useEffect(() => {
console.log('HomeSidebar挂载完成');
initSelect();
if (!localStorage.getItem('token')) {
localStorage.setItem('token', 'test-token');
localStorage.setItem('userEmail', 'test@example.com');
}
return () => console.log('HomeSidebar卸载');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -77,6 +81,12 @@ export default function HomeSidebar({
}
}
function handleLogout() {
localStorage.removeItem('token');
localStorage.removeItem('userEmail');
window.location.href = '/login';
}
return (
<div className={`${styles.sidebarContainer}`}>
<div className={`${styles.sidebarTopContainer}`}>
@@ -97,7 +107,7 @@ export default function HomeSidebar({
</div>
</div>
{/* 菜单列表,后期可升级成配置驱动 */}
<div>
<div className={styles.sidebarItemsContainer}>
{sidebarConfigList.map((config) => {
return (
<div
@@ -123,20 +133,6 @@ export default function HomeSidebar({
</div>
<div className={`${styles.sidebarBottomContainer}`}>
{/* <SidebarChild
onClick={() => {}}
isSelected={false}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M2 11.9998C2 11.1353 2.1097 10.2964 2.31595 9.49631C3.40622 9.55283 4.48848 9.01015 5.0718 7.99982C5.65467 6.99025 5.58406 5.78271 4.99121 4.86701C6.18354 3.69529 7.66832 2.82022 9.32603 2.36133C9.8222 3.33385 10.8333 3.99982 12 3.99982C13.1667 3.99982 14.1778 3.33385 14.674 2.36133C16.3317 2.82022 17.8165 3.69529 19.0088 4.86701C18.4159 5.78271 18.3453 6.99025 18.9282 7.99982C19.5115 9.01015 20.5938 9.55283 21.6841 9.49631C21.8903 10.2964 22 11.1353 22 11.9998C22 12.8643 21.8903 13.7032 21.6841 14.5033C20.5938 14.4468 19.5115 14.9895 18.9282 15.9998C18.3453 17.0094 18.4159 18.2169 19.0088 19.1326C17.8165 20.3043 16.3317 21.1794 14.674 21.6383C14.1778 20.6658 13.1667 19.9998 12 19.9998C10.8333 19.9998 9.8222 20.6658 9.32603 21.6383C7.66832 21.1794 6.18354 20.3043 4.99121 19.1326C5.58406 18.2169 5.65467 17.0094 5.0718 15.9998C4.48848 14.9895 3.40622 14.4468 2.31595 14.5033C2.1097 13.7032 2 12.8643 2 11.9998ZM6.80385 14.9998C7.43395 16.0912 7.61458 17.3459 7.36818 18.5236C7.77597 18.8138 8.21005 19.0652 8.66489 19.2741C9.56176 18.4712 10.7392 17.9998 12 17.9998C13.2608 17.9998 14.4382 18.4712 15.3351 19.2741C15.7899 19.0652 16.224 18.8138 16.6318 18.5236C16.3854 17.3459 16.566 16.0912 17.1962 14.9998C17.8262 13.9085 18.8225 13.1248 19.9655 12.7493C19.9884 12.5015 20 12.2516 20 11.9998C20 11.7481 19.9884 11.4981 19.9655 11.2504C18.8225 10.8749 17.8262 10.0912 17.1962 8.99982C16.566 7.90845 16.3854 6.65378 16.6318 5.47605C16.224 5.18588 15.7899 4.93447 15.3351 4.72552C14.4382 5.52844 13.2608 5.99982 12 5.99982C10.7392 5.99982 9.56176 5.52844 8.66489 4.72552C8.21005 4.93447 7.77597 5.18588 7.36818 5.47605C7.61458 6.65378 7.43395 7.90845 6.80385 8.99982C6.17376 10.0912 5.17754 10.8749 4.03451 11.2504C4.01157 11.4981 4 11.7481 4 11.9998C4 12.2516 4.01157 12.5015 4.03451 12.7493C5.17754 13.1248 6.17376 13.9085 6.80385 14.9998ZM12 14.9998C10.3431 14.9998 9 13.6567 9 11.9998C9 10.343 10.3431 8.99982 12 8.99982C13.6569 8.99982 15 10.343 15 11.9998C15 13.6567 13.6569 14.9998 12 14.9998ZM12 12.9998C12.5523 12.9998 13 12.5521 13 11.9998C13 11.4475 12.5523 10.9998 12 10.9998C11.4477 10.9998 11 11.4475 11 11.9998C11 12.5521 11.4477 12.9998 12 12.9998Z"></path>
</svg>
}
name="系统设置"
/> */}
<SidebarChild
onClick={() => {
// open docs.langbot.app
@@ -154,6 +150,22 @@ export default function HomeSidebar({
}
name="帮助文档"
/>
<SidebarChild
onClick={() => {
handleLogout();
}}
isSelected={false}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M4 18H6V20H18V4H6V6H4V3C4 2.44772 4.44772 2 5 2H19C19.5523 2 20 2.44772 20 3V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V18ZM6 11H13V13H6V16L1 12L6 8V11Z"></path>
</svg>
}
name="退出登录"
/>
</div>
</div>
);

View File

@@ -3,6 +3,7 @@ import PluginInstalledComponent, {
PluginInstalledComponentRef,
} from '@/app/home/plugins/plugin-installed/PluginInstalledComponent';
import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent';
import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog';
import styles from './plugins.module.css';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Button } from '@/components/ui/button';
@@ -19,6 +20,7 @@ import { GithubIcon } from 'lucide-react';
import { useState, useRef } from 'react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { toast } from 'sonner';
enum PluginInstallStatus {
WAIT_INPUT = 'wait_input',
INSTALLING = 'installing',
@@ -27,6 +29,7 @@ enum PluginInstallStatus {
export default function PluginConfigPage() {
const [modalOpen, setModalOpen] = useState(false);
const [sortModalOpen, setSortModalOpen] = useState(false);
const [pluginInstallStatus, setPluginInstallStatus] =
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
const [installError, setInstallError] = useState<string | null>(null);
@@ -90,6 +93,15 @@ export default function PluginConfigPage() {
</TabsList>
<div className="flex flex-row justify-end items-center">
<Button
variant="outline"
className="px-6 py-4 cursor-pointer mr-2"
onClick={() => {
setSortModalOpen(true);
}}
>
</Button>
<Button
variant="default"
className="px-6 py-4 cursor-pointer"
@@ -166,6 +178,14 @@ export default function PluginConfigPage() {
</DialogFooter>
</DialogContent>
</Dialog>
<PluginSortDialog
open={sortModalOpen}
onOpenChange={setSortModalOpen}
onSortComplete={() => {
pluginInstalledRef.current?.refreshPluginList();
}}
/>
</div>
);
}

View File

@@ -0,0 +1,206 @@
'use client';
import * as React from 'react';
import { useState, useEffect } from 'react';
import { PluginCardVO } from '@/app/home/plugins/plugin-installed/PluginCardVO';
import { httpClient } from '@/app/infra/http/HttpClient';
import { PluginReorderElement } from '@/app/infra/entities/api';
import { toast } from 'sonner';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
interface PluginSortDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onSortComplete: () => void;
}
function SortablePluginItem({ plugin }: { plugin: PluginCardVO }) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({
id: `${plugin.author}-${plugin.name}`,
});
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="bg-white dark:bg-gray-800 p-4 rounded-md shadow-sm border mb-2 cursor-move"
>
<div className="flex flex-col">
<div className="text-sm text-gray-600 dark:text-gray-400">
{plugin.author}
</div>
<div className="text-lg font-medium">{plugin.name}</div>
<div className="text-sm line-clamp-2 text-gray-500 dark:text-gray-400 mt-1">
{plugin.description}
</div>
</div>
</div>
);
}
export default function PluginSortDialog({
open,
onOpenChange,
onSortComplete,
}: PluginSortDialogProps) {
const [sortedPlugins, setSortedPlugins] = useState<PluginCardVO[]>([]);
const [isLoading, setIsLoading] = useState(false);
function getPluginList() {
httpClient.getPlugins().then((value) => {
setSortedPlugins(
value.plugins.map((plugin) => {
return new PluginCardVO({
author: plugin.author,
description: plugin.description.zh_CN,
enabled: plugin.enabled,
name: plugin.name,
version: plugin.version,
status: plugin.status,
tools: plugin.tools,
event_handlers: plugin.event_handlers,
repository: plugin.repository,
priority: plugin.priority,
});
}),
);
});
}
useEffect(() => {
if (open) {
getPluginList();
}
}, [open]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
console.log('Drag end event:', { active, over });
if (over && active.id !== over.id) {
setSortedPlugins((items) => {
const oldIndex = items.findIndex(
(item) => `${item.author}-${item.name}` === active.id,
);
const newIndex = items.findIndex(
(item) => `${item.author}-${item.name}` === over.id,
);
const newItems = arrayMove(items, oldIndex, newIndex);
return newItems;
});
}
}
function handleSave() {
setIsLoading(true);
const reorderElements: PluginReorderElement[] = sortedPlugins.map(
(plugin, index) => ({
author: plugin.author,
name: plugin.name,
priority: index,
}),
);
httpClient
.reorderPlugins(reorderElements)
.then(() => {
toast.success('插件排序成功');
onSortComplete();
onOpenChange(false);
})
.catch((err) => {
toast.error('排序失败:' + err.message);
})
.finally(() => {
setIsLoading(false);
});
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="w-[700px] max-h-[80vh] p-0 flex flex-col">
<DialogHeader className="px-6 pt-6 pb-0">
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 py-0">
<p className="text-sm text-gray-500 mb-4">
</p>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={sortedPlugins.map(
(plugin) => `${plugin.author}-${plugin.name}`,
)}
strategy={verticalListSortingStrategy}
>
{sortedPlugins.map((plugin) => (
<SortablePluginItem
key={`${plugin.author}-${plugin.name}`}
plugin={plugin}
/>
))}
</SortableContext>
</DndContext>
</div>
<DialogFooter className="px-6 py-4">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
disabled={isLoading}
>
</Button>
<Button onClick={handleSave} disabled={isLoading}>
{isLoading ? '保存中...' : '保存'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -375,7 +375,7 @@ class HttpClient {
}
public reorderPlugins(plugins: PluginReorderElement[]): Promise<object> {
return this.post('/api/v1/plugins/reorder', plugins);
return this.put('/api/v1/plugins/reorder', { plugins });
}
public updatePlugin(

View File

@@ -82,6 +82,7 @@ export default function Login() {
.authUser(username, password)
.then((res) => {
localStorage.setItem('token', res.token);
localStorage.setItem('userEmail', username);
console.log('login success: ', res);
router.push('/home');
toast.success('登录成功');

View File

@@ -0,0 +1,44 @@
'use client';
import * as React from 'react';
import * as HoverCardPrimitive from '@radix-ui/react-hover-card';
import { cn } from '@/lib/utils';
function HoverCard({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
}
function HoverCardTrigger({
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
return (
<HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
);
}
function HoverCardContent({
className,
align = 'center',
sideOffset = 4,
...props
}: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
return (
<HoverCardPrimitive.Portal data-slot="hover-card-portal">
<HoverCardPrimitive.Content
data-slot="hover-card-content"
align={align}
sideOffset={sideOffset}
className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
className,
)}
{...props}
/>
</HoverCardPrimitive.Portal>
);
}
export { HoverCard, HoverCardTrigger, HoverCardContent };