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,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识保存工具
|
|
3
|
+
*
|
|
4
|
+
* 提供 save_knowledge 工具,让 AI 可以将当前会话中的有价值内容保存到知识库。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Type } from '@sinclair/typebox';
|
|
8
|
+
import type { ToolDefinition, AgentToolResult, ExtensionContext } from '@mariozechner/pi-coding-agent';
|
|
9
|
+
import { createDocument, type KnowledgeType, type KnowledgeSourceType } from './db';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 知识保存工具的参数 Schema
|
|
13
|
+
*/
|
|
14
|
+
const saveKnowledgeSchema = Type.Object({
|
|
15
|
+
title: Type.String({
|
|
16
|
+
description: '知识标题',
|
|
17
|
+
}),
|
|
18
|
+
content: Type.String({
|
|
19
|
+
description: '知识内容,包括问题分析、解决方案、最佳实践等',
|
|
20
|
+
}),
|
|
21
|
+
author: Type.Optional(Type.String({
|
|
22
|
+
description: '作者ID,默认为当前用户',
|
|
23
|
+
})),
|
|
24
|
+
category: Type.Optional(Type.String({
|
|
25
|
+
description: '分类,如 "运维"、"开发"、"排查" 等',
|
|
26
|
+
})),
|
|
27
|
+
tags: Type.Optional(
|
|
28
|
+
Type.Array(Type.String(), {
|
|
29
|
+
description: '标签数组,用于分类和检索',
|
|
30
|
+
})
|
|
31
|
+
),
|
|
32
|
+
type: Type.Optional(Type.String({
|
|
33
|
+
description: '知识类型: documentation(文档), troubleshooting(问题排查), best-practice(最佳实践)',
|
|
34
|
+
})),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 知识保存工具的详情类型
|
|
39
|
+
*/
|
|
40
|
+
interface SaveKnowledgeDetails {
|
|
41
|
+
success: boolean;
|
|
42
|
+
knowledge?: {
|
|
43
|
+
id: string;
|
|
44
|
+
title: string;
|
|
45
|
+
category?: string;
|
|
46
|
+
type?: string;
|
|
47
|
+
};
|
|
48
|
+
error?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 创建知识保存工具定义
|
|
53
|
+
*/
|
|
54
|
+
export const saveKnowledgeTool: ToolDefinition<typeof saveKnowledgeSchema, SaveKnowledgeDetails> = {
|
|
55
|
+
name: 'save_knowledge',
|
|
56
|
+
label: 'Save Knowledge',
|
|
57
|
+
description: `将当前会话中的知识保存到知识库。
|
|
58
|
+
|
|
59
|
+
使用场景:
|
|
60
|
+
- 用户明确要求"记住这个"、"保存为知识"
|
|
61
|
+
- 讨论了有价值的问题解决方案,值得记录
|
|
62
|
+
- 总结了最佳实践或操作指南
|
|
63
|
+
- 发现了常见问题及其解决方法
|
|
64
|
+
|
|
65
|
+
保存的知识将在未来的会话中被激活引用,帮助更好地解决类似问题。
|
|
66
|
+
|
|
67
|
+
参数说明:
|
|
68
|
+
- title: 知识标题,应简洁明了
|
|
69
|
+
- content: 知识内容,应包含问题描述、解决方案、步骤等详细信息
|
|
70
|
+
- author: 作者ID,AI 应自动从会话上下文获取当前用户ID并传入
|
|
71
|
+
- category: 可选分类,如"运维"、"开发"、"排查"等
|
|
72
|
+
- tags: 可选标签数组,便于检索
|
|
73
|
+
- type: 知识类型,默认为 best-practice`,
|
|
74
|
+
|
|
75
|
+
parameters: saveKnowledgeSchema,
|
|
76
|
+
|
|
77
|
+
async execute(
|
|
78
|
+
toolCallId: string,
|
|
79
|
+
params: any,
|
|
80
|
+
signal: AbortSignal | undefined,
|
|
81
|
+
onUpdate: ((result: AgentToolResult<SaveKnowledgeDetails>) => void) | undefined,
|
|
82
|
+
ctx: ExtensionContext
|
|
83
|
+
): Promise<AgentToolResult<SaveKnowledgeDetails>> {
|
|
84
|
+
try {
|
|
85
|
+
const { title, content, author, category, tags, type } = params;
|
|
86
|
+
|
|
87
|
+
// 验证类型
|
|
88
|
+
const validTypes: KnowledgeType[] = ['documentation', 'troubleshooting', 'best-practice', 'conversation-summary'];
|
|
89
|
+
const knowledgeType: KnowledgeType = validTypes.includes(type) ? type : 'best-practice';
|
|
90
|
+
|
|
91
|
+
// 创建知识条目
|
|
92
|
+
const doc = await createDocument({
|
|
93
|
+
title,
|
|
94
|
+
content,
|
|
95
|
+
category: category || '知识',
|
|
96
|
+
tags: tags || undefined,
|
|
97
|
+
author: author || 'ai-assistant',
|
|
98
|
+
type: knowledgeType,
|
|
99
|
+
sourceType: 'ai-generated' as KnowledgeSourceType,
|
|
100
|
+
isActive: true,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result: AgentToolResult<SaveKnowledgeDetails> = {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: `✅ 知识已保存到知识库
|
|
108
|
+
|
|
109
|
+
**标题:** ${title}
|
|
110
|
+
**分类:** ${category || '知识'}
|
|
111
|
+
**类型:** ${knowledgeType}
|
|
112
|
+
|
|
113
|
+
此知识已可在未来的会话中被引用。`,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
details: {
|
|
117
|
+
success: true,
|
|
118
|
+
knowledge: {
|
|
119
|
+
id: doc.id,
|
|
120
|
+
title: doc.title,
|
|
121
|
+
category: doc.category,
|
|
122
|
+
type: doc.type,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// 通知更新
|
|
128
|
+
onUpdate?.(result);
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
const errorResult: AgentToolResult<SaveKnowledgeDetails> = {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: `❌ 保存知识失败: ${error.message}`,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
details: {
|
|
140
|
+
success: false,
|
|
141
|
+
error: error.message,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
onUpdate?.(errorResult);
|
|
146
|
+
|
|
147
|
+
return errorResult;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export default saveKnowledgeTool;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识库类型定义
|
|
3
|
+
*
|
|
4
|
+
* 定义知识库系统的核心类型和接口。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { KnowledgeType, KnowledgeSourceType } from './db';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 知识类型描述映射
|
|
11
|
+
*/
|
|
12
|
+
export const KNOWLEDGE_TYPE_LABELS: Record<KnowledgeType, string> = {
|
|
13
|
+
'documentation': '文档',
|
|
14
|
+
'troubleshooting': '问题排查',
|
|
15
|
+
'best-practice': '最佳实践',
|
|
16
|
+
'conversation-summary': '会话总结',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 知识来源描述映射
|
|
21
|
+
*/
|
|
22
|
+
export const KNOWLEDGE_SOURCE_LABELS: Record<KnowledgeSourceType, string> = {
|
|
23
|
+
'manual': '手动创建',
|
|
24
|
+
'ai-generated': 'AI生成',
|
|
25
|
+
'conversation-summary': '会话总结',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 保存知识请求(用于 save_knowledge 工具)
|
|
30
|
+
*/
|
|
31
|
+
export interface SaveKnowledgeRequest {
|
|
32
|
+
title: string;
|
|
33
|
+
content: string;
|
|
34
|
+
category?: string;
|
|
35
|
+
tags?: string[];
|
|
36
|
+
type?: KnowledgeType;
|
|
37
|
+
sourceType?: KnowledgeSourceType;
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 知识条目(用于前端展示)
|
|
43
|
+
*/
|
|
44
|
+
export interface KnowledgeItem {
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
content: string;
|
|
48
|
+
category?: string;
|
|
49
|
+
tags?: string[];
|
|
50
|
+
author: string;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
type?: KnowledgeType;
|
|
54
|
+
sourceType?: KnowledgeSourceType;
|
|
55
|
+
sessionId?: string;
|
|
56
|
+
isActive: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 从会话创建知识的请求
|
|
61
|
+
*/
|
|
62
|
+
export interface CreateFromSessionRequest {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
summary: string;
|
|
66
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Market Client for AI Assistant
|
|
3
|
+
*
|
|
4
|
+
* This client manages market operations with two modes:
|
|
5
|
+
* - Local mode (no MARKET_SERVICE_URL): Use local SQLite database
|
|
6
|
+
* - Remote mode (MARKET_SERVICE_URL configured): Use remote market service API
|
|
7
|
+
*
|
|
8
|
+
* In remote mode, failures will throw errors instead of falling back to local
|
|
9
|
+
* to prevent data inconsistency.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const MARKET_SERVICE_URL = process.env.MARKET_SERVICE_URL;
|
|
13
|
+
const API_KEY = process.env.MARKET_API_KEY || 'market-dev-key-1';
|
|
14
|
+
const TIMEOUT = 5000; // 5 seconds
|
|
15
|
+
|
|
16
|
+
export class MarketClient {
|
|
17
|
+
private mode: 'local' | 'remote';
|
|
18
|
+
private baseUrl: string | null = null;
|
|
19
|
+
private apiKey: string | null = null;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
if (MARKET_SERVICE_URL && API_KEY) {
|
|
23
|
+
this.mode = 'remote';
|
|
24
|
+
this.baseUrl = MARKET_SERVICE_URL;
|
|
25
|
+
this.apiKey = API_KEY;
|
|
26
|
+
console.log('[MarketClient] Remote mode enabled:', this.baseUrl);
|
|
27
|
+
} else {
|
|
28
|
+
this.mode = 'local';
|
|
29
|
+
console.log('[MarketClient] Local mode (no remote configured)');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if running in remote mode
|
|
35
|
+
*/
|
|
36
|
+
isRemoteMode(): boolean {
|
|
37
|
+
return this.mode === 'remote';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Check if running in local mode
|
|
42
|
+
*/
|
|
43
|
+
isLocalMode(): boolean {
|
|
44
|
+
return this.mode === 'local';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if remote service is configured (backward compatibility)
|
|
49
|
+
*/
|
|
50
|
+
isRemoteEnabled(): boolean {
|
|
51
|
+
return this.mode === 'remote';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get skill by ID
|
|
56
|
+
*/
|
|
57
|
+
async getSkill(skillId: string): Promise<any | null> {
|
|
58
|
+
if (this.mode === 'local') {
|
|
59
|
+
const { getMarketSkillById } = await import('./market-db');
|
|
60
|
+
return getMarketSkillById(skillId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Remote mode: call API, throw error on failure
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(
|
|
69
|
+
`${this.baseUrl}/api/market/skills/${skillId}?api_key=${this.apiKey}`,
|
|
70
|
+
{ signal: controller.signal }
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
clearTimeout(timeoutId);
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`Market service error: ${response.status}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return await response.json();
|
|
80
|
+
} catch (error: any) {
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
console.error('[MarketClient] Get skill failed:', error.message);
|
|
83
|
+
throw new Error('远程市场服务不可用,请联系系统管理员');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Search skills
|
|
89
|
+
*/
|
|
90
|
+
async searchSkills(options: {
|
|
91
|
+
search?: string;
|
|
92
|
+
status?: 'all' | 'pending' | 'approved' | 'rejected' | 'archived';
|
|
93
|
+
author?: string;
|
|
94
|
+
isOfficial?: boolean;
|
|
95
|
+
limit?: number;
|
|
96
|
+
offset?: number;
|
|
97
|
+
} = {}): Promise<{ skills: any[]; total: number }> {
|
|
98
|
+
if (this.mode === 'local') {
|
|
99
|
+
const { listMarketSkills } = await import('./market-db');
|
|
100
|
+
return listMarketSkills(options);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Remote mode: call API, throw error on failure
|
|
104
|
+
const params = new URLSearchParams({ api_key: this.apiKey! });
|
|
105
|
+
if (options.search) params.set('search', options.search);
|
|
106
|
+
if (options.status) params.set('status', options.status);
|
|
107
|
+
if (options.author) params.set('author', options.author);
|
|
108
|
+
if (options.isOfficial !== undefined) params.set('isOfficial', options.isOfficial.toString());
|
|
109
|
+
if (options.limit) params.set('limit', options.limit.toString());
|
|
110
|
+
if (options.offset) params.set('offset', options.offset.toString());
|
|
111
|
+
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const response = await fetch(
|
|
117
|
+
`${this.baseUrl}/api/market/skills?${params}`,
|
|
118
|
+
{ signal: controller.signal }
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
clearTimeout(timeoutId);
|
|
122
|
+
|
|
123
|
+
if (!response.ok) {
|
|
124
|
+
throw new Error(`Market service error: ${response.status}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return await response.json();
|
|
128
|
+
} catch (error: any) {
|
|
129
|
+
clearTimeout(timeoutId);
|
|
130
|
+
console.error('[MarketClient] Search skills failed:', error.message);
|
|
131
|
+
throw new Error('远程市场服务不可用,请联系系统管理员');
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Install skill
|
|
137
|
+
* Note: Skill files are still saved to user's local directory
|
|
138
|
+
*/
|
|
139
|
+
async installSkill(userId: string, skillId: string): Promise<any> {
|
|
140
|
+
if (this.mode === 'local') {
|
|
141
|
+
const { addUserInstalledSkill } = await import('./market-db');
|
|
142
|
+
addUserInstalledSkill(userId, skillId);
|
|
143
|
+
const skill = await this.getSkill(skillId);
|
|
144
|
+
return skill;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Remote mode: download from market service
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT * 2);
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(
|
|
153
|
+
`${this.baseUrl}/api/market/skills/${skillId}/download?api_key=${this.apiKey}`,
|
|
154
|
+
{ signal: controller.signal }
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
clearTimeout(timeoutId);
|
|
158
|
+
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error('Failed to download skill');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Save to user directory
|
|
164
|
+
const { join } = await import('path');
|
|
165
|
+
const { writeFileSync, mkdirSync, existsSync } = await import('fs');
|
|
166
|
+
|
|
167
|
+
const userSkillDir = join(process.cwd(), 'data', 'skills', 'users', userId);
|
|
168
|
+
|
|
169
|
+
if (!existsSync(userSkillDir)) {
|
|
170
|
+
mkdirSync(userSkillDir, { recursive: true });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const zipPath = join(userSkillDir, `${skillId}.zip`);
|
|
174
|
+
|
|
175
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
176
|
+
writeFileSync(zipPath, Buffer.from(arrayBuffer));
|
|
177
|
+
|
|
178
|
+
const { unpackSkillFromZip } = await import('./zip-tool');
|
|
179
|
+
unpackSkillFromZip(zipPath, userSkillDir);
|
|
180
|
+
|
|
181
|
+
const { addUserInstalledSkill } = await import('./market-db');
|
|
182
|
+
addUserInstalledSkill(userId, skillId);
|
|
183
|
+
|
|
184
|
+
return await this.getSkill(skillId);
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
clearTimeout(timeoutId);
|
|
187
|
+
console.error('[MarketClient] Install skill failed:', error.message);
|
|
188
|
+
throw new Error('安装技能失败,请稍后重试或联系管理员');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get knowledge list from market
|
|
194
|
+
*/
|
|
195
|
+
async getKnowledge(options: {
|
|
196
|
+
category?: string;
|
|
197
|
+
search?: string;
|
|
198
|
+
limit?: number;
|
|
199
|
+
} = {}): Promise<any[]> {
|
|
200
|
+
if (this.mode === 'local') {
|
|
201
|
+
// Local mode: query local published knowledge
|
|
202
|
+
const { getMarketKnowledge } = await import('./db');
|
|
203
|
+
const result = getMarketKnowledge(options);
|
|
204
|
+
return Array.isArray(result) ? result : result.documents || [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Remote mode: call API, throw error on failure
|
|
208
|
+
const params = new URLSearchParams({ api_key: this.apiKey! });
|
|
209
|
+
if (options.category) params.set('category', options.category);
|
|
210
|
+
if (options.search) params.set('search', options.search);
|
|
211
|
+
if (options.limit) params.set('limit', options.limit.toString());
|
|
212
|
+
|
|
213
|
+
const controller = new AbortController();
|
|
214
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const response = await fetch(
|
|
218
|
+
`${this.baseUrl}/api/market/knowledge?${params}`,
|
|
219
|
+
{ signal: controller.signal }
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
clearTimeout(timeoutId);
|
|
223
|
+
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
throw new Error(`Market service error: ${response.status}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const data = await response.json();
|
|
229
|
+
return Array.isArray(data) ? data : data.knowledge || data.documents || [];
|
|
230
|
+
} catch (error: any) {
|
|
231
|
+
clearTimeout(timeoutId);
|
|
232
|
+
console.error('[MarketClient] Get knowledge failed:', error.message);
|
|
233
|
+
throw new Error('远程市场服务不可用,请联系系统管理员');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Publish knowledge to market
|
|
239
|
+
*/
|
|
240
|
+
async publishKnowledge(knowledgeId: string, author: string): Promise<any | null> {
|
|
241
|
+
if (this.mode === 'local') {
|
|
242
|
+
// Local mode: update local market_status
|
|
243
|
+
const { publishKnowledgeToMarket } = await import('./db');
|
|
244
|
+
return publishKnowledgeToMarket(knowledgeId, author);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Remote mode: push to remote market
|
|
248
|
+
const controller = new AbortController();
|
|
249
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// Get local knowledge content first
|
|
253
|
+
const { getDocument, updateDocument } = await import('./db');
|
|
254
|
+
const doc = getDocument(knowledgeId);
|
|
255
|
+
|
|
256
|
+
if (!doc) {
|
|
257
|
+
throw new Error('知识不存在');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Push to remote market with full content
|
|
261
|
+
const response = await fetch(
|
|
262
|
+
`${this.baseUrl}/api/market/knowledge`,
|
|
263
|
+
{
|
|
264
|
+
method: 'POST',
|
|
265
|
+
signal: controller.signal,
|
|
266
|
+
headers: {
|
|
267
|
+
'X-API-Key': this.apiKey!,
|
|
268
|
+
'Content-Type': 'application/json',
|
|
269
|
+
},
|
|
270
|
+
body: JSON.stringify({
|
|
271
|
+
id: knowledgeId,
|
|
272
|
+
title: doc.title,
|
|
273
|
+
content: doc.content,
|
|
274
|
+
category: doc.category,
|
|
275
|
+
tags: doc.tags,
|
|
276
|
+
author,
|
|
277
|
+
}),
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
clearTimeout(timeoutId);
|
|
282
|
+
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
throw new Error(`Publish failed: ${response.status}`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const data = await response.json();
|
|
288
|
+
console.log('[MarketClient] Published knowledge to remote market:', knowledgeId);
|
|
289
|
+
|
|
290
|
+
// Update local market_status to indicate published
|
|
291
|
+
await updateDocument(knowledgeId, { marketStatus: 'published' } as any);
|
|
292
|
+
|
|
293
|
+
return data.knowledge || data;
|
|
294
|
+
} catch (error: any) {
|
|
295
|
+
clearTimeout(timeoutId);
|
|
296
|
+
console.error('[MarketClient] Publish to remote failed:', error.message);
|
|
297
|
+
throw new Error('发布到远程市场失败,请稍后重试或联系管理员');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Singleton instance
|
|
303
|
+
let client: MarketClient | null = null;
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get market client instance
|
|
307
|
+
*/
|
|
308
|
+
export function getMarketClient(): MarketClient {
|
|
309
|
+
if (!client) {
|
|
310
|
+
client = new MarketClient();
|
|
311
|
+
}
|
|
312
|
+
return client;
|
|
313
|
+
}
|