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
package/lib/market-db.ts
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 技能市场数据库操作
|
|
3
|
+
* 提供技能市场的所有CRUD操作
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, unlinkSync } from 'fs';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import {
|
|
9
|
+
type MarketSkill,
|
|
10
|
+
type MarketSkillDetail,
|
|
11
|
+
type SkillRating,
|
|
12
|
+
type UploadToMarketRequest,
|
|
13
|
+
type RateSkillRequest,
|
|
14
|
+
type MarketSkillStatus,
|
|
15
|
+
} from './market-types';
|
|
16
|
+
import {
|
|
17
|
+
readSkillJsonFromZip,
|
|
18
|
+
readSkillMdFromZip,
|
|
19
|
+
cleanupZipFile,
|
|
20
|
+
} from './zip-tool';
|
|
21
|
+
import { getUserSkill } from './user-skills';
|
|
22
|
+
import { getUserById, getDb } from './db';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 生成唯一ID
|
|
26
|
+
*/
|
|
27
|
+
function generateId(prefix: string): string {
|
|
28
|
+
const timestamp = Date.now().toString(36);
|
|
29
|
+
const random = Math.random().toString(36).substring(2, 9);
|
|
30
|
+
return `${prefix}-${timestamp}${random}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 上传技能到市场
|
|
35
|
+
*/
|
|
36
|
+
export async function uploadSkillToMarket(
|
|
37
|
+
request: UploadToMarketRequest,
|
|
38
|
+
zipFilePath: string,
|
|
39
|
+
): Promise<MarketSkill> {
|
|
40
|
+
const db = getDb();
|
|
41
|
+
|
|
42
|
+
// 获取用户技能信息
|
|
43
|
+
const userSkill = getUserSkill(request.userId, request.skillName);
|
|
44
|
+
if (!userSkill) {
|
|
45
|
+
throw new Error('User skill not found');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 从ZIP读取技能元数据
|
|
49
|
+
const skillJson = readSkillJsonFromZip(zipFilePath);
|
|
50
|
+
|
|
51
|
+
// 获取用户名
|
|
52
|
+
const userName = request.userName || skillJson.author || request.userId;
|
|
53
|
+
const now = new Date().toISOString();
|
|
54
|
+
|
|
55
|
+
// 检查是否已存在同名技能
|
|
56
|
+
const existing = db
|
|
57
|
+
.prepare('SELECT id, author, file_path, display_name FROM skill_market WHERE name = ?')
|
|
58
|
+
.get(skillJson.name) as { id: string; author: string; file_path: string; display_name: string } | undefined;
|
|
59
|
+
|
|
60
|
+
if (existing) {
|
|
61
|
+
// 已有同名技能,追加作者名到 display_name
|
|
62
|
+
const authorSuffix = `-${userName}`;
|
|
63
|
+
let newDisplayName = skillJson.displayName || skillJson.name;
|
|
64
|
+
|
|
65
|
+
// 如果 display_name 中不包含作者名,则追加
|
|
66
|
+
if (!newDisplayName.includes(authorSuffix)) {
|
|
67
|
+
newDisplayName = `${newDisplayName}${authorSuffix}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 检查是否是同一作者
|
|
71
|
+
if (existing.author !== request.userId) {
|
|
72
|
+
// 不同作者:创建新记录(使用新的 name)
|
|
73
|
+
const newSkillId = generateId('skill');
|
|
74
|
+
const newName = `${skillJson.name}-${request.userId.slice(0, 8)}`;
|
|
75
|
+
|
|
76
|
+
const stmt = db.prepare(`
|
|
77
|
+
INSERT INTO skill_market (
|
|
78
|
+
id, name, display_name, description, version,
|
|
79
|
+
author, author_name, status, is_official,
|
|
80
|
+
rating_avg, rating_count, download_count, file_path,
|
|
81
|
+
created_at, updated_at
|
|
82
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
83
|
+
`);
|
|
84
|
+
|
|
85
|
+
stmt.run(
|
|
86
|
+
newSkillId,
|
|
87
|
+
newName,
|
|
88
|
+
newDisplayName,
|
|
89
|
+
skillJson.description || '',
|
|
90
|
+
skillJson.version || '1.0.0',
|
|
91
|
+
request.userId,
|
|
92
|
+
userName,
|
|
93
|
+
'pending',
|
|
94
|
+
0,
|
|
95
|
+
0,
|
|
96
|
+
0,
|
|
97
|
+
0,
|
|
98
|
+
zipFilePath,
|
|
99
|
+
now,
|
|
100
|
+
now,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return getMarketSkillById(newSkillId)!;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 同一作者:覆盖更新
|
|
107
|
+
const oldFilePath = existing.file_path;
|
|
108
|
+
|
|
109
|
+
const stmt = db.prepare(`
|
|
110
|
+
UPDATE skill_market
|
|
111
|
+
SET display_name = ?, description = ?, version = ?,
|
|
112
|
+
file_path = ?, updated_at = ?
|
|
113
|
+
WHERE id = ?
|
|
114
|
+
`);
|
|
115
|
+
|
|
116
|
+
stmt.run(
|
|
117
|
+
newDisplayName,
|
|
118
|
+
skillJson.description || '',
|
|
119
|
+
skillJson.version || '1.0.0',
|
|
120
|
+
zipFilePath,
|
|
121
|
+
now,
|
|
122
|
+
existing.id,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// 清理旧的 ZIP 文件
|
|
126
|
+
if (oldFilePath && existsSync(oldFilePath) && oldFilePath !== zipFilePath) {
|
|
127
|
+
try {
|
|
128
|
+
unlinkSync(oldFilePath);
|
|
129
|
+
console.log(`[market-db] Deleted old skill file: ${oldFilePath}`);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
console.error('[market-db] Failed to delete old skill file:', error);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return getMarketSkillById(existing.id)!;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 创建新的市场技能记录
|
|
139
|
+
const skillId = generateId('skill');
|
|
140
|
+
const stmt = db.prepare(`
|
|
141
|
+
INSERT INTO skill_market (
|
|
142
|
+
id, name, display_name, description, version,
|
|
143
|
+
author, author_name, status, is_official,
|
|
144
|
+
rating_avg, rating_count, download_count, file_path,
|
|
145
|
+
created_at, updated_at
|
|
146
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
147
|
+
`);
|
|
148
|
+
|
|
149
|
+
stmt.run(
|
|
150
|
+
skillId,
|
|
151
|
+
skillJson.name,
|
|
152
|
+
skillJson.displayName || skillJson.name,
|
|
153
|
+
skillJson.description || '',
|
|
154
|
+
skillJson.version || '1.0.0',
|
|
155
|
+
request.userId,
|
|
156
|
+
userName,
|
|
157
|
+
'approved', // 新技能默认审核通过
|
|
158
|
+
0, // 非官方
|
|
159
|
+
0, // 初始评分
|
|
160
|
+
0, // 初始评分数
|
|
161
|
+
0, // 初始下载次数
|
|
162
|
+
zipFilePath, // ZIP文件路径
|
|
163
|
+
now,
|
|
164
|
+
now,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return getMarketSkillById(skillId)!;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 根据ID获取技能元数据(不含内容)
|
|
172
|
+
*/
|
|
173
|
+
export function getMarketSkillById(skillId: string): MarketSkill | null {
|
|
174
|
+
const db = getDb();
|
|
175
|
+
const row = db
|
|
176
|
+
.prepare(`
|
|
177
|
+
SELECT * FROM skill_market WHERE id = ?
|
|
178
|
+
`)
|
|
179
|
+
.get(skillId) as any;
|
|
180
|
+
|
|
181
|
+
if (!row) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
id: row.id,
|
|
187
|
+
name: row.name,
|
|
188
|
+
displayName: row.display_name,
|
|
189
|
+
description: row.description,
|
|
190
|
+
version: row.version,
|
|
191
|
+
author: row.author,
|
|
192
|
+
authorName: row.author_name,
|
|
193
|
+
status: row.status as MarketSkillStatus,
|
|
194
|
+
isOfficial: row.is_official === 1,
|
|
195
|
+
ratingAvg: row.rating_avg,
|
|
196
|
+
ratingCount: row.rating_count,
|
|
197
|
+
downloadCount: row.download_count,
|
|
198
|
+
reviewedBy: row.reviewed_by,
|
|
199
|
+
reviewedAt: row.reviewed_at,
|
|
200
|
+
reviewComment: row.review_comment,
|
|
201
|
+
createdAt: row.created_at,
|
|
202
|
+
updatedAt: row.updated_at,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* 根据名称获取技能
|
|
208
|
+
*/
|
|
209
|
+
export function getMarketSkillByName(skillName: string): MarketSkill | null {
|
|
210
|
+
const db = getDb();
|
|
211
|
+
const row = db
|
|
212
|
+
.prepare(`
|
|
213
|
+
SELECT * FROM skill_market WHERE name = ?
|
|
214
|
+
`)
|
|
215
|
+
.get(skillName) as any;
|
|
216
|
+
|
|
217
|
+
if (!row) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
id: row.id,
|
|
223
|
+
name: row.name,
|
|
224
|
+
displayName: row.display_name,
|
|
225
|
+
description: row.description,
|
|
226
|
+
version: row.version,
|
|
227
|
+
author: row.author,
|
|
228
|
+
authorName: row.author_name,
|
|
229
|
+
status: row.status as MarketSkillStatus,
|
|
230
|
+
isOfficial: row.is_official === 1,
|
|
231
|
+
ratingAvg: row.rating_avg,
|
|
232
|
+
ratingCount: row.rating_count,
|
|
233
|
+
downloadCount: row.download_count,
|
|
234
|
+
reviewedBy: row.reviewed_by,
|
|
235
|
+
reviewedAt: row.reviewed_at,
|
|
236
|
+
reviewComment: row.review_comment,
|
|
237
|
+
createdAt: row.created_at,
|
|
238
|
+
updatedAt: row.updated_at,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 根据ID获取技能详情(包含内容)
|
|
244
|
+
*/
|
|
245
|
+
export function getMarketSkillDetailById(skillId: string): MarketSkillDetail | null {
|
|
246
|
+
const skill = getMarketSkillById(skillId);
|
|
247
|
+
if (!skill) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const db = getDb();
|
|
252
|
+
const row = db
|
|
253
|
+
.prepare('SELECT file_path FROM skill_market WHERE id = ?')
|
|
254
|
+
.get(skillId) as any;
|
|
255
|
+
|
|
256
|
+
if (!row || !row.file_path) {
|
|
257
|
+
throw new Error('Skill file not found');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 从ZIP读取内容
|
|
261
|
+
let content: string;
|
|
262
|
+
let riskConfig: any;
|
|
263
|
+
try {
|
|
264
|
+
content = readSkillMdFromZip(row.file_path);
|
|
265
|
+
const skillJson = readSkillJsonFromZip(row.file_path);
|
|
266
|
+
riskConfig = skillJson.riskConfig;
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error('Failed to read skill content from ZIP:', error);
|
|
269
|
+
throw new Error('Failed to read skill content');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
...skill,
|
|
274
|
+
content,
|
|
275
|
+
riskConfig,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 列出技能市场中的技能(支持过滤)
|
|
281
|
+
*/
|
|
282
|
+
export interface ListMarketSkillsOptions {
|
|
283
|
+
status?: MarketSkillStatus | 'all';
|
|
284
|
+
author?: string;
|
|
285
|
+
isOfficial?: boolean;
|
|
286
|
+
search?: string;
|
|
287
|
+
limit?: number;
|
|
288
|
+
offset?: number;
|
|
289
|
+
orderBy?: 'rating' | 'downloads' | 'created' | 'updated';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function listMarketSkills(
|
|
293
|
+
options: ListMarketSkillsOptions = {},
|
|
294
|
+
): { skills: MarketSkill[]; total: number } {
|
|
295
|
+
const db = getDb();
|
|
296
|
+
|
|
297
|
+
let query = 'SELECT * FROM skill_market WHERE 1=1';
|
|
298
|
+
const params: any[] = [];
|
|
299
|
+
|
|
300
|
+
// 状态过滤
|
|
301
|
+
if (options.status && options.status !== 'all') {
|
|
302
|
+
query += ' AND status = ?';
|
|
303
|
+
params.push(options.status);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// 作者过滤
|
|
307
|
+
if (options.author) {
|
|
308
|
+
query += ' AND author = ?';
|
|
309
|
+
params.push(options.author);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 官方认证过滤
|
|
313
|
+
if (options.isOfficial !== undefined) {
|
|
314
|
+
query += ' AND is_official = ?';
|
|
315
|
+
params.push(options.isOfficial ? 1 : 0);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// 搜索过滤
|
|
319
|
+
if (options.search) {
|
|
320
|
+
query += ' AND (display_name LIKE ? OR description LIKE ?)';
|
|
321
|
+
const searchTerm = `%${options.search}%`;
|
|
322
|
+
params.push(searchTerm, searchTerm);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 获取总数
|
|
326
|
+
const countQuery = query.replace('SELECT *', 'SELECT COUNT(*) as count');
|
|
327
|
+
const totalRow = db.prepare(countQuery).get(...params) as { count: number };
|
|
328
|
+
const total = totalRow.count;
|
|
329
|
+
|
|
330
|
+
// 排序
|
|
331
|
+
switch (options.orderBy) {
|
|
332
|
+
case 'rating':
|
|
333
|
+
query += ' ORDER BY rating_avg DESC, rating_count DESC';
|
|
334
|
+
break;
|
|
335
|
+
case 'downloads':
|
|
336
|
+
query += ' ORDER BY download_count DESC';
|
|
337
|
+
break;
|
|
338
|
+
case 'created':
|
|
339
|
+
query += ' ORDER BY created_at DESC';
|
|
340
|
+
break;
|
|
341
|
+
case 'updated':
|
|
342
|
+
default:
|
|
343
|
+
query += ' ORDER BY updated_at DESC';
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 分页
|
|
348
|
+
if (options.limit) {
|
|
349
|
+
query += ' LIMIT ?';
|
|
350
|
+
params.push(options.limit);
|
|
351
|
+
}
|
|
352
|
+
if (options.offset) {
|
|
353
|
+
query += ' OFFSET ?';
|
|
354
|
+
params.push(options.offset);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const rows = db.prepare(query).all(...params) as any[];
|
|
358
|
+
|
|
359
|
+
const skills: MarketSkill[] = rows.map((row) => ({
|
|
360
|
+
id: row.id,
|
|
361
|
+
name: row.name,
|
|
362
|
+
displayName: row.display_name,
|
|
363
|
+
description: row.description,
|
|
364
|
+
version: row.version,
|
|
365
|
+
author: row.author,
|
|
366
|
+
authorName: row.author_name,
|
|
367
|
+
status: row.status as MarketSkillStatus,
|
|
368
|
+
isOfficial: row.is_official === 1,
|
|
369
|
+
ratingAvg: row.rating_avg,
|
|
370
|
+
ratingCount: row.rating_count,
|
|
371
|
+
downloadCount: row.download_count,
|
|
372
|
+
reviewedBy: row.reviewed_by,
|
|
373
|
+
reviewedAt: row.reviewed_at,
|
|
374
|
+
reviewComment: row.review_comment,
|
|
375
|
+
createdAt: row.created_at,
|
|
376
|
+
updatedAt: row.updated_at,
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
return { skills, total };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 审核技能(管理员)
|
|
384
|
+
*/
|
|
385
|
+
export interface ReviewSkillRequest {
|
|
386
|
+
reviewerId: string;
|
|
387
|
+
reviewerName: string;
|
|
388
|
+
approved: boolean;
|
|
389
|
+
comment?: string;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function reviewSkill(
|
|
393
|
+
skillId: string,
|
|
394
|
+
request: ReviewSkillRequest,
|
|
395
|
+
): MarketSkill {
|
|
396
|
+
const db = getDb();
|
|
397
|
+
|
|
398
|
+
// 检查技能是否存在
|
|
399
|
+
const skill = getMarketSkillById(skillId);
|
|
400
|
+
if (!skill) {
|
|
401
|
+
throw new Error('Skill not found');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 只能审核pending状态的技能
|
|
405
|
+
if (skill.status !== 'pending') {
|
|
406
|
+
throw new Error('Skill is not pending review');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const now = new Date().toISOString();
|
|
410
|
+
const newStatus = request.approved ? 'approved' : 'rejected';
|
|
411
|
+
|
|
412
|
+
const stmt = db.prepare(`
|
|
413
|
+
UPDATE skill_market
|
|
414
|
+
SET status = ?, reviewed_by = ?, reviewed_at = ?, review_comment = ?, updated_at = ?
|
|
415
|
+
WHERE id = ?
|
|
416
|
+
`);
|
|
417
|
+
|
|
418
|
+
stmt.run(
|
|
419
|
+
newStatus,
|
|
420
|
+
request.reviewerId,
|
|
421
|
+
now,
|
|
422
|
+
request.comment || '',
|
|
423
|
+
now,
|
|
424
|
+
skillId,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
return getMarketSkillById(skillId)!;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 设置/取消官方认证
|
|
432
|
+
*/
|
|
433
|
+
export function certifySkill(
|
|
434
|
+
skillId: string,
|
|
435
|
+
isOfficial: boolean,
|
|
436
|
+
reviewerId: string,
|
|
437
|
+
): MarketSkill {
|
|
438
|
+
const db = getDb();
|
|
439
|
+
|
|
440
|
+
// 检查技能是否存在
|
|
441
|
+
const skill = getMarketSkillById(skillId);
|
|
442
|
+
if (!skill) {
|
|
443
|
+
throw new Error('Skill not found');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 只能认证已审核通过的技能
|
|
447
|
+
if (skill.status !== 'approved') {
|
|
448
|
+
throw new Error('Can only certify approved skills');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const now = new Date().toISOString();
|
|
452
|
+
|
|
453
|
+
const stmt = db.prepare(`
|
|
454
|
+
UPDATE skill_market
|
|
455
|
+
SET is_official = ?, reviewed_by = ?, reviewed_at = ?, updated_at = ?
|
|
456
|
+
WHERE id = ?
|
|
457
|
+
`);
|
|
458
|
+
|
|
459
|
+
stmt.run(
|
|
460
|
+
isOfficial ? 1 : 0,
|
|
461
|
+
reviewerId,
|
|
462
|
+
now,
|
|
463
|
+
now,
|
|
464
|
+
skillId,
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
return getMarketSkillById(skillId)!;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* 删除技能(包括文件清理)
|
|
472
|
+
* @param skillId 技能ID
|
|
473
|
+
* @param userId 用户ID
|
|
474
|
+
* @param userRole 用户角色,admin 可以删除任何技能
|
|
475
|
+
*/
|
|
476
|
+
export function deleteMarketSkill(skillId: string, userId: string, userRole?: string): boolean {
|
|
477
|
+
const db = getDb();
|
|
478
|
+
|
|
479
|
+
// 检查技能是否存在
|
|
480
|
+
const skill = getMarketSkillById(skillId);
|
|
481
|
+
if (!skill) {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 检查权限:只有作者或管理员可以删除
|
|
486
|
+
if (skill.author !== userId && userRole !== 'admin') {
|
|
487
|
+
throw new Error('You do not have permission to delete this skill');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 获取文件路径
|
|
491
|
+
const row = db
|
|
492
|
+
.prepare('SELECT file_path FROM skill_market WHERE id = ?')
|
|
493
|
+
.get(skillId) as any;
|
|
494
|
+
const filePath = row?.file_path;
|
|
495
|
+
|
|
496
|
+
// 删除数据库记录(外键会自动删除评分)
|
|
497
|
+
const stmt = db.prepare('DELETE FROM skill_market WHERE id = ?');
|
|
498
|
+
const result = stmt.run(skillId);
|
|
499
|
+
|
|
500
|
+
if (result.changes === 0) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// 清理文件
|
|
505
|
+
if (filePath && existsSync(filePath)) {
|
|
506
|
+
try {
|
|
507
|
+
unlinkSync(filePath);
|
|
508
|
+
console.log(`[market-db] Deleted skill file: ${filePath}`);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
console.error('[market-db] Failed to delete skill file:', error);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* 增加下载次数
|
|
519
|
+
*/
|
|
520
|
+
export function incrementDownloadCount(skillId: string): void {
|
|
521
|
+
const db = getDb();
|
|
522
|
+
|
|
523
|
+
const stmt = db.prepare(`
|
|
524
|
+
UPDATE skill_market
|
|
525
|
+
SET download_count = download_count + 1
|
|
526
|
+
WHERE id = ?
|
|
527
|
+
`);
|
|
528
|
+
|
|
529
|
+
stmt.run(skillId);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 添加/更新用户评分
|
|
534
|
+
*/
|
|
535
|
+
export function addRating(skillId: string, request: RateSkillRequest): SkillRating {
|
|
536
|
+
const db = getDb();
|
|
537
|
+
|
|
538
|
+
// 检查技能是否存在
|
|
539
|
+
const skill = getMarketSkillById(skillId);
|
|
540
|
+
if (!skill) {
|
|
541
|
+
throw new Error('Skill not found');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 验证评分范围
|
|
545
|
+
if (request.rating < 1 || request.rating > 5) {
|
|
546
|
+
throw new Error('Rating must be between 1 and 5');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// 检查是否已评分
|
|
550
|
+
const existing = db
|
|
551
|
+
.prepare('SELECT id FROM skill_ratings WHERE skill_id = ? AND user_id = ?')
|
|
552
|
+
.get(skillId, request.userId) as any;
|
|
553
|
+
|
|
554
|
+
const now = new Date().toISOString();
|
|
555
|
+
|
|
556
|
+
if (existing) {
|
|
557
|
+
// 更新现有评分
|
|
558
|
+
const stmt = db.prepare(`
|
|
559
|
+
UPDATE skill_ratings
|
|
560
|
+
SET rating = ?, comment = ?, created_at = ?
|
|
561
|
+
WHERE skill_id = ? AND user_id = ?
|
|
562
|
+
`);
|
|
563
|
+
stmt.run(request.rating, request.comment || null, now, skillId, request.userId);
|
|
564
|
+
} else {
|
|
565
|
+
// 创建新评分
|
|
566
|
+
const ratingId = generateId('rating');
|
|
567
|
+
const stmt = db.prepare(`
|
|
568
|
+
INSERT INTO skill_ratings (id, skill_id, user_id, rating, comment, created_at)
|
|
569
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
570
|
+
`);
|
|
571
|
+
stmt.run(ratingId, skillId, request.userId, request.rating, request.comment || null, now);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// 更新平均评分
|
|
575
|
+
updateAverageRating(skillId);
|
|
576
|
+
|
|
577
|
+
// 返回更新后的评分
|
|
578
|
+
return getSkillRatings(skillId).find((r) => r.userId === request.userId)!;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* 获取技能的所有评分
|
|
583
|
+
*/
|
|
584
|
+
export function getSkillRatings(skillId: string): SkillRating[] {
|
|
585
|
+
const db = getDb();
|
|
586
|
+
|
|
587
|
+
const rows = db
|
|
588
|
+
.prepare('SELECT * FROM skill_ratings WHERE skill_id = ? ORDER BY created_at DESC')
|
|
589
|
+
.all(skillId) as any[];
|
|
590
|
+
|
|
591
|
+
return rows.map((row) => ({
|
|
592
|
+
id: row.id,
|
|
593
|
+
skillId: row.skill_id,
|
|
594
|
+
userId: row.user_id,
|
|
595
|
+
rating: row.rating,
|
|
596
|
+
comment: row.comment,
|
|
597
|
+
createdAt: row.created_at,
|
|
598
|
+
}));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* 获取用户对特定技能的评分
|
|
603
|
+
*/
|
|
604
|
+
export function getUserRating(skillId: string, userId: string): SkillRating | null {
|
|
605
|
+
const db = getDb();
|
|
606
|
+
|
|
607
|
+
const row = db
|
|
608
|
+
.prepare('SELECT * FROM skill_ratings WHERE skill_id = ? AND user_id = ?')
|
|
609
|
+
.get(skillId, userId) as any;
|
|
610
|
+
|
|
611
|
+
if (!row) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
return {
|
|
616
|
+
id: row.id,
|
|
617
|
+
skillId: row.skill_id,
|
|
618
|
+
userId: row.user_id,
|
|
619
|
+
rating: row.rating,
|
|
620
|
+
comment: row.comment,
|
|
621
|
+
createdAt: row.created_at,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* 更新平均评分(内部函数)
|
|
627
|
+
*/
|
|
628
|
+
function updateAverageRating(skillId: string): void {
|
|
629
|
+
const db = getDb();
|
|
630
|
+
|
|
631
|
+
// 计算新的平均评分
|
|
632
|
+
const result = db
|
|
633
|
+
.prepare(`
|
|
634
|
+
SELECT
|
|
635
|
+
COUNT(*) as count,
|
|
636
|
+
AVG(rating) as average
|
|
637
|
+
FROM skill_ratings
|
|
638
|
+
WHERE skill_id = ?
|
|
639
|
+
`)
|
|
640
|
+
.get(skillId) as { count: number; average: number };
|
|
641
|
+
|
|
642
|
+
const stmt = db.prepare(`
|
|
643
|
+
UPDATE skill_market
|
|
644
|
+
SET rating_avg = ?, rating_count = ?, updated_at = ?
|
|
645
|
+
WHERE id = ?
|
|
646
|
+
`);
|
|
647
|
+
|
|
648
|
+
stmt.run(
|
|
649
|
+
result.average || 0,
|
|
650
|
+
result.count || 0,
|
|
651
|
+
new Date().toISOString(),
|
|
652
|
+
skillId,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* 获取用户已安装的市场技能ID列表
|
|
658
|
+
*/
|
|
659
|
+
export function getUserInstalledSkills(userId: string): string[] {
|
|
660
|
+
const db = getDb();
|
|
661
|
+
|
|
662
|
+
const rows = db
|
|
663
|
+
.prepare('SELECT skill_id FROM user_skills WHERE user_id = ? AND source = ? AND skill_id IS NOT NULL')
|
|
664
|
+
.all(userId, 'market') as { skill_id: string }[];
|
|
665
|
+
|
|
666
|
+
return rows.map((row) => row.skill_id).filter(Boolean);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* 添加用户已安装技能记录
|
|
671
|
+
*/
|
|
672
|
+
export function addUserInstalledSkill(userId: string, skillId: string): void {
|
|
673
|
+
const db = getDb();
|
|
674
|
+
|
|
675
|
+
const skill = db.prepare('SELECT name, display_name FROM skill_market WHERE id = ?').get(skillId) as { name: string; display_name: string } | undefined;
|
|
676
|
+
if (!skill) {
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const now = new Date().toISOString();
|
|
681
|
+
const skillName = skill.display_name || skill.name;
|
|
682
|
+
|
|
683
|
+
const existing = db.prepare('SELECT id FROM user_skills WHERE user_id = ? AND skill_name = ?').get(userId, skillName) as { id: string } | undefined;
|
|
684
|
+
if (!existing) {
|
|
685
|
+
const userSkillId = `us-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
686
|
+
db.prepare(`
|
|
687
|
+
INSERT INTO user_skills (id, user_id, skill_name, dir_name, source, skill_id, use_count, installed_at)
|
|
688
|
+
VALUES (?, ?, ?, ?, 'market', ?, 0, ?)
|
|
689
|
+
`).run(userSkillId, userId, skillName, skillName, skillId, now);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* 检查用户是否已安装技能
|
|
695
|
+
*/
|
|
696
|
+
export function isSkillInstalledByUser(userId: string, skillId: string): boolean {
|
|
697
|
+
const db = getDb();
|
|
698
|
+
|
|
699
|
+
const row = db
|
|
700
|
+
.prepare('SELECT id FROM user_skills WHERE user_id = ? AND skill_id = ? AND source = ?')
|
|
701
|
+
.get(userId, skillId, 'market') as { id: string } | undefined;
|
|
702
|
+
|
|
703
|
+
return !!row;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* 删除用户已安装技能记录
|
|
708
|
+
*/
|
|
709
|
+
export function deleteUserInstalledSkill(userId: string, skillId: string): boolean {
|
|
710
|
+
const db = getDb();
|
|
711
|
+
|
|
712
|
+
const stmt = db.prepare(`
|
|
713
|
+
DELETE FROM user_skills
|
|
714
|
+
WHERE user_id = ? AND skill_id = ? AND source = 'market'
|
|
715
|
+
`);
|
|
716
|
+
|
|
717
|
+
const result = stmt.run(userId, skillId);
|
|
718
|
+
return result.changes > 0;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* 获取技能作者信息(用于显示)
|
|
723
|
+
*/
|
|
724
|
+
export async function getSkillAuthorInfo(authorId: string): Promise<{
|
|
725
|
+
id: string;
|
|
726
|
+
username?: string;
|
|
727
|
+
} | null> {
|
|
728
|
+
const user = getUserById(authorId);
|
|
729
|
+
if (!user) {
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
id: user.id,
|
|
734
|
+
username: user.username,
|
|
735
|
+
};
|
|
736
|
+
}
|