plugin-agent-orchestrator 1.0.22 → 1.0.25

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.
Files changed (103) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/dist/client/index.js +1 -1
  4. package/dist/client-v2/214.723affb37c13bf7a.js +10 -0
  5. package/dist/client-v2/264.0533912e6c5ea2d7.js +10 -0
  6. package/dist/client-v2/41.1805b2edfaa4afe2.js +10 -0
  7. package/dist/client-v2/418.5ae055abf141820e.js +10 -0
  8. package/dist/client-v2/619.d99d3c9e61c99064.js +10 -0
  9. package/dist/client-v2/70.a15d7fcec7c41768.js +10 -0
  10. package/dist/client-v2/892.72db4161511c8a16.js +10 -0
  11. package/dist/client-v2/926.87f660b670d85bcc.js +10 -0
  12. package/dist/client-v2/index.js +10 -0
  13. package/dist/externalVersion.js +8 -6
  14. package/dist/locale/en-US.json +7 -0
  15. package/dist/locale/vi-VN.json +7 -0
  16. package/dist/locale/zh-CN.json +27 -0
  17. package/dist/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.js +63 -0
  18. package/dist/server/plugin.js +32 -1
  19. package/dist/server/services/AgentHarness.js +52 -27
  20. package/dist/server/services/AgentLoopController.js +8 -2
  21. package/dist/server/services/AgentLoopService.js +1 -1
  22. package/dist/server/services/AgentRegistryService.js +53 -42
  23. package/dist/server/services/CircuitBreaker.js +7 -2
  24. package/dist/server/services/CodeValidator.js +48 -14
  25. package/dist/server/services/SandboxRunner.js +18 -14
  26. package/dist/server/skill-hub/plugin.js +44 -17
  27. package/dist/server/tools/delegate-task.js +7 -2
  28. package/dist/server/tools/skill-execute.js +33 -2
  29. package/dist/server/utils/ai-manager.js +51 -0
  30. package/dist/server/utils/ctx-utils.js +11 -0
  31. package/dist/server/utils/skill-settings.js +122 -0
  32. package/package.json +49 -45
  33. package/src/client/AIEmployeesContext.tsx +60 -19
  34. package/src/client/AgentRunsTab.tsx +769 -764
  35. package/src/client/HarnessProfilesTab.tsx +257 -247
  36. package/src/client/RulesTab.tsx +787 -716
  37. package/src/client/TracingTab.tsx +9 -6
  38. package/src/client/plugin.tsx +34 -27
  39. package/src/client/skill-hub/components/ExecutionHistory.tsx +9 -8
  40. package/src/client/skill-hub/components/GitSkillImport.tsx +12 -5
  41. package/src/client/skill-hub/components/LoopSettings.tsx +2 -2
  42. package/src/client/skill-hub/components/SkillEditor.tsx +2 -2
  43. package/src/client/skill-hub/components/SkillManager.tsx +2 -2
  44. package/src/client/skill-hub/components/SkillMetrics.tsx +157 -124
  45. package/src/client/skill-hub/components/SkillTestPanel.tsx +14 -13
  46. package/src/client/skill-hub/index.tsx +58 -51
  47. package/src/client/skill-hub/locale.ts +1 -1
  48. package/src/client/skill-hub/tools/InteractionSchemasProvider.tsx +132 -99
  49. package/src/client/skill-hub/tools/registerSkillLoopCards.ts +71 -58
  50. package/src/client/tools/PlanApprovalCard.tsx +3 -2
  51. package/src/client/tools/registerOrchestratorCards.ts +17 -7
  52. package/src/client-v2/components/AIEmployeeSelect.tsx +47 -0
  53. package/src/client-v2/components/AIEmployeesContext.tsx +110 -0
  54. package/src/client-v2/components/AgentRunsTab.tsx +767 -0
  55. package/src/client-v2/components/HarnessProfilesTab.tsx +254 -0
  56. package/src/client-v2/components/RulesTab.tsx +782 -0
  57. package/src/client-v2/components/TracingTab.tsx +432 -0
  58. package/src/client-v2/hooks/useApiRequest.ts +114 -0
  59. package/src/client-v2/index.tsx +1 -0
  60. package/src/client-v2/pages/AgentRunsPage.tsx +13 -0
  61. package/src/client-v2/pages/ExecutionHistoryPage.tsx +10 -0
  62. package/src/client-v2/pages/HarnessProfilesPage.tsx +10 -0
  63. package/src/client-v2/pages/LoopSettingsPage.tsx +10 -0
  64. package/src/client-v2/pages/RulesPage.tsx +13 -0
  65. package/src/client-v2/pages/SkillDefinitionsPage.tsx +10 -0
  66. package/src/client-v2/pages/SkillMetricsPage.tsx +10 -0
  67. package/src/client-v2/pages/TracingPage.tsx +13 -0
  68. package/src/client-v2/plugin.tsx +70 -0
  69. package/src/client-v2/skill-hub/components/ExecutionHistory.tsx +196 -0
  70. package/src/client-v2/skill-hub/components/FileLinkList.tsx +37 -0
  71. package/src/client-v2/skill-hub/components/GitSkillImport.tsx +539 -0
  72. package/src/client-v2/skill-hub/components/LoopSettings.tsx +331 -0
  73. package/src/client-v2/skill-hub/components/SkillEditor.tsx +453 -0
  74. package/src/client-v2/skill-hub/components/SkillManager.tsx +174 -0
  75. package/src/client-v2/skill-hub/components/SkillMetrics.tsx +157 -0
  76. package/src/client-v2/skill-hub/components/SkillTestPanel.tsx +135 -0
  77. package/src/client-v2/skill-hub/locale.ts +13 -0
  78. package/src/client-v2/skill-hub/tools/loopTemplates.ts +52 -0
  79. package/src/client-v2/skill-hub/utils/jsonFields.ts +41 -0
  80. package/src/client-v2/utils/jsonFields.ts +41 -0
  81. package/src/locale/en-US.json +7 -0
  82. package/src/locale/vi-VN.json +7 -0
  83. package/src/locale/zh-CN.json +27 -0
  84. package/src/server/__tests__/agent-registry-service.test.ts +147 -0
  85. package/src/server/__tests__/code-validator.test.ts +63 -0
  86. package/src/server/__tests__/skill-execute.test.ts +33 -0
  87. package/src/server/__tests__/skill-settings.test.ts +63 -0
  88. package/src/server/migrations/20260615000000-normalize-ai-employee-tool-bindings.ts +39 -0
  89. package/src/server/plugin.ts +62 -21
  90. package/src/server/services/AgentHarness.ts +49 -22
  91. package/src/server/services/AgentLoopController.ts +17 -6
  92. package/src/server/services/AgentLoopService.ts +1 -1
  93. package/src/server/services/AgentPlannerService.ts +10 -0
  94. package/src/server/services/AgentRegistryService.ts +89 -47
  95. package/src/server/services/CircuitBreaker.ts +10 -0
  96. package/src/server/services/CodeValidator.ts +237 -159
  97. package/src/server/services/SandboxRunner.ts +203 -189
  98. package/src/server/skill-hub/plugin.ts +933 -898
  99. package/src/server/tools/delegate-task.ts +12 -9
  100. package/src/server/tools/skill-execute.ts +194 -160
  101. package/src/server/utils/ai-manager.ts +24 -0
  102. package/src/server/utils/ctx-utils.ts +14 -0
  103. package/src/server/utils/skill-settings.ts +116 -0
