@jiangxiaosheng/digital-agent-platform 1.0.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.
Files changed (94) hide show
  1. package/README.md +37 -0
  2. package/eslint.config.mjs +20 -0
  3. package/messages/en.json +184 -0
  4. package/messages/zh.json +184 -0
  5. package/middleware.ts +9 -0
  6. package/next-env.d.ts +5 -0
  7. package/next.config.ts +16 -0
  8. package/package.json +59 -0
  9. package/postcss.config.mjs +5 -0
  10. package/src/app/[locale]/agents/[id]/chat/page.tsx +833 -0
  11. package/src/app/[locale]/agents/[id]/edit/page.tsx +58 -0
  12. package/src/app/[locale]/agents/new/page.tsx +52 -0
  13. package/src/app/[locale]/agents/page.tsx +122 -0
  14. package/src/app/[locale]/knowledge/page.tsx +305 -0
  15. package/src/app/[locale]/layout.tsx +46 -0
  16. package/src/app/[locale]/page.tsx +6 -0
  17. package/src/app/[locale]/settings/page.tsx +1204 -0
  18. package/src/app/api/agents/[id]/route.ts +27 -0
  19. package/src/app/api/agents/route.ts +27 -0
  20. package/src/app/api/auth-config/test/route.ts +69 -0
  21. package/src/app/api/bash-output/route.ts +57 -0
  22. package/src/app/api/chat/[agentId]/route.ts +164 -0
  23. package/src/app/api/env-vars/[key]/route.ts +22 -0
  24. package/src/app/api/env-vars/route.ts +26 -0
  25. package/src/app/api/env-vars/scan/route.ts +66 -0
  26. package/src/app/api/file-serve/route.ts +58 -0
  27. package/src/app/api/fs/browse/route.ts +47 -0
  28. package/src/app/api/knowledge/[id]/documents/[docId]/route.ts +22 -0
  29. package/src/app/api/knowledge/[id]/route.ts +21 -0
  30. package/src/app/api/knowledge/[id]/upload/route.ts +90 -0
  31. package/src/app/api/knowledge/route.ts +15 -0
  32. package/src/app/api/knowledge/search/route.ts +26 -0
  33. package/src/app/api/models/route.ts +160 -0
  34. package/src/app/api/models-config/route.ts +41 -0
  35. package/src/app/api/models-config/test/route.ts +84 -0
  36. package/src/app/api/sessions/[agentId]/[sessionId]/route.ts +12 -0
  37. package/src/app/api/sessions/[agentId]/route.ts +19 -0
  38. package/src/app/api/sessions/history/route.ts +166 -0
  39. package/src/app/api/settings/route.ts +45 -0
  40. package/src/app/api/skills/[name]/route.ts +40 -0
  41. package/src/app/api/skills/install/route.ts +35 -0
  42. package/src/app/api/skills/route.ts +54 -0
  43. package/src/app/api/skills/search/route.ts +60 -0
  44. package/src/app/api/tts/route.ts +235 -0
  45. package/src/app/api/voice-provider/route.ts +98 -0
  46. package/src/app/api/workspace-files/route.ts +151 -0
  47. package/src/app/globals.css +176 -0
  48. package/src/app/layout.tsx +13 -0
  49. package/src/app/page.tsx +6 -0
  50. package/src/components/agents/agent-form.tsx +457 -0
  51. package/src/components/agents/model-selector.tsx +290 -0
  52. package/src/components/agents/skills-manager.tsx +347 -0
  53. package/src/components/chat/input-bar.tsx +561 -0
  54. package/src/components/chat/message-bubble.tsx +395 -0
  55. package/src/components/layout/sidebar.tsx +304 -0
  56. package/src/components/providers.tsx +19 -0
  57. package/src/components/ui/badge.tsx +26 -0
  58. package/src/components/ui/button.tsx +43 -0
  59. package/src/components/ui/card.tsx +46 -0
  60. package/src/components/ui/dialog.tsx +74 -0
  61. package/src/components/ui/directory-picker.tsx +185 -0
  62. package/src/components/ui/input.tsx +18 -0
  63. package/src/components/ui/label.tsx +16 -0
  64. package/src/components/ui/scroll-area.tsx +41 -0
  65. package/src/components/ui/select.tsx +93 -0
  66. package/src/components/ui/slider.tsx +35 -0
  67. package/src/components/ui/switch.tsx +35 -0
  68. package/src/components/ui/tabs.tsx +48 -0
  69. package/src/components/ui/textarea.tsx +17 -0
  70. package/src/components/ui/toaster.tsx +50 -0
  71. package/src/hooks/use-toast.ts +43 -0
  72. package/src/i18n/request.ts +13 -0
  73. package/src/i18n/routing.ts +7 -0
  74. package/src/lib/agent-factory.ts +200 -0
  75. package/src/lib/agents/store.ts +95 -0
  76. package/src/lib/db/client.ts +25 -0
  77. package/src/lib/db/schema.ts +66 -0
  78. package/src/lib/env-vars.ts +33 -0
  79. package/src/lib/knowledge/chunker.ts +34 -0
  80. package/src/lib/knowledge/embedder.ts +48 -0
  81. package/src/lib/knowledge/retriever.ts +94 -0
  82. package/src/lib/knowledge/store.ts +74 -0
  83. package/src/lib/models-config.ts +55 -0
  84. package/src/lib/settings.ts +33 -0
  85. package/src/lib/skills.ts +79 -0
  86. package/src/lib/system-prompt.ts +52 -0
  87. package/src/lib/tool-builder.ts +19 -0
  88. package/src/lib/utils.ts +6 -0
  89. package/src/lib/voice-providers-config.ts +71 -0
  90. package/src/middleware.ts +9 -0
  91. package/src/types/agent.ts +124 -0
  92. package/src/types/knowledge.ts +39 -0
  93. package/src/types/settings.ts +31 -0
  94. package/tsconfig.json +40 -0
