@weibaohui/skills-management 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 +79 -0
- package/client/bundle.js +1380 -0
- package/client/components/skills/helpers.ts +108 -0
- package/client/hooks/index.ts +10 -0
- package/client/hooks/useSkillCatalog.ts +201 -0
- package/client/hooks/useSkillDetail.ts +89 -0
- package/client/hooks/useSkillInstall.ts +74 -0
- package/client/index.js +1366 -0
- package/cordis.patch.yml +12 -0
- package/package.json +61 -0
- package/src/index.js +1073 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills management helpers - ported from ntd
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// 根据文件扩展名返回对应图标颜色
|
|
6
|
+
export function getFileColor(filename: string): string {
|
|
7
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
8
|
+
const colorMap: Record<string, string> = {
|
|
9
|
+
md: '#0891b2',
|
|
10
|
+
ts: '#3178c6',
|
|
11
|
+
tsx: '#3178c6',
|
|
12
|
+
js: '#f7df1e',
|
|
13
|
+
jsx: '#f7df1e',
|
|
14
|
+
json: '#f59e0b',
|
|
15
|
+
yaml: '#e11d48',
|
|
16
|
+
yml: '#e11d48',
|
|
17
|
+
toml: '#9333ea',
|
|
18
|
+
txt: '#94a3b8',
|
|
19
|
+
css: '#06b6d4',
|
|
20
|
+
html: '#ea580c',
|
|
21
|
+
};
|
|
22
|
+
return colorMap[ext || ''] || '#94a3b8';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 格式化文件大小
|
|
26
|
+
export function formatSize(bytes: number): string {
|
|
27
|
+
if (!Number.isFinite(bytes) || bytes < 0) return '-';
|
|
28
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
29
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
30
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 格式化时间
|
|
34
|
+
export function formatTime(isoString: string | null | undefined): string {
|
|
35
|
+
if (!isoString) return '-';
|
|
36
|
+
try {
|
|
37
|
+
const date = new Date(isoString);
|
|
38
|
+
const now = new Date();
|
|
39
|
+
const diff = now.getTime() - date.getTime();
|
|
40
|
+
const seconds = Math.floor(diff / 1000);
|
|
41
|
+
const minutes = Math.floor(seconds / 60);
|
|
42
|
+
const hours = Math.floor(minutes / 60);
|
|
43
|
+
const days = Math.floor(hours / 24);
|
|
44
|
+
|
|
45
|
+
if (days > 30) {
|
|
46
|
+
return date.toLocaleDateString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric' });
|
|
47
|
+
} else if (days > 0) {
|
|
48
|
+
return `${days}天前`;
|
|
49
|
+
} else if (hours > 0) {
|
|
50
|
+
return `${hours}小时前`;
|
|
51
|
+
} else if (minutes > 0) {
|
|
52
|
+
return `${minutes}分钟前`;
|
|
53
|
+
} else {
|
|
54
|
+
return '刚刚';
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
return '-';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 分割技能名称(category/shortName)
|
|
62
|
+
export function splitSkillName(name: string): { category: string | null; shortName: string } {
|
|
63
|
+
if (!name.includes('/')) return { category: null, shortName: name };
|
|
64
|
+
const parts = name.split('/');
|
|
65
|
+
return { category: parts[0], shortName: parts.slice(1).join('/') };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 规范化执行器名称
|
|
69
|
+
export function normalizeExecutor(name: string): string {
|
|
70
|
+
return name.toLowerCase().replace(/[_\s-]/g, '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 渐变背景色生成器
|
|
74
|
+
export function generateGradient(name: string): string {
|
|
75
|
+
let hash = 0;
|
|
76
|
+
for (let i = 0; i < name.length; i++) {
|
|
77
|
+
hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
|
|
78
|
+
}
|
|
79
|
+
const h1 = Math.abs(hash) % 360;
|
|
80
|
+
const h2 = (h1 + 40) % 360;
|
|
81
|
+
return `linear-gradient(135deg, hsl(${h1}, 70%, 60%), hsl(${h2}, 60%, 50%))`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 执行器颜色映射
|
|
85
|
+
export const EXECUTOR_COLORS: Record<string, string> = {
|
|
86
|
+
claudecode: '#7C3AED',
|
|
87
|
+
codebuddy: '#0891B2',
|
|
88
|
+
opencode: '#059669',
|
|
89
|
+
mobilecoder: '#DC2626',
|
|
90
|
+
atomcode: '#D97706',
|
|
91
|
+
hermes: '#7C3AED',
|
|
92
|
+
kimi: '#2563EB',
|
|
93
|
+
codex: '#10B981',
|
|
94
|
+
pi: '#F59E0B',
|
|
95
|
+
mimo: '#EC4899',
|
|
96
|
+
zhanlu: '#8B5CF6',
|
|
97
|
+
agents: '#6366F1',
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// 导出任务状态
|
|
101
|
+
export interface ExportTask {
|
|
102
|
+
id: string;
|
|
103
|
+
skillName: string;
|
|
104
|
+
status: 'pending' | 'exporting' | 'completed' | 'failed';
|
|
105
|
+
progress: number;
|
|
106
|
+
error?: string;
|
|
107
|
+
blobUrl?: string;
|
|
108
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills management hooks - barrel export
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { useSkillCatalog } from './useSkillCatalog';
|
|
6
|
+
export type { SkillCatalogState, SkillMeta, SkillSourceMeta, InstalledSkillMeta, SkillDetailData } from './useSkillCatalog';
|
|
7
|
+
export { useSkillDetail } from './useSkillDetail';
|
|
8
|
+
export type { SkillDetailState, SkillDetailData as SkillDetailData2, SkillFileInfo, SkillMeta as SkillMeta2 } from './useSkillDetail';
|
|
9
|
+
export { useSkillInstall } from './useSkillInstall';
|
|
10
|
+
export type { SkillInstallState } from './useSkillInstall';
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSkillCatalog — 技能市场目录(列表/分页/视图/筛选)数据族 hook
|
|
3
|
+
* Ported from ntd frontend/src/hooks/useSkillCatalog.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
const API = '/skills-management/api';
|
|
9
|
+
const PAGE_SIZE = 30;
|
|
10
|
+
|
|
11
|
+
export interface SkillSourceMeta {
|
|
12
|
+
source: string;
|
|
13
|
+
skills: number;
|
|
14
|
+
displayName: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SkillMeta {
|
|
18
|
+
name: string;
|
|
19
|
+
shortName: string;
|
|
20
|
+
source: string;
|
|
21
|
+
description: string;
|
|
22
|
+
keywords: string[];
|
|
23
|
+
version?: string;
|
|
24
|
+
installed: boolean;
|
|
25
|
+
totalSize: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface InstalledSkillMeta {
|
|
29
|
+
name: string;
|
|
30
|
+
description: string;
|
|
31
|
+
path: string;
|
|
32
|
+
fileCount: number;
|
|
33
|
+
totalSize: number;
|
|
34
|
+
modifiedAt?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SkillDetailData {
|
|
38
|
+
name: string;
|
|
39
|
+
shortName: string;
|
|
40
|
+
dir: string;
|
|
41
|
+
isInstalled: boolean;
|
|
42
|
+
content: string;
|
|
43
|
+
contentWithMeta: string;
|
|
44
|
+
meta: Record<string, any>;
|
|
45
|
+
files: { path: string; size: number; modifiedAt: string }[];
|
|
46
|
+
fileCount: number;
|
|
47
|
+
totalSize: number;
|
|
48
|
+
modifiedAt?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type ViewMode = 'browse-sources' | 'all-skills' | 'installed';
|
|
52
|
+
|
|
53
|
+
export interface SkillCatalogState {
|
|
54
|
+
skills: SkillMeta[];
|
|
55
|
+
sources: SkillSourceMeta[];
|
|
56
|
+
installed: InstalledSkillMeta[];
|
|
57
|
+
loading: boolean;
|
|
58
|
+
total: number;
|
|
59
|
+
viewMode: ViewMode;
|
|
60
|
+
activeSource: string | null;
|
|
61
|
+
searchText: string;
|
|
62
|
+
filterSource: string;
|
|
63
|
+
browseSourcesPage: number;
|
|
64
|
+
browseSkillsPage: number;
|
|
65
|
+
allPage: number;
|
|
66
|
+
setSearchText: (v: string) => void;
|
|
67
|
+
setFilterSource: (v: string) => void;
|
|
68
|
+
setBrowseSourcesPage: (v: number) => void;
|
|
69
|
+
setBrowseSkillsPage: (v: number) => void;
|
|
70
|
+
setAllPage: (v: number) => void;
|
|
71
|
+
switchToSourceBrowse: () => void;
|
|
72
|
+
switchToAllSkills: () => void;
|
|
73
|
+
switchToInstalled: () => void;
|
|
74
|
+
enterSource: (sourceKey: string) => void;
|
|
75
|
+
backToSourceGrid: () => void;
|
|
76
|
+
refresh: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function useSkillCatalog(): SkillCatalogState & { message: { error: (msg: string) => void } } {
|
|
80
|
+
// ── Data states ──
|
|
81
|
+
const [skills, setSkills] = useState<SkillMeta[]>([]);
|
|
82
|
+
const [sources, setSources] = useState<SkillSourceMeta[]>([]);
|
|
83
|
+
const [installed, setInstalled] = useState<InstalledSkillMeta[]>([]);
|
|
84
|
+
const [loading, setLoading] = useState(false);
|
|
85
|
+
|
|
86
|
+
// ── Pagination states ──
|
|
87
|
+
const [browseSourcesPage, setBrowseSourcesPage] = useState(1);
|
|
88
|
+
const [browseSkillsPage, setBrowseSkillsPage] = useState(1);
|
|
89
|
+
const [allPage, setAllPage] = useState(1);
|
|
90
|
+
const [total, setTotal] = useState(0);
|
|
91
|
+
|
|
92
|
+
// ── View states ──
|
|
93
|
+
const [viewMode, setViewMode] = useState<ViewMode>('browse-sources');
|
|
94
|
+
const [activeSource, setActiveSource] = useState<string | null>(null);
|
|
95
|
+
|
|
96
|
+
// ── Filter states ──
|
|
97
|
+
const [searchText, setSearchText] = useState('');
|
|
98
|
+
const [filterSource, setFilterSource] = useState<string>('all');
|
|
99
|
+
|
|
100
|
+
// Request race guard
|
|
101
|
+
const reqGenRef = useRef(0);
|
|
102
|
+
|
|
103
|
+
// ── Load data ──
|
|
104
|
+
const loadData = useCallback(async () => {
|
|
105
|
+
const myGen = ++reqGenRef.current;
|
|
106
|
+
setLoading(true);
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch(API);
|
|
109
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
110
|
+
const data = await res.json();
|
|
111
|
+
if (myGen !== reqGenRef.current) return;
|
|
112
|
+
|
|
113
|
+
setSources(data.sources || []);
|
|
114
|
+
setSkills(data.market || []);
|
|
115
|
+
setInstalled(data.installed || []);
|
|
116
|
+
setTotal((data.market || []).length);
|
|
117
|
+
} catch (e: any) {
|
|
118
|
+
if (myGen !== reqGenRef.current) return;
|
|
119
|
+
console.error('Failed to load skills:', e);
|
|
120
|
+
} finally {
|
|
121
|
+
if (myGen === reqGenRef.current) setLoading(false);
|
|
122
|
+
}
|
|
123
|
+
}, []);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
loadData();
|
|
127
|
+
}, [loadData]);
|
|
128
|
+
|
|
129
|
+
// ── View switchers ──
|
|
130
|
+
const switchToSourceBrowse = useCallback(() => {
|
|
131
|
+
setViewMode('browse-sources');
|
|
132
|
+
setActiveSource(null);
|
|
133
|
+
setSearchText('');
|
|
134
|
+
setBrowseSourcesPage(1);
|
|
135
|
+
setBrowseSkillsPage(1);
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
const switchToAllSkills = useCallback(() => {
|
|
139
|
+
setViewMode('all-skills');
|
|
140
|
+
setActiveSource(null);
|
|
141
|
+
setFilterSource('all');
|
|
142
|
+
setSearchText('');
|
|
143
|
+
setAllPage(1);
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const switchToInstalled = useCallback(() => {
|
|
147
|
+
setViewMode('installed');
|
|
148
|
+
setActiveSource(null);
|
|
149
|
+
setSearchText('');
|
|
150
|
+
}, []);
|
|
151
|
+
|
|
152
|
+
const enterSource = useCallback((sourceKey: string) => {
|
|
153
|
+
setActiveSource(sourceKey);
|
|
154
|
+
setBrowseSkillsPage(1);
|
|
155
|
+
setSearchText('');
|
|
156
|
+
}, []);
|
|
157
|
+
|
|
158
|
+
const backToSourceGrid = useCallback(() => {
|
|
159
|
+
setActiveSource(null);
|
|
160
|
+
setBrowseSkillsPage(1);
|
|
161
|
+
setBrowseSourcesPage(1);
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const refresh = useCallback(() => {
|
|
165
|
+
loadData();
|
|
166
|
+
}, [loadData]);
|
|
167
|
+
|
|
168
|
+
// Simple message mock (DSH uses App.useApp())
|
|
169
|
+
const message = {
|
|
170
|
+
error: (msg: string) => console.error(msg),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
skills,
|
|
175
|
+
sources,
|
|
176
|
+
installed,
|
|
177
|
+
loading,
|
|
178
|
+
total,
|
|
179
|
+
viewMode,
|
|
180
|
+
activeSource,
|
|
181
|
+
searchText,
|
|
182
|
+
filterSource,
|
|
183
|
+
browseSourcesPage,
|
|
184
|
+
browseSkillsPage,
|
|
185
|
+
allPage,
|
|
186
|
+
setSearchText,
|
|
187
|
+
setFilterSource,
|
|
188
|
+
setBrowseSourcesPage,
|
|
189
|
+
setBrowseSkillsPage,
|
|
190
|
+
setAllPage,
|
|
191
|
+
switchToSourceBrowse,
|
|
192
|
+
switchToAllSkills,
|
|
193
|
+
switchToInstalled,
|
|
194
|
+
enterSource,
|
|
195
|
+
backToSourceGrid,
|
|
196
|
+
refresh,
|
|
197
|
+
message,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { PAGE_SIZE as ALL_SKILLS_PAGE_SIZE };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSkillDetail - Skill detail drawer state hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useRef, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
const API = '/skills-management/api';
|
|
8
|
+
|
|
9
|
+
export interface SkillFileInfo {
|
|
10
|
+
path: string;
|
|
11
|
+
size: number;
|
|
12
|
+
modifiedAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface SkillDetailData {
|
|
16
|
+
name: string;
|
|
17
|
+
shortName: string;
|
|
18
|
+
dir: string;
|
|
19
|
+
isInstalled: boolean;
|
|
20
|
+
content: string;
|
|
21
|
+
contentWithMeta: string;
|
|
22
|
+
meta: Record<string, any>;
|
|
23
|
+
files: SkillFileInfo[];
|
|
24
|
+
fileCount: number;
|
|
25
|
+
totalSize: number;
|
|
26
|
+
modifiedAt?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SkillMeta {
|
|
30
|
+
name: string;
|
|
31
|
+
shortName: string;
|
|
32
|
+
source: string;
|
|
33
|
+
description: string;
|
|
34
|
+
keywords: string[];
|
|
35
|
+
version?: string;
|
|
36
|
+
installed: boolean;
|
|
37
|
+
totalSize: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SkillDetailState {
|
|
41
|
+
selectedSkill: SkillMeta | null;
|
|
42
|
+
drawerOpen: boolean;
|
|
43
|
+
detail: SkillDetailData | null;
|
|
44
|
+
contentLoading: boolean;
|
|
45
|
+
openDetail: (skill: SkillMeta) => Promise<void>;
|
|
46
|
+
closeDetail: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function useSkillDetail(): SkillDetailState {
|
|
50
|
+
const [selectedSkill, setSelectedSkill] = useState<SkillMeta | null>(null);
|
|
51
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
52
|
+
const [detail, setDetail] = useState<SkillDetailData | null>(null);
|
|
53
|
+
const [contentLoading, setContentLoading] = useState(false);
|
|
54
|
+
|
|
55
|
+
const detailReqIdRef = useRef(0);
|
|
56
|
+
|
|
57
|
+
const openDetail = async (skill: SkillMeta) => {
|
|
58
|
+
const reqId = ++detailReqIdRef.current;
|
|
59
|
+
setSelectedSkill(skill);
|
|
60
|
+
setDrawerOpen(true);
|
|
61
|
+
setDetail(null);
|
|
62
|
+
setContentLoading(true);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const res = await fetch(API + '/detail?name=' + encodeURIComponent(skill.name));
|
|
66
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
|
|
69
|
+
if (reqId !== detailReqIdRef.current) return;
|
|
70
|
+
setDetail(data);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (reqId !== detailReqIdRef.current) return;
|
|
73
|
+
console.error('Failed to load skill detail:', e);
|
|
74
|
+
} finally {
|
|
75
|
+
if (reqId === detailReqIdRef.current) setContentLoading(false);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const closeDetail = () => setDrawerOpen(false);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
selectedSkill,
|
|
83
|
+
drawerOpen,
|
|
84
|
+
detail,
|
|
85
|
+
contentLoading,
|
|
86
|
+
openDetail,
|
|
87
|
+
closeDetail,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSkillInstall - Skill install state hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useCallback } from 'react';
|
|
6
|
+
import { SkillMeta } from './useSkillDetail';
|
|
7
|
+
|
|
8
|
+
const API = '/skills-management/api';
|
|
9
|
+
|
|
10
|
+
export interface SkillInstallState {
|
|
11
|
+
installModalOpen: boolean;
|
|
12
|
+
installing: boolean;
|
|
13
|
+
installResult: { success: number; failed: number } | null;
|
|
14
|
+
openInstall: (skill: SkillMeta) => void;
|
|
15
|
+
closeInstall: () => void;
|
|
16
|
+
install: (skill: SkillMeta) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function useSkillInstall(onDone?: () => void): SkillInstallState {
|
|
20
|
+
const [installModalOpen, setInstallModalOpen] = useState(false);
|
|
21
|
+
const [installing, setInstalling] = useState(false);
|
|
22
|
+
const [installResult, setInstallResult] = useState<{ success: number; failed: number } | null>(null);
|
|
23
|
+
const [targetSkill, setTargetSkill] = useState<SkillMeta | null>(null);
|
|
24
|
+
|
|
25
|
+
const openInstall = (skill: SkillMeta) => {
|
|
26
|
+
setTargetSkill(skill);
|
|
27
|
+
setInstallModalOpen(true);
|
|
28
|
+
setInstallResult(null);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const closeInstall = () => {
|
|
32
|
+
setInstallModalOpen(false);
|
|
33
|
+
setInstallResult(null);
|
|
34
|
+
setTargetSkill(null);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const install = async (skill: SkillMeta) => {
|
|
38
|
+
setInstalling(true);
|
|
39
|
+
setInstallResult(null);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(API + '/install', {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
body: JSON.stringify({ name: skill.name }),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const err = await res.json().catch(() => ({ error: 'Unknown error' }));
|
|
50
|
+
throw new Error(err.error || 'HTTP ' + res.status);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
setInstallResult({ success: 1, failed: 0 });
|
|
54
|
+
console.log('Install success:', skill.shortName || skill.name);
|
|
55
|
+
onDone?.();
|
|
56
|
+
|
|
57
|
+
setTimeout(() => { closeInstall(); }, 1500);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
setInstallResult({ success: 0, failed: 1 });
|
|
60
|
+
console.error('Install failed:', e);
|
|
61
|
+
} finally {
|
|
62
|
+
setInstalling(false);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
installModalOpen,
|
|
68
|
+
installing,
|
|
69
|
+
installResult,
|
|
70
|
+
openInstall,
|
|
71
|
+
closeInstall,
|
|
72
|
+
install,
|
|
73
|
+
};
|
|
74
|
+
}
|