opc-agent 0.9.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +145 -144
- package/dist/channels/discord.d.ts +44 -0
- package/dist/channels/discord.js +189 -0
- package/dist/channels/feishu.d.ts +47 -0
- package/dist/channels/feishu.js +221 -0
- package/dist/channels/web.js +118 -39
- package/dist/cli.js +109 -1
- package/dist/core/errors.d.ts +68 -0
- package/dist/core/errors.js +149 -0
- package/dist/core/security.d.ts +48 -0
- package/dist/core/security.js +146 -0
- package/dist/core/watch.d.ts +73 -0
- package/dist/core/watch.js +106 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +32 -1
- package/dist/plugins/index.d.ts +24 -3
- package/dist/plugins/index.js +109 -4
- package/dist/schema/oad.d.ts +54 -0
- package/dist/schema/oad.js +6 -1
- package/package.json +1 -1
- package/src/channels/discord.ts +192 -0
- package/src/channels/feishu.ts +236 -0
- package/src/channels/web.ts +118 -39
- package/src/cli.ts +108 -1
- package/src/core/errors.ts +148 -0
- package/src/core/security.ts +171 -0
- package/src/core/watch.ts +178 -0
- package/src/index.ts +15 -0
- package/src/plugins/index.ts +128 -7
- package/src/schema/oad.ts +6 -0
- package/tests/errors.test.ts +83 -0
- package/tests/security.test.ts +60 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FeishuChannel = void 0;
|
|
37
|
+
const index_1 = require("./index");
|
|
38
|
+
class FeishuChannel extends index_1.BaseChannel {
|
|
39
|
+
type = 'feishu';
|
|
40
|
+
config;
|
|
41
|
+
server = null;
|
|
42
|
+
tokenCache = null;
|
|
43
|
+
processedEvents = new Set();
|
|
44
|
+
constructor(config = {}) {
|
|
45
|
+
super();
|
|
46
|
+
this.config = {
|
|
47
|
+
appId: config.appId ?? process.env.FEISHU_APP_ID ?? '',
|
|
48
|
+
appSecret: config.appSecret ?? process.env.FEISHU_APP_SECRET ?? '',
|
|
49
|
+
verificationToken: config.verificationToken ?? process.env.FEISHU_VERIFICATION_TOKEN ?? '',
|
|
50
|
+
encryptKey: config.encryptKey ?? process.env.FEISHU_ENCRYPT_KEY,
|
|
51
|
+
port: config.port ?? 3002,
|
|
52
|
+
apiBase: config.apiBase ?? 'https://open.feishu.cn',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async start() {
|
|
56
|
+
if (!this.config.appId || !this.config.appSecret) {
|
|
57
|
+
console.warn('[FeishuChannel] Missing appId/appSecret. Set FEISHU_APP_ID and FEISHU_APP_SECRET.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const express = (await Promise.resolve().then(() => __importStar(require('express')))).default;
|
|
61
|
+
const app = express();
|
|
62
|
+
app.use(express.json());
|
|
63
|
+
// Event subscription endpoint
|
|
64
|
+
app.post('/feishu/event', async (req, res) => {
|
|
65
|
+
try {
|
|
66
|
+
const body = req.body;
|
|
67
|
+
// URL verification challenge
|
|
68
|
+
if (body.type === 'url_verification') {
|
|
69
|
+
res.json({ challenge: body.challenge });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Deduplicate events
|
|
73
|
+
const eventId = body.header?.event_id;
|
|
74
|
+
if (eventId && this.processedEvents.has(eventId)) {
|
|
75
|
+
res.json({ ok: true });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (eventId) {
|
|
79
|
+
this.processedEvents.add(eventId);
|
|
80
|
+
// Prune old events (keep last 1000)
|
|
81
|
+
if (this.processedEvents.size > 1000) {
|
|
82
|
+
const arr = [...this.processedEvents];
|
|
83
|
+
this.processedEvents = new Set(arr.slice(-500));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Verify token
|
|
87
|
+
if (this.config.verificationToken && body.header?.token !== this.config.verificationToken) {
|
|
88
|
+
res.status(403).json({ error: 'Invalid verification token' });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Handle im.message.receive_v1
|
|
92
|
+
const event = body.event;
|
|
93
|
+
if (body.header?.event_type === 'im.message.receive_v1' && this.handler) {
|
|
94
|
+
const msgBody = event?.message;
|
|
95
|
+
if (!msgBody) {
|
|
96
|
+
res.json({ ok: true });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Only handle text messages for now
|
|
100
|
+
const msgType = msgBody.message_type;
|
|
101
|
+
let content = '';
|
|
102
|
+
if (msgType === 'text') {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = JSON.parse(msgBody.content);
|
|
105
|
+
content = parsed.text ?? '';
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
content = msgBody.content ?? '';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Acknowledge non-text silently
|
|
113
|
+
res.json({ ok: true });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// Strip @bot mentions
|
|
117
|
+
content = content.replace(/@_user_\d+/g, '').trim();
|
|
118
|
+
if (!content) {
|
|
119
|
+
res.json({ ok: true });
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const chatId = msgBody.chat_id;
|
|
123
|
+
const senderId = event.sender?.sender_id?.open_id ?? 'unknown';
|
|
124
|
+
const msg = {
|
|
125
|
+
id: `feishu_${msgBody.message_id}`,
|
|
126
|
+
role: 'user',
|
|
127
|
+
content,
|
|
128
|
+
timestamp: parseInt(msgBody.create_time, 10) || Date.now(),
|
|
129
|
+
metadata: {
|
|
130
|
+
sessionId: `feishu_${chatId}`,
|
|
131
|
+
chatId,
|
|
132
|
+
userId: senderId,
|
|
133
|
+
platform: 'feishu',
|
|
134
|
+
messageId: msgBody.message_id,
|
|
135
|
+
chatType: msgBody.chat_type, // 'p2p' or 'group'
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
const response = await this.handler(msg);
|
|
139
|
+
await this.sendTextMessage(chatId, response.content);
|
|
140
|
+
}
|
|
141
|
+
res.json({ ok: true });
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
console.error('[FeishuChannel] Error handling event:', err);
|
|
145
|
+
res.status(500).json({ error: 'Internal error' });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
app.get('/health', (_req, res) => {
|
|
149
|
+
res.json({ status: 'ok', channel: 'feishu' });
|
|
150
|
+
});
|
|
151
|
+
this.server = app.listen(this.config.port, () => {
|
|
152
|
+
console.log(`[FeishuChannel] Listening on port ${this.config.port}`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
async stop() {
|
|
156
|
+
if (this.server) {
|
|
157
|
+
this.server.close();
|
|
158
|
+
this.server = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Get tenant access token (cached) */
|
|
162
|
+
async getAccessToken() {
|
|
163
|
+
if (this.tokenCache && Date.now() < this.tokenCache.expiresAt) {
|
|
164
|
+
return this.tokenCache.token;
|
|
165
|
+
}
|
|
166
|
+
const resp = await fetch(`${this.config.apiBase}/open-apis/auth/v3/tenant_access_token/internal`, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
app_id: this.config.appId,
|
|
171
|
+
app_secret: this.config.appSecret,
|
|
172
|
+
}),
|
|
173
|
+
});
|
|
174
|
+
const data = await resp.json();
|
|
175
|
+
if (data.code !== 0) {
|
|
176
|
+
throw new Error(`[FeishuChannel] Failed to get access token: ${JSON.stringify(data)}`);
|
|
177
|
+
}
|
|
178
|
+
this.tokenCache = {
|
|
179
|
+
token: data.tenant_access_token,
|
|
180
|
+
expiresAt: Date.now() + (data.expire - 60) * 1000, // refresh 60s early
|
|
181
|
+
};
|
|
182
|
+
return this.tokenCache.token;
|
|
183
|
+
}
|
|
184
|
+
/** Send a text message to a chat */
|
|
185
|
+
async sendTextMessage(chatId, text) {
|
|
186
|
+
const token = await this.getAccessToken();
|
|
187
|
+
const resp = await fetch(`${this.config.apiBase}/open-apis/im/v1/messages?receive_id_type=chat_id`, {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: {
|
|
190
|
+
'Content-Type': 'application/json',
|
|
191
|
+
'Authorization': `Bearer ${token}`,
|
|
192
|
+
},
|
|
193
|
+
body: JSON.stringify({
|
|
194
|
+
receive_id: chatId,
|
|
195
|
+
msg_type: 'text',
|
|
196
|
+
content: JSON.stringify({ text }),
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
if (!resp.ok) {
|
|
200
|
+
console.error('[FeishuChannel] Failed to send message:', await resp.text());
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** Send an interactive card message */
|
|
204
|
+
async sendCardMessage(chatId, card) {
|
|
205
|
+
const token = await this.getAccessToken();
|
|
206
|
+
await fetch(`${this.config.apiBase}/open-apis/im/v1/messages?receive_id_type=chat_id`, {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
headers: {
|
|
209
|
+
'Content-Type': 'application/json',
|
|
210
|
+
'Authorization': `Bearer ${token}`,
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify({
|
|
213
|
+
receive_id: chatId,
|
|
214
|
+
msg_type: 'interactive',
|
|
215
|
+
content: JSON.stringify(card),
|
|
216
|
+
}),
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
exports.FeishuChannel = FeishuChannel;
|
|
221
|
+
//# sourceMappingURL=feishu.js.map
|
package/dist/channels/web.js
CHANGED
|
@@ -62,52 +62,127 @@ const CHAT_HTML = `<!DOCTYPE html>
|
|
|
62
62
|
<html lang="en">
|
|
63
63
|
<head>
|
|
64
64
|
<meta charset="UTF-8">
|
|
65
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
65
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
66
66
|
<title>OPC Agent</title>
|
|
67
67
|
<style>
|
|
68
|
+
:root{--bg:#0a0a0f;--surface:#12121a;--border:#1e1e2e;--text:#e0e0e0;--text-dim:#888;--accent:#818cf8;--accent-hover:#6366f1;--user-bg:#2563eb;--user-hover:#1d4ed8;--error-bg:#7f1d1d;--error-text:#fca5a5;--success:#22c55e;--radius:12px}
|
|
68
69
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
69
|
-
body{background
|
|
70
|
-
header{background
|
|
71
|
-
header
|
|
72
|
-
header .
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
.
|
|
70
|
+
body{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',sans-serif;height:100vh;height:100dvh;display:flex;flex-direction:column;overflow:hidden}
|
|
71
|
+
header{background:var(--surface);padding:14px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:12px;flex-shrink:0;backdrop-filter:blur(12px)}
|
|
72
|
+
header .avatar{width:36px;height:36px;border-radius:50%;background:linear-gradient(135deg,var(--accent),#6366f1);display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
|
|
73
|
+
header .info{flex:1;min-width:0}
|
|
74
|
+
header h1{font-size:16px;font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
75
|
+
header .status{font-size:12px;color:var(--success);display:flex;align-items:center;gap:4px}
|
|
76
|
+
header .status .dot{width:6px;height:6px;border-radius:50%;background:var(--success);animation:pulse 2s infinite}
|
|
77
|
+
nav.header-nav{display:flex;gap:4px}
|
|
78
|
+
nav.header-nav a{color:var(--text-dim);text-decoration:none;font-size:12px;padding:4px 10px;border-radius:6px;transition:all .2s}
|
|
79
|
+
nav.header-nav a:hover{color:#fff;background:rgba(255,255,255,.06)}
|
|
80
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|
81
|
+
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
|
82
|
+
@keyframes slideIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}
|
|
83
|
+
#messages{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:12px;scroll-behavior:smooth}
|
|
84
|
+
#messages::-webkit-scrollbar{width:4px}
|
|
85
|
+
#messages::-webkit-scrollbar-track{background:transparent}
|
|
86
|
+
#messages::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
|
|
87
|
+
.msg-wrap{display:flex;flex-direction:column;animation:fadeIn .3s ease-out}
|
|
88
|
+
.msg-wrap.user{align-items:flex-end}
|
|
89
|
+
.msg-wrap.assistant{align-items:flex-start}
|
|
90
|
+
.msg{max-width:min(720px,85%);padding:10px 14px;border-radius:var(--radius);line-height:1.7;font-size:14px;word-break:break-word;position:relative;transition:all .2s}
|
|
91
|
+
.msg.user{background:var(--user-bg);color:#fff;border-bottom-right-radius:4px}
|
|
92
|
+
.msg.assistant{background:var(--surface);color:var(--text);border:1px solid var(--border);border-bottom-left-radius:4px}
|
|
93
|
+
.msg.error{background:var(--error-bg);color:var(--error-text);border:1px solid rgba(239,68,68,.3)}
|
|
94
|
+
.msg pre{background:rgba(0,0,0,.4);padding:12px;border-radius:8px;overflow-x:auto;margin:8px 0;font-size:13px;font-family:'JetBrains Mono','Fira Code','Cascadia Code',monospace;line-height:1.5}
|
|
95
|
+
.msg code{font-family:'JetBrains Mono','Fira Code','Cascadia Code',monospace;font-size:13px;background:rgba(0,0,0,.3);padding:1px 5px;border-radius:4px}
|
|
96
|
+
.msg pre code{background:none;padding:0}
|
|
97
|
+
.msg .cursor{display:inline-block;width:2px;height:14px;background:var(--accent);animation:blink .6s infinite;vertical-align:text-bottom;margin-left:2px}
|
|
79
98
|
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
|
|
80
|
-
.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
99
|
+
.typing{display:flex;gap:4px;padding:12px 16px;align-items:center}
|
|
100
|
+
.typing span{width:6px;height:6px;border-radius:50%;background:var(--text-dim);animation:typingDot 1.4s infinite}
|
|
101
|
+
.typing span:nth-child(2){animation-delay:.2s}
|
|
102
|
+
.typing span:nth-child(3){animation-delay:.4s}
|
|
103
|
+
@keyframes typingDot{0%,60%,100%{opacity:.3;transform:scale(.8)}30%{opacity:1;transform:scale(1)}}
|
|
104
|
+
.reactions{display:flex;gap:4px;margin-top:4px}
|
|
105
|
+
.reactions button{background:rgba(255,255,255,.06);border:1px solid transparent;border-radius:16px;padding:2px 8px;font-size:13px;cursor:pointer;transition:all .15s;color:var(--text-dim)}
|
|
106
|
+
.reactions button:hover{background:rgba(255,255,255,.12);border-color:var(--border)}
|
|
107
|
+
.reactions button.active{background:rgba(99,102,241,.2);border-color:var(--accent);color:var(--accent)}
|
|
108
|
+
.msg-time{font-size:11px;color:var(--text-dim);margin-top:2px;opacity:0;transition:opacity .2s}
|
|
109
|
+
.msg-wrap:hover .msg-time{opacity:1}
|
|
110
|
+
.attachment{display:flex;align-items:center;gap:8px;background:rgba(0,0,0,.3);padding:8px 12px;border-radius:8px;margin-top:6px;font-size:13px}
|
|
111
|
+
.attachment .icon{font-size:18px}
|
|
112
|
+
#input-area{background:var(--surface);padding:12px 20px 16px;border-top:1px solid var(--border);display:flex;gap:10px;align-items:flex-end;flex-shrink:0}
|
|
113
|
+
#input{flex:1;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;color:#fff;font-size:14px;outline:none;resize:none;max-height:150px;min-height:42px;font-family:inherit;line-height:1.5;transition:border-color .2s}
|
|
114
|
+
#input:focus{border-color:var(--accent)}
|
|
115
|
+
#input::placeholder{color:var(--text-dim)}
|
|
116
|
+
#send{background:var(--user-bg);color:#fff;border:none;border-radius:var(--radius);width:42px;height:42px;font-size:18px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
|
117
|
+
#send:hover{background:var(--user-hover);transform:scale(1.05)}
|
|
118
|
+
#send:disabled{background:#334155;cursor:not-allowed;transform:none}
|
|
119
|
+
.empty-state{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;color:var(--text-dim);gap:12px;padding:40px;text-align:center}
|
|
120
|
+
.empty-state .logo{font-size:48px;opacity:.6}
|
|
121
|
+
.empty-state h2{color:var(--text);font-size:20px;font-weight:500}
|
|
122
|
+
.empty-state p{font-size:14px;max-width:400px;line-height:1.6}
|
|
123
|
+
@media(max-width:640px){
|
|
124
|
+
header{padding:10px 14px}
|
|
125
|
+
#messages{padding:12px}
|
|
126
|
+
#input-area{padding:10px 14px 14px}
|
|
127
|
+
.msg{max-width:90%;font-size:14px}
|
|
128
|
+
nav.header-nav{display:none}
|
|
129
|
+
}
|
|
87
130
|
</style>
|
|
88
131
|
</head>
|
|
89
132
|
<body>
|
|
90
|
-
<header
|
|
91
|
-
<div id="
|
|
133
|
+
<header>
|
|
134
|
+
<div class="avatar" id="avatar">🤖</div>
|
|
135
|
+
<div class="info"><h1 id="title">OPC Agent</h1><div class="status"><span class="dot"></span>Online</div></div>
|
|
136
|
+
<nav class="header-nav"><a href="/dashboard">Dashboard</a><a href="/templates">Templates</a></nav>
|
|
137
|
+
</header>
|
|
138
|
+
<div id="messages">
|
|
139
|
+
<div class="empty-state" id="empty"><div class="logo">💬</div><h2>Start a conversation</h2><p>Type a message below to chat with your AI agent.</p></div>
|
|
140
|
+
</div>
|
|
92
141
|
<div id="input-area">
|
|
93
|
-
<textarea id="input" rows="1" placeholder="Type a message
|
|
94
|
-
<button id="send"
|
|
142
|
+
<textarea id="input" rows="1" placeholder="Type a message…" autocomplete="off"></textarea>
|
|
143
|
+
<button id="send" aria-label="Send">↑</button>
|
|
95
144
|
</div>
|
|
96
145
|
<script>
|
|
97
|
-
const msgs=document.getElementById('messages'),input=document.getElementById('input'),btn=document.getElementById('send');
|
|
146
|
+
const msgs=document.getElementById('messages'),input=document.getElementById('input'),btn=document.getElementById('send'),empty=document.getElementById('empty');
|
|
98
147
|
let sessionId=crypto.randomUUID(),sending=false;
|
|
99
148
|
|
|
100
|
-
function
|
|
149
|
+
function esc(s){const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
|
150
|
+
function fmtTime(){return new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}
|
|
151
|
+
function renderMd(text){
|
|
152
|
+
let h=esc(text);
|
|
153
|
+
h=h.replace(/\`\`\`(\\w*)\\n([\\s\\S]*?)\`\`\`/g,'<pre><code>$2</code></pre>');
|
|
154
|
+
h=h.replace(/\`([^\`]+)\`/g,'<code>$1</code>');
|
|
155
|
+
h=h.replace(/\\*\\*(.+?)\\*\\*/g,'<strong>$1</strong>');
|
|
156
|
+
h=h.replace(/\\n/g,'<br>');
|
|
157
|
+
return h;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function addMsg(role,text,opts){
|
|
161
|
+
if(empty)empty.remove();
|
|
162
|
+
const wrap=document.createElement('div');
|
|
163
|
+
wrap.className='msg-wrap '+role;
|
|
101
164
|
const d=document.createElement('div');
|
|
102
165
|
d.className='msg '+role;
|
|
103
|
-
d.textContent=text;
|
|
104
|
-
|
|
166
|
+
if(opts?.html)d.innerHTML=text;else if(role==='assistant'&&text)d.innerHTML=renderMd(text);else d.textContent=text;
|
|
167
|
+
wrap.appendChild(d);
|
|
168
|
+
const time=document.createElement('div');
|
|
169
|
+
time.className='msg-time';
|
|
170
|
+
time.textContent=fmtTime();
|
|
171
|
+
wrap.appendChild(time);
|
|
172
|
+
if(role==='assistant'&&text){
|
|
173
|
+
const rx=document.createElement('div');rx.className='reactions';
|
|
174
|
+
rx.innerHTML='<button data-r="👍" onclick="react(this)">👍</button><button data-r="👎" onclick="react(this)">👎</button>';
|
|
175
|
+
wrap.appendChild(rx);
|
|
176
|
+
}
|
|
177
|
+
msgs.appendChild(wrap);
|
|
105
178
|
msgs.scrollTop=msgs.scrollHeight;
|
|
106
179
|
return d;
|
|
107
180
|
}
|
|
108
181
|
|
|
182
|
+
window.react=function(el){el.classList.toggle('active')};
|
|
183
|
+
|
|
109
184
|
input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
|
|
110
|
-
input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,
|
|
185
|
+
input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,150)+'px'});
|
|
111
186
|
btn.addEventListener('click',send);
|
|
112
187
|
|
|
113
188
|
async function send(){
|
|
@@ -116,8 +191,13 @@ async function send(){
|
|
|
116
191
|
sending=true;btn.disabled=true;
|
|
117
192
|
input.value='';input.style.height='auto';
|
|
118
193
|
addMsg('user',text);
|
|
119
|
-
const
|
|
120
|
-
|
|
194
|
+
const wrap=document.createElement('div');wrap.className='msg-wrap assistant';
|
|
195
|
+
const d=document.createElement('div');d.className='msg assistant';
|
|
196
|
+
d.innerHTML='<div class="typing"><span></span><span></span><span></span></div>';
|
|
197
|
+
wrap.appendChild(d);
|
|
198
|
+
const time=document.createElement('div');time.className='msg-time';time.textContent=fmtTime();
|
|
199
|
+
wrap.appendChild(time);
|
|
200
|
+
msgs.appendChild(wrap);msgs.scrollTop=msgs.scrollHeight;
|
|
121
201
|
try{
|
|
122
202
|
const res=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,sessionId})});
|
|
123
203
|
if(!res.ok)throw new Error('HTTP '+res.status);
|
|
@@ -130,21 +210,20 @@ async function send(){
|
|
|
130
210
|
const lines=chunk.split('\\n');
|
|
131
211
|
for(const line of lines){
|
|
132
212
|
if(!line.startsWith('data: '))continue;
|
|
133
|
-
const
|
|
134
|
-
if(
|
|
135
|
-
try{const j=JSON.parse(d);if(j.content)full+=j.content;if(j.error)full='Error: '+j.error;}catch{}
|
|
213
|
+
const dd=line.slice(6);if(dd==='[DONE]')continue;
|
|
214
|
+
try{const j=JSON.parse(dd);if(j.content)full+=j.content;if(j.error)full='Error: '+j.error;}catch{}
|
|
136
215
|
}
|
|
137
|
-
|
|
216
|
+
d.innerHTML=renderMd(full)+'<span class="cursor"></span>';
|
|
138
217
|
msgs.scrollTop=msgs.scrollHeight;
|
|
139
218
|
}
|
|
140
|
-
if(!full)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
219
|
+
if(!full){d.textContent='(empty response)';}else{d.innerHTML=renderMd(full);}
|
|
220
|
+
const rx=document.createElement('div');rx.className='reactions';
|
|
221
|
+
rx.innerHTML='<button data-r="👍" onclick="react(this)">👍</button><button data-r="👎" onclick="react(this)">👎</button>';
|
|
222
|
+
wrap.appendChild(rx);
|
|
223
|
+
}catch(e){d.className='msg error';d.textContent='Error: '+e.message;}
|
|
144
224
|
sending=false;btn.disabled=false;input.focus();
|
|
145
225
|
}
|
|
146
226
|
|
|
147
|
-
// Fetch agent info
|
|
148
227
|
fetch('/api/info').then(r=>r.json()).then(d=>{if(d.name)document.getElementById('title').textContent=d.name}).catch(()=>{});
|
|
149
228
|
</script>
|
|
150
229
|
</body>
|
|
@@ -365,7 +444,7 @@ class WebChannel extends index_1.BaseChannel {
|
|
|
365
444
|
timestamp: Date.now(),
|
|
366
445
|
uptime: uptimeMs,
|
|
367
446
|
uptimeHuman: `${Math.floor(uptimeMs / 3600000)}h ${Math.floor((uptimeMs % 3600000) / 60000)}m`,
|
|
368
|
-
version: '0.
|
|
447
|
+
version: '1.0.0',
|
|
369
448
|
agent: this.agentName,
|
|
370
449
|
stats: {
|
|
371
450
|
sessions: this.stats.sessions,
|
package/dist/cli.js
CHANGED
|
@@ -119,7 +119,7 @@ async function select(question, options) {
|
|
|
119
119
|
program
|
|
120
120
|
.name('opc')
|
|
121
121
|
.description('OPC Agent - Open Agent Framework for business workstations')
|
|
122
|
-
.version('0.
|
|
122
|
+
.version('1.0.0');
|
|
123
123
|
// ── Init command ─────────────────────────────────────────────
|
|
124
124
|
program
|
|
125
125
|
.command('init')
|
|
@@ -761,5 +761,113 @@ program
|
|
|
761
761
|
process.exit(1);
|
|
762
762
|
}
|
|
763
763
|
});
|
|
764
|
+
// 🔌 Plugin commands ────────────────────────────────────────
|
|
765
|
+
const pluginCmd = program.command('plugin').description('Manage plugins');
|
|
766
|
+
pluginCmd.command('list')
|
|
767
|
+
.description('List available built-in plugins')
|
|
768
|
+
.action(() => {
|
|
769
|
+
const builtIn = [
|
|
770
|
+
{ name: 'logging', description: 'Logs all messages and responses' },
|
|
771
|
+
{ name: 'analytics', description: 'Tracks message counts and error rates' },
|
|
772
|
+
{ name: 'rate-limit', description: 'Per-user rate limiting' },
|
|
773
|
+
];
|
|
774
|
+
console.log(`\n${icon.gear} ${color.bold('Available Plugins')}\n`);
|
|
775
|
+
for (const p of builtIn) {
|
|
776
|
+
console.log(` ${color.cyan(p.name.padEnd(16))} ${p.description}`);
|
|
777
|
+
}
|
|
778
|
+
console.log(`\n Add to oad.yaml: ${color.dim('plugins: [{ name: "logging" }]')}\n`);
|
|
779
|
+
});
|
|
780
|
+
pluginCmd.command('add')
|
|
781
|
+
.argument('<name>', 'Plugin name')
|
|
782
|
+
.option('-f, --file <file>', 'OAD file', 'oad.yaml')
|
|
783
|
+
.description('Add a plugin to your agent configuration')
|
|
784
|
+
.action((name, opts) => {
|
|
785
|
+
const validPlugins = ['logging', 'analytics', 'rate-limit'];
|
|
786
|
+
if (!validPlugins.includes(name)) {
|
|
787
|
+
console.error(`${icon.error} Unknown plugin: ${color.bold(name)}. Available: ${validPlugins.join(', ')}`);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
try {
|
|
791
|
+
const raw = fs.readFileSync(opts.file, 'utf-8');
|
|
792
|
+
const config = yaml.load(raw);
|
|
793
|
+
if (!config.spec.plugins)
|
|
794
|
+
config.spec.plugins = [];
|
|
795
|
+
if (config.spec.plugins.some((p) => p.name === name)) {
|
|
796
|
+
console.log(`${icon.info} Plugin "${name}" already in config.`);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
config.spec.plugins.push({ name });
|
|
800
|
+
fs.writeFileSync(opts.file, yaml.dump(config, { lineWidth: 120 }));
|
|
801
|
+
console.log(`${icon.success} Added plugin "${color.cyan(name)}" to ${opts.file}`);
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
console.error(`${icon.error} Failed:`, err instanceof Error ? err.message : err);
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
// 🔄 Migrate command ────────────────────────────────────────
|
|
809
|
+
program
|
|
810
|
+
.command('migrate')
|
|
811
|
+
.description('Migrate OAD to latest schema version')
|
|
812
|
+
.option('-f, --file <file>', 'OAD file', 'oad.yaml')
|
|
813
|
+
.option('--dry-run', 'Show changes without writing')
|
|
814
|
+
.action(async (opts) => {
|
|
815
|
+
try {
|
|
816
|
+
const raw = fs.readFileSync(opts.file, 'utf-8');
|
|
817
|
+
const config = yaml.load(raw);
|
|
818
|
+
let changed = false;
|
|
819
|
+
// Migration: add apiVersion if missing
|
|
820
|
+
if (!config.apiVersion) {
|
|
821
|
+
config.apiVersion = 'opc/v1';
|
|
822
|
+
changed = true;
|
|
823
|
+
}
|
|
824
|
+
// Migration: add kind if missing
|
|
825
|
+
if (!config.kind) {
|
|
826
|
+
config.kind = 'Agent';
|
|
827
|
+
changed = true;
|
|
828
|
+
}
|
|
829
|
+
// Migration: ensure metadata.version
|
|
830
|
+
if (!config.metadata?.version) {
|
|
831
|
+
if (!config.metadata)
|
|
832
|
+
config.metadata = {};
|
|
833
|
+
config.metadata.version = '1.0.0';
|
|
834
|
+
changed = true;
|
|
835
|
+
}
|
|
836
|
+
// Migration: ensure spec.channels is array
|
|
837
|
+
if (config.spec?.channels && !Array.isArray(config.spec.channels)) {
|
|
838
|
+
config.spec.channels = [config.spec.channels];
|
|
839
|
+
changed = true;
|
|
840
|
+
}
|
|
841
|
+
// Migration: ensure spec.skills is array
|
|
842
|
+
if (config.spec?.skills && !Array.isArray(config.spec.skills)) {
|
|
843
|
+
config.spec.skills = [config.spec.skills];
|
|
844
|
+
changed = true;
|
|
845
|
+
}
|
|
846
|
+
// Migration: old model format
|
|
847
|
+
if (config.spec?.llm?.model && !config.spec?.model) {
|
|
848
|
+
config.spec.model = config.spec.llm.model;
|
|
849
|
+
delete config.spec.llm;
|
|
850
|
+
changed = true;
|
|
851
|
+
}
|
|
852
|
+
if (!changed) {
|
|
853
|
+
console.log(`${icon.success} OAD is already up to date.`);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (opts.dryRun) {
|
|
857
|
+
console.log(`\n${icon.info} Would migrate:\n`);
|
|
858
|
+
console.log(yaml.dump(config, { lineWidth: 120 }));
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
// Backup
|
|
862
|
+
fs.writeFileSync(opts.file + '.bak', raw);
|
|
863
|
+
fs.writeFileSync(opts.file, yaml.dump(config, { lineWidth: 120 }));
|
|
864
|
+
console.log(`${icon.success} Migrated ${color.bold(opts.file)} (backup: ${opts.file}.bak)`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
catch (err) {
|
|
868
|
+
console.error(`${icon.error} Migration failed:`, err instanceof Error ? err.message : err);
|
|
869
|
+
process.exit(1);
|
|
870
|
+
}
|
|
871
|
+
});
|
|
764
872
|
program.parse();
|
|
765
873
|
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPC Agent Error Hierarchy - v1.0.0
|
|
3
|
+
* Custom error classes with user-friendly messages and recovery hints.
|
|
4
|
+
*/
|
|
5
|
+
export declare class OPCError extends Error {
|
|
6
|
+
readonly code: string;
|
|
7
|
+
readonly hint?: string;
|
|
8
|
+
readonly context?: Record<string, unknown>;
|
|
9
|
+
readonly timestamp: number;
|
|
10
|
+
constructor(message: string, opts?: {
|
|
11
|
+
code?: string;
|
|
12
|
+
hint?: string;
|
|
13
|
+
context?: Record<string, unknown>;
|
|
14
|
+
cause?: Error;
|
|
15
|
+
});
|
|
16
|
+
toJSON(): Record<string, unknown>;
|
|
17
|
+
toUserMessage(): string;
|
|
18
|
+
}
|
|
19
|
+
export declare class ProviderError extends OPCError {
|
|
20
|
+
readonly provider: string;
|
|
21
|
+
readonly statusCode?: number;
|
|
22
|
+
constructor(provider: string, message: string, opts?: {
|
|
23
|
+
statusCode?: number;
|
|
24
|
+
hint?: string;
|
|
25
|
+
cause?: Error;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export declare class ValidationError extends OPCError {
|
|
29
|
+
readonly field?: string;
|
|
30
|
+
readonly errors: string[];
|
|
31
|
+
constructor(message: string, errors?: string[], field?: string);
|
|
32
|
+
}
|
|
33
|
+
export declare class ConfigError extends OPCError {
|
|
34
|
+
constructor(message: string, hint?: string);
|
|
35
|
+
}
|
|
36
|
+
export declare class ChannelError extends OPCError {
|
|
37
|
+
readonly channelType: string;
|
|
38
|
+
constructor(channelType: string, message: string, opts?: {
|
|
39
|
+
hint?: string;
|
|
40
|
+
cause?: Error;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export declare class PluginError extends OPCError {
|
|
44
|
+
readonly pluginName: string;
|
|
45
|
+
constructor(pluginName: string, message: string, opts?: {
|
|
46
|
+
hint?: string;
|
|
47
|
+
cause?: Error;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export declare class RateLimitError extends OPCError {
|
|
51
|
+
readonly retryAfterMs?: number;
|
|
52
|
+
constructor(message?: string, retryAfterMs?: number);
|
|
53
|
+
}
|
|
54
|
+
export declare class SecurityError extends OPCError {
|
|
55
|
+
constructor(message: string, hint?: string);
|
|
56
|
+
}
|
|
57
|
+
export declare class TimeoutError extends OPCError {
|
|
58
|
+
constructor(operation: string, timeoutMs: number);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wrap an unknown thrown value into an OPCError.
|
|
62
|
+
*/
|
|
63
|
+
export declare function wrapError(err: unknown, fallbackMessage?: string): OPCError;
|
|
64
|
+
/**
|
|
65
|
+
* Format error for user display (no stack traces).
|
|
66
|
+
*/
|
|
67
|
+
export declare function formatErrorForUser(err: unknown): string;
|
|
68
|
+
//# sourceMappingURL=errors.d.ts.map
|