@@ -0,0 +1,40 @@
1
+ import { NextResponse } from "next/server";
2
+ import { listInstalledSkills } from "@/lib/skills";
3
+ import path from "path";
4
+ import fs from "fs";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ type Params = { params: Promise<{ name: string }> };
9
+
10
+ export async function GET(_req: Request, { params }: Params) {
11
+ const { name } = await params;
12
+ const skills = listInstalledSkills();
13
+ const skill = skills.find((s) => s.name === decodeURIComponent(name));
14
+ if (!skill) return NextResponse.json({ error: "Not found" }, { status: 404 });
15
+
16
+ const mdPath = fs.statSync(skill.filePath).isDirectory()
17
+ ? path.join(skill.filePath, "SKILL.md")
18
+ : skill.filePath;
19
+ const content = fs.existsSync(mdPath) ? fs.readFileSync(mdPath, "utf8") : "";
20
+ return NextResponse.json({ ...skill, content });
21
+ }
22
+
23
+ export async function DELETE(_req: Request, { params }: Params) {
24
+ const { name } = await params;
25
+ const skills = listInstalledSkills();
26
+ const skill = skills.find((s) => s.name === decodeURIComponent(name));
27
+ if (!skill) return NextResponse.json({ error: "Skill not found" }, { status: 404 });
28
+
29
+ try {
30
+ const stat = fs.statSync(skill.filePath);
31
+ if (stat.isDirectory()) {
32
+ fs.rmSync(skill.filePath, { recursive: true, force: true });
33
+ } else {
34
+ fs.unlinkSync(skill.filePath);
35
+ }
36
+ return NextResponse.json({ success: true });
37
+ } catch (e) {
38
+ return NextResponse.json({ error: String(e) }, { status: 500 });
39
+ }
40
+ }
@@ -0,0 +1,35 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { exec } from "child_process";
3
+ import { promisify } from "util";
4
+ import { getSkillsDir } from "@/lib/skills";
5
+
6
+ export const dynamic = "force-dynamic";
7
+ const execAsync = promisify(exec);
8
+
9
+ export async function POST(req: NextRequest) {
10
+ try {
11
+ const { package: pkg } = await req.json() as { package: string };
12
+ if (!pkg?.trim()) {
13
+ return NextResponse.json({ error: "package required" }, { status: 400 });
14
+ }
15
+
16
+ const skillsDir = getSkillsDir();
17
+ // Install globally into ~/.pi/agent/skills/ via npx skills add
18
+ const cmd = `npx --yes skills add "${pkg.trim()}" -y --agent pi -g`;
19
+ const { stdout, stderr } = await execAsync(cmd, {
20
+ env: { ...process.env, SKILLS_DIR: skillsDir },
21
+ timeout: 60000,
22
+ });
23
+
24
+ return NextResponse.json({
25
+ success: true,
26
+ output: stdout || stderr || "安装完成",
27
+ });
28
+ } catch (e: unknown) {
29
+ const err = e as { message?: string; stdout?: string; stderr?: string };
30
+ return NextResponse.json({
31
+ error: err.message ?? String(e),
32
+ output: err.stderr ?? err.stdout ?? "",
33
+ }, { status: 500 });
34
+ }
35
+ }
@@ -0,0 +1,54 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { listInstalledSkills, getSkillsDir, parseFrontmatter } from "@/lib/skills";
3
+ import path from "path";
4
+ import fs from "fs";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ // GET — list installed skills
9
+ export async function GET() {
10
+ return NextResponse.json(listInstalledSkills());
11
+ }
12
+
13
+ // POST — install skill from URL or raw markdown
14
+ export async function POST(req: NextRequest) {
15
+ try {
16
+ const { url, content, name } = await req.json() as {
17
+ url?: string; content?: string; name?: string;
18
+ };
19
+
20
+ let markdown = content ?? "";
21
+
22
+ if (url && !markdown) {
23
+ const res = await fetch(url, { signal: AbortSignal.timeout(15000) });
24
+ if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
25
+ markdown = await res.text();
26
+ }
27
+
28
+ if (!markdown.trim()) {
29
+ return NextResponse.json({ error: "No skill content provided" }, { status: 400 });
30
+ }
31
+
32
+ const fm = parseFrontmatter(markdown);
33
+ const skillName = ((fm.name as string) || name || "custom-skill")
34
+ .trim().toLowerCase()
35
+ .replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
36
+
37
+ if (!skillName) {
38
+ return NextResponse.json({ error: "Could not determine skill name" }, { status: 400 });
39
+ }
40
+
41
+ const dir = getSkillsDir();
42
+ fs.mkdirSync(dir, { recursive: true });
43
+
44
+ const skillDir = path.join(dir, skillName);
45
+ fs.mkdirSync(skillDir, { recursive: true });
46
+ fs.writeFileSync(path.join(skillDir, "SKILL.md"), markdown, "utf8");
47
+
48
+ const { loadSkillFromPath } = await import("@/lib/skills");
49
+ const skill = loadSkillFromPath(skillDir, skillName);
50
+ return NextResponse.json({ success: true, skill }, { status: 201 });
51
+ } catch (e) {
52
+ return NextResponse.json({ error: String(e) }, { status: 500 });
53
+ }
54
+ }
@@ -0,0 +1,60 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+
3
+ export const dynamic = "force-dynamic";
4
+
5
+ const SKILLS_REGISTRY = "https://skills.sh";
6
+
7
+ // Actual response shape from skills.sh/api/search
8
+ interface RegistrySkill {
9
+ id: string; // "tavily-ai/skills/tavily-search"
10
+ skillId: string; // "tavily-search"
11
+ name: string;
12
+ installs: number;
13
+ source: string; // "tavily-ai/skills"
14
+ }
15
+
16
+ export interface SkillSearchResult {
17
+ package: string; // "tavily-ai/skills@tavily-search"
18
+ name: string;
19
+ installs: number;
20
+ installsDisplay: string;
21
+ url: string;
22
+ }
23
+
24
+ function formatInstalls(n: number): string {
25
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
26
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
27
+ return String(n);
28
+ }
29
+
30
+ export async function POST(req: NextRequest) {
31
+ try {
32
+ const { query, limit = 30 } = await req.json() as { query: string; limit?: number };
33
+ if (!query?.trim()) return NextResponse.json({ results: [] });
34
+
35
+ const url = `${SKILLS_REGISTRY}/api/search?q=${encodeURIComponent(query.trim())}&limit=${Math.min(limit, 50)}`;
36
+ const res = await fetch(url, {
37
+ headers: { "User-Agent": "digital-agent-platform/1.0" },
38
+ signal: AbortSignal.timeout(10000),
39
+ });
40
+
41
+ if (!res.ok) {
42
+ return NextResponse.json({ error: `Registry ${res.status}`, results: [] }, { status: 502 });
43
+ }
44
+
45
+ const data = await res.json() as { skills?: RegistrySkill[] };
46
+ const skills: RegistrySkill[] = data.skills ?? [];
47
+
48
+ const results: SkillSearchResult[] = skills.map(s => ({
49
+ package: `${s.source}@${s.skillId}`,
50
+ name: s.name,
51
+ installs: s.installs,
52
+ installsDisplay: formatInstalls(s.installs),
53
+ url: `${SKILLS_REGISTRY}/${s.source}/${s.skillId}`,
54
+ }));
55
+
56
+ return NextResponse.json({ results });
57
+ } catch (e) {
58
+ return NextResponse.json({ error: String(e), results: [] }, { status: 500 });
59
+ }
60
+ }
@@ -0,0 +1,235 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import OpenAI from "openai";
3
+ import { readVoiceProviderConfig } from "@/lib/voice-providers-config";
4
+
5
+ export async function POST(req: NextRequest) {
6
+ try {
7
+ const { text, speed = 1.0 } = await req.json();
8
+
9
+ if (!text) {
10
+ return NextResponse.json({ error: "Text is required" }, { status: 400 });
11
+ }
12
+
13
+ // 读取全局语音供应商配置
14
+ const voiceConfig = readVoiceProviderConfig();
15
+ const provider = voiceConfig.currentProvider;
16
+
17
+ // 根据provider调用不同的TTS服务
18
+ switch (provider) {
19
+ case "openai":
20
+ return await handleOpenAITTS(text, speed, voiceConfig);
21
+ case "aliyun":
22
+ return await handleAliyunTTS(text, speed, voiceConfig);
23
+ case "tencent":
24
+ return await handleTencentTTS(text, speed, voiceConfig);
25
+ case "baidu":
26
+ return await handleBaiduTTS(text, speed, voiceConfig);
27
+ default:
28
+ return NextResponse.json({ error: "Unknown provider" }, { status: 400 });
29
+ }
30
+ } catch (error) {
31
+ console.error("TTS error:", error);
32
+ return NextResponse.json(
33
+ { error: error instanceof Error ? error.message : "TTS generation failed" },
34
+ { status: 500 }
35
+ );
36
+ }
37
+ }
38
+
39
+ async function handleOpenAITTS(text: string, speed: number, voiceConfig: any) {
40
+ const config = voiceConfig.providers.openai;
41
+ if (!config?.apiKey) {
42
+ return NextResponse.json({ error: "OpenAI API key not configured" }, { status: 500 });
43
+ }
44
+
45
+ const openai = new OpenAI({ apiKey: config.apiKey });
46
+
47
+ const mp3 = await openai.audio.speech.create({
48
+ model: (config.model || "tts-1") as "tts-1" | "tts-1-hd",
49
+ voice: (config.voice || "alloy") as "alloy" | "echo" | "fable" | "onyx" | "nova" | "shimmer",
50
+ input: text,
51
+ speed: speed,
52
+ });
53
+
54
+ const buffer = Buffer.from(await mp3.arrayBuffer());
55
+
56
+ return new NextResponse(buffer, {
57
+ headers: {
58
+ "Content-Type": "audio/mpeg",
59
+ "Content-Length": buffer.length.toString(),
60
+ },
61
+ });
62
+ }
63
+
64
+ async function handleAliyunTTS(text: string, speed: number, voiceConfig: any) {
65
+ const config = voiceConfig.providers.aliyun;
66
+ if (!config?.accessKeyId || !config?.accessKeySecret) {
67
+ return NextResponse.json({ error: "阿里云 AccessKey 未配置" }, { status: 500 });
68
+ }
69
+
70
+ // 阿里云TTS使用REST API
71
+ const appKey = config.appKey || "default";
72
+ const voice = config.voice || "xiaoyun";
73
+ const format = "mp3";
74
+ const sampleRate = 16000;
75
+
76
+ try {
77
+ // 生成Token(简化版,生产环境应使用完整的签名流程)
78
+ const tokenUrl = "https://nls-meta.cn-shanghai.aliyuncs.com/token";
79
+ const tokenResponse = await fetch(tokenUrl, {
80
+ method: "POST",
81
+ headers: {
82
+ "Content-Type": "application/json",
83
+ },
84
+ body: JSON.stringify({
85
+ AccessKeyId: config.accessKeyId,
86
+ Action: "CreateToken",
87
+ }),
88
+ });
89
+
90
+ if (!tokenResponse.ok) {
91
+ return NextResponse.json({ error: "获取阿里云Token失败" }, { status: 500 });
92
+ }
93
+
94
+ const tokenData = await tokenResponse.json();
95
+ const token = tokenData.Token?.Id;
96
+
97
+ if (!token) {
98
+ return NextResponse.json({ error: "阿里云Token无效" }, { status: 500 });
99
+ }
100
+
101
+ // 调用TTS API
102
+ const ttsUrl = `https://nls-gateway.cn-shanghai.aliyuncs.com/stream/v1/tts`;
103
+ const ttsResponse = await fetch(ttsUrl, {
104
+ method: "POST",
105
+ headers: {
106
+ "Content-Type": "application/json",
107
+ "X-NLS-Token": token,
108
+ },
109
+ body: JSON.stringify({
110
+ appkey: appKey,
111
+ text: text,
112
+ voice: voice,
113
+ format: format,
114
+ sample_rate: sampleRate,
115
+ speech_rate: Math.round(speed * 100),
116
+ }),
117
+ });
118
+
119
+ if (!ttsResponse.ok) {
120
+ return NextResponse.json({ error: "阿里云TTS调用失败" }, { status: 500 });
121
+ }
122
+
123
+ const audioBuffer = await ttsResponse.arrayBuffer();
124
+ const buffer = Buffer.from(audioBuffer);
125
+
126
+ return new NextResponse(buffer, {
127
+ headers: {
128
+ "Content-Type": "audio/mpeg",
129
+ "Content-Length": buffer.length.toString(),
130
+ },
131
+ });
132
+ } catch (error) {
133
+ console.error("阿里云TTS错误:", error);
134
+ return NextResponse.json({ error: "阿里云TTS处理失败" }, { status: 500 });
135
+ }
136
+ }
137
+
138
+ async function handleTencentTTS(text: string, speed: number, voiceConfig: any) {
139
+ const config = voiceConfig.providers.tencent;
140
+ if (!config?.secretId || !config?.secretKey) {
141
+ return NextResponse.json({ error: "腾讯云 SecretId/SecretKey 未配置" }, { status: 500 });
142
+ }
143
+
144
+ // 腾讯云TTS API参数
145
+ const voice = config.voice || "101001"; // 智瑜
146
+ const codec = "mp3";
147
+ const voiceType = 0;
148
+
149
+ try {
150
+ // 使用腾讯云TTS REST API
151
+ const endpoint = "https://tts.cloud.tencent.com/stream";
152
+ const params = new URLSearchParams({
153
+ Action: "TextToStreamAudio",
154
+ AppId: config.appId || "0",
155
+ SecretId: config.secretId,
156
+ Text: text,
157
+ ModelType: "1",
158
+ VoiceType: voice,
159
+ Codec: codec,
160
+ Speed: speed.toString(),
161
+ Timestamp: Math.floor(Date.now() / 1000).toString(),
162
+ });
163
+
164
+ const response = await fetch(`${endpoint}?${params.toString()}`);
165
+
166
+ if (!response.ok) {
167
+ return NextResponse.json({ error: "腾讯云TTS调用失败" }, { status: 500 });
168
+ }
169
+
170
+ const audioBuffer = await response.arrayBuffer();
171
+ const buffer = Buffer.from(audioBuffer);
172
+
173
+ return new NextResponse(buffer, {
174
+ headers: {
175
+ "Content-Type": "audio/mpeg",
176
+ "Content-Length": buffer.length.toString(),
177
+ },
178
+ });
179
+ } catch (error) {
180
+ console.error("腾讯云TTS错误:", error);
181
+ return NextResponse.json({ error: "腾讯云TTS处理失败" }, { status: 500 });
182
+ }
183
+ }
184
+
185
+ async function handleBaiduTTS(text: string, speed: number, voiceConfig: any) {
186
+ const config = voiceConfig.providers.baidu;
187
+ if (!config?.apiKey || !config?.secretKey) {
188
+ return NextResponse.json({ error: "百度 API Key/Secret Key 未配置" }, { status: 500 });
189
+ }
190
+
191
+ try {
192
+ // 获取百度access_token
193
+ const tokenUrl = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${config.apiKey}&client_secret=${config.secretKey}`;
194
+ const tokenResponse = await fetch(tokenUrl);
195
+ const tokenData = await tokenResponse.json();
196
+
197
+ if (!tokenData.access_token) {
198
+ return NextResponse.json({ error: "获取百度access_token失败" }, { status: 500 });
199
+ }
200
+
201
+ // 调用百度TTS API
202
+ const ttsUrl = `https://tsn.baidu.com/text2audio`;
203
+ const params = new URLSearchParams({
204
+ tex: text,
205
+ tok: tokenData.access_token,
206
+ cuid: "pi-web",
207
+ ctp: "1",
208
+ lan: "zh",
209
+ spd: Math.round(speed * 5).toString(), // 百度速度范围0-15
210
+ pit: "5",
211
+ vol: "5",
212
+ per: config.voice || "0", // 0=女声, 1=男声, 3=情感男声, 4=情感女声
213
+ aue: "3", // 3=mp3
214
+ });
215
+
216
+ const response = await fetch(`${ttsUrl}?${params.toString()}`);
217
+
218
+ if (!response.ok) {
219
+ return NextResponse.json({ error: "百度TTS调用失败" }, { status: 500 });
220
+ }
221
+
222
+ const audioBuffer = await response.arrayBuffer();
223
+ const buffer = Buffer.from(audioBuffer);
224
+
225
+ return new NextResponse(buffer, {
226
+ headers: {
227
+ "Content-Type": "audio/mpeg",
228
+ "Content-Length": buffer.length.toString(),
229
+ },
230
+ });
231
+ } catch (error) {
232
+ console.error("百度TTS错误:", error);
233
+ return NextResponse.json({ error: "百度TTS处理失败" }, { status: 500 });
234
+ }
235
+ }
@@ -0,0 +1,98 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import {
3
+ readVoiceProviderConfig,
4
+ writeVoiceProviderConfig,
5
+ type VoiceProviderConfig,
6
+ } from "@/lib/voice-providers-config";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ // GET - 返回当前语音供应商配置(API key脱敏)
11
+ export async function GET() {
12
+ const config = readVoiceProviderConfig();
13
+
14
+ // 脱敏处理:只显示key的最后4位
15
+ const maskedConfig = {
16
+ currentProvider: config.currentProvider,
17
+ providers: {} as any,
18
+ };
19
+
20
+ if (config.providers.aliyun) {
21
+ maskedConfig.providers.aliyun = {
22
+ appKey: maskKey(config.providers.aliyun.appKey),
23
+ accessKeyId: maskKey(config.providers.aliyun.accessKeyId),
24
+ accessKeySecret: maskKey(config.providers.aliyun.accessKeySecret),
25
+ };
26
+ }
27
+
28
+ if (config.providers.tencent) {
29
+ maskedConfig.providers.tencent = {
30
+ secretId: maskKey(config.providers.tencent.secretId),
31
+ secretKey: maskKey(config.providers.tencent.secretKey),
32
+ region: config.providers.tencent.region,
33
+ };
34
+ }
35
+
36
+ if (config.providers.baidu) {
37
+ maskedConfig.providers.baidu = {
38
+ apiKey: maskKey(config.providers.baidu.apiKey),
39
+ secretKey: maskKey(config.providers.baidu.secretKey),
40
+ };
41
+ }
42
+
43
+ if (config.providers.openai) {
44
+ maskedConfig.providers.openai = {
45
+ apiKey: maskKey(config.providers.openai.apiKey),
46
+ model: config.providers.openai.model,
47
+ voice: config.providers.openai.voice,
48
+ };
49
+ }
50
+
51
+ return NextResponse.json(maskedConfig);
52
+ }
53
+
54
+ function maskKey(key: string): string {
55
+ if (!key) return "";
56
+ if (key.length <= 4) return "***";
57
+ return "***" + key.slice(-4);
58
+ }
59
+
60
+ // PUT - 保存语音供应商配置
61
+ export async function PUT(req: NextRequest) {
62
+ try {
63
+ const body = await req.json() as Partial<VoiceProviderConfig>;
64
+ const current = readVoiceProviderConfig();
65
+
66
+ // 更新配置
67
+ if (body.currentProvider) {
68
+ current.currentProvider = body.currentProvider;
69
+ }
70
+
71
+ if (body.providers) {
72
+ // 处理各个provider的配置,跳过脱敏的值
73
+ for (const [provider, config] of Object.entries(body.providers)) {
74
+ if (!config) continue;
75
+
76
+ const masked: any = {};
77
+ for (const [key, value] of Object.entries(config)) {
78
+ // 跳过脱敏的值(以***开头)
79
+ if (typeof value === "string" && value.startsWith("***")) {
80
+ continue;
81
+ }
82
+ masked[key] = value;
83
+ }
84
+
85
+ // 合并到当前配置
86
+ current.providers[provider as keyof typeof current.providers] = {
87
+ ...(current.providers[provider as keyof typeof current.providers] || {}),
88
+ ...masked,
89
+ } as any;
90
+ }
91
+ }
92
+
93
+ writeVoiceProviderConfig(current);
94
+ return NextResponse.json({ success: true });
95
+ } catch (e) {
96
+ return NextResponse.json({ error: String(e) }, { status: 500 });
97
+ }
98
+ }
@@ -0,0 +1,151 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ import { agentStore } from "@/lib/agents/store";
6
+ import { resolveWorkspace } from "@/lib/agent-factory";
7
+
8
+ export const dynamic = "force-dynamic";
9
+
10
+ const IGNORED_DIRS = new Set([
11
+ "node_modules", ".git", ".next", "dist", "build", "__pycache__",
12
+ ".turbo", ".cache", "coverage", ".pytest_cache", ".mypy_cache",
13
+ "target", "vendor", ".DS_Store", ".venv", "venv",
14
+ ]);
15
+
16
+ const CODE_EXTS = new Set([
17
+ "ts", "tsx", "js", "jsx", "py", "rb", "go", "rs", "java", "cpp",
18
+ "c", "h", "cs", "swift", "kt", "vue", "svelte", "html", "css",
19
+ "scss", "json", "yaml", "yml", "toml", "md", "txt", "sh", "bash",
20
+ "zsh", "fish", "sql", "graphql", "proto", "xml",
21
+ ]);
22
+
23
+ function getFileIcon(name: string, isDir: boolean): string {
24
+ if (isDir) return "folder";
25
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
26
+ if (["ts", "tsx"].includes(ext)) return "file-ts";
27
+ if (["js", "jsx", "mjs"].includes(ext)) return "file-js";
28
+ if (["py"].includes(ext)) return "file-py";
29
+ if (["json", "jsonc"].includes(ext)) return "file-json";
30
+ if (["md", "mdx"].includes(ext)) return "file-md";
31
+ if (["css", "scss", "less"].includes(ext)) return "file-css";
32
+ if (["html", "htm"].includes(ext)) return "file-html";
33
+ if (["sh", "bash", "zsh"].includes(ext)) return "file-sh";
34
+ if (["yml", "yaml"].includes(ext)) return "file-yaml";
35
+ if (["png", "jpg", "jpeg", "gif", "svg", "webp", "ico"].includes(ext)) return "file-img";
36
+ if (CODE_EXTS.has(ext)) return "file-code";
37
+ return "file";
38
+ }
39
+
40
+ function formatSize(bytes: number): string {
41
+ if (bytes < 1024) return `${bytes}B`;
42
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}K`;
43
+ return `${(bytes / 1024 / 1024).toFixed(1)}M`;
44
+ }
45
+
46
+ export interface WorkspaceEntry {
47
+ name: string;
48
+ path: string; // absolute path
49
+ relativePath: string; // relative to workspace root
50
+ isDir: boolean;
51
+ size?: number;
52
+ sizeDisplay?: string;
53
+ modified?: number;
54
+ icon: string;
55
+ ext?: string;
56
+ }
57
+
58
+ export interface WorkspaceFilesResponse {
59
+ workspaceRoot: string;
60
+ workspaceDisplay: string;
61
+ currentPath: string;
62
+ currentDisplay: string;
63
+ parentPath: string | null;
64
+ entries: WorkspaceEntry[];
65
+ error?: string;
66
+ }
67
+
68
+ export async function GET(req: NextRequest) {
69
+ const agentId = req.nextUrl.searchParams.get("agentId");
70
+ const subPath = req.nextUrl.searchParams.get("path") ?? "";
71
+
72
+ if (!agentId) {
73
+ return NextResponse.json({ error: "agentId required" }, { status: 400 });
74
+ }
75
+
76
+ const agent = agentStore.getById(agentId);
77
+ if (!agent) {
78
+ return NextResponse.json({ error: "Agent not found" }, { status: 404 });
79
+ }
80
+
81
+ const workspaceRoot = resolveWorkspace(agent);
82
+ const targetPath = subPath
83
+ ? path.resolve(workspaceRoot, subPath)
84
+ : workspaceRoot;
85
+
86
+ // Security: must stay within workspace root
87
+ if (!targetPath.startsWith(workspaceRoot)) {
88
+ return NextResponse.json({ error: "Access denied" }, { status: 403 });
89
+ }
90
+
91
+ try {
92
+ const stat = fs.statSync(targetPath);
93
+ if (!stat.isDirectory()) {
94
+ return NextResponse.json({ error: "Not a directory" }, { status: 400 });
95
+ }
96
+
97
+ const raw = fs.readdirSync(targetPath, { withFileTypes: true });
98
+ const entries: WorkspaceEntry[] = [];
99
+
100
+ for (const e of raw) {
101
+ if (e.name.startsWith(".") && e.name !== ".env") continue;
102
+ const isDir = e.isDirectory() || (e.isSymbolicLink() &&
103
+ (() => { try { return fs.statSync(path.join(targetPath, e.name)).isDirectory(); } catch { return false; } })());
104
+ if (isDir && IGNORED_DIRS.has(e.name)) continue;
105
+
106
+ const fullPath = path.join(targetPath, e.name);
107
+ const relativePath = path.relative(workspaceRoot, fullPath);
108
+ const ext = e.name.split(".").pop()?.toLowerCase();
109
+ let size: number | undefined;
110
+ let modified: number | undefined;
111
+ try {
112
+ const s = fs.statSync(fullPath);
113
+ size = isDir ? undefined : s.size;
114
+ modified = s.mtimeMs;
115
+ } catch { /* ignore */ }
116
+
117
+ entries.push({
118
+ name: e.name,
119
+ path: fullPath,
120
+ relativePath,
121
+ isDir,
122
+ size,
123
+ sizeDisplay: size !== undefined ? formatSize(size) : undefined,
124
+ modified,
125
+ icon: getFileIcon(e.name, isDir),
126
+ ext,
127
+ });
128
+ }
129
+
130
+ // Sort: dirs first, then files, alphabetically
131
+ entries.sort((a, b) => {
132
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
133
+ return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
134
+ });
135
+
136
+ const home = os.homedir();
137
+ const parentPath = targetPath !== workspaceRoot ? path.dirname(targetPath) : null;
138
+
139
+ const resp: WorkspaceFilesResponse = {
140
+ workspaceRoot,
141
+ workspaceDisplay: workspaceRoot.replace(home, "~"),
142
+ currentPath: targetPath,
143
+ currentDisplay: targetPath.replace(home, "~"),
144
+ parentPath,
145
+ entries,
146
+ };
147
+ return NextResponse.json(resp);
148
+ } catch (e) {
149
+ return NextResponse.json({ error: String(e) }, { status: 500 });
150
+ }
151
+ }