flowmind 1.5.3 → 1.5.6
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/CHANGELOG.md +31 -1
- package/README.md +2 -0
- package/README_CN.md +2 -0
- package/bin/flowmind.js +104 -118
- package/core/adapters/mcp-adapter.js +9 -8
- package/core/ai/providers/mimo.js +1 -1
- package/core/cli-ink.js +79 -0
- package/core/index.js +139 -1
- package/core/log-query-parser.js +324 -0
- package/core/providers/aliyun/dms-adapter.js +7 -35
- package/core/providers/aliyun/redis-adapter.js +4 -20
- package/core/providers/aliyun/sls-adapter.js +3 -10
- package/core/providers/friday/report-adapter.js +5 -25
- package/core/providers/yapi/yapi-adapter.js +6 -30
- package/core/providers/yuque/yuque-adapter.js +7 -35
- package/core/update-notifier.js +74 -0
- package/dashboard/app.jsx +69 -10
- package/dashboard/components/ActivityFeed.jsx +5 -4
- package/dashboard/components/DragonPanel.jsx +17 -1
- package/dashboard/components/McpStatusBar.jsx +19 -1
- package/dashboard/components/StatsRow.jsx +27 -2
- package/package.json +2 -1
- package/scripts/check-update.js +52 -0
- package/skills/auto-flow/index.js +451 -122
- package/skills/log-audit/index.js +146 -25
- package/skills/sls-log-audit/index.js +7 -30
- package/skills/yapi-sync-interface/index.js +146 -13
- package/skills/yuque-sync-design/index.js +132 -13
- package/tui/app.jsx +86 -9
- package/tui/components/ChatPanel.jsx +10 -7
- package/tui/components/DragonTotem.jsx +12 -1
- package/tui/components/Sidebar.jsx +19 -7
- package/tui/components/StatusBar.jsx +28 -1
- package/tui/format-result.js +60 -0
- package/tui/layout.js +60 -0
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Analyzes application logs, traces requests, debugs performance issues
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { parseLogQueryParams } = require('../../core/log-query-parser');
|
|
7
|
+
|
|
6
8
|
module.exports = {
|
|
7
9
|
canHandle(input, context) {
|
|
8
10
|
if (!input) return false;
|
|
@@ -11,7 +13,11 @@ module.exports = {
|
|
|
11
13
|
|
|
12
14
|
async execute(input, context) {
|
|
13
15
|
const logService = context.componentRegistry?.getAdapter('logService');
|
|
14
|
-
const params =
|
|
16
|
+
const params = parseLogQueryParams(input, {
|
|
17
|
+
allowNaturalKeyword: true,
|
|
18
|
+
allowNaturalLevel: true,
|
|
19
|
+
allowQuotedKeyword: true
|
|
20
|
+
});
|
|
15
21
|
|
|
16
22
|
if (!logService && !params.mock) {
|
|
17
23
|
return {
|
|
@@ -29,18 +35,23 @@ module.exports = {
|
|
|
29
35
|
? logService.buildQueryParams(queryInput)
|
|
30
36
|
: queryInput;
|
|
31
37
|
const execution = await logService.queryLogs(queryParams);
|
|
38
|
+
const summary = summarizeLogExecution(execution);
|
|
39
|
+
const noRecordsMessage = summary.empty
|
|
40
|
+
? buildNoRecordsMessage(params)
|
|
41
|
+
: null;
|
|
32
42
|
|
|
33
43
|
return {
|
|
34
44
|
type: 'result',
|
|
35
45
|
skill: 'log-audit',
|
|
36
|
-
message: params.traceId
|
|
37
|
-
?
|
|
38
|
-
:
|
|
46
|
+
message: noRecordsMessage || (params.traceId
|
|
47
|
+
? `已執行 trace 查詢:${params.traceId}`
|
|
48
|
+
: `已執行日誌查詢:${params.service || '全部服務'},${params.level || '全部級別'},${params.timeRange || '最近 1 小時'}`),
|
|
39
49
|
data: {
|
|
40
50
|
action: params.traceId ? 'trace' : 'query',
|
|
41
51
|
provider: logService.providerName,
|
|
42
52
|
queryParams,
|
|
43
|
-
execution
|
|
53
|
+
execution: summary,
|
|
54
|
+
noRecords: summary.empty
|
|
44
55
|
},
|
|
45
56
|
input,
|
|
46
57
|
timestamp: new Date().toISOString()
|
|
@@ -48,26 +59,6 @@ module.exports = {
|
|
|
48
59
|
}
|
|
49
60
|
};
|
|
50
61
|
|
|
51
|
-
function parseLogParams(input) {
|
|
52
|
-
const params = {};
|
|
53
|
-
const traceMatch = input.match(/(?:trace[_-]?id|调用链)\s*[:=]?\s*(\S+)/i);
|
|
54
|
-
if (traceMatch) params.traceId = traceMatch[1];
|
|
55
|
-
|
|
56
|
-
const serviceMatch = input.match(/(?:服务|service)\s*[:=]?\s*(\S+)/i);
|
|
57
|
-
if (serviceMatch) params.service = serviceMatch[1];
|
|
58
|
-
|
|
59
|
-
const levelMatch = input.match(/(?:级别|level)\s*[:=]?\s*(ERROR|WARN|INFO|DEBUG)/i);
|
|
60
|
-
if (levelMatch) params.level = levelMatch[1].toUpperCase();
|
|
61
|
-
|
|
62
|
-
const timeMatch = input.match(/(?:最近|last)\s*(\d+)\s*(分钟|小时|分钟|min|hour)/i);
|
|
63
|
-
if (timeMatch) params.timeRange = `last ${timeMatch[1]} ${timeMatch[2]}`;
|
|
64
|
-
|
|
65
|
-
const keywordMatch = input.match(/(?:关键词|keyword|搜索|search)\s*[:=]?\s*(.+?)(?:\s*$)/i);
|
|
66
|
-
if (keywordMatch) params.keyword = keywordMatch[1].trim();
|
|
67
|
-
|
|
68
|
-
return params;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
62
|
function buildSLSQuery(params) {
|
|
72
63
|
const parts = [];
|
|
73
64
|
if (params.service) parts.push(`service: ${params.service}`);
|
|
@@ -87,3 +78,133 @@ function buildLogQueryInput(params) {
|
|
|
87
78
|
line: params.limit || 100
|
|
88
79
|
};
|
|
89
80
|
}
|
|
81
|
+
|
|
82
|
+
function summarizeLogExecution(execution) {
|
|
83
|
+
const payload = unwrapPayload(execution);
|
|
84
|
+
const data = payload && typeof payload === 'object' ? payload : {};
|
|
85
|
+
const records = extractRecords(data);
|
|
86
|
+
const empty = (extractStatus(data) || 'ok') === 'ok' && records.length === 0;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
status: extractStatus(data) || 'ok',
|
|
90
|
+
total: extractTotal(data, records),
|
|
91
|
+
records: records.slice(0, 5).map(summarizeRecord),
|
|
92
|
+
requestId: data.requestId || data.traceId || data.id || null,
|
|
93
|
+
empty,
|
|
94
|
+
rawSummary: summarizeRawExecution(execution, data)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function unwrapPayload(payload) {
|
|
99
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
100
|
+
return payload;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const textPayload = extractContentText(payload);
|
|
104
|
+
if (textPayload) {
|
|
105
|
+
return parseMaybeJson(textPayload);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
|
109
|
+
return payload.data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return payload;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractRecords(data) {
|
|
116
|
+
const candidates = [data.logs, data.records, data.items, data.result, data.data];
|
|
117
|
+
for (const candidate of candidates) {
|
|
118
|
+
if (Array.isArray(candidate)) {
|
|
119
|
+
return candidate;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractStatus(data) {
|
|
127
|
+
if (!data || typeof data !== 'object') {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof data.status === 'string') {
|
|
132
|
+
return data.status;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (data.success === false) {
|
|
136
|
+
return 'error';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function extractTotal(data, records) {
|
|
143
|
+
if (typeof data.total === 'number') return data.total;
|
|
144
|
+
if (typeof data.count === 'number') return data.count;
|
|
145
|
+
return records.length;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function summarizeRecord(record) {
|
|
149
|
+
if (!record || typeof record !== 'object') {
|
|
150
|
+
return record;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
time: record.time || record.timestamp || record.__time__ || null,
|
|
155
|
+
level: record.level || record.severity || null,
|
|
156
|
+
message: record.message || record.content || record.msg || null,
|
|
157
|
+
traceId: record.traceId || record.trace_id || null
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function extractContentText(payload) {
|
|
162
|
+
const content = Array.isArray(payload.content) ? payload.content : [];
|
|
163
|
+
const textItem = content.find((item) => item && typeof item.text === 'string');
|
|
164
|
+
return textItem ? textItem.text : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function parseMaybeJson(value) {
|
|
168
|
+
if (typeof value !== 'string') {
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(value);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
return { text: value };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function buildNoRecordsMessage(params) {
|
|
180
|
+
if (params.traceId) {
|
|
181
|
+
return `未找到符合 traceId:${params.traceId} 的日誌。請補充服務、關鍵字或時間範圍。`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (params.keyword) {
|
|
185
|
+
return `未找到符合關鍵字:${params.keyword} 的日誌。請補充服務或時間範圍。`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return '未找到符合條件的日誌。請補充關鍵字、服務或時間範圍。';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function summarizeRawExecution(execution, data) {
|
|
192
|
+
const topLevelKeys = data && typeof data === 'object'
|
|
193
|
+
? Object.keys(data).slice(0, 12)
|
|
194
|
+
: [];
|
|
195
|
+
const contentText = extractContentText(execution);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
topLevelKeys,
|
|
199
|
+
hasContent: Array.isArray(execution?.content) && execution.content.length > 0,
|
|
200
|
+
contentPreview: contentText ? truncateText(contentText, 300) : null
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function truncateText(value, maxLength) {
|
|
205
|
+
const text = String(value);
|
|
206
|
+
if (text.length <= maxLength) {
|
|
207
|
+
return text;
|
|
208
|
+
}
|
|
209
|
+
return `${text.slice(0, maxLength - 1)}…`;
|
|
210
|
+
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* Query Alibaba Cloud SLS logs, trace ID chain analysis, performance analysis
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { parseLogQueryParams } = require('../../core/log-query-parser');
|
|
7
|
+
|
|
6
8
|
module.exports = {
|
|
7
9
|
canHandle(input, context) {
|
|
8
10
|
if (!input) return false;
|
|
@@ -10,7 +12,11 @@ module.exports = {
|
|
|
10
12
|
},
|
|
11
13
|
|
|
12
14
|
async execute(input, context) {
|
|
13
|
-
const params =
|
|
15
|
+
const params = parseLogQueryParams(input, {
|
|
16
|
+
allowNaturalKeyword: true,
|
|
17
|
+
allowNaturalLevel: true,
|
|
18
|
+
allowQuotedKeyword: true
|
|
19
|
+
});
|
|
14
20
|
|
|
15
21
|
// Determine default endpoint based on environment
|
|
16
22
|
const env = params.env || 'test';
|
|
@@ -78,35 +84,6 @@ module.exports = {
|
|
|
78
84
|
}
|
|
79
85
|
};
|
|
80
86
|
|
|
81
|
-
function parseSLSParams(input) {
|
|
82
|
-
const params = {};
|
|
83
|
-
const traceMatch = input.match(/trace[_-]?id\s*[:=]?\s*(\S+)/i);
|
|
84
|
-
if (traceMatch) params.traceId = traceMatch[1];
|
|
85
|
-
|
|
86
|
-
const serviceMatch = input.match(/(?:服务|service)\s*[:=]?\s*(\S+)/i);
|
|
87
|
-
if (serviceMatch) params.service = serviceMatch[1];
|
|
88
|
-
|
|
89
|
-
const levelMatch = input.match(/(?:级别|level)\s*[:=]?\s*(ERROR|WARN|INFO|DEBUG)/i);
|
|
90
|
-
if (levelMatch) params.level = levelMatch[1].toUpperCase();
|
|
91
|
-
|
|
92
|
-
const projectMatch = input.match(/(?:项目|project)\s*[:=]?\s*(\S+)/i);
|
|
93
|
-
if (projectMatch) params.project = projectMatch[1];
|
|
94
|
-
|
|
95
|
-
const logstoreMatch = input.match(/(?:logstore|日志库)\s*[:=]?\s*(\S+)/i);
|
|
96
|
-
if (logstoreMatch) params.logstore = logstoreMatch[1];
|
|
97
|
-
|
|
98
|
-
const envMatch = input.match(/(?:环境|env)\s*[:=]?\s*(test|uat|gray|prod)/i);
|
|
99
|
-
if (envMatch) params.env = envMatch[1].toLowerCase();
|
|
100
|
-
|
|
101
|
-
const timeMatch = input.match(/(?:最近|last)\s*(\d+)\s*(分钟|小时|min|hour)/i);
|
|
102
|
-
if (timeMatch) params.timeRange = `last ${timeMatch[1]} ${timeMatch[2]}`;
|
|
103
|
-
|
|
104
|
-
const keywordMatch = input.match(/(?:关键词|keyword|搜索)\s*[:=]?\s*(.+?)(?:\s*$)/i);
|
|
105
|
-
if (keywordMatch) params.keyword = keywordMatch[1].trim();
|
|
106
|
-
|
|
107
|
-
return params;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
87
|
function buildSLSQuery(params) {
|
|
111
88
|
const parts = [];
|
|
112
89
|
if (params.service) parts.push(`service: ${params.service}`);
|
|
@@ -10,7 +10,9 @@ module.exports = {
|
|
|
10
10
|
},
|
|
11
11
|
|
|
12
12
|
async execute(input, context) {
|
|
13
|
-
const apiDoc = context.componentRegistry?.getAdapter('apiDoc')
|
|
13
|
+
const apiDoc = context.componentRegistry?.getAdapter('apiDoc')
|
|
14
|
+
|| context.componentRegistry?.getAdapterByProvider?.('apiDoc', 'yapi')
|
|
15
|
+
|| null;
|
|
14
16
|
const learnedBinding = context.resourceBinding?.componentType === 'apiDoc'
|
|
15
17
|
? context.resourceBinding
|
|
16
18
|
: null;
|
|
@@ -26,17 +28,38 @@ module.exports = {
|
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
if (!apiDoc) {
|
|
32
|
+
return {
|
|
33
|
+
type: 'result',
|
|
34
|
+
skill: 'yapi-sync-interface',
|
|
35
|
+
message: 'YApi adapter is not available in the current registry.',
|
|
36
|
+
data: {
|
|
37
|
+
hint: 'Make sure the apiDoc component is configured and loaded',
|
|
38
|
+
binding: learnedBinding
|
|
39
|
+
},
|
|
40
|
+
input,
|
|
41
|
+
timestamp: new Date().toISOString()
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
29
45
|
const params = parseYApiParams(input);
|
|
30
46
|
|
|
31
47
|
if (params.action === 'search') {
|
|
48
|
+
const args = {
|
|
49
|
+
projectKeyword: params.project || learnedBinding?.connection?.project,
|
|
50
|
+
nameKeyword: params.keyword,
|
|
51
|
+
limit: 20
|
|
52
|
+
};
|
|
53
|
+
const execution = await apiDoc.searchApis(args);
|
|
32
54
|
return {
|
|
33
55
|
type: 'result',
|
|
34
56
|
skill: 'yapi-sync-interface',
|
|
35
57
|
message: `Searching YApi for: ${params.keyword || 'all'}`,
|
|
36
58
|
data: {
|
|
37
|
-
|
|
38
|
-
args
|
|
39
|
-
binding: learnedBinding
|
|
59
|
+
action: 'search',
|
|
60
|
+
args,
|
|
61
|
+
binding: learnedBinding,
|
|
62
|
+
execution: summarizeYApiExecution('search', execution)
|
|
40
63
|
},
|
|
41
64
|
input,
|
|
42
65
|
timestamp: new Date().toISOString()
|
|
@@ -44,14 +67,17 @@ module.exports = {
|
|
|
44
67
|
}
|
|
45
68
|
|
|
46
69
|
if (params.action === 'list') {
|
|
70
|
+
const projectId = params.project || learnedBinding?.connection?.project;
|
|
71
|
+
const execution = await apiDoc.getCategories(projectId);
|
|
47
72
|
return {
|
|
48
73
|
type: 'result',
|
|
49
74
|
skill: 'yapi-sync-interface',
|
|
50
|
-
message: `Listing YApi categories for project: ${
|
|
75
|
+
message: `Listing YApi categories for project: ${projectId || 'default'}`,
|
|
51
76
|
data: {
|
|
52
|
-
|
|
53
|
-
args: { projectId
|
|
54
|
-
binding: learnedBinding
|
|
77
|
+
action: 'list',
|
|
78
|
+
args: { projectId },
|
|
79
|
+
binding: learnedBinding,
|
|
80
|
+
execution: summarizeYApiExecution('list', execution)
|
|
55
81
|
},
|
|
56
82
|
input,
|
|
57
83
|
timestamp: new Date().toISOString()
|
|
@@ -59,14 +85,17 @@ module.exports = {
|
|
|
59
85
|
}
|
|
60
86
|
|
|
61
87
|
if (params.action === 'export') {
|
|
88
|
+
const projectId = params.project || learnedBinding?.connection?.project;
|
|
89
|
+
const execution = await apiDoc.exportProject(projectId, params.format || 'swagger');
|
|
62
90
|
return {
|
|
63
91
|
type: 'result',
|
|
64
92
|
skill: 'yapi-sync-interface',
|
|
65
|
-
message: `Exporting YApi project: ${
|
|
93
|
+
message: `Exporting YApi project: ${projectId || 'default'}`,
|
|
66
94
|
data: {
|
|
67
|
-
|
|
68
|
-
args: { projectId
|
|
69
|
-
binding: learnedBinding
|
|
95
|
+
action: 'export',
|
|
96
|
+
args: { projectId, type: params.format || 'swagger' },
|
|
97
|
+
binding: learnedBinding,
|
|
98
|
+
execution: summarizeYApiExecution('export', execution)
|
|
70
99
|
},
|
|
71
100
|
input,
|
|
72
101
|
timestamp: new Date().toISOString()
|
|
@@ -98,7 +127,7 @@ function parseYApiParams(input) {
|
|
|
98
127
|
const projectMatch = input.match(/(?:项目|project)\s*[:=]?\s*(\S+)/i);
|
|
99
128
|
if (projectMatch) params.project = projectMatch[1];
|
|
100
129
|
|
|
101
|
-
const keywordMatch = input.match(/(?:关键词|keyword
|
|
130
|
+
const keywordMatch = input.match(/(?:关键词|keyword|名称|name)\s*[:=]?\s*(.+?)(?=\s+(?:项目|project|格式|format)\s*[:=]|\s*$)/i);
|
|
102
131
|
if (keywordMatch) params.keyword = keywordMatch[1].trim();
|
|
103
132
|
|
|
104
133
|
const formatMatch = input.match(/(?:格式|format)\s*[:=]?\s*(json|markdown|swagger)/i);
|
|
@@ -106,3 +135,107 @@ function parseYApiParams(input) {
|
|
|
106
135
|
|
|
107
136
|
return params;
|
|
108
137
|
}
|
|
138
|
+
|
|
139
|
+
function summarizeYApiExecution(action, execution) {
|
|
140
|
+
const payload = unwrapPayload(execution);
|
|
141
|
+
const data = getPayloadData(payload);
|
|
142
|
+
const items = extractListItems(data);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
action,
|
|
146
|
+
status: extractStatus(data) || extractStatus(payload) || 'ok',
|
|
147
|
+
total: extractTotal(data, payload, items),
|
|
148
|
+
items: items.slice(0, 5).map(summarizeItem),
|
|
149
|
+
runId: extractRunId(data) || extractRunId(payload) || null
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function unwrapPayload(payload) {
|
|
154
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
155
|
+
return payload;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const textPayload = extractContentText(payload);
|
|
159
|
+
if (textPayload) {
|
|
160
|
+
return parseMaybeJson(textPayload);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if ('data' in payload && payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
|
164
|
+
return payload.data;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return payload;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getPayloadData(payload) {
|
|
171
|
+
return payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function extractListItems(data) {
|
|
175
|
+
const candidates = [data.list, data.items, data.records, data.result, data.data];
|
|
176
|
+
for (const candidate of candidates) {
|
|
177
|
+
if (Array.isArray(candidate)) {
|
|
178
|
+
return candidate;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return [];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function extractStatus(payload) {
|
|
186
|
+
if (!payload || typeof payload !== 'object') {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (typeof payload.status === 'string') {
|
|
191
|
+
return payload.status;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (payload.success === false) {
|
|
195
|
+
return 'error';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return typeof payload.code === 'string' ? payload.code : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function extractTotal(data, payload, items) {
|
|
202
|
+
return data.total || payload.total || items.length || null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function extractRunId(payload) {
|
|
206
|
+
if (!payload || typeof payload !== 'object') {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return payload.runId || payload.id || payload.requestId || null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function summarizeItem(item) {
|
|
214
|
+
if (!item || typeof item !== 'object') {
|
|
215
|
+
return item;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
id: item._id || item.id || item.api_id || item.catid || null,
|
|
220
|
+
name: item.name || item.title || item.path || item.url || null,
|
|
221
|
+
method: item.method || item.req_method || null
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function extractContentText(payload) {
|
|
226
|
+
const content = Array.isArray(payload.content) ? payload.content : [];
|
|
227
|
+
const textItem = content.find((item) => item && typeof item.text === 'string');
|
|
228
|
+
return textItem ? textItem.text : null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function parseMaybeJson(value) {
|
|
232
|
+
if (typeof value !== 'string') {
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
return JSON.parse(value);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return { text: value };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -13,7 +13,9 @@ module.exports = {
|
|
|
13
13
|
},
|
|
14
14
|
|
|
15
15
|
async execute(input, context) {
|
|
16
|
-
const knowledgeBase = context.componentRegistry?.getAdapter('knowledgeBase')
|
|
16
|
+
const knowledgeBase = context.componentRegistry?.getAdapter('knowledgeBase')
|
|
17
|
+
|| context.componentRegistry?.getAdapterByProvider?.('knowledgeBase', 'yuque')
|
|
18
|
+
|| null;
|
|
17
19
|
const learnedBinding = context.resourceBinding?.componentType === 'knowledgeBase'
|
|
18
20
|
? context.resourceBinding
|
|
19
21
|
: null;
|
|
@@ -29,17 +31,33 @@ module.exports = {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
if (!knowledgeBase) {
|
|
35
|
+
return {
|
|
36
|
+
type: 'result',
|
|
37
|
+
skill: 'yuque-sync-design',
|
|
38
|
+
message: 'Yuque adapter is not available in the current registry.',
|
|
39
|
+
data: {
|
|
40
|
+
hint: 'Make sure the knowledgeBase component is configured and loaded',
|
|
41
|
+
binding: learnedBinding
|
|
42
|
+
},
|
|
43
|
+
input,
|
|
44
|
+
timestamp: new Date().toISOString()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
const params = parseYuqueParams(input);
|
|
33
49
|
|
|
34
50
|
if (params.action === 'search') {
|
|
51
|
+
const execution = await knowledgeBase.search(params.keyword, 'doc');
|
|
35
52
|
return {
|
|
36
53
|
type: 'result',
|
|
37
54
|
skill: 'yuque-sync-design',
|
|
38
55
|
message: `Searching Yuque for: ${params.keyword || 'all'}`,
|
|
39
56
|
data: {
|
|
40
|
-
|
|
57
|
+
action: 'search',
|
|
41
58
|
args: { q: params.keyword, type: 'doc' },
|
|
42
|
-
binding: learnedBinding
|
|
59
|
+
binding: learnedBinding,
|
|
60
|
+
execution: summarizeYuqueExecution('search', execution)
|
|
43
61
|
},
|
|
44
62
|
input,
|
|
45
63
|
timestamp: new Date().toISOString()
|
|
@@ -47,14 +65,16 @@ module.exports = {
|
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
if (params.action === 'list') {
|
|
68
|
+
const execution = await knowledgeBase.getRepos();
|
|
50
69
|
return {
|
|
51
70
|
type: 'result',
|
|
52
71
|
skill: 'yuque-sync-design',
|
|
53
72
|
message: 'Listing Yuque repositories',
|
|
54
73
|
data: {
|
|
55
|
-
|
|
74
|
+
action: 'list',
|
|
56
75
|
args: {},
|
|
57
|
-
binding: learnedBinding
|
|
76
|
+
binding: learnedBinding,
|
|
77
|
+
execution: summarizeYuqueExecution('list', execution)
|
|
58
78
|
},
|
|
59
79
|
input,
|
|
60
80
|
timestamp: new Date().toISOString()
|
|
@@ -62,7 +82,7 @@ module.exports = {
|
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
if (params.action === 'sync') {
|
|
65
|
-
return syncDesignDoc(params, input, learnedBinding);
|
|
85
|
+
return syncDesignDoc(params, input, learnedBinding, knowledgeBase);
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
return {
|
|
@@ -82,9 +102,9 @@ module.exports = {
|
|
|
82
102
|
|
|
83
103
|
function parseYuqueParams(input) {
|
|
84
104
|
const params = {};
|
|
85
|
-
if (/搜索|search|查找/i.test(input)) params.action = 'search';
|
|
86
|
-
if (/列表|list|仓库|repo/i.test(input)) params.action = 'list';
|
|
87
105
|
if (/同步|sync|上传|push/i.test(input)) params.action = 'sync';
|
|
106
|
+
else if (/搜索|search|查找/i.test(input)) params.action = 'search';
|
|
107
|
+
else if (/列表|list|仓库|repo/i.test(input)) params.action = 'list';
|
|
88
108
|
|
|
89
109
|
const pathMatch = input.match(/(?:路径|path|文件|file)\s*[:=]?\s*(\S+)/i);
|
|
90
110
|
if (pathMatch) params.path = pathMatch[1];
|
|
@@ -98,7 +118,7 @@ function parseYuqueParams(input) {
|
|
|
98
118
|
return params;
|
|
99
119
|
}
|
|
100
120
|
|
|
101
|
-
async function syncDesignDoc(params, input, learnedBinding) {
|
|
121
|
+
async function syncDesignDoc(params, input, learnedBinding, knowledgeBase) {
|
|
102
122
|
const filePath = params.path || 'DESIGN.md';
|
|
103
123
|
|
|
104
124
|
if (!(await fs.pathExists(filePath))) {
|
|
@@ -113,27 +133,126 @@ async function syncDesignDoc(params, input, learnedBinding) {
|
|
|
113
133
|
|
|
114
134
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
115
135
|
const title = extractTitle(content) || path.basename(filePath, '.md');
|
|
136
|
+
const namespace = params.repo || learnedBinding?.connection?.namespace;
|
|
137
|
+
const execution = await knowledgeBase.createDoc(namespace, {
|
|
138
|
+
slug: title.toLowerCase().replace(/\s+/g, '-'),
|
|
139
|
+
title,
|
|
140
|
+
body: content,
|
|
141
|
+
format: 'markdown'
|
|
142
|
+
});
|
|
116
143
|
|
|
117
144
|
return {
|
|
118
145
|
type: 'result',
|
|
119
146
|
skill: 'yuque-sync-design',
|
|
120
147
|
message: `Ready to sync "${title}" to Yuque`,
|
|
121
148
|
data: {
|
|
122
|
-
|
|
149
|
+
action: 'sync',
|
|
123
150
|
args: {
|
|
124
|
-
namespace
|
|
151
|
+
namespace,
|
|
125
152
|
slug: title.toLowerCase().replace(/\s+/g, '-'),
|
|
126
153
|
title,
|
|
127
|
-
body: content,
|
|
128
154
|
format: 'markdown'
|
|
129
155
|
},
|
|
130
|
-
binding: learnedBinding
|
|
156
|
+
binding: learnedBinding,
|
|
157
|
+
execution: summarizeYuqueExecution('sync', execution)
|
|
131
158
|
},
|
|
132
159
|
input,
|
|
133
160
|
timestamp: new Date().toISOString()
|
|
134
161
|
};
|
|
135
162
|
}
|
|
136
163
|
|
|
164
|
+
function summarizeYuqueExecution(action, execution) {
|
|
165
|
+
const payload = unwrapPayload(execution);
|
|
166
|
+
const data = payload && typeof payload === 'object' ? payload : {};
|
|
167
|
+
const items = extractItems(data);
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
action,
|
|
171
|
+
status: extractStatus(data) || 'ok',
|
|
172
|
+
total: extractTotal(data, items),
|
|
173
|
+
items: items.slice(0, 5).map(summarizeItem),
|
|
174
|
+
id: data.id || data.doc_id || data.slug || null
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function unwrapPayload(payload) {
|
|
179
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
180
|
+
return payload;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const textPayload = extractContentText(payload);
|
|
184
|
+
if (textPayload) {
|
|
185
|
+
return parseMaybeJson(textPayload);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (payload.data && typeof payload.data === 'object' && !Array.isArray(payload.data)) {
|
|
189
|
+
return payload.data;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return payload;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function extractItems(data) {
|
|
196
|
+
const candidates = [data.list, data.items, data.repos, data.docs, data.result];
|
|
197
|
+
for (const candidate of candidates) {
|
|
198
|
+
if (Array.isArray(candidate)) {
|
|
199
|
+
return candidate;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function extractStatus(data) {
|
|
207
|
+
if (!data || typeof data !== 'object') {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (typeof data.status === 'string') {
|
|
212
|
+
return data.status;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (data.success === false) {
|
|
216
|
+
return 'error';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function extractTotal(data, items) {
|
|
223
|
+
return data.total || items.length || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function extractContentText(payload) {
|
|
227
|
+
const content = Array.isArray(payload.content) ? payload.content : [];
|
|
228
|
+
const textItem = content.find((item) => item && typeof item.text === 'string');
|
|
229
|
+
return textItem ? textItem.text : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseMaybeJson(value) {
|
|
233
|
+
if (typeof value !== 'string') {
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(value);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
return { text: value };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function summarizeItem(item) {
|
|
245
|
+
if (!item || typeof item !== 'object') {
|
|
246
|
+
return item;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
id: item.id || item.slug || item.namespace || null,
|
|
251
|
+
title: item.title || item.name || item.slug || null,
|
|
252
|
+
type: item.type || item.format || null
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
137
256
|
function extractTitle(content) {
|
|
138
257
|
const match = content.match(/^#\s+(.+)$/m);
|
|
139
258
|
return match ? match[1].trim() : null;
|