@@ -18,7 +18,8 @@ import {
18
18
  Form,
19
19
  } from 'antd';
20
20
  import { EyeOutlined, CheckCircleOutlined, CloseCircleOutlined, ReloadOutlined } from '@ant-design/icons';
21
- import { useAPIClient, useRequest } from '@nocobase/client';
21
+ import { useRequest } from 'ahooks';
22
+ import { useApp } from '@nocobase/client-v2';
22
23
  import { useAIEmployees } from './AIEmployeesContext';
23
24
 
24
25
  const { Text, Paragraph } = Typography;
@@ -32,7 +33,7 @@ type FilterState = {
32
33
  };
33
34
 
34
35
  export const TracingTab: React.FC = () => {
35
- const api = useAPIClient();
36
+ const api = useApp().apiClient;
36
37
  const [selectedLog, setSelectedLog] = useState<any>(null);
37
38
  const [detailLoading, setDetailLoading] = useState(false);
38
39
 
@@ -61,10 +62,11 @@ export const TracingTab: React.FC = () => {
61
62
  }, [page, pageSize, filters]);
62
63
 
63
64
  const { data, loading, refresh } = useRequest(
64
- {
65
- url: 'orchestratorTracing:list',
66
- params: requestParams,
67
- },
65
+ () =>
66
+ api.request({
67
+ url: 'orchestratorTracing:list',
68
+ params: requestParams,
69
+ }),
68
70
  {
69
71
  refreshDeps: [requestParams],
70
72
  },
@@ -269,6 +271,7 @@ export const TracingTab: React.FC = () => {
269
271
  dataSource={logs}
270
272
  columns={columns}
271
273
  size="middle"
274
+ scroll={{ x: 'max-content' }}
272
275
  pagination={{
273
276
  current: page,
274
277
  pageSize,
@@ -1,27 +1,34 @@
1
- import { Plugin } from '@nocobase/client';
2
- import { OrchestratorSettings } from './OrchestratorSettings';
3
- import { InteractionSchemasProvider } from './skill-hub/tools/InteractionSchemasProvider';
4
- import { registerSkillLoopCards } from './skill-hub/tools/registerSkillLoopCards';
5
- import { registerOrchestratorCards } from './tools/registerOrchestratorCards';
6
-
7
- export class PluginAgentOrchestratorClient extends Plugin {
8
- async load() {
9
- (this as any).app.use(InteractionSchemasProvider);
10
-
11
- // Register under the "AI" settings group for consistency with other AI plugins
12
- (this as any).app.pluginSettingsManager.add('ai.orchestrator', {
13
- title: 'Agent Orchestrator',
14
- icon: 'ApartmentOutlined',
15
- Component: OrchestratorSettings,
16
- });
17
-
18
- await this.registerSkillUiCards();
19
- }
20
-
21
- private async registerSkillUiCards() {
22
- await registerOrchestratorCards((this as any).app);
23
- await registerSkillLoopCards((this as any).app);
24
- }
25
- }
26
-
27
- export default PluginAgentOrchestratorClient;
1
+ import { Plugin } from '@nocobase/client';
2
+ import { OrchestratorSettings } from './OrchestratorSettings';
3
+ import { InteractionSchemasProvider } from './skill-hub/tools/InteractionSchemasProvider';
4
+ import { registerSkillLoopCards } from './skill-hub/tools/registerSkillLoopCards';
5
+ import { registerOrchestratorCards } from './tools/registerOrchestratorCards';
6
+
7
+ export class PluginAgentOrchestratorClient extends Plugin {
8
+ async load() {
9
+ (this as any).app.use(InteractionSchemasProvider);
10
+
11
+ // Register under the "AI" settings group for consistency with other AI plugins
12
+ (this as any).app.pluginSettingsManager.add('ai.orchestrator', {
13
+ title: 'Agent Orchestrator',
14
+ icon: 'ApartmentOutlined',
15
+ Component: OrchestratorSettings,
16
+ });
17
+
18
+ (this as any).app.eventBus?.addEventListener?.('auth:tokenChanged', (event: Event) => {
19
+ const token = (event as CustomEvent<{ token?: string | null }>).detail?.token;
20
+ if (token) {
21
+ registerSkillLoopCards((this as any).app);
22
+ }
23
+ });
24
+
25
+ await this.registerSkillUiCards();
26
+ }
27
+
28
+ private async registerSkillUiCards() {
29
+ await registerOrchestratorCards((this as any).app);
30
+ await registerSkillLoopCards((this as any).app);
31
+ }
32
+ }
33
+
34
+ export default PluginAgentOrchestratorClient;
@@ -1,9 +1,10 @@
1
1
  import React, { useState, useEffect, useCallback } from 'react';
2
2
  import { Card, Table, Tag, Button, Typography, Space, Tooltip, Popconfirm, message } from 'antd';
3
3
  import { ReloadOutlined, DownloadOutlined, DeleteOutlined } from '@ant-design/icons';
4
- import { useAPIClient, Upload } from '@nocobase/client';
5
- import { useT } from '../locale';
6
- import { parseJsonText } from '../utils/jsonFields';
4
+ import { Upload } from '@nocobase/client';
5
+ import { useApp } from '@nocobase/client-v2';
6
+ import { useT } from '../locale';
7
+ import { parseJsonText } from '../utils/jsonFields';
7
8
 
8
9
  const STATUS_COLORS: Record<string, string> = {
9
10
  pending: 'default',
@@ -15,7 +16,7 @@ const STATUS_COLORS: Record<string, string> = {
15
16
  };
16
17
 
17
18
  export const ExecutionHistory: React.FC = () => {
18
- const api = useAPIClient();
19
+ const api = useApp().apiClient;
19
20
  const t = useT();
20
21
  const [executions, setExecutions] = useState<any[]>([]);
21
22
  const [loading, setLoading] = useState(false);
@@ -96,12 +97,12 @@ export const ExecutionHistory: React.FC = () => {
96
97
  },
97
98
  {
98
99
  title: t('Files'),
99
- dataIndex: 'outputFiles',
100
+ dataIndex: 'outputFiles',
100
101
  key: 'files',
101
102
  width: 250,
102
- render: (files: any[], record: any) => {
103
- files = parseJsonText(files, []);
104
- if (!Array.isArray(files) || !files.length) return '-';
103
+ render: (files: any[], record: any) => {
104
+ files = parseJsonText(files, []);
105
+ if (!Array.isArray(files) || !files.length) return '-';
105
106
  const formattedFiles = files.map((f, i) => ({
106
107
  id: `${record.id}-${f.name}-${i}`,
107
108
  title: f.name,
@@ -25,7 +25,7 @@ import {
25
25
  DatabaseOutlined,
26
26
  SearchOutlined,
27
27
  } from '@ant-design/icons';
28
- import { useAPIClient } from '@nocobase/client';
28
+ import { useApp } from '@nocobase/client-v2';
29
29
  import { useT } from '../locale';
30
30
 
31
31
  const { Text } = Typography;
@@ -39,7 +39,7 @@ interface GitSkillImportProps {
39
39
  export const GitSkillImport: React.FC<GitSkillImportProps> = ({ open, onClose }) => {
40
40
  const t = useT();
41
41
  const { token } = useToken();
42
- const api = useAPIClient();
42
+ const api = useApp().apiClient;
43
43
 
44
44
  const [step, setStep] = useState(0);
45
45
 
@@ -178,8 +178,7 @@ export const GitSkillImport: React.FC<GitSkillImportProps> = ({ open, onClose })
178
178
  const lower = searchText.toLowerCase();
179
179
  return skills.filter(
180
180
  (s) =>
181
- (s.title || s.name || '').toLowerCase().includes(lower) ||
182
- (s.description || '').toLowerCase().includes(lower),
181
+ (s.title || s.name || '').toLowerCase().includes(lower) || (s.description || '').toLowerCase().includes(lower),
183
182
  );
184
183
  }, [skills, searchText]);
185
184
 
@@ -531,6 +530,7 @@ export const GitSkillImport: React.FC<GitSkillImportProps> = ({ open, onClose })
531
530
  selectedRowKeys: selectedSkills,
532
531
  onChange: (keys) => setSelectedSkills(keys as string[]),
533
532
  }}
533
+ scroll={{ x: 'max-content' }}
534
534
  />
535
535
  </div>
536
536
  )}
@@ -547,7 +547,14 @@ export const GitSkillImport: React.FC<GitSkillImportProps> = ({ open, onClose })
547
547
  syncResults.filter((r) => r.status === 'updated').length
548
548
  } ${t('updated')}, ${syncResults.filter((r) => r.status === 'skipped').length} ${t('skipped')}`}
549
549
  />
550
- <Table dataSource={syncResults} columns={resultColumns} rowKey="folder" size="small" pagination={false} />
550
+ <Table
551
+ dataSource={syncResults}
552
+ columns={resultColumns}
553
+ rowKey="folder"
554
+ size="small"
555
+ pagination={false}
556
+ scroll={{ x: 'max-content' }}
557
+ />
551
558
  </div>
552
559
  )}
553
560
  </Modal>
@@ -16,7 +16,7 @@ import {
16
16
  Typography,
17
17
  } from 'antd';
18
18
  import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
19
- import { useAPIClient, useApp } from '@nocobase/client';
19
+ import { useApp } from '@nocobase/client-v2';
20
20
  import { useT } from '../locale';
21
21
  import { formatJsonText, parseJsonText, stringifyJsonText } from '../utils/jsonFields';
22
22
  import { getLoopTemplate, LOOP_TEMPLATES } from '../tools/loopTemplates';
@@ -30,8 +30,8 @@ const extractList = (data: any) => {
30
30
  };
31
31
 
32
32
  export const LoopSettings: React.FC = () => {
33
- const api = useAPIClient();
34
33
  const app = useApp();
34
+ const api = app.apiClient;
35
35
  const t = useT();
36
36
  const [form] = Form.useForm();
37
37
  const [skills, setSkills] = useState<any[]>([]);
@@ -1,7 +1,7 @@
1
1
  import React, { useEffect, useState } from 'react';
2
2
  import { Modal, Form, Input, Select, InputNumber, Switch, message, Upload, Radio, Button, Space, theme } from 'antd';
3
3
  import { InboxOutlined, CloseOutlined, SaveOutlined } from '@ant-design/icons';
4
- import { useAPIClient } from '@nocobase/client';
4
+ import { useApp } from '@nocobase/client-v2';
5
5
  import { useT } from '../locale';
6
6
  import { formatJsonText, stringifyJsonText, parseJsonText } from '../utils/jsonFields';
7
7
 
@@ -14,7 +14,7 @@ interface SkillEditorProps {
14
14
  }
15
15
 
16
16
  export const SkillEditor: React.FC<SkillEditorProps> = ({ skill, onClose }) => {
17
- const api = useAPIClient();
17
+ const api = useApp().apiClient;
18
18
  const t = useT();
19
19
  const { token } = useToken();
20
20
  const [form] = Form.useForm();
@@ -17,7 +17,7 @@ import {
17
17
  Tooltip,
18
18
  } from 'antd';
19
19
  import { PlusOutlined, EditOutlined, DeleteOutlined, PlayCircleOutlined, BranchesOutlined } from '@ant-design/icons';
20
- import { useAPIClient } from '@nocobase/client';
20
+ import { useApp } from '@nocobase/client-v2';
21
21
  import { useT } from '../locale';
22
22
  import { SkillEditor } from './SkillEditor';
23
23
  import { SkillTestPanel } from './SkillTestPanel';
@@ -26,7 +26,7 @@ import { GitSkillImport } from './GitSkillImport';
26
26
  const { TextArea } = Input;
27
27
 
28
28
  export const SkillManager: React.FC = () => {
29
- const api = useAPIClient();
29
+ const api = useApp().apiClient;
30
30
  const t = useT();
31
31
  const [skills, setSkills] = useState<any[]>([]);
32
32
  const [loading, setLoading] = useState(false);
@@ -1,124 +1,157 @@
1
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
- import { Card, Table, Typography, Space, Row, Col, Statistic, Progress } from 'antd';
3
- import { SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
4
- import { useAPIClient } from '@nocobase/client';
5
- import { useT } from '../locale';
6
-
7
- const { Title, Text } = Typography;
8
-
9
- export const SkillMetrics: React.FC = () => {
10
- const api = useAPIClient();
11
- const t = useT();
12
- const [executions, setExecutions] = useState<any[]>([]);
13
- const [loading, setLoading] = useState(false);
14
-
15
- const fetchExecutions = useCallback(async () => {
16
- setLoading(true);
17
- try {
18
- // Fetch up to 1000 recent executions to calculate basic metrics
19
- const { data } = await api.request({
20
- url: 'skillExecutions:list',
21
- params: {
22
- pageSize: 1000,
23
- sort: ['-createdAt'],
24
- appends: ['skill'],
25
- },
26
- });
27
- const rawData = data?.data?.data ?? data?.data ?? [];
28
- setExecutions(Array.isArray(rawData) ? rawData : []);
29
- } catch {
30
- // ignore
31
- } finally {
32
- setLoading(false);
33
- }
34
- }, [api]);
35
-
36
- useEffect(() => {
37
- fetchExecutions();
38
- }, [fetchExecutions]);
39
-
40
- const metrics = useMemo(() => {
41
- const total = executions.length;
42
- const succeeded = executions.filter((e) => e.status === 'succeeded').length;
43
- const failed = executions.filter((e) => e.status === 'failed').length;
44
- const timeout = executions.filter((e) => e.status === 'timeout').length;
45
- const canceled = executions.filter((e) => e.status === 'canceled').length;
46
-
47
- // Group by skill
48
- const bySkill: Record<string, any> = {};
49
- executions.forEach((e) => {
50
- const skillName = e.skill?.title || e.skill?.name || 'Unknown';
51
- if (!bySkill[skillName]) {
52
- bySkill[skillName] = { name: skillName, total: 0, succeeded: 0, failed: 0, timeout: 0, canceled: 0, totalDuration: 0, durationCount: 0 };
53
- }
54
- bySkill[skillName].total += 1;
55
- bySkill[skillName][e.status] = (bySkill[skillName][e.status] || 0) + 1;
56
- if (e.durationMs) {
57
- bySkill[skillName].totalDuration += e.durationMs;
58
- bySkill[skillName].durationCount += 1;
59
- }
60
- });
61
-
62
- const skillData = Object.values(bySkill).map((s) => ({
63
- ...s,
64
- successRate: s.total > 0 ? (s.succeeded / s.total) * 100 : 0,
65
- avgDuration: s.durationCount > 0 ? (s.totalDuration / s.durationCount / 1000).toFixed(2) : 0,
66
- })).sort((a: any, b: any) => b.total - a.total);
67
-
68
- return { total, succeeded, failed, timeout, canceled, skillData };
69
- }, [executions]);
70
-
71
- const columns = [
72
- { title: t('Skill'), dataIndex: 'name', key: 'name', width: 200 },
73
- { title: t('Total Runs'), dataIndex: 'total', key: 'total', width: 100 },
74
- {
75
- title: t('Success Rate'),
76
- dataIndex: 'successRate',
77
- key: 'successRate',
78
- width: 150,
79
- render: (val: number) => <Progress percent={Math.round(val)} size="small" status={val === 100 ? 'success' : val > 50 ? 'active' : 'exception'} />
80
- },
81
- { title: t('Success'), dataIndex: 'succeeded', key: 'succeeded', width: 100 },
82
- { title: t('Failed'), dataIndex: 'failed', key: 'failed', width: 100 },
83
- { title: t('Timeout'), dataIndex: 'timeout', key: 'timeout', width: 100 },
84
- { title: t('Avg Duration (s)'), dataIndex: 'avgDuration', key: 'avgDuration', width: 120 },
85
- ];
86
-
87
- return (
88
- <Space direction="vertical" size="large" style={{ width: '100%', padding: '0 16px' }}>
89
- <Row gutter={16}>
90
- <Col span={6}>
91
- <Card size="small">
92
- <Statistic title="Total Executions (Recent)" value={metrics.total} prefix={<SyncOutlined />} />
93
- </Card>
94
- </Col>
95
- <Col span={6}>
96
- <Card size="small">
97
- <Statistic title="Succeeded" value={metrics.succeeded} valueStyle={{ color: '#3f8600' }} prefix={<CheckCircleOutlined />} />
98
- </Card>
99
- </Col>
100
- <Col span={6}>
101
- <Card size="small">
102
- <Statistic title="Failed" value={metrics.failed} valueStyle={{ color: '#cf1322' }} prefix={<CloseCircleOutlined />} />
103
- </Card>
104
- </Col>
105
- <Col span={6}>
106
- <Card size="small">
107
- <Statistic title="Timeout/Canceled" value={metrics.timeout + metrics.canceled} valueStyle={{ color: '#faad14' }} prefix={<ClockCircleOutlined />} />
108
- </Card>
109
- </Col>
110
- </Row>
111
-
112
- <Card title={t('Metrics by Skill (Recent)')}>
113
- <Table
114
- dataSource={metrics.skillData}
115
- columns={columns}
116
- rowKey="name"
117
- loading={loading}
118
- pagination={false}
119
- size="middle"
120
- />
121
- </Card>
122
- </Space>
123
- );
124
- };
1
+ import React, { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { Card, Table, Typography, Space, Row, Col, Statistic, Progress } from 'antd';
3
+ import { SyncOutlined, CheckCircleOutlined, CloseCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
4
+ import { useApp } from '@nocobase/client-v2';
5
+ import { useT } from '../locale';
6
+
7
+ const { Title, Text } = Typography;
8
+
9
+ export const SkillMetrics: React.FC = () => {
10
+ const api = useApp().apiClient;
11
+ const t = useT();
12
+ const [executions, setExecutions] = useState<any[]>([]);
13
+ const [loading, setLoading] = useState(false);
14
+
15
+ const fetchExecutions = useCallback(async () => {
16
+ setLoading(true);
17
+ try {
18
+ // Fetch up to 1000 recent executions to calculate basic metrics
19
+ const { data } = await api.request({
20
+ url: 'skillExecutions:list',
21
+ params: {
22
+ pageSize: 1000,
23
+ sort: ['-createdAt'],
24
+ appends: ['skill'],
25
+ },
26
+ });
27
+ const rawData = data?.data?.data ?? data?.data ?? [];
28
+ setExecutions(Array.isArray(rawData) ? rawData : []);
29
+ } catch {
30
+ // ignore
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ }, [api]);
35
+
36
+ useEffect(() => {
37
+ fetchExecutions();
38
+ }, [fetchExecutions]);
39
+
40
+ const metrics = useMemo(() => {
41
+ const total = executions.length;
42
+ const succeeded = executions.filter((e) => e.status === 'succeeded').length;
43
+ const failed = executions.filter((e) => e.status === 'failed').length;
44
+ const timeout = executions.filter((e) => e.status === 'timeout').length;
45
+ const canceled = executions.filter((e) => e.status === 'canceled').length;
46
+
47
+ // Group by skill
48
+ const bySkill: Record<string, any> = {};
49
+ executions.forEach((e) => {
50
+ const skillName = e.skill?.title || e.skill?.name || 'Unknown';
51
+ if (!bySkill[skillName]) {
52
+ bySkill[skillName] = {
53
+ name: skillName,
54
+ total: 0,
55
+ succeeded: 0,
56
+ failed: 0,
57
+ timeout: 0,
58
+ canceled: 0,
59
+ totalDuration: 0,
60
+ durationCount: 0,
61
+ };
62
+ }
63
+ bySkill[skillName].total += 1;
64
+ bySkill[skillName][e.status] = (bySkill[skillName][e.status] || 0) + 1;
65
+ if (e.durationMs) {
66
+ bySkill[skillName].totalDuration += e.durationMs;
67
+ bySkill[skillName].durationCount += 1;
68
+ }
69
+ });
70
+
71
+ const skillData = Object.values(bySkill)
72
+ .map((s) => ({
73
+ ...s,
74
+ successRate: s.total > 0 ? (s.succeeded / s.total) * 100 : 0,
75
+ avgDuration: s.durationCount > 0 ? (s.totalDuration / s.durationCount / 1000).toFixed(2) : 0,
76
+ }))
77
+ .sort((a: any, b: any) => b.total - a.total);
78
+
79
+ return { total, succeeded, failed, timeout, canceled, skillData };
80
+ }, [executions]);
81
+
82
+ const columns = [
83
+ { title: t('Skill'), dataIndex: 'name', key: 'name', width: 200 },
84
+ { title: t('Total Runs'), dataIndex: 'total', key: 'total', width: 100 },
85
+ {
86
+ title: t('Success Rate'),
87
+ dataIndex: 'successRate',
88
+ key: 'successRate',
89
+ width: 150,
90
+ render: (val: number) => (
91
+ <Progress
92
+ percent={Math.round(val)}
93
+ size="small"
94
+ status={val === 100 ? 'success' : val > 50 ? 'active' : 'exception'}
95
+ />
96
+ ),
97
+ },
98
+ { title: t('Success'), dataIndex: 'succeeded', key: 'succeeded', width: 100 },
99
+ { title: t('Failed'), dataIndex: 'failed', key: 'failed', width: 100 },
100
+ { title: t('Timeout'), dataIndex: 'timeout', key: 'timeout', width: 100 },
101
+ { title: t('Avg Duration (s)'), dataIndex: 'avgDuration', key: 'avgDuration', width: 120 },
102
+ ];
103
+
104
+ return (
105
+ <Space direction="vertical" size="large" style={{ width: '100%', padding: '0 16px' }}>
106
+ <Row gutter={16}>
107
+ <Col span={6}>
108
+ <Card size="small">
109
+ <Statistic title="Total Executions (Recent)" value={metrics.total} prefix={<SyncOutlined />} />
110
+ </Card>
111
+ </Col>
112
+ <Col span={6}>
113
+ <Card size="small">
114
+ <Statistic
115
+ title="Succeeded"
116
+ value={metrics.succeeded}
117
+ valueStyle={{ color: '#3f8600' }}
118
+ prefix={<CheckCircleOutlined />}
119
+ />
120
+ </Card>
121
+ </Col>
122
+ <Col span={6}>
123
+ <Card size="small">
124
+ <Statistic
125
+ title="Failed"
126
+ value={metrics.failed}
127
+ valueStyle={{ color: '#cf1322' }}
128
+ prefix={<CloseCircleOutlined />}
129
+ />
130
+ </Card>
131
+ </Col>
132
+ <Col span={6}>
133
+ <Card size="small">
134
+ <Statistic
135
+ title="Timeout/Canceled"
136
+ value={metrics.timeout + metrics.canceled}
137
+ valueStyle={{ color: '#faad14' }}
138
+ prefix={<ClockCircleOutlined />}
139
+ />
140
+ </Card>
141
+ </Col>
142
+ </Row>
143
+
144
+ <Card title={t('Metrics by Skill (Recent)')}>
145
+ <Table
146
+ dataSource={metrics.skillData}
147
+ columns={columns}
148
+ rowKey="name"
149
+ loading={loading}
150
+ pagination={false}
151
+ size="middle"
152
+ scroll={{ x: 'max-content' }}
153
+ />
154
+ </Card>
155
+ </Space>
156
+ );
157
+ };
@@ -1,8 +1,9 @@
1
1
  import React, { useState } from 'react';
2
2
  import { Modal, Input, Button, Alert, Typography, Space, Spin } from 'antd';
3
- import { useAPIClient, Upload } from '@nocobase/client';
4
- import { useT } from '../locale';
5
- import { parseJsonText } from '../utils/jsonFields';
3
+ import { Upload } from '@nocobase/client';
4
+ import { useApp } from '@nocobase/client-v2';
5
+ import { useT } from '../locale';
6
+ import { parseJsonText } from '../utils/jsonFields';
6
7
 
7
8
  const { TextArea } = Input;
8
9
 
@@ -11,16 +12,16 @@ interface SkillTestPanelProps {
11
12
  onClose: () => void;
12
13
  }
13
14
 
14
- export const SkillTestPanel: React.FC<SkillTestPanelProps> = ({ skill, onClose }) => {
15
- const api = useAPIClient();
16
- const t = useT();
17
- const inputSchema = parseJsonText(skill.inputSchema, null);
18
- const [input, setInput] = useState(
19
- inputSchema?.properties
20
- ? JSON.stringify(
21
- Object.fromEntries(
22
- Object.keys(inputSchema.properties).map((k) => [k, '']),
23
- ),
15
+ export const SkillTestPanel: React.FC<SkillTestPanelProps> = ({ skill, onClose }) => {
16
+ const api = useApp().apiClient;
17
+ const t = useT();
18
+ const inputSchema = parseJsonText(skill.inputSchema, null);
19
+ const [input, setInput] = useState(
20
+ inputSchema?.properties
21
+ ? JSON.stringify(
22
+ Object.fromEntries(
23
+ Object.keys(inputSchema.properties).map((k) => [k, '']),
24
+ ),
24
25
  null,
25
26
  2,
26
27
  )