chore: sync current workspace changes

This commit is contained in:
boos
2026-07-14 03:21:10 +08:00
parent 1512fa23ce
commit 557914fea8
745 changed files with 124917 additions and 11222 deletions
+47 -10
View File
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { App, Button, Card, Input, Radio, Space, Tag, Typography, theme } from 'antd';
import { App, Button, Card, Input, Radio, Tag, Typography, theme } from 'antd';
import { BookOpen, FileText, Presentation, Sparkles, Wrench } from 'lucide-react';
import { ModelSelect, parseModelValue } from '@/components/shared/ModelSelect';
import { invoke } from '@/lib/invoke';
import {
findSkillByPreset,
@@ -39,6 +40,7 @@ export function DocumentsPage() {
const setActivePage = useUIStore((s) => s.setActivePage);
const [selectedPresetKey, setSelectedPresetKey] = useState('gorden-ppt');
const [prompt, setPrompt] = useState('');
const [selectedModelValue, setSelectedModelValue] = useState<string | undefined>();
const [submitting, setSubmitting] = useState(false);
const [installingKey, setInstallingKey] = useState<string | null>(null);
@@ -67,6 +69,33 @@ export function DocumentsPage() {
return null;
}, [providers]);
const selectedChatModel = useMemo(() => {
const parsed = parseModelValue(selectedModelValue);
if (!parsed) return null;
const provider = providers.find((item) => item.enabled && item.id === parsed.providerId);
const model = provider?.models.find((item) => item.model_id === parsed.modelId && isUsableChatModel(item));
if (!provider || !model) return null;
return {
providerId: provider.id,
modelId: model.model_id,
providerName: provider.name,
modelName: model.name || model.model_id,
};
}, [providers, selectedModelValue]);
const defaultChatModelValue = defaultChatModel
? `${defaultChatModel.providerId}::${defaultChatModel.modelId}`
: undefined;
useEffect(() => {
if (!defaultChatModelValue) return;
if (!selectedModelValue || !selectedChatModel) {
setSelectedModelValue(defaultChatModelValue);
}
}, [defaultChatModelValue, selectedChatModel, selectedModelValue]);
const activeChatModel = selectedChatModel ?? defaultChatModel;
const ensureSkillReady = useCallback(async (preset: SkillPreset) => {
const existing = findSkillByPreset(useSkillStore.getState().skills, preset);
if (!existing) {
@@ -91,7 +120,7 @@ export function DocumentsPage() {
return;
}
if (!selectedPreset || submitting) return;
if (!defaultChatModel) {
if (!activeChatModel) {
message.warning('先在服务商管理里启用一个可对话模型');
return;
}
@@ -101,13 +130,13 @@ export function DocumentsPage() {
await ensureSkillReady(selectedPreset);
const conversation = await createConversation(
`${selectedPreset.title} · ${text.slice(0, 18)}`,
defaultChatModel.modelId,
defaultChatModel.providerId,
activeChatModel.modelId,
activeChatModel.providerId,
);
await updateConversation(conversation.id, {
mode: 'agent',
provider_id: defaultChatModel.providerId,
model_id: defaultChatModel.modelId,
provider_id: activeChatModel.providerId,
model_id: activeChatModel.modelId,
});
await invoke('agent_update_session', {
conversationId: conversation.id,
@@ -124,7 +153,7 @@ export function DocumentsPage() {
}
}, [
createConversation,
defaultChatModel,
activeChatModel,
ensureSkillReady,
message,
prompt,
@@ -180,10 +209,18 @@ export function DocumentsPage() {
placeholder="例如:根据 BossAI 的产品介绍生成一份 12 页融资路演 PPT,包含市场、产品、技术架构、商业模式和路线图。"
/>
<div className="documents-compose-footer">
<Space size={8} wrap>
<div className="documents-compose-meta">
<Tag>{selectedPreset?.name}</Tag>
<Tag>{defaultChatModel ? `${defaultChatModel.providerName} · ${defaultChatModel.modelName}` : '未选择对话模型'}</Tag>
</Space>
<span className="documents-model-label">对话模型</span>
<ModelSelect
value={selectedModelValue}
onChange={setSelectedModelValue}
placeholder="选择对话模型"
allowClear={false}
filterModel={isUsableChatModel}
style={{ minWidth: 280, maxWidth: 460, flex: '1 1 280px' }}
/>
</div>
<Button
type="primary"
icon={<Wrench size={16} />}
+149 -8
View File
@@ -13,6 +13,12 @@ import { useTranslation } from 'react-i18next';
import { useSkillStore } from '@/stores';
import type { Skill, MarketplaceSkill } from '@/types';
import { CopyButton } from '@/components/common/CopyButton';
import {
findSkillByPreset,
getSkillPresetStatus,
SKILL_PRESETS,
type SkillPreset,
} from '@/lib/skillPresets';
const INSTALL_TARGETS = [
{ key: 'boosapi', label: '~/.boosapi/skills/', desc: 'BoosAPI Desktop', icon: <Sparkles size={14} /> },
@@ -265,6 +271,90 @@ function MarketplaceCard({
);
}
function PresetSkillCard({
preset,
status,
installing,
onInstall,
onOpenGithub,
}: {
preset: SkillPreset;
status: 'enabled' | 'installed' | 'missing';
installing: string | null;
onInstall: (preset: SkillPreset, target: string) => void;
onOpenGithub: (source: string) => void;
}) {
const categoryLabel = preset.category === 'document'
? '文档'
: preset.category === 'cad'
? 'CAD'
: preset.category === 'media'
? '媒体'
: '视频';
const installed = status !== 'missing';
return (
<Card
size="small"
className="skill-card-hover"
style={{ marginBottom: 8 }}
styles={{ body: { padding: '12px 16px' } }}
>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<Text strong className="skill-card-title">{preset.title}</Text>
<Tag>{categoryLabel}</Tag>
{status === 'enabled' && <Tag color="green">已启用</Tag>}
{status === 'installed' && <Tag color="blue">已安装</Tag>}
</div>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 2 }}>
{preset.description}
</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{preset.source}</Text>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Button
size="small"
type="text"
icon={<Github size={14} />}
onClick={() => onOpenGithub(preset.source)}
>
GitHub
</Button>
{installed && status === 'enabled' ? (
<Button size="small" disabled>
已可用
</Button>
) : (
<Dropdown
menu={{
items: INSTALL_TARGETS.map((target) => ({
key: target.key,
icon: target.icon,
label: `${target.desc} (${target.label})`,
})),
onClick: ({ key }) => onInstall(preset, key),
}}
trigger={['click']}
disabled={installing === preset.key}
>
<Button
size="small"
type={status === 'installed' ? 'default' : 'primary'}
loading={installing === preset.key}
icon={<Download size={14} />}
>
{status === 'installed' ? '启用' : '安装'}
</Button>
</Dropdown>
)}
</div>
</div>
</Card>
);
}
export function SkillsPage() {
const { t } = useTranslation();
const { token } = theme.useToken();
@@ -331,6 +421,29 @@ export function SkillsPage() {
}
}, [installSkill, messageApi, t]);
const handleInstallPreset = useCallback(async (preset: SkillPreset, target: string) => {
if (installing) return;
setInstalling(preset.key);
try {
const existingSkill = findSkillByPreset(skills, preset);
if (existingSkill && !existingSkill.enabled) {
await toggleSkill(existingSkill.name, true);
messageApi.success(`${preset.title} Skill 已启用`);
} else if (!existingSkill) {
const name = await installSkill(preset.source, target);
messageApi.success(t('skills.installSuccess', { name }));
}
} catch (e) {
messageApi.error(String(e));
} finally {
setInstalling(null);
}
}, [installSkill, installing, messageApi, skills, t, toggleSkill]);
const handleOpenPresetGithub = useCallback((source: string) => {
openExternalUrl(`https://github.com/${source}`);
}, []);
const handleToggle = useCallback((name: string, enabled: boolean) => {
toggleSkill(name, enabled);
}, [toggleSkill]);
@@ -394,6 +507,13 @@ export function SkillsPage() {
return skills.filter(s => s.source === sourceFilter);
}, [skills, sourceFilter]);
const recommendedSkillPresets = useMemo(() => (
SKILL_PRESETS.map((preset) => ({
preset,
status: getSkillPresetStatus(skills, preset),
}))
), [skills]);
const groupedSkills = useMemo(() => {
const groups = new Map<string, Skill[]>();
const ungrouped: Skill[] = [];
@@ -505,15 +625,36 @@ export function SkillsPage() {
<Spin />
</div>
) : filteredSkills.length === 0 ? (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<div>
<div>{t('skills.empty')}</div>
<Text type="secondary" style={{ fontSize: 12 }}>{t('skills.emptyDesc')}</Text>
(sourceFilter === 'all' || sourceFilter === 'boosapi') ? (
<div>
<div style={{ margin: '8px 0 12px' }}>
<Text strong>BoosAPI 内置推荐技能</Text>
<Text type="secondary" style={{ display: 'block', fontSize: 12, marginTop: 2 }}>
这些技能已内置到项目清单里,点击即可安装到 Desktop 并在 Agent 对话中调用。
</Text>
</div>
}
/>
{recommendedSkillPresets.map(({ preset, status }) => (
<PresetSkillCard
key={preset.key}
preset={preset}
status={status}
installing={installing}
onInstall={handleInstallPreset}
onOpenGithub={handleOpenPresetGithub}
/>
))}
</div>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<div>
<div>{t('skills.empty')}</div>
<Text type="secondary" style={{ fontSize: 12 }}>{t('skills.emptyDesc')}</Text>
</div>
}
/>
)
) : (
<>
{groupedSkills.ungrouped.map((skill) => (