opc-agent 0.6.0 → 0.8.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/dist/channels/email.d.ts +69 -0
- package/dist/channels/email.js +118 -0
- package/dist/channels/slack.d.ts +62 -0
- package/dist/channels/slack.js +107 -0
- package/dist/channels/web.d.ts +7 -1
- package/dist/channels/web.js +165 -4
- package/dist/channels/wechat.d.ts +62 -0
- package/dist/channels/wechat.js +104 -0
- package/dist/core/auth.d.ts +13 -0
- package/dist/core/auth.js +41 -0
- package/dist/core/compose.d.ts +35 -0
- package/dist/core/compose.js +49 -0
- package/dist/core/orchestrator.d.ts +68 -0
- package/dist/core/orchestrator.js +145 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +34 -1
- package/dist/schema/oad.d.ts +69 -0
- package/dist/schema/oad.js +7 -1
- package/dist/skills/document.d.ts +27 -0
- package/dist/skills/document.js +80 -0
- package/dist/skills/http.d.ts +8 -0
- package/dist/skills/http.js +34 -0
- package/dist/skills/scheduler.d.ts +21 -0
- package/dist/skills/scheduler.js +70 -0
- package/dist/skills/webhook-trigger.d.ts +17 -0
- package/dist/skills/webhook-trigger.js +49 -0
- package/dist/tools/calculator.d.ts +7 -0
- package/dist/tools/calculator.js +70 -0
- package/dist/tools/datetime.d.ts +7 -0
- package/dist/tools/datetime.js +159 -0
- package/dist/tools/json-transform.d.ts +7 -0
- package/dist/tools/json-transform.js +184 -0
- package/dist/tools/text-analysis.d.ts +8 -0
- package/dist/tools/text-analysis.js +113 -0
- package/package.json +1 -1
- package/src/channels/email.ts +177 -0
- package/src/channels/slack.ts +160 -0
- package/src/channels/web.ts +175 -4
- package/src/channels/wechat.ts +149 -0
- package/src/core/auth.ts +57 -0
- package/src/core/compose.ts +77 -0
- package/src/core/orchestrator.ts +215 -0
- package/src/index.ts +27 -0
- package/src/schema/oad.ts +7 -0
- package/src/skills/document.ts +100 -0
- package/src/skills/http.ts +35 -0
- package/src/skills/scheduler.ts +80 -0
- package/src/skills/webhook-trigger.ts +59 -0
- package/src/tools/calculator.ts +73 -0
- package/src/tools/datetime.ts +149 -0
- package/src/tools/json-transform.ts +187 -0
- package/src/tools/text-analysis.ts +116 -0
- package/tests/v070.test.ts +76 -0
package/src/channels/web.ts
CHANGED
|
@@ -4,6 +4,59 @@ import type { Message } from '../core/types';
|
|
|
4
4
|
import { BaseChannel } from './index';
|
|
5
5
|
import { KnowledgeBase } from '../core/knowledge';
|
|
6
6
|
import { createProvider, type LLMProvider } from '../providers';
|
|
7
|
+
import { createAuthMiddleware, type AuthConfig } from '../core/auth';
|
|
8
|
+
|
|
9
|
+
const AGENT_TEMPLATES = [
|
|
10
|
+
{ id: 'customer-service', name: 'Customer Service', description: 'Handle support tickets, FAQs, and customer inquiries', icon: '🎧', category: 'Business' },
|
|
11
|
+
{ id: 'code-reviewer', name: 'Code Reviewer', description: 'Review PRs, suggest improvements, check for bugs', icon: '🔍', category: 'Engineering' },
|
|
12
|
+
{ id: 'content-writer', name: 'Content Writer', description: 'Write blogs, social media posts, and marketing copy', icon: '✍️', category: 'Marketing' },
|
|
13
|
+
{ id: 'executive-assistant', name: 'Executive Assistant', description: 'Schedule management, email drafting, meeting prep', icon: '📋', category: 'Business' },
|
|
14
|
+
{ id: 'knowledge-base', name: 'Knowledge Base', description: 'RAG-powered Q&A over your documents', icon: '📚', category: 'Knowledge' },
|
|
15
|
+
{ id: 'project-manager', name: 'Project Manager', description: 'Track tasks, milestones, and team coordination', icon: '📊', category: 'Business' },
|
|
16
|
+
{ id: 'sales-assistant', name: 'Sales Assistant', description: 'Lead qualification, outreach drafting, CRM updates', icon: '💼', category: 'Sales' },
|
|
17
|
+
{ id: 'financial-advisor', name: 'Financial Advisor', description: 'Budget analysis, financial planning, cost optimization', icon: '💰', category: 'Finance' },
|
|
18
|
+
{ id: 'hr-recruiter', name: 'HR Recruiter', description: 'Resume screening, interview scheduling, candidate comms', icon: '👥', category: 'HR' },
|
|
19
|
+
{ id: 'legal-assistant', name: 'Legal Assistant', description: 'Contract review, compliance checks, legal research', icon: '⚖️', category: 'Legal' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const TEMPLATES_HTML = `<!DOCTYPE html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
26
|
+
<title>Agent Templates</title>
|
|
27
|
+
<style>
|
|
28
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
29
|
+
body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
|
|
30
|
+
h1{font-size:28px;margin-bottom:8px;color:#fff}
|
|
31
|
+
.sub{color:#888;margin-bottom:32px;font-size:14px}
|
|
32
|
+
nav{margin-bottom:24px}
|
|
33
|
+
nav a{color:#818cf8;text-decoration:none;margin-right:16px;font-size:14px}
|
|
34
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
|
|
35
|
+
.card{background:#12121a;border:1px solid #1e1e2e;border-radius:12px;padding:24px;cursor:pointer;transition:all .2s}
|
|
36
|
+
.card:hover{border-color:#818cf8;transform:translateY(-2px)}
|
|
37
|
+
.card .icon{font-size:32px;margin-bottom:12px}
|
|
38
|
+
.card h3{font-size:16px;color:#fff;margin-bottom:8px}
|
|
39
|
+
.card p{font-size:13px;color:#888;line-height:1.5}
|
|
40
|
+
.card .cat{font-size:11px;color:#818cf8;text-transform:uppercase;letter-spacing:1px;margin-top:12px}
|
|
41
|
+
.btn{display:inline-block;background:#2563eb;color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer;margin-top:12px}
|
|
42
|
+
.btn:hover{background:#1d4ed8}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<nav><a href="/">← Chat</a><a href="/dashboard">Dashboard</a><a href="/templates">Templates</a></nav>
|
|
47
|
+
<h1>🧩 Agent Templates</h1>
|
|
48
|
+
<p class="sub">Create a new agent from a pre-built template in one click.</p>
|
|
49
|
+
<div class="grid" id="grid"></div>
|
|
50
|
+
<script>
|
|
51
|
+
fetch('/api/templates').then(r=>r.json()).then(d=>{
|
|
52
|
+
const g=document.getElementById('grid');
|
|
53
|
+
d.templates.forEach(t=>{
|
|
54
|
+
g.innerHTML+=\`<div class="card"><div class="icon">\${t.icon}</div><h3>\${t.name}</h3><p>\${t.description}</p><div class="cat">\${t.category}</div><button class="btn" onclick="alert('Creating agent from template: '+'\${t.id}'+'\\\\nRun: opc init --template \${t.id}')">Use Template</button></div>\`;
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
</script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>`;
|
|
7
60
|
|
|
8
61
|
const CHAT_HTML = `<!DOCTYPE html>
|
|
9
62
|
<html lang="en">
|
|
@@ -157,8 +210,12 @@ export class WebChannel extends BaseChannel {
|
|
|
157
210
|
private streamHandler: ((msg: Message, res: Response) => Promise<void>) | null = null;
|
|
158
211
|
private agentName: string = 'OPC Agent';
|
|
159
212
|
private currentProvider: string = 'openai';
|
|
160
|
-
private stats = { sessions: 0, messages: 0, totalResponseMs: 0, tokenUsage: 0, knowledgeFiles: 0, startedAt: Date.now() };
|
|
213
|
+
private stats = { sessions: 0, messages: 0, totalResponseMs: 0, tokenUsage: 0, knowledgeFiles: 0, startedAt: Date.now(), errors: 0 };
|
|
161
214
|
private eventHandlers: Map<string, Function[]> = new Map();
|
|
215
|
+
private conversations: Map<string, Message[]> = new Map();
|
|
216
|
+
private requestCount = 0;
|
|
217
|
+
private llmLatencySum = 0;
|
|
218
|
+
private llmCalls = 0;
|
|
162
219
|
|
|
163
220
|
private emit(event: string, data: any): void {
|
|
164
221
|
const handlers = this.eventHandlers.get(event) ?? [];
|
|
@@ -175,15 +232,23 @@ export class WebChannel extends BaseChannel {
|
|
|
175
232
|
this.stats.messages++;
|
|
176
233
|
this.stats.totalResponseMs += responseMs;
|
|
177
234
|
this.stats.tokenUsage += tokens;
|
|
235
|
+
this.requestCount++;
|
|
236
|
+
this.llmLatencySum += responseMs;
|
|
237
|
+
this.llmCalls++;
|
|
178
238
|
}
|
|
179
239
|
|
|
240
|
+
trackError(): void { this.stats.errors++; }
|
|
241
|
+
|
|
180
242
|
trackSession(): void { this.stats.sessions++; }
|
|
181
243
|
|
|
182
|
-
constructor(port: number = 3000) {
|
|
244
|
+
constructor(port: number = 3000, authConfig?: AuthConfig) {
|
|
183
245
|
super();
|
|
184
246
|
this.port = port;
|
|
185
247
|
this.app = express();
|
|
186
|
-
this.app.use(express.json());
|
|
248
|
+
this.app.use(express.json({ limit: '10mb' }));
|
|
249
|
+
if (authConfig && authConfig.apiKeys.length > 0) {
|
|
250
|
+
this.app.use(createAuthMiddleware(authConfig));
|
|
251
|
+
}
|
|
187
252
|
this.setupRoutes();
|
|
188
253
|
}
|
|
189
254
|
|
|
@@ -211,6 +276,7 @@ export class WebChannel extends BaseChannel {
|
|
|
211
276
|
// Streaming chat endpoint
|
|
212
277
|
this.app.post('/api/chat', async (req: Request, res: Response) => {
|
|
213
278
|
const { message, sessionId } = req.body;
|
|
279
|
+
const sid = sessionId ?? 'default';
|
|
214
280
|
if (!message) {
|
|
215
281
|
res.status(400).json({ error: 'message is required' });
|
|
216
282
|
return;
|
|
@@ -221,9 +287,13 @@ export class WebChannel extends BaseChannel {
|
|
|
221
287
|
role: 'user',
|
|
222
288
|
content: message,
|
|
223
289
|
timestamp: Date.now(),
|
|
224
|
-
metadata: { sessionId:
|
|
290
|
+
metadata: { sessionId: sid },
|
|
225
291
|
};
|
|
226
292
|
|
|
293
|
+
// Track conversation
|
|
294
|
+
if (!this.conversations.has(sid)) this.conversations.set(sid, []);
|
|
295
|
+
this.conversations.get(sid)!.push(msg);
|
|
296
|
+
|
|
227
297
|
if (this.streamHandler) {
|
|
228
298
|
try {
|
|
229
299
|
await this.streamHandler(msg, res);
|
|
@@ -301,6 +371,107 @@ export class WebChannel extends BaseChannel {
|
|
|
301
371
|
} catch { res.json({ totalEntries: 0, sources: [] }); }
|
|
302
372
|
});
|
|
303
373
|
|
|
374
|
+
// --- Health Check (detailed) ---
|
|
375
|
+
this.app.get('/api/health', (_req: Request, res: Response) => {
|
|
376
|
+
const uptimeMs = Date.now() - this.stats.startedAt;
|
|
377
|
+
res.json({
|
|
378
|
+
status: 'ok',
|
|
379
|
+
timestamp: Date.now(),
|
|
380
|
+
uptime: uptimeMs,
|
|
381
|
+
uptimeHuman: `${Math.floor(uptimeMs / 3600000)}h ${Math.floor((uptimeMs % 3600000) / 60000)}m`,
|
|
382
|
+
version: '0.7.0',
|
|
383
|
+
agent: this.agentName,
|
|
384
|
+
stats: {
|
|
385
|
+
sessions: this.stats.sessions,
|
|
386
|
+
messages: this.stats.messages,
|
|
387
|
+
errors: this.stats.errors,
|
|
388
|
+
avgResponseMs: this.stats.messages > 0 ? Math.round(this.stats.totalResponseMs / this.stats.messages) : 0,
|
|
389
|
+
},
|
|
390
|
+
memory: {
|
|
391
|
+
rss: process.memoryUsage().rss,
|
|
392
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// --- Prometheus Metrics ---
|
|
398
|
+
this.app.get('/api/metrics', (_req: Request, res: Response) => {
|
|
399
|
+
const uptimeMs = Date.now() - this.stats.startedAt;
|
|
400
|
+
const avgLatency = this.llmCalls > 0 ? this.llmLatencySum / this.llmCalls : 0;
|
|
401
|
+
const mem = process.memoryUsage();
|
|
402
|
+
res.type('text/plain').send(
|
|
403
|
+
`# HELP opc_uptime_seconds Agent uptime in seconds\n` +
|
|
404
|
+
`# TYPE opc_uptime_seconds gauge\n` +
|
|
405
|
+
`opc_uptime_seconds ${(uptimeMs / 1000).toFixed(1)}\n` +
|
|
406
|
+
`# HELP opc_requests_total Total requests\n` +
|
|
407
|
+
`# TYPE opc_requests_total counter\n` +
|
|
408
|
+
`opc_requests_total ${this.requestCount}\n` +
|
|
409
|
+
`# HELP opc_messages_total Total messages processed\n` +
|
|
410
|
+
`# TYPE opc_messages_total counter\n` +
|
|
411
|
+
`opc_messages_total ${this.stats.messages}\n` +
|
|
412
|
+
`# HELP opc_errors_total Total errors\n` +
|
|
413
|
+
`# TYPE opc_errors_total counter\n` +
|
|
414
|
+
`opc_errors_total ${this.stats.errors}\n` +
|
|
415
|
+
`# HELP opc_llm_latency_avg_ms Average LLM response latency\n` +
|
|
416
|
+
`# TYPE opc_llm_latency_avg_ms gauge\n` +
|
|
417
|
+
`opc_llm_latency_avg_ms ${avgLatency.toFixed(1)}\n` +
|
|
418
|
+
`# HELP opc_sessions_total Total sessions\n` +
|
|
419
|
+
`# TYPE opc_sessions_total counter\n` +
|
|
420
|
+
`opc_sessions_total ${this.stats.sessions}\n` +
|
|
421
|
+
`# HELP opc_token_usage_total Total token usage\n` +
|
|
422
|
+
`# TYPE opc_token_usage_total counter\n` +
|
|
423
|
+
`opc_token_usage_total ${this.stats.tokenUsage}\n` +
|
|
424
|
+
`# HELP process_resident_memory_bytes Resident memory size\n` +
|
|
425
|
+
`# TYPE process_resident_memory_bytes gauge\n` +
|
|
426
|
+
`process_resident_memory_bytes ${mem.rss}\n`
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// --- Conversation tracking & export ---
|
|
431
|
+
this.app.get('/api/conversations/export', (req: Request, res: Response) => {
|
|
432
|
+
const sessionId = req.query.sessionId as string;
|
|
433
|
+
const format = (req.query.format as string) ?? 'json';
|
|
434
|
+
|
|
435
|
+
const messages = sessionId ? (this.conversations.get(sessionId) ?? []) : Array.from(this.conversations.values()).flat();
|
|
436
|
+
|
|
437
|
+
if (format === 'markdown') {
|
|
438
|
+
const md = messages.map(m => `**${m.role}** (${new Date(m.timestamp).toISOString()}):\n${m.content}`).join('\n\n---\n\n');
|
|
439
|
+
res.type('text/markdown').send(md);
|
|
440
|
+
} else if (format === 'csv') {
|
|
441
|
+
const header = 'id,role,content,timestamp\n';
|
|
442
|
+
const rows = messages.map(m => `"${m.id}","${m.role}","${m.content.replace(/"/g, '""')}",${m.timestamp}`).join('\n');
|
|
443
|
+
res.type('text/csv').send(header + rows);
|
|
444
|
+
} else {
|
|
445
|
+
res.json({ sessionId: sessionId ?? 'all', messages, count: messages.length });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// --- Document Upload ---
|
|
450
|
+
this.app.post('/api/documents/upload', async (req: Request, res: Response) => {
|
|
451
|
+
try {
|
|
452
|
+
const { content, filename, mimeType } = req.body;
|
|
453
|
+
if (!content || !filename) {
|
|
454
|
+
res.status(400).json({ error: 'content and filename are required' });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const kb = new KnowledgeBase('.');
|
|
458
|
+
const result = await kb.addText(content, filename);
|
|
459
|
+
this.stats.knowledgeFiles++;
|
|
460
|
+
res.json({ ok: true, filename, chunks: result.chunks, chars: content.length });
|
|
461
|
+
} catch (err) {
|
|
462
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Upload failed' });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// --- Agent Templates Gallery ---
|
|
467
|
+
this.app.get('/api/templates', (_req: Request, res: Response) => {
|
|
468
|
+
res.json({ templates: AGENT_TEMPLATES });
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
this.app.get('/templates', (_req: Request, res: Response) => {
|
|
472
|
+
res.type('html').send(TEMPLATES_HTML);
|
|
473
|
+
});
|
|
474
|
+
|
|
304
475
|
// Legacy endpoint
|
|
305
476
|
this.app.post('/chat', async (req: Request, res: Response) => {
|
|
306
477
|
if (!this.handler) {
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { BaseChannel } from './index';
|
|
2
|
+
import type { Message } from '../core/types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WeChat Channel (Stub) — v0.8.0
|
|
6
|
+
* WeChat Official Account message handling, template messages, QR code login.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface WeChatChannelConfig {
|
|
10
|
+
/** WeChat Official Account AppID */
|
|
11
|
+
appId: string;
|
|
12
|
+
/** WeChat Official Account AppSecret */
|
|
13
|
+
appSecret: string;
|
|
14
|
+
/** Verification token for message validation */
|
|
15
|
+
token: string;
|
|
16
|
+
/** AES encoding key for encrypted messages */
|
|
17
|
+
encodingAESKey?: string;
|
|
18
|
+
/** HTTP server port (default: 3002) */
|
|
19
|
+
port?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WeChatMessage {
|
|
23
|
+
toUserName: string;
|
|
24
|
+
fromUserName: string;
|
|
25
|
+
createTime: number;
|
|
26
|
+
msgType: 'text' | 'image' | 'voice' | 'video' | 'event';
|
|
27
|
+
content?: string;
|
|
28
|
+
msgId?: string;
|
|
29
|
+
event?: string;
|
|
30
|
+
eventKey?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface TemplateMessageData {
|
|
34
|
+
toUser: string;
|
|
35
|
+
templateId: string;
|
|
36
|
+
url?: string;
|
|
37
|
+
data: Record<string, { value: string; color?: string }>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class WeChatChannel extends BaseChannel {
|
|
41
|
+
type = 'wechat';
|
|
42
|
+
private config: WeChatChannelConfig;
|
|
43
|
+
private accessToken: string | null = null;
|
|
44
|
+
private tokenExpiry = 0;
|
|
45
|
+
|
|
46
|
+
constructor(config: WeChatChannelConfig) {
|
|
47
|
+
super();
|
|
48
|
+
this.config = config;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async start(): Promise<void> {
|
|
52
|
+
// TODO: Start HTTP server to receive WeChat push messages
|
|
53
|
+
// 1. Verify signature on GET requests
|
|
54
|
+
// 2. Parse XML messages on POST requests
|
|
55
|
+
// 3. Route to handler and reply with XML
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async stop(): Promise<void> {
|
|
59
|
+
// TODO: Stop HTTP server
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Get or refresh access token */
|
|
63
|
+
async getAccessToken(): Promise<string> {
|
|
64
|
+
if (this.accessToken && Date.now() < this.tokenExpiry) {
|
|
65
|
+
return this.accessToken;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// TODO: Implement token refresh
|
|
69
|
+
// const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${this.config.appId}&secret=${this.config.appSecret}`;
|
|
70
|
+
// const res = await fetch(url);
|
|
71
|
+
// const data = await res.json();
|
|
72
|
+
// this.accessToken = data.access_token;
|
|
73
|
+
// this.tokenExpiry = Date.now() + (data.expires_in - 300) * 1000;
|
|
74
|
+
|
|
75
|
+
return this.accessToken ?? '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Handle incoming WeChat message */
|
|
79
|
+
async handleMessage(wxMsg: WeChatMessage): Promise<string> {
|
|
80
|
+
if (wxMsg.msgType === 'event') {
|
|
81
|
+
return this.handleEvent(wxMsg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const message = this.wechatToMessage(wxMsg);
|
|
85
|
+
if (this.handler) {
|
|
86
|
+
const reply = await this.handler(message);
|
|
87
|
+
return reply.content;
|
|
88
|
+
}
|
|
89
|
+
return '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Handle WeChat events (subscribe, scan, etc.) */
|
|
93
|
+
private handleEvent(wxMsg: WeChatMessage): string {
|
|
94
|
+
switch (wxMsg.event) {
|
|
95
|
+
case 'subscribe':
|
|
96
|
+
return 'Welcome! How can I help you?';
|
|
97
|
+
case 'SCAN':
|
|
98
|
+
return `QR code scanned: ${wxMsg.eventKey}`;
|
|
99
|
+
default:
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Convert WeChat message to internal Message */
|
|
105
|
+
private wechatToMessage(wxMsg: WeChatMessage): Message {
|
|
106
|
+
return {
|
|
107
|
+
id: wxMsg.msgId ?? `wx-${wxMsg.createTime}`,
|
|
108
|
+
role: 'user',
|
|
109
|
+
content: wxMsg.content ?? '',
|
|
110
|
+
timestamp: wxMsg.createTime * 1000,
|
|
111
|
+
metadata: {
|
|
112
|
+
channel: 'wechat',
|
|
113
|
+
fromUser: wxMsg.fromUserName,
|
|
114
|
+
toUser: wxMsg.toUserName,
|
|
115
|
+
msgType: wxMsg.msgType,
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Send template message */
|
|
121
|
+
async sendTemplateMessage(data: TemplateMessageData): Promise<boolean> {
|
|
122
|
+
// TODO: Implement
|
|
123
|
+
// const token = await this.getAccessToken();
|
|
124
|
+
// const url = `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`;
|
|
125
|
+
// const res = await fetch(url, {
|
|
126
|
+
// method: 'POST',
|
|
127
|
+
// body: JSON.stringify({
|
|
128
|
+
// touser: data.toUser,
|
|
129
|
+
// template_id: data.templateId,
|
|
130
|
+
// url: data.url,
|
|
131
|
+
// data: data.data,
|
|
132
|
+
// }),
|
|
133
|
+
// });
|
|
134
|
+
void data;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Generate QR code for login (stub) */
|
|
139
|
+
async generateLoginQR(): Promise<{ ticket: string; url: string; expireSeconds: number }> {
|
|
140
|
+
// TODO: Implement with WeChat QR code API
|
|
141
|
+
// const token = await this.getAccessToken();
|
|
142
|
+
// POST to https://api.weixin.qq.com/cgi-bin/qrcode/create
|
|
143
|
+
return {
|
|
144
|
+
ticket: 'stub-ticket',
|
|
145
|
+
url: 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=stub-ticket',
|
|
146
|
+
expireSeconds: 300,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/core/auth.ts
ADDED
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { AgentContext, Message } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent Composition — v0.8.0
|
|
5
|
+
* Combine multiple agents into a pipeline: Agent A output → Agent B input.
|
|
6
|
+
* Configurable in OAD: `compose: [agent-a, agent-b]`
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type AgentHandler = (context: AgentContext, message: Message) => Promise<Message>;
|
|
10
|
+
|
|
11
|
+
export interface ComposableAgent {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
handler: AgentHandler;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ComposeOptions {
|
|
18
|
+
/** Stop pipeline if any agent returns empty content */
|
|
19
|
+
stopOnEmpty?: boolean;
|
|
20
|
+
/** Transform output between agents */
|
|
21
|
+
transform?: (output: Message, nextAgentId: string) => Message;
|
|
22
|
+
/** Timeout per agent in ms */
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class AgentPipeline {
|
|
27
|
+
private agents: ComposableAgent[] = [];
|
|
28
|
+
private options: ComposeOptions;
|
|
29
|
+
|
|
30
|
+
constructor(agents: ComposableAgent[], options: ComposeOptions = {}) {
|
|
31
|
+
this.agents = agents;
|
|
32
|
+
this.options = options;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Run the pipeline sequentially: each agent's output becomes the next agent's input */
|
|
36
|
+
async execute(context: AgentContext, initialMessage: Message): Promise<Message> {
|
|
37
|
+
let currentMessage = initialMessage;
|
|
38
|
+
|
|
39
|
+
for (const agent of this.agents) {
|
|
40
|
+
if (this.options.stopOnEmpty && !currentMessage.content.trim()) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Apply transform if provided
|
|
45
|
+
if (this.options.transform) {
|
|
46
|
+
currentMessage = this.options.transform(currentMessage, agent.id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.options.timeoutMs) {
|
|
50
|
+
const result = await Promise.race([
|
|
51
|
+
agent.handler(context, currentMessage),
|
|
52
|
+
new Promise<never>((_, reject) =>
|
|
53
|
+
setTimeout(() => reject(new Error(`Agent ${agent.id} timed out`)), this.options.timeoutMs)
|
|
54
|
+
),
|
|
55
|
+
]);
|
|
56
|
+
currentMessage = result;
|
|
57
|
+
} else {
|
|
58
|
+
currentMessage = await agent.handler(context, currentMessage);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return currentMessage;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Get the pipeline agent IDs in order */
|
|
66
|
+
getAgentIds(): string[] {
|
|
67
|
+
return this.agents.map((a) => a.id);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a pipeline from an array of composable agents.
|
|
73
|
+
* Usage in OAD: `compose: [agent-a, agent-b, agent-c]`
|
|
74
|
+
*/
|
|
75
|
+
export function compose(agents: ComposableAgent[], options?: ComposeOptions): AgentPipeline {
|
|
76
|
+
return new AgentPipeline(agents, options);
|
|
77
|
+
}
|