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,135 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const FEISHU_APP_ID = process.env.FEISHU_APP_ID || '';
|
|
4
|
+
const FEISHU_APP_SECRET = process.env.FEISHU_APP_SECRET || '';
|
|
5
|
+
const FEISHU_REDIRECT_URI = process.env.FEISHU_REDIRECT_URI ||
|
|
6
|
+
`http://work-agent.agent-platform.dev.aimstek.cn/api/auth/feishu/callback`;
|
|
7
|
+
|
|
8
|
+
export interface FeishuUserInfo {
|
|
9
|
+
union_id: string;
|
|
10
|
+
open_id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
avatar_url?: string;
|
|
13
|
+
avatar_thumb?: string;
|
|
14
|
+
avatar_middle?: string;
|
|
15
|
+
avatar_big?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FeishuTokenResponse {
|
|
19
|
+
access_token: string;
|
|
20
|
+
token_type: string;
|
|
21
|
+
expires_in: number;
|
|
22
|
+
refresh_token?: string;
|
|
23
|
+
scope?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let cachedAppAccessToken: string | null = null;
|
|
27
|
+
let appAccessTokenExpiresAt: number = 0;
|
|
28
|
+
|
|
29
|
+
export function getFeishuLoginUrl(state?: string): string {
|
|
30
|
+
const params = new URLSearchParams({
|
|
31
|
+
app_id: FEISHU_APP_ID,
|
|
32
|
+
redirect_uri: FEISHU_REDIRECT_URI,
|
|
33
|
+
state: state || '',
|
|
34
|
+
});
|
|
35
|
+
return `https://open.feishu.cn/open-apis/authen/v1/index?${params.toString()}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function getAppAccessToken(): Promise<string> {
|
|
39
|
+
const now = Date.now();
|
|
40
|
+
if (cachedAppAccessToken && now < appAccessTokenExpiresAt) {
|
|
41
|
+
return cachedAppAccessToken;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const response = await axios.post(
|
|
45
|
+
'https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal',
|
|
46
|
+
{
|
|
47
|
+
app_id: FEISHU_APP_ID,
|
|
48
|
+
app_secret: FEISHU_APP_SECRET,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (response.data.code !== 0) {
|
|
58
|
+
throw new Error(`获取app_access_token失败: ${response.data.msg} (code: ${response.data.code})`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const data = response.data;
|
|
62
|
+
if (!data || !data.app_access_token) {
|
|
63
|
+
throw new Error(`获取app_access_token返回数据异常: ${JSON.stringify(data)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
cachedAppAccessToken = data.app_access_token;
|
|
67
|
+
appAccessTokenExpiresAt = Date.now() + (data.expire - 300) * 1000;
|
|
68
|
+
|
|
69
|
+
return cachedAppAccessToken!;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function getFeishuAccessToken(code: string): Promise<FeishuTokenResponse> {
|
|
73
|
+
const appAccessToken = await getAppAccessToken();
|
|
74
|
+
|
|
75
|
+
const response = await axios.post(
|
|
76
|
+
'https://open.feishu.cn/open-apis/authen/v1/oidc/access_token',
|
|
77
|
+
{
|
|
78
|
+
grant_type: 'authorization_code',
|
|
79
|
+
code,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
headers: {
|
|
83
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
84
|
+
'Authorization': `Bearer ${appAccessToken}`,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (response.data.code !== 0) {
|
|
90
|
+
throw new Error(`获取飞书access_token失败: ${response.data.msg} (code: ${response.data.code})`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return response.data.data;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function getFeishuUserInfo(accessToken: string): Promise<FeishuUserInfo> {
|
|
97
|
+
const response = await axios.get(
|
|
98
|
+
'https://open.feishu.cn/open-apis/authen/v1/user_info',
|
|
99
|
+
{
|
|
100
|
+
headers: {
|
|
101
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (response.data.code !== 0) {
|
|
107
|
+
throw new Error(`获取飞书用户信息失败: ${response.data.msg}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return response.data.data;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function refreshFeishuAccessToken(refreshToken: string): Promise<FeishuTokenResponse> {
|
|
114
|
+
const appAccessToken = await getAppAccessToken();
|
|
115
|
+
|
|
116
|
+
const response = await axios.post(
|
|
117
|
+
'https://open.feishu.cn/open-apis/authen/v1/oidc/refresh_access_token',
|
|
118
|
+
{
|
|
119
|
+
grant_type: 'refresh_token',
|
|
120
|
+
refresh_token: refreshToken,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
125
|
+
'Authorization': `Bearer ${appAccessToken}`,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (response.data.code !== 0) {
|
|
131
|
+
throw new Error(`刷新飞书access_token失败: ${response.data.msg}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return response.data.data;
|
|
135
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { mkdirSync, existsSync, writeFileSync, readFileSync, unlinkSync, statSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { createHash } from 'crypto';
|
|
4
|
+
import type { FileMetadata } from './db';
|
|
5
|
+
|
|
6
|
+
export interface UploadFileOptions {
|
|
7
|
+
userId: string;
|
|
8
|
+
filename: string;
|
|
9
|
+
mimeType: string;
|
|
10
|
+
buffer: Buffer;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface StorageProvider {
|
|
14
|
+
upload(options: UploadFileOptions): Promise<{ storageKey: string; md5: string }>;
|
|
15
|
+
download(storageKey: string): Promise<Buffer>;
|
|
16
|
+
delete(storageKey: string): Promise<void>;
|
|
17
|
+
getUrl(storageKey: string): string;
|
|
18
|
+
exists(storageKey: string): boolean;
|
|
19
|
+
getSize(storageKey: string): number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class LocalStorageProvider implements StorageProvider {
|
|
23
|
+
private baseDir: string;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
this.baseDir = join(process.cwd(), 'data', 'users');
|
|
27
|
+
this.ensureBaseDir();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private ensureBaseDir() {
|
|
31
|
+
if (!existsSync(this.baseDir)) {
|
|
32
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private getUserDir(userId: string): string {
|
|
37
|
+
const userDir = join(this.baseDir, userId, 'files');
|
|
38
|
+
if (!existsSync(userDir)) {
|
|
39
|
+
mkdirSync(userDir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
return userDir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async upload(options: UploadFileOptions): Promise<{ storageKey: string; md5: string }> {
|
|
45
|
+
const { userId, filename, buffer } = options;
|
|
46
|
+
const userDir = this.getUserDir(userId);
|
|
47
|
+
|
|
48
|
+
const md5 = createHash('md5').update(buffer).digest('hex');
|
|
49
|
+
const uuid = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
50
|
+
const safeFilename = filename.replace(/[^a-zA-Z0-9\u4e00-\u9fa5._-]/g, '_');
|
|
51
|
+
const storageKey = `${uuid}_${safeFilename}`;
|
|
52
|
+
|
|
53
|
+
const filePath = join(userDir, storageKey);
|
|
54
|
+
writeFileSync(filePath, buffer);
|
|
55
|
+
|
|
56
|
+
return { storageKey, md5 };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async download(storageKey: string): Promise<Buffer> {
|
|
60
|
+
const parts = storageKey.split('_');
|
|
61
|
+
if (parts.length < 2) {
|
|
62
|
+
throw new Error('Invalid storage key format');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const userId = parts[0];
|
|
66
|
+
const userDir = this.getUserDir(userId);
|
|
67
|
+
const filePath = join(userDir, storageKey);
|
|
68
|
+
|
|
69
|
+
if (!existsSync(filePath)) {
|
|
70
|
+
throw new Error(`File not found: ${storageKey}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return readFileSync(filePath);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async delete(storageKey: string): Promise<void> {
|
|
77
|
+
const parts = storageKey.split('_');
|
|
78
|
+
if (parts.length < 2) {
|
|
79
|
+
throw new Error('Invalid storage key format');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const userId = parts[0];
|
|
83
|
+
const userDir = this.getUserDir(userId);
|
|
84
|
+
const filePath = join(userDir, storageKey);
|
|
85
|
+
|
|
86
|
+
if (existsSync(filePath)) {
|
|
87
|
+
unlinkSync(filePath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
getUrl(storageKey: string): string {
|
|
92
|
+
return `/api/my/files/${storageKey}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
exists(storageKey: string): boolean {
|
|
96
|
+
try {
|
|
97
|
+
const parts = storageKey.split('_');
|
|
98
|
+
if (parts.length < 2) return false;
|
|
99
|
+
|
|
100
|
+
const userId = parts[0];
|
|
101
|
+
const userDir = this.getUserDir(userId);
|
|
102
|
+
const filePath = join(userDir, storageKey);
|
|
103
|
+
|
|
104
|
+
return existsSync(filePath);
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getSize(storageKey: string): number {
|
|
111
|
+
const parts = storageKey.split('_');
|
|
112
|
+
if (parts.length < 2) {
|
|
113
|
+
throw new Error('Invalid storage key format');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const userId = parts[0];
|
|
117
|
+
const userDir = this.getUserDir(userId);
|
|
118
|
+
const filePath = join(userDir, storageKey);
|
|
119
|
+
|
|
120
|
+
if (!existsSync(filePath)) {
|
|
121
|
+
throw new Error(`File not found: ${storageKey}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return statSync(filePath).size;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
class LocalStorageProviderV2 implements StorageProvider {
|
|
129
|
+
private baseDir: string;
|
|
130
|
+
|
|
131
|
+
constructor() {
|
|
132
|
+
this.baseDir = join(process.cwd(), 'data', 'users');
|
|
133
|
+
this.ensureBaseDir();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private ensureBaseDir() {
|
|
137
|
+
if (!existsSync(this.baseDir)) {
|
|
138
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private getUserDir(userId: string): string {
|
|
143
|
+
const userDir = join(this.baseDir, userId, 'files');
|
|
144
|
+
if (!existsSync(userDir)) {
|
|
145
|
+
mkdirSync(userDir, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
return userDir;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async upload(options: UploadFileOptions): Promise<{ storageKey: string; md5: string }> {
|
|
151
|
+
const { userId, filename, buffer } = options;
|
|
152
|
+
const userDir = this.getUserDir(userId);
|
|
153
|
+
|
|
154
|
+
const md5 = createHash('md5').update(buffer).digest('hex');
|
|
155
|
+
const uuid = `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
156
|
+
const safeFilename = filename.replace(/[^a-zA-Z0-9\u4e00-\u9fa5._-]/g, '_');
|
|
157
|
+
const storageKey = `${userId}/${uuid}_${safeFilename}`;
|
|
158
|
+
|
|
159
|
+
const filePath = join(userDir, `${uuid}_${safeFilename}`);
|
|
160
|
+
writeFileSync(filePath, buffer);
|
|
161
|
+
|
|
162
|
+
return { storageKey, md5 };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async download(storageKey: string): Promise<Buffer> {
|
|
166
|
+
const parts = storageKey.split('/');
|
|
167
|
+
if (parts.length < 2) {
|
|
168
|
+
throw new Error('Invalid storage key format');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const userId = parts[0];
|
|
172
|
+
const filename = parts.slice(1).join('/');
|
|
173
|
+
const userDir = this.getUserDir(userId);
|
|
174
|
+
const filePath = join(userDir, filename);
|
|
175
|
+
|
|
176
|
+
if (!existsSync(filePath)) {
|
|
177
|
+
throw new Error(`File not found: ${storageKey}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return readFileSync(filePath);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async delete(storageKey: string): Promise<void> {
|
|
184
|
+
const parts = storageKey.split('/');
|
|
185
|
+
if (parts.length < 2) {
|
|
186
|
+
throw new Error('Invalid storage key format');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const userId = parts[0];
|
|
190
|
+
const filename = parts.slice(1).join('/');
|
|
191
|
+
const userDir = this.getUserDir(userId);
|
|
192
|
+
const filePath = join(userDir, filename);
|
|
193
|
+
|
|
194
|
+
if (existsSync(filePath)) {
|
|
195
|
+
unlinkSync(filePath);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getUrl(storageKey: string): string {
|
|
200
|
+
const encodedKey = encodeURIComponent(storageKey);
|
|
201
|
+
return `/api/my/files/download?key=${encodedKey}`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
exists(storageKey: string): boolean {
|
|
205
|
+
try {
|
|
206
|
+
const parts = storageKey.split('/');
|
|
207
|
+
if (parts.length < 2) return false;
|
|
208
|
+
|
|
209
|
+
const userId = parts[0];
|
|
210
|
+
const filename = parts.slice(1).join('/');
|
|
211
|
+
const userDir = this.getUserDir(userId);
|
|
212
|
+
const filePath = join(userDir, filename);
|
|
213
|
+
|
|
214
|
+
return existsSync(filePath);
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
getSize(storageKey: string): number {
|
|
221
|
+
const parts = storageKey.split('/');
|
|
222
|
+
if (parts.length < 2) {
|
|
223
|
+
throw new Error('Invalid storage key format');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const userId = parts[0];
|
|
227
|
+
const filename = parts.slice(1).join('/');
|
|
228
|
+
const userDir = this.getUserDir(userId);
|
|
229
|
+
const filePath = join(userDir, filename);
|
|
230
|
+
|
|
231
|
+
if (!existsSync(filePath)) {
|
|
232
|
+
throw new Error(`File not found: ${storageKey}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return statSync(filePath).size;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let storageProvider: StorageProvider | null = null;
|
|
240
|
+
|
|
241
|
+
export function getStorageProvider(): StorageProvider {
|
|
242
|
+
if (!storageProvider) {
|
|
243
|
+
const storageType = process.env.FILE_STORAGE_TYPE || 'local';
|
|
244
|
+
switch (storageType) {
|
|
245
|
+
case 'local':
|
|
246
|
+
default:
|
|
247
|
+
storageProvider = new LocalStorageProviderV2();
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return storageProvider;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export function resetStorageProvider(): void {
|
|
255
|
+
storageProvider = null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const MAX_FILE_SIZE = 50 * 1024 * 1024;
|
|
259
|
+
|
|
260
|
+
export const ALLOWED_MIME_TYPES = [
|
|
261
|
+
'image/jpeg',
|
|
262
|
+
'image/png',
|
|
263
|
+
'image/gif',
|
|
264
|
+
'image/webp',
|
|
265
|
+
'application/pdf',
|
|
266
|
+
'application/msword',
|
|
267
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
268
|
+
'application/vnd.ms-excel',
|
|
269
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
270
|
+
'application/vnd.ms-powerpoint',
|
|
271
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
272
|
+
'text/plain',
|
|
273
|
+
'text/csv',
|
|
274
|
+
'text/markdown',
|
|
275
|
+
'application/json',
|
|
276
|
+
'application/xml',
|
|
277
|
+
'text/html',
|
|
278
|
+
'text/css',
|
|
279
|
+
'application/javascript',
|
|
280
|
+
'text/javascript',
|
|
281
|
+
'application/zip',
|
|
282
|
+
'application/x-tar',
|
|
283
|
+
'application/gzip',
|
|
284
|
+
'application/x-gzip',
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
export function isAllowedMimeType(mimeType: string): boolean {
|
|
288
|
+
if (!mimeType || mimeType === '') return true;
|
|
289
|
+
return ALLOWED_MIME_TYPES.includes(mimeType) ||
|
|
290
|
+
mimeType.startsWith('text/') ||
|
|
291
|
+
mimeType.startsWith('image/') ||
|
|
292
|
+
mimeType === 'application/octet-stream';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function getFileExtension(filename: string): string {
|
|
296
|
+
const parts = filename.split('.');
|
|
297
|
+
return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : '';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function formatFileSize(bytes: number): string {
|
|
301
|
+
if (bytes === 0) return '0 B';
|
|
302
|
+
const k = 1024;
|
|
303
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
304
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
305
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
306
|
+
}
|