mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 12:05:54 +00:00
Compare commits
2 Commits
v4.8.4
...
feat/napca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46a993b9a3 | ||
|
|
1eda076b93 |
@@ -18,6 +18,7 @@ import langbot_plugin.api.entities.builtin.provider.session as provider_session
|
||||
import langbot_plugin.api.entities.builtin.platform.events as platform_events
|
||||
import langbot_plugin.api.entities.builtin.platform.message as platform_message
|
||||
import langbot_plugin.api.definition.abstract.platform.adapter as abstract_platform_adapter
|
||||
import langbot_plugin.api.entities.events as events
|
||||
|
||||
|
||||
class RuntimeBot:
|
||||
@@ -141,6 +142,56 @@ class RuntimeBot:
|
||||
self.adapter.register_listener(platform_events.FriendMessage, on_friend_message)
|
||||
self.adapter.register_listener(platform_events.GroupMessage, on_group_message)
|
||||
|
||||
async def on_notice(
|
||||
event: platform_events.NoticeEvent,
|
||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
|
||||
):
|
||||
await self.logger.info(f'Notice event: {event.notice_type} {event.sub_type}')
|
||||
|
||||
try:
|
||||
event_obj = events.NoticeReceived(
|
||||
notice_type=event.notice_type,
|
||||
sub_type=event.sub_type,
|
||||
group_id=event.group_id,
|
||||
user_id=event.user_id,
|
||||
operator_id=event.operator_id,
|
||||
target_id=event.target_id,
|
||||
message_id=event.message_id,
|
||||
duration=event.duration,
|
||||
file=event.file,
|
||||
honor_type=event.honor_type,
|
||||
)
|
||||
|
||||
if hasattr(self.ap, 'plugin_connector') and self.ap.plugin_connector:
|
||||
await self.ap.plugin_connector.emit_event(event_obj)
|
||||
except Exception:
|
||||
await self.logger.error(f'Error emitting notice event: {traceback.format_exc()}')
|
||||
|
||||
self.adapter.register_listener(platform_events.NoticeEvent, on_notice)
|
||||
|
||||
async def on_request(
|
||||
event: platform_events.RequestEvent,
|
||||
adapter: abstract_platform_adapter.AbstractMessagePlatformAdapter,
|
||||
):
|
||||
await self.logger.info(f'Request event: {event.request_type} {event.sub_type}')
|
||||
|
||||
try:
|
||||
event_obj = events.RequestReceived(
|
||||
request_type=event.request_type,
|
||||
sub_type=event.sub_type,
|
||||
user_id=event.user_id,
|
||||
group_id=event.group_id,
|
||||
comment=event.comment,
|
||||
flag=event.flag,
|
||||
)
|
||||
|
||||
if hasattr(self.ap, 'plugin_connector') and self.ap.plugin_connector:
|
||||
await self.ap.plugin_connector.emit_event(event_obj)
|
||||
except Exception:
|
||||
await self.logger.error(f'Error emitting request event: {traceback.format_exc()}')
|
||||
|
||||
self.adapter.register_listener(platform_events.RequestEvent, on_request)
|
||||
|
||||
async def run(self):
|
||||
async def exception_wrapper():
|
||||
try:
|
||||
|
||||
@@ -306,9 +306,8 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
|
||||
@staticmethod
|
||||
async def target2yiri(event: aiocqhttp.Event, bot=None):
|
||||
yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id, bot)
|
||||
|
||||
if event.message_type == 'group':
|
||||
yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id, bot)
|
||||
permission = 'MEMBER'
|
||||
|
||||
if 'role' in event.sender:
|
||||
@@ -334,6 +333,7 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
)
|
||||
return converted_event
|
||||
elif event.message_type == 'private':
|
||||
yiri_chain = await AiocqhttpMessageConverter.target2yiri(event.message, event.message_id, bot)
|
||||
return platform_events.FriendMessage(
|
||||
sender=platform_entities.Friend(
|
||||
id=event.sender['user_id'],
|
||||
@@ -344,6 +344,57 @@ class AiocqhttpEventConverter(abstract_platform_adapter.AbstractEventConverter):
|
||||
time=event.time,
|
||||
source_platform_object=event,
|
||||
)
|
||||
elif event.post_type == 'notice':
|
||||
yiri_chain = platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Source(id=-1, time=datetime.datetime.now()),
|
||||
platform_message.Notice(
|
||||
notice_type=event.get('notice_type', ''),
|
||||
sub_type=event.get('sub_type', ''),
|
||||
user_id=event.get('user_id', None),
|
||||
target_id=event.get('target_id', None),
|
||||
group_id=event.get('group_id', None),
|
||||
operator_id=event.get('operator_id', None),
|
||||
message_id=event.get('message_id', None),
|
||||
duration=event.get('duration', None),
|
||||
file=event.get('file', None),
|
||||
honor_type=event.get('honor_type', None),
|
||||
),
|
||||
]
|
||||
)
|
||||
return platform_events.NoticeEvent(
|
||||
notice_type=event.get('notice_type', ''),
|
||||
sub_type=event.get('sub_type', ''),
|
||||
user_id=event.get('user_id', None),
|
||||
target_id=event.get('target_id', None),
|
||||
group_id=event.get('group_id', None),
|
||||
time=event.time,
|
||||
source_platform_object=event,
|
||||
)
|
||||
elif event.post_type == 'request':
|
||||
yiri_chain = platform_message.MessageChain(
|
||||
[
|
||||
platform_message.Source(id=-1, time=datetime.datetime.now()),
|
||||
platform_message.Request(
|
||||
request_type=event.get('request_type', ''),
|
||||
sub_type=event.get('sub_type', ''),
|
||||
user_id=event.get('user_id', None),
|
||||
group_id=event.get('group_id', None),
|
||||
comment=event.get('comment', ''),
|
||||
flag=event.get('flag', ''),
|
||||
),
|
||||
]
|
||||
)
|
||||
return platform_events.RequestEvent(
|
||||
request_type=event.get('request_type', ''),
|
||||
sub_type=event.get('sub_type', ''),
|
||||
user_id=event.get('user_id', None),
|
||||
group_id=event.get('group_id', None),
|
||||
comment=event.get('comment', ''),
|
||||
flag=event.get('flag', ''),
|
||||
time=event.time,
|
||||
source_platform_object=event,
|
||||
)
|
||||
|
||||
|
||||
class AiocqhttpAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter):
|
||||
@@ -413,12 +464,31 @@ class AiocqhttpAdapter(abstract_platform_adapter.AbstractMessagePlatformAdapter)
|
||||
await self.logger.error(f'Error in on_message: {traceback.format_exc()}')
|
||||
traceback.print_exc()
|
||||
|
||||
async def on_notice(event: aiocqhttp.Event):
|
||||
self.bot_account_id = event.self_id
|
||||
try:
|
||||
return await callback(await self.event_converter.target2yiri(event, self.bot), self)
|
||||
except Exception:
|
||||
await self.logger.error(f'Error in on_notice: {traceback.format_exc()}')
|
||||
traceback.print_exc()
|
||||
|
||||
async def on_request(event: aiocqhttp.Event):
|
||||
self.bot_account_id = event.self_id
|
||||
try:
|
||||
return await callback(await self.event_converter.target2yiri(event, self.bot), self)
|
||||
except Exception:
|
||||
await self.logger.error(f'Error in on_request: {traceback.format_exc()}')
|
||||
traceback.print_exc()
|
||||
|
||||
if event_type == platform_events.GroupMessage:
|
||||
self.bot.on_message('group')(on_message)
|
||||
# self.bot.on_notice()(on_message)
|
||||
elif event_type == platform_events.FriendMessage:
|
||||
self.bot.on_message('private')(on_message)
|
||||
# self.bot.on_notice()(on_message)
|
||||
elif event_type == platform_events.NoticeEvent:
|
||||
self.bot.on_notice()(on_notice)
|
||||
elif event_type == platform_events.RequestEvent:
|
||||
self.bot.on_request()(on_request)
|
||||
# print(event_type)
|
||||
|
||||
async def on_websocket_connection(event: aiocqhttp.Event):
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useRef, Suspense } from 'react';
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useRef,
|
||||
Suspense,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
@@ -23,6 +30,8 @@ import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
||||
import { TagsFilter } from './TagsFilter';
|
||||
import { PluginTag } from '@/app/infra/http/CloudServiceClient';
|
||||
|
||||
import { RecommendationLists, RecommendationList } from './RecommendationLists';
|
||||
|
||||
interface SortOption {
|
||||
value: string;
|
||||
label: string;
|
||||
@@ -50,6 +59,9 @@ function MarketPageContent({
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [sortOption, setSortOption] = useState('install_count_desc');
|
||||
const [recommendationLists, setRecommendationLists] = useState<
|
||||
RecommendationList[]
|
||||
>([]);
|
||||
|
||||
const pageSize = 16; // 每页16个,4行x4列
|
||||
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -203,6 +215,20 @@ function MarketPageContent({
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch recommendation lists
|
||||
useEffect(() => {
|
||||
async function fetchRecommendationLists() {
|
||||
try {
|
||||
const response =
|
||||
await getCloudServiceClientSync().getRecommendationLists();
|
||||
setRecommendationLists(response.lists || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch recommendation lists:', error);
|
||||
}
|
||||
}
|
||||
fetchRecommendationLists();
|
||||
}, []);
|
||||
|
||||
// 搜索功能
|
||||
const handleSearch = useCallback(
|
||||
(query: string) => {
|
||||
@@ -306,6 +332,39 @@ function MarketPageContent({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 计算所有推荐插件的 ID 集合
|
||||
const recommendedPluginIds = useMemo(() => {
|
||||
const ids = new Set<string>();
|
||||
recommendationLists.forEach((list) => {
|
||||
list.plugins.forEach((plugin) => {
|
||||
ids.add(`${plugin.author} / ${plugin.name}`);
|
||||
});
|
||||
});
|
||||
return ids;
|
||||
}, [recommendationLists]);
|
||||
|
||||
// 过滤掉已在推荐列表中展示的插件
|
||||
// 仅在显示推荐列表的条件下(无搜索、无筛选、第一页或后续页的累积数据中)进行过滤
|
||||
// 注意:如果用户翻页,我们希望一直保持去重,否则推荐过的插件会在第二页出现
|
||||
// 但是推荐列表只在第一页且无筛选时显示。
|
||||
// 如果用户进行了筛选/搜索,推荐列表不显示,此时不需要去重。
|
||||
const visiblePlugins = useMemo(() => {
|
||||
const showRecommendations =
|
||||
!searchQuery && componentFilter === 'all' && selectedTags.length === 0;
|
||||
|
||||
if (!showRecommendations) {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
return plugins.filter((p) => !recommendedPluginIds.has(p.pluginId));
|
||||
}, [
|
||||
plugins,
|
||||
recommendedPluginIds,
|
||||
searchQuery,
|
||||
componentFilter,
|
||||
selectedTags,
|
||||
]);
|
||||
|
||||
// 加载更多
|
||||
const loadMore = useCallback(() => {
|
||||
if (!isLoadingMore && hasMore) {
|
||||
@@ -494,6 +553,20 @@ function MarketPageContent({
|
||||
ref={scrollContainerRef}
|
||||
className="flex-1 overflow-y-auto px-3 sm:px-4"
|
||||
>
|
||||
{/* Recommendation Lists */}
|
||||
{!searchQuery &&
|
||||
componentFilter === 'all' &&
|
||||
selectedTags.length === 0 &&
|
||||
currentPage === 1 && (
|
||||
<div className="pt-4">
|
||||
<RecommendationLists
|
||||
lists={recommendationLists}
|
||||
tagNames={tagNames}
|
||||
onInstall={handleInstallPlugin}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<LoadingSpinner text={t('market.loading')} />
|
||||
@@ -507,7 +580,7 @@ function MarketPageContent({
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6 pb-6 pt-4">
|
||||
{plugins.map((plugin) => (
|
||||
{visiblePlugins.map((plugin) => (
|
||||
<PluginMarketCardComponent
|
||||
key={plugin.pluginId}
|
||||
cardVO={plugin}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Star } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import PluginMarketCardComponent from './plugin-market-card/PluginMarketCardComponent';
|
||||
import { PluginMarketCardVO } from './plugin-market-card/PluginMarketCardVO';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { I18nObject } from '@/app/infra/entities/common';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { getCloudServiceClientSync } from '@/app/infra/http';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface RecommendationList {
|
||||
uuid: string;
|
||||
label: I18nObject;
|
||||
sort_order: number;
|
||||
plugins: PluginV4[];
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 4; // plugins per page in a recommendation row
|
||||
|
||||
function pluginToVO(
|
||||
plugin: PluginV4,
|
||||
t: (key: string) => string,
|
||||
): PluginMarketCardVO {
|
||||
return new PluginMarketCardVO({
|
||||
pluginId: plugin.author + ' / ' + plugin.name,
|
||||
author: plugin.author,
|
||||
pluginName: plugin.name,
|
||||
label: extractI18nObject(plugin.label),
|
||||
description:
|
||||
extractI18nObject(plugin.description) || t('market.noDescription'),
|
||||
installCount: plugin.install_count,
|
||||
iconURL: getCloudServiceClientSync().getPluginIconURL(
|
||||
plugin.author,
|
||||
plugin.name,
|
||||
),
|
||||
githubURL: plugin.repository,
|
||||
version: plugin.latest_version,
|
||||
components: plugin.components,
|
||||
tags: plugin.tags || [],
|
||||
});
|
||||
}
|
||||
|
||||
function RecommendationListRow({
|
||||
list,
|
||||
tagNames,
|
||||
onInstall,
|
||||
}: {
|
||||
list: RecommendationList;
|
||||
tagNames: Record<string, string>;
|
||||
onInstall: (author: string, pluginName: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const plugins = list.plugins || [];
|
||||
const totalPages = Math.ceil(plugins.length / PAGE_SIZE);
|
||||
const start = page * PAGE_SIZE;
|
||||
const visiblePlugins = plugins.slice(start, start + PAGE_SIZE);
|
||||
|
||||
if (plugins.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="w-4 h-4 text-yellow-500" />
|
||||
<h3 className="font-semibold text-base">
|
||||
{extractI18nObject(list.label)}
|
||||
</h3>
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<span className="text-xs text-muted-foreground px-1">
|
||||
{page + 1} / {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="h-7 w-7 p-0"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4 gap-6">
|
||||
{visiblePlugins.map((plugin) => (
|
||||
<PluginMarketCardComponent
|
||||
key={plugin.author + ' / ' + plugin.name}
|
||||
cardVO={pluginToVO(plugin, t)}
|
||||
tagNames={tagNames}
|
||||
onInstall={onInstall}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{totalPages > 1 && <div className="border-b border-border mt-6" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RecommendationLists({
|
||||
lists,
|
||||
tagNames,
|
||||
onInstall,
|
||||
}: {
|
||||
lists: RecommendationList[];
|
||||
tagNames: Record<string, string>;
|
||||
onInstall: (author: string, pluginName: string) => void;
|
||||
}) {
|
||||
if (!lists || lists.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-6">
|
||||
{lists.map((list) => (
|
||||
<RecommendationListRow
|
||||
key={list.uuid}
|
||||
list={list}
|
||||
tagNames={tagNames}
|
||||
onInstall={onInstall}
|
||||
/>
|
||||
))}
|
||||
<div className="border-b border-border mb-6" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import {
|
||||
ApiRespMarketplacePluginDetail,
|
||||
ApiRespMarketplacePlugins,
|
||||
} from '@/app/infra/entities/api';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { I18nObject } from '@/app/infra/entities/common';
|
||||
|
||||
/**
|
||||
* 云服务客户端
|
||||
@@ -98,6 +100,19 @@ export class CloudServiceClient extends BaseHttpClient {
|
||||
public getAllTags(): Promise<{ tags: PluginTag[] }> {
|
||||
return this.get<{ tags: PluginTag[] }>('/api/v1/marketplace/tags');
|
||||
}
|
||||
|
||||
public getRecommendationLists(): Promise<{ lists: RecommendationList[] }> {
|
||||
return this.get<{ lists: RecommendationList[] }>(
|
||||
'/api/v1/marketplace/recommendation-lists',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RecommendationList {
|
||||
uuid: string;
|
||||
label: I18nObject;
|
||||
sort_order: number;
|
||||
plugins: PluginV4[];
|
||||
}
|
||||
|
||||
export interface PluginTag {
|
||||
|
||||
Reference in New Issue
Block a user