opc-agent 1.3.0 → 1.3.2
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 +6 -0
- package/CONTRIBUTING.md +75 -75
- package/README.md +358 -235
- package/README.zh-CN.md +415 -415
- package/dist/core/dashboard.d.ts +35 -0
- package/dist/core/dashboard.js +157 -0
- package/dist/core/priority.d.ts +52 -0
- package/dist/core/priority.js +102 -0
- package/dist/core/streaming.d.ts +56 -0
- package/dist/core/streaming.js +160 -0
- package/dist/deploy/hermes.js +22 -22
- package/dist/deploy/openclaw.js +31 -31
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -1
- package/dist/templates/code-reviewer.js +5 -5
- package/dist/templates/customer-service.js +2 -2
- package/dist/templates/data-analyst.js +5 -5
- package/dist/templates/knowledge-base.js +2 -2
- package/dist/templates/sales-assistant.js +4 -4
- package/dist/templates/teacher.js +6 -6
- package/dist/tools/gateway.d.ts +28 -0
- package/dist/tools/gateway.js +177 -0
- package/docs/.vitepress/config.ts +103 -103
- package/docs/api/cli.md +48 -48
- package/docs/api/oad-schema.md +64 -64
- package/docs/api/sdk.md +80 -80
- package/docs/guide/concepts.md +51 -51
- package/docs/guide/configuration.md +79 -79
- package/docs/guide/deployment.md +42 -42
- package/docs/guide/getting-started.md +44 -44
- package/docs/guide/templates.md +28 -28
- package/docs/guide/testing.md +84 -84
- package/docs/index.md +27 -27
- package/docs/zh/api/cli.md +54 -54
- package/docs/zh/api/oad-schema.md +87 -87
- package/docs/zh/api/sdk.md +102 -102
- package/docs/zh/guide/concepts.md +104 -104
- package/docs/zh/guide/configuration.md +135 -135
- package/docs/zh/guide/deployment.md +81 -81
- package/docs/zh/guide/getting-started.md +82 -82
- package/docs/zh/guide/templates.md +84 -84
- package/docs/zh/guide/testing.md +88 -88
- package/docs/zh/index.md +27 -27
- package/examples/customer-service-demo/README.md +90 -90
- package/examples/customer-service-demo/oad.yaml +107 -107
- package/package.json +50 -50
- package/src/analytics/index.ts +66 -66
- package/src/channels/discord.ts +192 -192
- package/src/channels/email.ts +177 -177
- package/src/channels/feishu.ts +236 -236
- package/src/channels/index.ts +15 -15
- package/src/channels/slack.ts +160 -160
- package/src/channels/telegram.ts +90 -90
- package/src/channels/voice.ts +106 -106
- package/src/channels/webhook.ts +199 -199
- package/src/channels/websocket.ts +87 -87
- package/src/channels/wechat.ts +149 -149
- package/src/cli.ts +1 -119
- package/src/core/a2a.ts +143 -143
- package/src/core/agent.ts +152 -152
- package/src/core/analytics-engine.ts +186 -186
- package/src/core/auth.ts +57 -57
- package/src/core/cache.ts +141 -141
- package/src/core/compose.ts +77 -77
- package/src/core/config.ts +14 -14
- package/src/core/dashboard.ts +219 -0
- package/src/core/errors.ts +148 -148
- package/src/core/hitl.ts +138 -138
- package/src/core/logger.ts +57 -57
- package/src/core/orchestrator.ts +215 -215
- package/src/core/performance.ts +187 -187
- package/src/core/priority.ts +140 -0
- package/src/core/rate-limiter.ts +128 -128
- package/src/core/room.ts +109 -109
- package/src/core/runtime.ts +152 -152
- package/src/core/sandbox.ts +101 -101
- package/src/core/security.ts +171 -171
- package/src/core/types.ts +68 -68
- package/src/core/versioning.ts +106 -106
- package/src/core/watch.ts +178 -178
- package/src/core/workflow.ts +235 -235
- package/src/deploy/hermes.ts +156 -156
- package/src/deploy/openclaw.ts +200 -200
- package/src/dtv/data.ts +29 -0
- package/src/dtv/trust.ts +43 -0
- package/src/dtv/value.ts +47 -0
- package/src/i18n/index.ts +216 -216
- package/src/index.ts +6 -4
- package/src/marketplace/index.ts +223 -0
- package/src/memory/deepbrain.ts +108 -108
- package/src/memory/index.ts +34 -34
- package/src/plugins/index.ts +208 -208
- package/src/schema/oad.ts +155 -154
- package/src/skills/base.ts +16 -16
- package/src/skills/document.ts +100 -100
- package/src/skills/http.ts +35 -35
- package/src/skills/index.ts +27 -27
- package/src/skills/scheduler.ts +80 -80
- package/src/skills/webhook-trigger.ts +59 -59
- package/src/templates/code-reviewer.ts +34 -30
- package/src/templates/customer-service.ts +80 -76
- package/src/templates/data-analyst.ts +70 -66
- package/src/templates/executive-assistant.ts +71 -71
- package/src/templates/financial-advisor.ts +60 -60
- package/src/templates/knowledge-base.ts +31 -27
- package/src/templates/legal-assistant.ts +71 -71
- package/src/templates/sales-assistant.ts +79 -75
- package/src/templates/teacher.ts +79 -75
- package/src/testing/index.ts +181 -181
- package/src/tools/calculator.ts +73 -73
- package/src/tools/datetime.ts +149 -149
- package/src/tools/json-transform.ts +187 -187
- package/src/tools/mcp.ts +76 -76
- package/src/tools/text-analysis.ts +116 -116
- package/templates/Dockerfile +15 -15
- package/templates/code-reviewer/README.md +27 -27
- package/templates/code-reviewer/oad.yaml +41 -41
- package/templates/customer-service/README.md +22 -22
- package/templates/customer-service/oad.yaml +36 -36
- package/templates/docker-compose.yml +21 -21
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/knowledge-base/README.md +28 -28
- package/templates/knowledge-base/oad.yaml +38 -38
- package/templates/sales-assistant/README.md +26 -26
- package/templates/sales-assistant/oad.yaml +43 -43
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/tests/a2a.test.ts +66 -66
- package/tests/agent.test.ts +72 -72
- package/tests/analytics.test.ts +50 -50
- package/tests/channel.test.ts +39 -39
- package/tests/e2e.test.ts +134 -134
- package/tests/errors.test.ts +83 -83
- package/tests/hitl.test.ts +71 -71
- package/tests/i18n.test.ts +41 -41
- package/tests/mcp.test.ts +54 -54
- package/tests/oad.test.ts +68 -68
- package/tests/performance.test.ts +115 -115
- package/tests/plugin.test.ts +74 -74
- package/tests/room.test.ts +106 -106
- package/tests/runtime.test.ts +42 -42
- package/tests/sandbox.test.ts +46 -46
- package/tests/security.test.ts +60 -60
- package/tests/templates.test.ts +77 -77
- package/tests/v070.test.ts +76 -76
- package/tests/versioning.test.ts +75 -75
- package/tests/voice.test.ts +61 -61
- package/tests/webhook.test.ts +29 -29
- package/tests/workflow.test.ts +143 -143
- package/tsconfig.json +19 -19
- package/vitest.config.ts +9 -9
- package/src/traces/index.ts +0 -132
|
@@ -1,186 +1,186 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Analytics Engine - Persistent analytics with JSON file storage.
|
|
3
|
-
* Tracks every message, LLM call, tool use, and error with timestamps.
|
|
4
|
-
*/
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import * as path from 'path';
|
|
7
|
-
|
|
8
|
-
export interface AnalyticsEvent {
|
|
9
|
-
type: 'message' | 'llm_call' | 'tool_use' | 'error';
|
|
10
|
-
timestamp: number;
|
|
11
|
-
data: Record<string, any>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface AnalyticsStats {
|
|
15
|
-
totalMessages: number;
|
|
16
|
-
totalLLMCalls: number;
|
|
17
|
-
totalToolUses: number;
|
|
18
|
-
totalErrors: number;
|
|
19
|
-
avgResponseTimeMs: number;
|
|
20
|
-
totalTokens: { input: number; output: number; total: number };
|
|
21
|
-
topSkills: { name: string; count: number }[];
|
|
22
|
-
topErrors: { message: string; count: number }[];
|
|
23
|
-
messagesPerDay: Record<string, number>;
|
|
24
|
-
period: { from: number; to: number };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class AnalyticsEngine {
|
|
28
|
-
private dataDir: string;
|
|
29
|
-
private eventsFile: string;
|
|
30
|
-
private events: AnalyticsEvent[] = [];
|
|
31
|
-
|
|
32
|
-
constructor(dataDir: string = '.') {
|
|
33
|
-
this.dataDir = path.resolve(dataDir, 'data');
|
|
34
|
-
this.eventsFile = path.join(this.dataDir, 'analytics.json');
|
|
35
|
-
this.load();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
private load(): void {
|
|
39
|
-
try {
|
|
40
|
-
if (fs.existsSync(this.eventsFile)) {
|
|
41
|
-
const raw = fs.readFileSync(this.eventsFile, 'utf-8');
|
|
42
|
-
this.events = JSON.parse(raw);
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
this.events = [];
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private save(): void {
|
|
50
|
-
if (!fs.existsSync(this.dataDir)) {
|
|
51
|
-
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
52
|
-
}
|
|
53
|
-
// Keep last 10000 events to prevent unbounded growth
|
|
54
|
-
if (this.events.length > 10000) {
|
|
55
|
-
this.events = this.events.slice(-10000);
|
|
56
|
-
}
|
|
57
|
-
fs.writeFileSync(this.eventsFile, JSON.stringify(this.events, null, 2));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
track(type: AnalyticsEvent['type'], data: Record<string, any>): void {
|
|
61
|
-
this.events.push({ type, timestamp: Date.now(), data });
|
|
62
|
-
this.save();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
trackMessage(userId: string, responseTimeMs: number, tokensIn: number, tokensOut: number): void {
|
|
66
|
-
this.track('message', { userId, responseTimeMs, tokensIn, tokensOut });
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
trackLLMCall(provider: string, model: string, tokensIn: number, tokensOut: number, latencyMs: number): void {
|
|
70
|
-
this.track('llm_call', { provider, model, tokensIn, tokensOut, latencyMs });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
trackToolUse(toolName: string, success: boolean, latencyMs: number): void {
|
|
74
|
-
this.track('tool_use', { toolName, success, latencyMs });
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
trackError(error: string, context?: string): void {
|
|
78
|
-
this.track('error', { error, context });
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
getStats(fromTs?: number, toTs?: number): AnalyticsStats {
|
|
82
|
-
const now = Date.now();
|
|
83
|
-
const from = fromTs ?? 0;
|
|
84
|
-
const to = toTs ?? now;
|
|
85
|
-
const filtered = this.events.filter(e => e.timestamp >= from && e.timestamp <= to);
|
|
86
|
-
|
|
87
|
-
const messages = filtered.filter(e => e.type === 'message');
|
|
88
|
-
const llmCalls = filtered.filter(e => e.type === 'llm_call');
|
|
89
|
-
const toolUses = filtered.filter(e => e.type === 'tool_use');
|
|
90
|
-
const errors = filtered.filter(e => e.type === 'error');
|
|
91
|
-
|
|
92
|
-
// Avg response time
|
|
93
|
-
const totalResponseTime = messages.reduce((sum, e) => sum + (e.data.responseTimeMs ?? 0), 0);
|
|
94
|
-
const avgResponseTimeMs = messages.length > 0 ? Math.round(totalResponseTime / messages.length) : 0;
|
|
95
|
-
|
|
96
|
-
// Total tokens
|
|
97
|
-
const totalTokensIn = llmCalls.reduce((sum, e) => sum + (e.data.tokensIn ?? 0), 0);
|
|
98
|
-
const totalTokensOut = llmCalls.reduce((sum, e) => sum + (e.data.tokensOut ?? 0), 0);
|
|
99
|
-
|
|
100
|
-
// Top skills (from tool_use)
|
|
101
|
-
const skillCounts: Record<string, number> = {};
|
|
102
|
-
for (const e of toolUses) {
|
|
103
|
-
const name = e.data.toolName ?? 'unknown';
|
|
104
|
-
skillCounts[name] = (skillCounts[name] ?? 0) + 1;
|
|
105
|
-
}
|
|
106
|
-
const topSkills = Object.entries(skillCounts)
|
|
107
|
-
.sort((a, b) => b[1] - a[1])
|
|
108
|
-
.slice(0, 10)
|
|
109
|
-
.map(([name, count]) => ({ name, count }));
|
|
110
|
-
|
|
111
|
-
// Top errors
|
|
112
|
-
const errorCounts: Record<string, number> = {};
|
|
113
|
-
for (const e of errors) {
|
|
114
|
-
const msg = e.data.error ?? 'unknown';
|
|
115
|
-
errorCounts[msg] = (errorCounts[msg] ?? 0) + 1;
|
|
116
|
-
}
|
|
117
|
-
const topErrors = Object.entries(errorCounts)
|
|
118
|
-
.sort((a, b) => b[1] - a[1])
|
|
119
|
-
.slice(0, 10)
|
|
120
|
-
.map(([message, count]) => ({ message, count }));
|
|
121
|
-
|
|
122
|
-
// Messages per day
|
|
123
|
-
const messagesPerDay: Record<string, number> = {};
|
|
124
|
-
for (const e of messages) {
|
|
125
|
-
const day = new Date(e.timestamp).toISOString().slice(0, 10);
|
|
126
|
-
messagesPerDay[day] = (messagesPerDay[day] ?? 0) + 1;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
totalMessages: messages.length,
|
|
131
|
-
totalLLMCalls: llmCalls.length,
|
|
132
|
-
totalToolUses: toolUses.length,
|
|
133
|
-
totalErrors: errors.length,
|
|
134
|
-
avgResponseTimeMs,
|
|
135
|
-
totalTokens: { input: totalTokensIn, output: totalTokensOut, total: totalTokensIn + totalTokensOut },
|
|
136
|
-
topSkills,
|
|
137
|
-
topErrors,
|
|
138
|
-
messagesPerDay,
|
|
139
|
-
period: { from, to },
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
getRecentEvents(limit: number = 50): AnalyticsEvent[] {
|
|
144
|
-
return this.events.slice(-limit);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
clear(): void {
|
|
148
|
-
this.events = [];
|
|
149
|
-
this.save();
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Format stats for CLI display.
|
|
154
|
-
*/
|
|
155
|
-
static formatStats(stats: AnalyticsStats): string {
|
|
156
|
-
const lines: string[] = [];
|
|
157
|
-
lines.push('');
|
|
158
|
-
lines.push('══════════════════════════════════════════');
|
|
159
|
-
lines.push(' OPC Agent Analytics');
|
|
160
|
-
lines.push('══════════════════════════════════════════');
|
|
161
|
-
lines.push('');
|
|
162
|
-
lines.push(` 📨 Messages: ${stats.totalMessages}`);
|
|
163
|
-
lines.push(` 🤖 LLM Calls: ${stats.totalLLMCalls}`);
|
|
164
|
-
lines.push(` 🔧 Tool Uses: ${stats.totalToolUses}`);
|
|
165
|
-
lines.push(` ❌ Errors: ${stats.totalErrors}`);
|
|
166
|
-
lines.push(` ⏱ Avg Response: ${stats.avgResponseTimeMs}ms`);
|
|
167
|
-
lines.push(` 🪙 Tokens: ${stats.totalTokens.total} (in: ${stats.totalTokens.input}, out: ${stats.totalTokens.output})`);
|
|
168
|
-
lines.push('');
|
|
169
|
-
if (stats.topSkills.length > 0) {
|
|
170
|
-
lines.push(' Top Skills:');
|
|
171
|
-
for (const s of stats.topSkills.slice(0, 5)) {
|
|
172
|
-
lines.push(` • ${s.name}: ${s.count}`);
|
|
173
|
-
}
|
|
174
|
-
lines.push('');
|
|
175
|
-
}
|
|
176
|
-
if (stats.topErrors.length > 0) {
|
|
177
|
-
lines.push(' Top Errors:');
|
|
178
|
-
for (const e of stats.topErrors.slice(0, 3)) {
|
|
179
|
-
lines.push(` • ${e.message}: ${e.count}`);
|
|
180
|
-
}
|
|
181
|
-
lines.push('');
|
|
182
|
-
}
|
|
183
|
-
lines.push('──────────────────────────────────────────');
|
|
184
|
-
return lines.join('\n');
|
|
185
|
-
}
|
|
186
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Engine - Persistent analytics with JSON file storage.
|
|
3
|
+
* Tracks every message, LLM call, tool use, and error with timestamps.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
export interface AnalyticsEvent {
|
|
9
|
+
type: 'message' | 'llm_call' | 'tool_use' | 'error';
|
|
10
|
+
timestamp: number;
|
|
11
|
+
data: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AnalyticsStats {
|
|
15
|
+
totalMessages: number;
|
|
16
|
+
totalLLMCalls: number;
|
|
17
|
+
totalToolUses: number;
|
|
18
|
+
totalErrors: number;
|
|
19
|
+
avgResponseTimeMs: number;
|
|
20
|
+
totalTokens: { input: number; output: number; total: number };
|
|
21
|
+
topSkills: { name: string; count: number }[];
|
|
22
|
+
topErrors: { message: string; count: number }[];
|
|
23
|
+
messagesPerDay: Record<string, number>;
|
|
24
|
+
period: { from: number; to: number };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class AnalyticsEngine {
|
|
28
|
+
private dataDir: string;
|
|
29
|
+
private eventsFile: string;
|
|
30
|
+
private events: AnalyticsEvent[] = [];
|
|
31
|
+
|
|
32
|
+
constructor(dataDir: string = '.') {
|
|
33
|
+
this.dataDir = path.resolve(dataDir, 'data');
|
|
34
|
+
this.eventsFile = path.join(this.dataDir, 'analytics.json');
|
|
35
|
+
this.load();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private load(): void {
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(this.eventsFile)) {
|
|
41
|
+
const raw = fs.readFileSync(this.eventsFile, 'utf-8');
|
|
42
|
+
this.events = JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
this.events = [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private save(): void {
|
|
50
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
51
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
// Keep last 10000 events to prevent unbounded growth
|
|
54
|
+
if (this.events.length > 10000) {
|
|
55
|
+
this.events = this.events.slice(-10000);
|
|
56
|
+
}
|
|
57
|
+
fs.writeFileSync(this.eventsFile, JSON.stringify(this.events, null, 2));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
track(type: AnalyticsEvent['type'], data: Record<string, any>): void {
|
|
61
|
+
this.events.push({ type, timestamp: Date.now(), data });
|
|
62
|
+
this.save();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
trackMessage(userId: string, responseTimeMs: number, tokensIn: number, tokensOut: number): void {
|
|
66
|
+
this.track('message', { userId, responseTimeMs, tokensIn, tokensOut });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
trackLLMCall(provider: string, model: string, tokensIn: number, tokensOut: number, latencyMs: number): void {
|
|
70
|
+
this.track('llm_call', { provider, model, tokensIn, tokensOut, latencyMs });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
trackToolUse(toolName: string, success: boolean, latencyMs: number): void {
|
|
74
|
+
this.track('tool_use', { toolName, success, latencyMs });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
trackError(error: string, context?: string): void {
|
|
78
|
+
this.track('error', { error, context });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getStats(fromTs?: number, toTs?: number): AnalyticsStats {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const from = fromTs ?? 0;
|
|
84
|
+
const to = toTs ?? now;
|
|
85
|
+
const filtered = this.events.filter(e => e.timestamp >= from && e.timestamp <= to);
|
|
86
|
+
|
|
87
|
+
const messages = filtered.filter(e => e.type === 'message');
|
|
88
|
+
const llmCalls = filtered.filter(e => e.type === 'llm_call');
|
|
89
|
+
const toolUses = filtered.filter(e => e.type === 'tool_use');
|
|
90
|
+
const errors = filtered.filter(e => e.type === 'error');
|
|
91
|
+
|
|
92
|
+
// Avg response time
|
|
93
|
+
const totalResponseTime = messages.reduce((sum, e) => sum + (e.data.responseTimeMs ?? 0), 0);
|
|
94
|
+
const avgResponseTimeMs = messages.length > 0 ? Math.round(totalResponseTime / messages.length) : 0;
|
|
95
|
+
|
|
96
|
+
// Total tokens
|
|
97
|
+
const totalTokensIn = llmCalls.reduce((sum, e) => sum + (e.data.tokensIn ?? 0), 0);
|
|
98
|
+
const totalTokensOut = llmCalls.reduce((sum, e) => sum + (e.data.tokensOut ?? 0), 0);
|
|
99
|
+
|
|
100
|
+
// Top skills (from tool_use)
|
|
101
|
+
const skillCounts: Record<string, number> = {};
|
|
102
|
+
for (const e of toolUses) {
|
|
103
|
+
const name = e.data.toolName ?? 'unknown';
|
|
104
|
+
skillCounts[name] = (skillCounts[name] ?? 0) + 1;
|
|
105
|
+
}
|
|
106
|
+
const topSkills = Object.entries(skillCounts)
|
|
107
|
+
.sort((a, b) => b[1] - a[1])
|
|
108
|
+
.slice(0, 10)
|
|
109
|
+
.map(([name, count]) => ({ name, count }));
|
|
110
|
+
|
|
111
|
+
// Top errors
|
|
112
|
+
const errorCounts: Record<string, number> = {};
|
|
113
|
+
for (const e of errors) {
|
|
114
|
+
const msg = e.data.error ?? 'unknown';
|
|
115
|
+
errorCounts[msg] = (errorCounts[msg] ?? 0) + 1;
|
|
116
|
+
}
|
|
117
|
+
const topErrors = Object.entries(errorCounts)
|
|
118
|
+
.sort((a, b) => b[1] - a[1])
|
|
119
|
+
.slice(0, 10)
|
|
120
|
+
.map(([message, count]) => ({ message, count }));
|
|
121
|
+
|
|
122
|
+
// Messages per day
|
|
123
|
+
const messagesPerDay: Record<string, number> = {};
|
|
124
|
+
for (const e of messages) {
|
|
125
|
+
const day = new Date(e.timestamp).toISOString().slice(0, 10);
|
|
126
|
+
messagesPerDay[day] = (messagesPerDay[day] ?? 0) + 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
totalMessages: messages.length,
|
|
131
|
+
totalLLMCalls: llmCalls.length,
|
|
132
|
+
totalToolUses: toolUses.length,
|
|
133
|
+
totalErrors: errors.length,
|
|
134
|
+
avgResponseTimeMs,
|
|
135
|
+
totalTokens: { input: totalTokensIn, output: totalTokensOut, total: totalTokensIn + totalTokensOut },
|
|
136
|
+
topSkills,
|
|
137
|
+
topErrors,
|
|
138
|
+
messagesPerDay,
|
|
139
|
+
period: { from, to },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getRecentEvents(limit: number = 50): AnalyticsEvent[] {
|
|
144
|
+
return this.events.slice(-limit);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
clear(): void {
|
|
148
|
+
this.events = [];
|
|
149
|
+
this.save();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Format stats for CLI display.
|
|
154
|
+
*/
|
|
155
|
+
static formatStats(stats: AnalyticsStats): string {
|
|
156
|
+
const lines: string[] = [];
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push('══════════════════════════════════════════');
|
|
159
|
+
lines.push(' OPC Agent Analytics');
|
|
160
|
+
lines.push('══════════════════════════════════════════');
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push(` 📨 Messages: ${stats.totalMessages}`);
|
|
163
|
+
lines.push(` 🤖 LLM Calls: ${stats.totalLLMCalls}`);
|
|
164
|
+
lines.push(` 🔧 Tool Uses: ${stats.totalToolUses}`);
|
|
165
|
+
lines.push(` ❌ Errors: ${stats.totalErrors}`);
|
|
166
|
+
lines.push(` ⏱ Avg Response: ${stats.avgResponseTimeMs}ms`);
|
|
167
|
+
lines.push(` 🪙 Tokens: ${stats.totalTokens.total} (in: ${stats.totalTokens.input}, out: ${stats.totalTokens.output})`);
|
|
168
|
+
lines.push('');
|
|
169
|
+
if (stats.topSkills.length > 0) {
|
|
170
|
+
lines.push(' Top Skills:');
|
|
171
|
+
for (const s of stats.topSkills.slice(0, 5)) {
|
|
172
|
+
lines.push(` • ${s.name}: ${s.count}`);
|
|
173
|
+
}
|
|
174
|
+
lines.push('');
|
|
175
|
+
}
|
|
176
|
+
if (stats.topErrors.length > 0) {
|
|
177
|
+
lines.push(' Top Errors:');
|
|
178
|
+
for (const e of stats.topErrors.slice(0, 3)) {
|
|
179
|
+
lines.push(` • ${e.message}: ${e.count}`);
|
|
180
|
+
}
|
|
181
|
+
lines.push('');
|
|
182
|
+
}
|
|
183
|
+
lines.push('──────────────────────────────────────────');
|
|
184
|
+
return lines.join('\n');
|
|
185
|
+
}
|
|
186
|
+
}
|
package/src/core/auth.ts
CHANGED
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
-
|
|
3
|
-
export interface AuthConfig {
|
|
4
|
-
apiKeys: string[];
|
|
5
|
-
sessionIsolation?: boolean;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface AuthSession {
|
|
9
|
-
apiKey: string;
|
|
10
|
-
userId: string;
|
|
11
|
-
createdAt: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const sessions = new Map<string, AuthSession>();
|
|
15
|
-
|
|
16
|
-
export function createAuthMiddleware(config: AuthConfig) {
|
|
17
|
-
return (req: Request, res: Response, next: NextFunction): void => {
|
|
18
|
-
// Skip auth for non-API routes and health/metrics
|
|
19
|
-
if (!req.path.startsWith('/api/') || req.path === '/api/health' || req.path === '/api/metrics') {
|
|
20
|
-
next();
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const apiKey = req.headers['x-api-key'] as string
|
|
25
|
-
?? req.headers['authorization']?.replace(/^Bearer\s+/i, '')
|
|
26
|
-
?? (req.query as any).apiKey;
|
|
27
|
-
|
|
28
|
-
if (!apiKey || !config.apiKeys.includes(apiKey)) {
|
|
29
|
-
res.status(401).json({ error: 'Unauthorized. Provide a valid API key via X-API-Key header, Bearer token, or ?apiKey query.' });
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Derive userId from API key for session isolation
|
|
34
|
-
const userId = `user_${hashKey(apiKey)}`;
|
|
35
|
-
if (!sessions.has(apiKey)) {
|
|
36
|
-
sessions.set(apiKey, { apiKey, userId, createdAt: Date.now() });
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Attach user info to request
|
|
40
|
-
(req as any).userId = userId;
|
|
41
|
-
(req as any).sessionPrefix = config.sessionIsolation ? `${userId}:` : '';
|
|
42
|
-
|
|
43
|
-
next();
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function hashKey(key: string): string {
|
|
48
|
-
let h = 0;
|
|
49
|
-
for (let i = 0; i < key.length; i++) {
|
|
50
|
-
h = ((h << 5) - h + key.charCodeAt(i)) | 0;
|
|
51
|
-
}
|
|
52
|
-
return Math.abs(h).toString(36);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function getActiveSessions(): AuthSession[] {
|
|
56
|
-
return Array.from(sessions.values());
|
|
57
|
-
}
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
export interface AuthConfig {
|
|
4
|
+
apiKeys: string[];
|
|
5
|
+
sessionIsolation?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AuthSession {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
userId: string;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sessions = new Map<string, AuthSession>();
|
|
15
|
+
|
|
16
|
+
export function createAuthMiddleware(config: AuthConfig) {
|
|
17
|
+
return (req: Request, res: Response, next: NextFunction): void => {
|
|
18
|
+
// Skip auth for non-API routes and health/metrics
|
|
19
|
+
if (!req.path.startsWith('/api/') || req.path === '/api/health' || req.path === '/api/metrics') {
|
|
20
|
+
next();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const apiKey = req.headers['x-api-key'] as string
|
|
25
|
+
?? req.headers['authorization']?.replace(/^Bearer\s+/i, '')
|
|
26
|
+
?? (req.query as any).apiKey;
|
|
27
|
+
|
|
28
|
+
if (!apiKey || !config.apiKeys.includes(apiKey)) {
|
|
29
|
+
res.status(401).json({ error: 'Unauthorized. Provide a valid API key via X-API-Key header, Bearer token, or ?apiKey query.' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Derive userId from API key for session isolation
|
|
34
|
+
const userId = `user_${hashKey(apiKey)}`;
|
|
35
|
+
if (!sessions.has(apiKey)) {
|
|
36
|
+
sessions.set(apiKey, { apiKey, userId, createdAt: Date.now() });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Attach user info to request
|
|
40
|
+
(req as any).userId = userId;
|
|
41
|
+
(req as any).sessionPrefix = config.sessionIsolation ? `${userId}:` : '';
|
|
42
|
+
|
|
43
|
+
next();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hashKey(key: string): string {
|
|
48
|
+
let h = 0;
|
|
49
|
+
for (let i = 0; i < key.length; i++) {
|
|
50
|
+
h = ((h << 5) - h + key.charCodeAt(i)) | 0;
|
|
51
|
+
}
|
|
52
|
+
return Math.abs(h).toString(36);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getActiveSessions(): AuthSession[] {
|
|
56
|
+
return Array.from(sessions.values());
|
|
57
|
+
}
|