work-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/app/(admin)/approvals/page.tsx +16 -0
- package/app/(admin)/audit/page.tsx +18 -0
- package/app/(admin)/layout.tsx +47 -0
- package/app/(admin)/scheduled-tasks/page.tsx +17 -0
- package/app/(admin)/settings/page.tsx +46 -0
- package/app/(admin)/skills/[name]/page.tsx +378 -0
- package/app/(admin)/skills/page.tsx +406 -0
- package/app/(admin)/statistics/page.tsx +416 -0
- package/app/(admin)/tickets/[id]/page.tsx +348 -0
- package/app/(admin)/tickets/new/page.tsx +309 -0
- package/app/(admin)/tickets/page.tsx +27 -0
- package/app/api/audit/route.ts +30 -0
- package/app/api/auth/feishu/callback/route.ts +72 -0
- package/app/api/auth/feishu/login/route.ts +17 -0
- package/app/api/auth/feishu/sso/route.ts +78 -0
- package/app/api/auth/login/route.ts +85 -0
- package/app/api/auth/oauth/route.ts +168 -0
- package/app/api/config/providers/route.ts +105 -0
- package/app/api/config/route.ts +115 -0
- package/app/api/config/status/route.ts +56 -0
- package/app/api/config/test/route.ts +212 -0
- package/app/api/documents/[id]/route.ts +88 -0
- package/app/api/documents/route.ts +53 -0
- package/app/api/health/route.ts +32 -0
- package/app/api/knowledge/[id]/route.ts +152 -0
- package/app/api/knowledge/from-session/route.ts +27 -0
- package/app/api/knowledge/route.ts +100 -0
- package/app/api/market/knowledge/[id]/route.ts +92 -0
- package/app/api/market/knowledge/route.ts +130 -0
- package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
- package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
- package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
- package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
- package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
- package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
- package/app/api/marketplace/skills/[id]/route.ts +177 -0
- package/app/api/marketplace/skills/route.ts +235 -0
- package/app/api/memory/route.ts +40 -0
- package/app/api/my/files/[id]/route.ts +52 -0
- package/app/api/my/files/route.ts +230 -0
- package/app/api/my/knowledge/route.ts +36 -0
- package/app/api/pi-chat/route.ts +443 -0
- package/app/api/recommend/route.ts +38 -0
- package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
- package/app/api/scheduled-tasks/[id]/route.ts +165 -0
- package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
- package/app/api/scheduled-tasks/route.ts +101 -0
- package/app/api/sessions/[id]/messages/route.ts +212 -0
- package/app/api/sessions/route.ts +101 -0
- package/app/api/share/file/[id]/route.ts +37 -0
- package/app/api/skills/[name]/execute/route.ts +121 -0
- package/app/api/skills/[name]/route.ts +167 -0
- package/app/api/skills/create/route.ts +65 -0
- package/app/api/skills/generate/route.ts +405 -0
- package/app/api/skills/installed/route.ts +151 -0
- package/app/api/skills/route.ts +174 -0
- package/app/api/skills/translate/route.ts +40 -0
- package/app/api/skills/user/[name]/route.ts +159 -0
- package/app/api/skills/user/route.ts +90 -0
- package/app/api/statistics/route.ts +94 -0
- package/app/api/task-executions/[id]/route.ts +34 -0
- package/app/api/task-executions/route.ts +29 -0
- package/app/api/tickets/[id]/approve/route.ts +129 -0
- package/app/api/tickets/[id]/execute/route.ts +201 -0
- package/app/api/tickets/[id]/route.ts +127 -0
- package/app/api/tickets/route.ts +103 -0
- package/app/api/user/skills/route.ts +175 -0
- package/app/api/users/route.ts +80 -0
- package/app/chat/page.tsx +5 -0
- package/app/globals.css +84 -0
- package/app/h5/layout.tsx +5 -0
- package/app/h5/mobile-approvals-page.tsx +167 -0
- package/app/h5/mobile-chat-page.tsx +951 -0
- package/app/h5/mobile-profile-page.tsx +147 -0
- package/app/h5/mobile-tickets-page.tsx +121 -0
- package/app/h5/page.tsx +23 -0
- package/app/h5/ticket-action-buttons.tsx +80 -0
- package/app/layout.tsx +26 -0
- package/app/login/page.tsx +318 -0
- package/app/market/knowledge/[id]/page.tsx +77 -0
- package/app/market/knowledge/page.tsx +358 -0
- package/app/market/layout.tsx +29 -0
- package/app/market/page.tsx +18 -0
- package/app/market/skills/page.tsx +397 -0
- package/app/my/files/page.tsx +511 -0
- package/app/my/knowledge/[id]/page.tsx +271 -0
- package/app/my/knowledge/new/page.tsx +234 -0
- package/app/my/knowledge/page.tsx +248 -0
- package/app/my/layout.tsx +32 -0
- package/app/my/memory/page.tsx +164 -0
- package/app/my/page.tsx +18 -0
- package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
- package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
- package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
- package/app/my/scheduled-tasks/new/page.tsx +230 -0
- package/app/my/scheduled-tasks/page.tsx +27 -0
- package/app/my/skills/[name]/page.tsx +320 -0
- package/app/my/skills/new/page.tsx +394 -0
- package/app/my/skills/page.tsx +303 -0
- package/app/page.tsx +2288 -0
- package/app/share/[sessionId]/page.tsx +226 -0
- package/app/share/file/[id]/page.tsx +140 -0
- package/bin/README.md +63 -0
- package/bin/generate-api-system +300 -0
- package/bin/postinstall.js +95 -0
- package/bin/work-agent.js +173 -0
- package/components/ai-elements/agent.tsx +142 -0
- package/components/ai-elements/artifact.tsx +149 -0
- package/components/ai-elements/attachments.tsx +427 -0
- package/components/ai-elements/audio-player.tsx +232 -0
- package/components/ai-elements/canvas.tsx +26 -0
- package/components/ai-elements/chain-of-thought.tsx +223 -0
- package/components/ai-elements/checkpoint.tsx +72 -0
- package/components/ai-elements/code-block.tsx +555 -0
- package/components/ai-elements/commit.tsx +449 -0
- package/components/ai-elements/confirmation.tsx +173 -0
- package/components/ai-elements/connection.tsx +28 -0
- package/components/ai-elements/context.tsx +410 -0
- package/components/ai-elements/controls.tsx +19 -0
- package/components/ai-elements/conversation.tsx +167 -0
- package/components/ai-elements/edge.tsx +144 -0
- package/components/ai-elements/environment-variables.tsx +325 -0
- package/components/ai-elements/file-tree.tsx +298 -0
- package/components/ai-elements/image.tsx +25 -0
- package/components/ai-elements/inline-citation.tsx +294 -0
- package/components/ai-elements/jsx-preview.tsx +250 -0
- package/components/ai-elements/message.tsx +367 -0
- package/components/ai-elements/mic-selector.tsx +372 -0
- package/components/ai-elements/model-selector.tsx +214 -0
- package/components/ai-elements/node.tsx +72 -0
- package/components/ai-elements/open-in-chat.tsx +367 -0
- package/components/ai-elements/package-info.tsx +235 -0
- package/components/ai-elements/panel.tsx +16 -0
- package/components/ai-elements/persona.tsx +280 -0
- package/components/ai-elements/plan.tsx +144 -0
- package/components/ai-elements/prompt-input.tsx +1341 -0
- package/components/ai-elements/queue.tsx +275 -0
- package/components/ai-elements/reasoning.tsx +355 -0
- package/components/ai-elements/sandbox.tsx +133 -0
- package/components/ai-elements/schema-display.tsx +473 -0
- package/components/ai-elements/shimmer.tsx +78 -0
- package/components/ai-elements/snippet.tsx +141 -0
- package/components/ai-elements/sources.tsx +78 -0
- package/components/ai-elements/speech-input.tsx +324 -0
- package/components/ai-elements/stack-trace.tsx +531 -0
- package/components/ai-elements/suggestion.tsx +58 -0
- package/components/ai-elements/task.tsx +88 -0
- package/components/ai-elements/terminal.tsx +277 -0
- package/components/ai-elements/test-results.tsx +497 -0
- package/components/ai-elements/tool.tsx +174 -0
- package/components/ai-elements/toolbar.tsx +17 -0
- package/components/ai-elements/transcription.tsx +126 -0
- package/components/ai-elements/voice-selector.tsx +525 -0
- package/components/ai-elements/web-preview.tsx +282 -0
- package/components/audit-log-list.tsx +114 -0
- package/components/chat/EmptyPreviewState.tsx +12 -0
- package/components/chat/KnowledgePickerDialog.tsx +464 -0
- package/components/chat/KnowledgePreview.tsx +70 -0
- package/components/chat/KnowledgePreviewPanel.tsx +86 -0
- package/components/chat/MentionInput.tsx +309 -0
- package/components/chat/OrganizeDialog.tsx +258 -0
- package/components/chat/RecommendationBanner.tsx +94 -0
- package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
- package/components/chat/SkillSelector.tsx +305 -0
- package/components/chat/SkillSwitcher.tsx +163 -0
- package/components/client-layout.tsx +15 -0
- package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
- package/components/layout-wrapper.tsx +18 -0
- package/components/mobile-layout.tsx +62 -0
- package/components/scheduled-task-list.tsx +356 -0
- package/components/setup-guide.tsx +484 -0
- package/components/sub-nav.tsx +54 -0
- package/components/ticket-detail-content.tsx +383 -0
- package/components/ticket-list.tsx +366 -0
- package/components/top-nav.tsx +132 -0
- package/components/ui/accordion.tsx +58 -0
- package/components/ui/alert.tsx +59 -0
- package/components/ui/avatar.tsx +50 -0
- package/components/ui/badge.tsx +36 -0
- package/components/ui/button-group.tsx +83 -0
- package/components/ui/button.tsx +57 -0
- package/components/ui/card.tsx +91 -0
- package/components/ui/carousel.tsx +262 -0
- package/components/ui/collapsible.tsx +11 -0
- package/components/ui/command.tsx +153 -0
- package/components/ui/dialog.tsx +122 -0
- package/components/ui/dropdown-menu.tsx +200 -0
- package/components/ui/hover-card.tsx +29 -0
- package/components/ui/input-group.tsx +170 -0
- package/components/ui/input.tsx +22 -0
- package/components/ui/label.tsx +26 -0
- package/components/ui/popover.tsx +31 -0
- package/components/ui/progress.tsx +28 -0
- package/components/ui/scroll-area.tsx +48 -0
- package/components/ui/select.tsx +174 -0
- package/components/ui/separator.tsx +31 -0
- package/components/ui/spinner.tsx +16 -0
- package/components/ui/switch.tsx +29 -0
- package/components/ui/table.tsx +120 -0
- package/components/ui/tabs.tsx +55 -0
- package/components/ui/textarea.tsx +22 -0
- package/components/ui/tooltip.tsx +30 -0
- package/components/welcome-guide.tsx +182 -0
- package/components.json +24 -0
- package/lib/command-parser.ts +331 -0
- package/lib/dangerous-commands.ts +672 -0
- package/lib/db.ts +2250 -0
- package/lib/feishu-auth.ts +135 -0
- package/lib/file-storage.ts +306 -0
- package/lib/file-tool.ts +583 -0
- package/lib/knowledge-tool.ts +152 -0
- package/lib/knowledge-types.ts +66 -0
- package/lib/market-client.ts +313 -0
- package/lib/market-db.ts +736 -0
- package/lib/market-types.ts +51 -0
- package/lib/memory-tool.ts +211 -0
- package/lib/memory.ts +197 -0
- package/lib/pi-config.ts +436 -0
- package/lib/pi-session.ts +799 -0
- package/lib/pinyin.ts +13 -0
- package/lib/recommendation.ts +227 -0
- package/lib/risk-estimator.ts +350 -0
- package/lib/scheduled-task-tool.ts +184 -0
- package/lib/scheduler-init.ts +43 -0
- package/lib/scheduler.ts +416 -0
- package/lib/secure-bash-tool.ts +413 -0
- package/lib/skill-engine.ts +396 -0
- package/lib/skill-generator.ts +269 -0
- package/lib/skill-loader.ts +234 -0
- package/lib/skill-tool.ts +188 -0
- package/lib/skill-types.ts +82 -0
- package/lib/skills-init.ts +58 -0
- package/lib/ticket-tool.ts +246 -0
- package/lib/user-skill-types.ts +30 -0
- package/lib/user-skills.ts +362 -0
- package/lib/utils.ts +6 -0
- package/lib/workflow.ts +154 -0
- package/lib/zip-tool.ts +191 -0
- package/next.config.js +8 -0
- package/package.json +106 -0
- package/public/.gitkeep +1 -0
- package/public/icon.svg +1 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Input } from '@/components/ui/input';
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from '@/components/ui/card';
|
|
13
|
+
import {
|
|
14
|
+
Select,
|
|
15
|
+
SelectContent,
|
|
16
|
+
SelectItem,
|
|
17
|
+
SelectTrigger,
|
|
18
|
+
SelectValue,
|
|
19
|
+
} from '@/components/ui/select';
|
|
20
|
+
import { AlertCircle, CheckCircle, Loader2, Settings, Key, Globe } from 'lucide-react';
|
|
21
|
+
|
|
22
|
+
interface SetupGuideProps {
|
|
23
|
+
mode?: 'full' | 'settings';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ConfigStatus {
|
|
27
|
+
isConfigured: boolean;
|
|
28
|
+
configDir: string;
|
|
29
|
+
hasProviders: boolean;
|
|
30
|
+
hasEnvApiKey: boolean;
|
|
31
|
+
envProvider?: string | null;
|
|
32
|
+
providers: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 预设的 AI 提供商配置
|
|
36
|
+
// API 类型: anthropic-messages (Anthropic/兼容), openai-completions (OpenAI 兼容)
|
|
37
|
+
const PROVIDER_PRESETS: Record<string, { name: string; baseUrl: string; api: string; docs: string; defaultModel?: string }> = {
|
|
38
|
+
zhipu: {
|
|
39
|
+
name: '智谱 AI (GLM)',
|
|
40
|
+
baseUrl: 'https://open.bigmodel.cn/api/anthropic',
|
|
41
|
+
api: 'anthropic-messages',
|
|
42
|
+
docs: 'https://open.bigmodel.cn/',
|
|
43
|
+
defaultModel: 'glm-4.7',
|
|
44
|
+
},
|
|
45
|
+
ollama: {
|
|
46
|
+
name: 'Ollama (本地)',
|
|
47
|
+
baseUrl: 'http://192.168.44.151:11434/v1',
|
|
48
|
+
api: 'openai-completions',
|
|
49
|
+
docs: 'https://ollama.ai/',
|
|
50
|
+
defaultModel: 'qwen3.5:27b',
|
|
51
|
+
},
|
|
52
|
+
custom: {
|
|
53
|
+
name: '自定义',
|
|
54
|
+
baseUrl: '',
|
|
55
|
+
api: 'openai-completions',
|
|
56
|
+
docs: '',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export function SetupGuide({ mode = 'full' }: SetupGuideProps) {
|
|
61
|
+
const [status, setStatus] = useState<ConfigStatus | null>(null);
|
|
62
|
+
const [loading, setLoading] = useState(true);
|
|
63
|
+
const [saving, setSaving] = useState(false);
|
|
64
|
+
const [testing, setTesting] = useState(false);
|
|
65
|
+
const [error, setError] = useState<string | null>(null);
|
|
66
|
+
const [success, setSuccess] = useState(false);
|
|
67
|
+
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
|
68
|
+
|
|
69
|
+
// Form state
|
|
70
|
+
const [selectedProvider, setSelectedProvider] = useState('zhipu');
|
|
71
|
+
const [apiKey, setApiKey] = useState('');
|
|
72
|
+
const [baseUrl, setBaseUrl] = useState(PROVIDER_PRESETS.zhipu.baseUrl);
|
|
73
|
+
const [modelId, setModelId] = useState('glm-4.7');
|
|
74
|
+
|
|
75
|
+
// Load current status
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
async function loadStatus() {
|
|
78
|
+
try {
|
|
79
|
+
const response = await fetch('/api/config/status');
|
|
80
|
+
if (response.ok) {
|
|
81
|
+
const data = await response.json();
|
|
82
|
+
setStatus(data);
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error('Failed to load config status:', e);
|
|
86
|
+
} finally {
|
|
87
|
+
setLoading(false);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
loadStatus();
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
// Update baseUrl and modelId when provider changes
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const preset = PROVIDER_PRESETS[selectedProvider];
|
|
96
|
+
if (preset) {
|
|
97
|
+
setBaseUrl(preset.baseUrl);
|
|
98
|
+
if (preset.defaultModel) {
|
|
99
|
+
setModelId(preset.defaultModel);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, [selectedProvider]);
|
|
103
|
+
|
|
104
|
+
const handleSave = async () => {
|
|
105
|
+
if (!apiKey.trim()) {
|
|
106
|
+
setError('请输入 API Key');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setSaving(true);
|
|
111
|
+
setError(null);
|
|
112
|
+
setSuccess(false);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const preset = PROVIDER_PRESETS[selectedProvider];
|
|
116
|
+
const providerId = selectedProvider === 'custom' ? 'custom' : selectedProvider;
|
|
117
|
+
|
|
118
|
+
const response = await fetch('/api/config/providers', {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
id: providerId,
|
|
123
|
+
baseUrl: baseUrl || preset.baseUrl,
|
|
124
|
+
api: preset.api,
|
|
125
|
+
apiKey: apiKey,
|
|
126
|
+
models: modelId ? [modelId] : undefined,
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
throw new Error(data.error || '保存失败');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
setSuccess(true);
|
|
136
|
+
setApiKey('');
|
|
137
|
+
setModelId('');
|
|
138
|
+
|
|
139
|
+
// Refresh status
|
|
140
|
+
const statusResponse = await fetch('/api/config/status');
|
|
141
|
+
if (statusResponse.ok) {
|
|
142
|
+
const data = await statusResponse.json();
|
|
143
|
+
setStatus(data);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Reload page after 2 seconds to apply new config
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
window.location.reload();
|
|
149
|
+
}, 2000);
|
|
150
|
+
} catch (e) {
|
|
151
|
+
setError(e instanceof Error ? e.message : '保存失败');
|
|
152
|
+
} finally {
|
|
153
|
+
setSaving(false);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const handleTest = async () => {
|
|
158
|
+
if (!baseUrl.trim()) {
|
|
159
|
+
setTestResult({ success: false, message: '请输入 API 地址' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setTesting(true);
|
|
164
|
+
setTestResult(null);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const preset = PROVIDER_PRESETS[selectedProvider];
|
|
168
|
+
const providerId = selectedProvider === 'custom' ? 'custom' : selectedProvider;
|
|
169
|
+
|
|
170
|
+
const response = await fetch('/api/config/test', {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
providerId,
|
|
175
|
+
baseUrl: baseUrl || preset.baseUrl,
|
|
176
|
+
api: preset.api,
|
|
177
|
+
apiKey: apiKey || 'test',
|
|
178
|
+
modelId: modelId || preset.defaultModel,
|
|
179
|
+
}),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const data = await response.json();
|
|
183
|
+
|
|
184
|
+
if (response.ok && data.success) {
|
|
185
|
+
setTestResult({ success: true, message: data.message || '连接成功!' });
|
|
186
|
+
} else {
|
|
187
|
+
setTestResult({ success: false, message: data.error || '连接失败' });
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
setTestResult({
|
|
191
|
+
success: false,
|
|
192
|
+
message: e instanceof Error ? e.message : '连接测试失败'
|
|
193
|
+
});
|
|
194
|
+
} finally {
|
|
195
|
+
setTesting(false);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
if (loading) {
|
|
200
|
+
return (
|
|
201
|
+
<div className="flex items-center justify-center min-h-[400px]">
|
|
202
|
+
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const preset = PROVIDER_PRESETS[selectedProvider];
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className="max-w-2xl mx-auto p-6 space-y-6">
|
|
211
|
+
{/* Header */}
|
|
212
|
+
<div className="text-center space-y-2">
|
|
213
|
+
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-4">
|
|
214
|
+
<Settings className="h-8 w-8 text-primary" />
|
|
215
|
+
</div>
|
|
216
|
+
<h1 className="text-2xl font-bold">
|
|
217
|
+
{mode === 'full' ? '欢迎使用智能助手' : '系统设置'}
|
|
218
|
+
</h1>
|
|
219
|
+
<p className="text-muted-foreground">
|
|
220
|
+
{mode === 'full' ? '请先配置 AI 提供商以开始使用' : '管理 AI 提供商配置'}
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* Success message */}
|
|
225
|
+
{success && (
|
|
226
|
+
<Card className="border-green-200 bg-green-50">
|
|
227
|
+
<CardContent className="pt-6">
|
|
228
|
+
<div className="flex items-center gap-2 text-green-700">
|
|
229
|
+
<CheckCircle className="h-5 w-5" />
|
|
230
|
+
<span className="font-medium">配置保存成功!页面即将刷新...</span>
|
|
231
|
+
</div>
|
|
232
|
+
</CardContent>
|
|
233
|
+
</Card>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{/* Current status */}
|
|
237
|
+
{status?.hasEnvApiKey && (
|
|
238
|
+
<Card className="border-blue-200 bg-blue-50">
|
|
239
|
+
<CardContent className="pt-6">
|
|
240
|
+
<div className="flex items-center gap-2 text-blue-700">
|
|
241
|
+
<CheckCircle className="h-5 w-5" />
|
|
242
|
+
<span>
|
|
243
|
+
检测到环境变量配置
|
|
244
|
+
{status.envProvider && ` (AI_PROVIDER=${status.envProvider})`}
|
|
245
|
+
,系统已配置
|
|
246
|
+
</span>
|
|
247
|
+
</div>
|
|
248
|
+
</CardContent>
|
|
249
|
+
</Card>
|
|
250
|
+
)}
|
|
251
|
+
|
|
252
|
+
{/* Configuration form */}
|
|
253
|
+
<Card>
|
|
254
|
+
<CardHeader>
|
|
255
|
+
<CardTitle className="text-lg">配置 AI 提供商</CardTitle>
|
|
256
|
+
<CardDescription>
|
|
257
|
+
选择一个 AI 提供商并输入您的 API Key
|
|
258
|
+
</CardDescription>
|
|
259
|
+
</CardHeader>
|
|
260
|
+
<CardContent className="space-y-4">
|
|
261
|
+
{/* Provider selection */}
|
|
262
|
+
<div className="space-y-2">
|
|
263
|
+
<label className="text-sm font-medium">选择提供商</label>
|
|
264
|
+
<Select value={selectedProvider} onValueChange={setSelectedProvider}>
|
|
265
|
+
<SelectTrigger>
|
|
266
|
+
<SelectValue />
|
|
267
|
+
</SelectTrigger>
|
|
268
|
+
<SelectContent>
|
|
269
|
+
{Object.entries(PROVIDER_PRESETS).map(([id, preset]) => (
|
|
270
|
+
<SelectItem key={id} value={id}>
|
|
271
|
+
{preset.name}
|
|
272
|
+
</SelectItem>
|
|
273
|
+
))}
|
|
274
|
+
</SelectContent>
|
|
275
|
+
</Select>
|
|
276
|
+
{preset.docs && (
|
|
277
|
+
<p className="text-xs text-muted-foreground">
|
|
278
|
+
获取 API Key: <a href={preset.docs} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">{preset.docs}</a>
|
|
279
|
+
</p>
|
|
280
|
+
)}
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* API Key */}
|
|
284
|
+
<div className="space-y-2">
|
|
285
|
+
<label className="text-sm font-medium flex items-center gap-2">
|
|
286
|
+
<Key className="h-4 w-4" />
|
|
287
|
+
API Key
|
|
288
|
+
</label>
|
|
289
|
+
<Input
|
|
290
|
+
type="password"
|
|
291
|
+
placeholder={
|
|
292
|
+
selectedProvider === 'ollama'
|
|
293
|
+
? 'Ollama不需要真实API Key,填写任意值(如:ollama)'
|
|
294
|
+
: selectedProvider === 'zhipu'
|
|
295
|
+
? 'xxx.xxx'
|
|
296
|
+
: 'sk-...'
|
|
297
|
+
}
|
|
298
|
+
value={apiKey}
|
|
299
|
+
onChange={(e) => setApiKey(e.target.value)}
|
|
300
|
+
/>
|
|
301
|
+
{selectedProvider === 'ollama' && (
|
|
302
|
+
<p className="text-xs text-amber-600 bg-amber-50 p-2 rounded flex items-start gap-2">
|
|
303
|
+
<span className="text-amber-500">💡</span>
|
|
304
|
+
<span>Ollama不需要真实的API Key,填写任意值即可(如:ollama)</span>
|
|
305
|
+
</p>
|
|
306
|
+
)}
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
{/* Base URL (for custom providers) */}
|
|
310
|
+
<div className="space-y-2">
|
|
311
|
+
<label className="text-sm font-medium flex items-center gap-2">
|
|
312
|
+
<Globe className="h-4 w-4" />
|
|
313
|
+
API 地址
|
|
314
|
+
</label>
|
|
315
|
+
<Input
|
|
316
|
+
placeholder="https://api.example.com/v1"
|
|
317
|
+
value={baseUrl}
|
|
318
|
+
onChange={(e) => setBaseUrl(e.target.value)}
|
|
319
|
+
disabled={selectedProvider !== 'custom' && selectedProvider !== 'ollama'}
|
|
320
|
+
/>
|
|
321
|
+
{selectedProvider === 'ollama' && (
|
|
322
|
+
<p className="text-xs text-muted-foreground">
|
|
323
|
+
💡 本地Ollama服务地址,默认为 http://192.168.44.151:11434/v1
|
|
324
|
+
</p>
|
|
325
|
+
)}
|
|
326
|
+
{selectedProvider !== 'custom' && selectedProvider !== 'ollama' && (
|
|
327
|
+
<p className="text-xs text-muted-foreground">
|
|
328
|
+
使用 {preset.name} 默认地址。如需自定义请选择"自定义"或"Ollama"提供商。
|
|
329
|
+
</p>
|
|
330
|
+
)}
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Model ID (optional) */}
|
|
334
|
+
<div className="space-y-2">
|
|
335
|
+
<label className="text-sm font-medium">模型 ID (可选)</label>
|
|
336
|
+
<Input
|
|
337
|
+
placeholder="claude-sonnet-4-20250514"
|
|
338
|
+
value={modelId}
|
|
339
|
+
onChange={(e) => setModelId(e.target.value)}
|
|
340
|
+
/>
|
|
341
|
+
<p className="text-xs text-muted-foreground">
|
|
342
|
+
留空使用默认模型
|
|
343
|
+
</p>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* Error message */}
|
|
347
|
+
{error && (
|
|
348
|
+
<div className="flex items-center gap-2 text-sm text-red-600 bg-red-50 p-3 rounded-lg">
|
|
349
|
+
<AlertCircle className="h-4 w-4" />
|
|
350
|
+
{error}
|
|
351
|
+
</div>
|
|
352
|
+
)}
|
|
353
|
+
|
|
354
|
+
{/* Test result */}
|
|
355
|
+
{testResult && (
|
|
356
|
+
<div className={`flex items-start gap-2 text-sm p-3 rounded-lg ${
|
|
357
|
+
testResult.success
|
|
358
|
+
? 'text-green-600 bg-green-50'
|
|
359
|
+
: 'text-red-600 bg-red-50'
|
|
360
|
+
}`}>
|
|
361
|
+
{testResult.success ? (
|
|
362
|
+
<CheckCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
|
363
|
+
) : (
|
|
364
|
+
<AlertCircle className="h-4 w-4 mt-0.5 flex-shrink-0" />
|
|
365
|
+
)}
|
|
366
|
+
<span>{testResult.message}</span>
|
|
367
|
+
</div>
|
|
368
|
+
)}
|
|
369
|
+
|
|
370
|
+
{/* Buttons */}
|
|
371
|
+
<div className="flex gap-3">
|
|
372
|
+
<Button
|
|
373
|
+
onClick={handleTest}
|
|
374
|
+
disabled={testing || !baseUrl.trim()}
|
|
375
|
+
variant="outline"
|
|
376
|
+
className="flex-1"
|
|
377
|
+
>
|
|
378
|
+
{testing ? (
|
|
379
|
+
<>
|
|
380
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
381
|
+
测试中...
|
|
382
|
+
</>
|
|
383
|
+
) : (
|
|
384
|
+
'测试连接'
|
|
385
|
+
)}
|
|
386
|
+
</Button>
|
|
387
|
+
<Button
|
|
388
|
+
onClick={handleSave}
|
|
389
|
+
disabled={saving || !apiKey.trim()}
|
|
390
|
+
className="flex-1"
|
|
391
|
+
>
|
|
392
|
+
{saving ? (
|
|
393
|
+
<>
|
|
394
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
395
|
+
保存中...
|
|
396
|
+
</>
|
|
397
|
+
) : (
|
|
398
|
+
'保存配置'
|
|
399
|
+
)}
|
|
400
|
+
</Button>
|
|
401
|
+
</div>
|
|
402
|
+
</CardContent>
|
|
403
|
+
</Card>
|
|
404
|
+
|
|
405
|
+
{/* Alternative: Environment variable */}
|
|
406
|
+
<Card>
|
|
407
|
+
<CardHeader>
|
|
408
|
+
<CardTitle className="text-lg">或者使用环境变量</CardTitle>
|
|
409
|
+
<CardDescription>
|
|
410
|
+
在 Docker 或系统环境中设置
|
|
411
|
+
</CardDescription>
|
|
412
|
+
</CardHeader>
|
|
413
|
+
<CardContent>
|
|
414
|
+
<div className="bg-muted rounded-lg p-4 font-mono text-sm space-y-3">
|
|
415
|
+
<div>
|
|
416
|
+
<p className="text-muted-foreground"># 使用 AI_ 前缀环境变量(推荐)</p>
|
|
417
|
+
<p>environment:</p>
|
|
418
|
+
<p className="pl-4">AI_PROVIDER: zhipu</p>
|
|
419
|
+
<p className="pl-4">AI_MODEL: glm-4</p>
|
|
420
|
+
<p className="pl-4">AI_API_KEY: your-api-key</p>
|
|
421
|
+
</div>
|
|
422
|
+
<div className="border-t border-border pt-3">
|
|
423
|
+
<p className="text-muted-foreground"># 本地 Ollama 配置</p>
|
|
424
|
+
<p>environment:</p>
|
|
425
|
+
<p className="pl-4">AI_PROVIDER: ollama</p>
|
|
426
|
+
<p className="pl-4">AI_MODEL: qwen3.5:27b</p>
|
|
427
|
+
<p className="pl-4">AI_BASE_URL: http://192.168.44.151:11434/v1</p>
|
|
428
|
+
<p className="pl-4">AI_API_KEY: ollama</p>
|
|
429
|
+
</div>
|
|
430
|
+
<div className="border-t border-border pt-3">
|
|
431
|
+
<p className="text-muted-foreground"># 或使用供应商特定环境变量</p>
|
|
432
|
+
<p>environment:</p>
|
|
433
|
+
<p className="pl-4">ZHIPU_API_KEY: your-api-key</p>
|
|
434
|
+
<p className="pl-4 text-muted-foreground"># 或</p>
|
|
435
|
+
<p className="pl-4">ANTHROPIC_API_KEY: your-api-key</p>
|
|
436
|
+
</div>
|
|
437
|
+
<div className="border-t border-border pt-3">
|
|
438
|
+
<p className="text-muted-foreground"># 或挂载配置文件</p>
|
|
439
|
+
<p>volumes:</p>
|
|
440
|
+
<p className="pl-4">- ~/.pi/agent:/home/nextjs/.pi/agent</p>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
|
444
|
+
<p className="text-sm text-blue-700 font-medium mb-2">支持的环境变量:</p>
|
|
445
|
+
<table className="text-xs text-blue-600 w-full">
|
|
446
|
+
<tbody>
|
|
447
|
+
<tr><td className="font-mono pr-4">AI_PROVIDER</td><td>供应商 ID (zhipu, anthropic, deepseek, openai, ollama)</td></tr>
|
|
448
|
+
<tr><td className="font-mono pr-4">AI_MODEL</td><td>模型 ID (如 glm-4, claude-sonnet-4-20250514, qwen3.5:27b)</td></tr>
|
|
449
|
+
<tr><td className="font-mono pr-4">AI_BASE_URL</td><td>自定义 API 地址</td></tr>
|
|
450
|
+
<tr><td className="font-mono pr-4">AI_API_KEY</td><td>通用 API Key</td></tr>
|
|
451
|
+
</tbody>
|
|
452
|
+
</table>
|
|
453
|
+
</div>
|
|
454
|
+
{status && (
|
|
455
|
+
<p className="mt-3 text-xs text-muted-foreground">
|
|
456
|
+
配置目录: <code className="bg-muted px-1 rounded">{status.configDir}</code>
|
|
457
|
+
</p>
|
|
458
|
+
)}
|
|
459
|
+
</CardContent>
|
|
460
|
+
</Card>
|
|
461
|
+
|
|
462
|
+
{/* Existing providers */}
|
|
463
|
+
{status?.providers && status.providers.length > 0 && (
|
|
464
|
+
<Card>
|
|
465
|
+
<CardHeader>
|
|
466
|
+
<CardTitle className="text-lg">已配置的提供商</CardTitle>
|
|
467
|
+
</CardHeader>
|
|
468
|
+
<CardContent>
|
|
469
|
+
<div className="flex flex-wrap gap-2">
|
|
470
|
+
{status.providers.map((provider) => (
|
|
471
|
+
<span
|
|
472
|
+
key={provider}
|
|
473
|
+
className="px-3 py-1 bg-primary/10 text-primary rounded-full text-sm"
|
|
474
|
+
>
|
|
475
|
+
{PROVIDER_PRESETS[provider]?.name || provider}
|
|
476
|
+
</span>
|
|
477
|
+
))}
|
|
478
|
+
</div>
|
|
479
|
+
</CardContent>
|
|
480
|
+
</Card>
|
|
481
|
+
)}
|
|
482
|
+
</div>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { usePathname } from 'next/navigation';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import { LucideIcon } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
export interface NavItem {
|
|
9
|
+
href: string;
|
|
10
|
+
label: string;
|
|
11
|
+
icon: LucideIcon;
|
|
12
|
+
badge?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SubNavProps {
|
|
16
|
+
items: NavItem[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SubNav({ items }: SubNavProps) {
|
|
20
|
+
const pathname = usePathname();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<nav className="w-48 flex-shrink-0 border-r bg-card py-4">
|
|
24
|
+
<ul className="space-y-1 px-2">
|
|
25
|
+
{items.map((item) => {
|
|
26
|
+
const isActive = pathname === item.href || pathname.startsWith(item.href + '/');
|
|
27
|
+
const Icon = item.icon;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<li key={item.href}>
|
|
31
|
+
<Link
|
|
32
|
+
href={item.href}
|
|
33
|
+
className={cn(
|
|
34
|
+
'flex items-center gap-2 px-3 py-2 rounded-md text-sm transition-colors',
|
|
35
|
+
isActive
|
|
36
|
+
? 'bg-blue-50 text-blue-700 font-medium'
|
|
37
|
+
: 'text-gray-700 hover:bg-gray-100'
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<Icon className="h-4 w-4" />
|
|
41
|
+
<span className="flex-1">{item.label}</span>
|
|
42
|
+
{item.badge !== undefined && item.badge > 0 && (
|
|
43
|
+
<span className="px-1.5 py-0.5 text-xs bg-blue-100 text-blue-700 rounded-full">
|
|
44
|
+
{item.badge}
|
|
45
|
+
</span>
|
|
46
|
+
)}
|
|
47
|
+
</Link>
|
|
48
|
+
</li>
|
|
49
|
+
);
|
|
50
|
+
})}
|
|
51
|
+
</ul>
|
|
52
|
+
</nav>
|
|
53
|
+
);
|
|
54
|
+
}
|