opc-agent 1.1.2 → 1.2.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.
@@ -27,14 +27,14 @@ const TEMPLATES_HTML = `<!DOCTYPE html>
27
27
  <title>Agent Templates</title>
28
28
  <style>
29
29
  *{margin:0;padding:0;box-sizing:border-box}
30
- body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
30
+ body{background:#0f0f23;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
31
31
  h1{font-size:28px;margin-bottom:8px;color:#fff}
32
- .sub{color:#888;margin-bottom:32px;font-size:14px}
32
+ .sub{color:#8a8aa0;margin-bottom:32px;font-size:14px}
33
33
  nav{margin-bottom:24px}
34
34
  nav a{color:#818cf8;text-decoration:none;margin-right:16px;font-size:14px}
35
35
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
36
- .card{background:#12121a;border:1px solid #1e1e2e;border-radius:12px;padding:24px;cursor:pointer;transition:all .2s}
37
- .card:hover{border-color:#818cf8;transform:translateY(-2px)}
36
+ .card{background:#1a1a3a;border:1px solid #2d2d4e;border-radius:14px;padding:24px;cursor:pointer;transition:all .2s}
37
+ .card:hover{border-color:#818cf8;transform:translateY(-2px);box-shadow:0 4px 20px rgba(129,140,248,.15)}
38
38
  .card .icon{font-size:32px;margin-bottom:12px}
39
39
  .card h3{font-size:16px;color:#fff;margin-bottom:8px}
40
40
  .card p{font-size:13px;color:#888;line-height:1.5}
@@ -65,7 +65,7 @@ const CHAT_HTML = `<!DOCTYPE html>
65
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
+ :root{--bg:#0f0f23;--surface:#1a1a3a;--border:#2d2d4e;--text:#e0e0e0;--text-dim:#8a8aa0;--accent:#818cf8;--accent-hover:#6366f1;--user-bg:#667eea;--user-hover:#5a6fd6;--error-bg:#7f1d1d;--error-text:#fca5a5;--success:#22c55e;--radius:14px}
69
69
  *{margin:0;padding:0;box-sizing:border-box}
70
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
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)}
@@ -88,8 +88,8 @@ nav.header-nav a:hover{color:#fff;background:rgba(255,255,255,.06)}
88
88
  .msg-wrap.user{align-items:flex-end}
89
89
  .msg-wrap.assistant{align-items:flex-start}
90
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}
91
+ .msg.user{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border-bottom-right-radius:4px;box-shadow:0 2px 8px rgba(102,126,234,.35)}
92
+ .msg.assistant{background:#2a2a4a;color:var(--text);border:1px solid var(--border);border-bottom-left-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.25)}
93
93
  .msg.error{background:var(--error-bg);color:var(--error-text);border:1px solid rgba(239,68,68,.3)}
94
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
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}
@@ -113,7 +113,7 @@ nav.header-nav a:hover{color:#fff;background:rgba(255,255,255,.06)}
113
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
114
  #input:focus{border-color:var(--accent)}
115
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}
116
+ #send{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:var(--radius);padding:0 16px;height:42px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;flex-shrink:0;letter-spacing:.5px}
117
117
  #send:hover{background:var(--user-hover);transform:scale(1.05)}
118
118
  #send:disabled{background:#334155;cursor:not-allowed;transform:none}
119
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}
@@ -132,15 +132,15 @@ nav.header-nav a:hover{color:#fff;background:rgba(255,255,255,.06)}
132
132
  <body>
133
133
  <header>
134
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>
135
+ <div class="info"><h1 id="title">OPC Agent</h1><div class="status"><span class="dot"></span>在线</div></div>
136
136
  <nav class="header-nav"><a href="/dashboard">Dashboard</a><a href="/templates">Templates</a></nav>
137
137
  </header>
138
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>
139
+ <div class="empty-state" id="empty"><div class="logo">💬</div><h2>开始对话</h2><p>在下方输入消息与 AI 助手对话。</p></div>
140
140
  </div>
141
141
  <div id="input-area">
142
- <textarea id="input" rows="1" placeholder="Type a message…" autocomplete="off"></textarea>
143
- <button id="send" aria-label="Send">↑</button>
142
+ <textarea id="input" rows="1" placeholder="输入消息…" autocomplete="off"></textarea>
143
+ <button id="send" aria-label="发送">发送</button>
144
144
  </div>
145
145
  <script>
146
146
  const msgs=document.getElementById('messages'),input=document.getElementById('input'),btn=document.getElementById('send'),empty=document.getElementById('empty');
@@ -193,7 +193,7 @@ async function send(){
193
193
  addMsg('user',text);
194
194
  const wrap=document.createElement('div');wrap.className='msg-wrap assistant';
195
195
  const d=document.createElement('div');d.className='msg assistant';
196
- d.innerHTML='<div class="typing"><span></span><span></span><span></span></div>';
196
+ d.innerHTML='<div class="typing"><span></span><span></span><span></span><small style="margin-left:6px;font-size:12px;color:#8a8aa0">思考中…</small></div>';
197
197
  wrap.appendChild(d);
198
198
  const time=document.createElement('div');time.className='msg-time';time.textContent=fmtTime();
199
199
  wrap.appendChild(time);
@@ -236,17 +236,17 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
236
236
  <title>OPC Dashboard</title>
237
237
  <style>
238
238
  *{margin:0;padding:0;box-sizing:border-box}
239
- body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
239
+ body{background:#0f0f23;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
240
240
  h1{font-size:24px;margin-bottom:24px;color:#fff}
241
241
  .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:32px}
242
- .card{background:#12121a;border:1px solid #1e1e2e;border-radius:12px;padding:20px}
243
- .card .label{font-size:12px;color:#888;text-transform:uppercase;letter-spacing:1px}
242
+ .card{background:#1a1a3a;border:1px solid #2d2d4e;border-radius:14px;padding:20px}
243
+ .card .label{font-size:12px;color:#8a8aa0;text-transform:uppercase;letter-spacing:1px}
244
244
  .card .value{font-size:32px;font-weight:700;color:#818cf8;margin-top:4px}
245
245
  .card .sub{font-size:12px;color:#555;margin-top:4px}
246
246
  nav{margin-bottom:24px}
247
247
  nav a{color:#818cf8;text-decoration:none;margin-right:16px;font-size:14px}
248
248
  nav a:hover{text-decoration:underline}
249
- .chart{background:#12121a;border:1px solid #1e1e2e;border-radius:12px;padding:20px;margin-bottom:16px}
249
+ .chart{background:#1a1a3a;border:1px solid #2d2d4e;border-radius:14px;padding:20px;margin-bottom:16px}
250
250
  .chart h3{font-size:14px;color:#888;margin-bottom:12px}
251
251
  </style>
252
252
  </head>
@@ -17,6 +17,11 @@ export declare class KnowledgeBase {
17
17
  }>>;
18
18
  /** Build context string for injection into LLM calls */
19
19
  getContext(query: string, topK?: number, minScore?: number): Promise<string>;
20
+ /**
21
+ * Query DeepBrain for semantic search enhancement.
22
+ * Activated when OPC_DEEPBRAIN_ENABLED=true and deepbrain CLI is globally installed.
23
+ */
24
+ private queryDeepBrain;
20
25
  getStats(): {
21
26
  totalEntries: number;
22
27
  sources: string[];
@@ -36,10 +36,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.KnowledgeBase = void 0;
37
37
  /**
38
38
  * Knowledge Base / RAG - Local vector storage with semantic search
39
+ * Supports optional DeepBrain semantic search enhancement via OPC_DEEPBRAIN_ENABLED=true
39
40
  */
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const crypto = __importStar(require("crypto"));
44
+ const child_process_1 = require("child_process");
43
45
  const CHUNK_SIZE = 500; // chars per chunk
44
46
  const CHUNK_OVERLAP = 50;
45
47
  const STORE_FILE = '.opc-knowledge.json';
@@ -185,9 +187,44 @@ class KnowledgeBase {
185
187
  async getContext(query, topK = 3, minScore = 0.1) {
186
188
  const results = await this.search(query, topK);
187
189
  const relevant = results.filter(r => r.score >= minScore);
188
- if (relevant.length === 0)
190
+ let context = '';
191
+ if (relevant.length > 0) {
192
+ context = `\n\n--- Relevant Knowledge ---\n${relevant.map((r, i) => `[${i + 1}] (source: ${r.source}, relevance: ${(r.score * 100).toFixed(0)}%)\n${r.content}`).join('\n\n')}\n--- End Knowledge ---\n`;
193
+ }
194
+ // Enhance with DeepBrain semantic search if enabled
195
+ const deepBrainCtx = this.queryDeepBrain(query, topK);
196
+ return context + deepBrainCtx;
197
+ }
198
+ /**
199
+ * Query DeepBrain for semantic search enhancement.
200
+ * Activated when OPC_DEEPBRAIN_ENABLED=true and deepbrain CLI is globally installed.
201
+ */
202
+ queryDeepBrain(query, topK) {
203
+ if (process.env.OPC_DEEPBRAIN_ENABLED !== 'true')
189
204
  return '';
190
- return `\n\n--- Relevant Knowledge ---\n${relevant.map((r, i) => `[${i + 1}] (source: ${r.source}, relevance: ${(r.score * 100).toFixed(0)}%)\n${r.content}`).join('\n\n')}\n--- End Knowledge ---\n`;
205
+ // Verify deepbrain is installed
206
+ const check = (0, child_process_1.spawnSync)('deepbrain', ['--version'], { encoding: 'utf-8', timeout: 3000 });
207
+ if (check.error || check.status !== 0)
208
+ return '';
209
+ try {
210
+ const result = (0, child_process_1.spawnSync)('deepbrain', ['query', query, '--top', String(topK), '--format', 'json'], { encoding: 'utf-8', timeout: 5000 });
211
+ if (result.status !== 0 || !result.stdout?.trim())
212
+ return '';
213
+ const parsed = JSON.parse(result.stdout);
214
+ const items = Array.isArray(parsed)
215
+ ? parsed
216
+ : (parsed.results ?? []);
217
+ if (items.length === 0)
218
+ return '';
219
+ return `\n\n--- DeepBrain Knowledge ---\n${items.map((r, i) => {
220
+ const relevance = r.score != null ? `${(r.score * 100).toFixed(0)}%` : 'n/a';
221
+ const text = r.content ?? r.text ?? '';
222
+ return `[${i + 1}] (source: ${r.source ?? 'deepbrain'}, relevance: ${relevance})\n${text}`;
223
+ }).join('\n\n')}\n--- End DeepBrain Knowledge ---\n`;
224
+ }
225
+ catch {
226
+ return '';
227
+ }
191
228
  }
192
229
  getStats() {
193
230
  const sources = [...new Set(this.store.entries.map(e => String(e.metadata.source)))];
@@ -5,5 +5,5 @@ export interface LLMProvider {
5
5
  chatStream(messages: Message[], systemPrompt?: string): AsyncIterable<string>;
6
6
  }
7
7
  export declare function createProvider(name?: string, model?: string, baseUrl?: string, apiKey?: string): LLMProvider;
8
- export declare const SUPPORTED_PROVIDERS: readonly ["openai", "deepseek", "qwen"];
8
+ export declare const SUPPORTED_PROVIDERS: readonly ["openai", "deepseek", "qwen", "gemini", "dashscope", "zhipu", "moonshot"];
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -69,20 +69,27 @@ class OpenAICompatibleProvider {
69
69
  throw new Error('No API key configured. Set OPC_LLM_API_KEY or OPENAI_API_KEY environment variable.');
70
70
  }
71
71
  const url = new URL(`${this.baseUrl}/chat/completions`);
72
+ const isGemini = url.hostname.includes('googleapis.com');
73
+ if (isGemini) {
74
+ url.searchParams.set('key', this.apiKey);
75
+ }
72
76
  const isHttps = url.protocol === 'https:';
73
77
  const lib = isHttps ? https : http;
74
78
  const postData = JSON.stringify(body);
79
+ const headers = {
80
+ 'Content-Type': 'application/json',
81
+ 'Content-Length': String(Buffer.byteLength(postData)),
82
+ };
83
+ if (!isGemini) {
84
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
85
+ }
75
86
  return new Promise((resolve, reject) => {
76
87
  const req = lib.request({
77
88
  hostname: url.hostname,
78
89
  port: url.port || (isHttps ? 443 : 80),
79
- path: url.pathname,
90
+ path: url.pathname + url.search,
80
91
  method: 'POST',
81
- headers: {
82
- 'Content-Type': 'application/json',
83
- Authorization: `Bearer ${this.apiKey}`,
84
- 'Content-Length': Buffer.byteLength(postData),
85
- },
92
+ headers,
86
93
  }, (res) => {
87
94
  let data = '';
88
95
  res.on('data', (chunk) => (data += chunk.toString()));
@@ -127,6 +134,10 @@ class OpenAICompatibleProvider {
127
134
  }
128
135
  const formatted = this.formatMessages(messages, systemPrompt);
129
136
  const url = new URL(`${this.baseUrl}/chat/completions`);
137
+ const isGemini = url.hostname.includes('googleapis.com');
138
+ if (isGemini) {
139
+ url.searchParams.set('key', this.apiKey);
140
+ }
130
141
  const isHttps = url.protocol === 'https:';
131
142
  const lib = isHttps ? https : http;
132
143
  const postData = JSON.stringify({
@@ -136,17 +147,20 @@ class OpenAICompatibleProvider {
136
147
  max_tokens: 2048,
137
148
  stream: true,
138
149
  });
150
+ const streamHeaders = {
151
+ 'Content-Type': 'application/json',
152
+ 'Content-Length': String(Buffer.byteLength(postData)),
153
+ };
154
+ if (!isGemini) {
155
+ streamHeaders['Authorization'] = `Bearer ${this.apiKey}`;
156
+ }
139
157
  const response = await new Promise((resolve, reject) => {
140
158
  const req = lib.request({
141
159
  hostname: url.hostname,
142
160
  port: url.port || (isHttps ? 443 : 80),
143
- path: url.pathname,
161
+ path: url.pathname + url.search,
144
162
  method: 'POST',
145
- headers: {
146
- 'Content-Type': 'application/json',
147
- Authorization: `Bearer ${this.apiKey}`,
148
- 'Content-Length': Buffer.byteLength(postData),
149
- },
163
+ headers: streamHeaders,
150
164
  }, resolve);
151
165
  req.on('error', reject);
152
166
  req.write(postData);
@@ -183,9 +197,139 @@ class OpenAICompatibleProvider {
183
197
  }
184
198
  }
185
199
  }
200
+ class GeminiNativeProvider {
201
+ name = 'gemini';
202
+ model;
203
+ apiKey;
204
+ constructor(model, apiKey) {
205
+ this.model = model;
206
+ this.apiKey = apiKey || getApiKey();
207
+ }
208
+ buildUrl(stream) {
209
+ const action = stream ? 'streamGenerateContent?alt=sse&' : 'generateContent?';
210
+ return `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:${action}key=${this.apiKey}`;
211
+ }
212
+ formatContents(messages, systemPrompt) {
213
+ const contents = [];
214
+ for (const m of messages) {
215
+ contents.push({ role: m.role === 'assistant' ? 'model' : 'user', parts: [{ text: m.content }] });
216
+ }
217
+ const result = { contents };
218
+ if (systemPrompt) {
219
+ result.systemInstruction = { parts: [{ text: systemPrompt }] };
220
+ }
221
+ return result;
222
+ }
223
+ async chat(messages, systemPrompt) {
224
+ if (!this.apiKey) {
225
+ const last = messages[messages.length - 1];
226
+ return `[gemini/${this.model} - no API key] Echo: ${last?.content ?? ''}`;
227
+ }
228
+ const body = this.formatContents(messages, systemPrompt);
229
+ const url = this.buildUrl(false);
230
+ const postData = JSON.stringify(body);
231
+ return new Promise((resolve, reject) => {
232
+ const parsedUrl = new URL(url);
233
+ const req = https.request({
234
+ hostname: parsedUrl.hostname,
235
+ path: parsedUrl.pathname + parsedUrl.search,
236
+ method: 'POST',
237
+ headers: { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(postData)) },
238
+ }, (res) => {
239
+ let data = '';
240
+ res.on('data', (chunk) => (data += chunk.toString()));
241
+ res.on('end', () => {
242
+ if (res.statusCode && res.statusCode >= 400) {
243
+ reject(new Error(`Gemini API error ${res.statusCode}: ${data}`));
244
+ return;
245
+ }
246
+ try {
247
+ const parsed = JSON.parse(data);
248
+ resolve(parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '');
249
+ }
250
+ catch {
251
+ reject(new Error(`Invalid Gemini response: ${data.slice(0, 200)}`));
252
+ }
253
+ });
254
+ });
255
+ req.on('error', reject);
256
+ req.write(postData);
257
+ req.end();
258
+ });
259
+ }
260
+ async *chatStream(messages, systemPrompt) {
261
+ if (!this.apiKey) {
262
+ const last = messages[messages.length - 1];
263
+ yield `[gemini/${this.model} - no API key] Echo: ${last?.content ?? ''}`;
264
+ return;
265
+ }
266
+ const body = this.formatContents(messages, systemPrompt);
267
+ const url = this.buildUrl(true);
268
+ const postData = JSON.stringify(body);
269
+ const parsedUrl = new URL(url);
270
+ const response = await new Promise((resolve, reject) => {
271
+ const req = https.request({
272
+ hostname: parsedUrl.hostname,
273
+ path: parsedUrl.pathname + parsedUrl.search,
274
+ method: 'POST',
275
+ headers: { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(postData)) },
276
+ }, resolve);
277
+ req.on('error', reject);
278
+ req.write(postData);
279
+ req.end();
280
+ });
281
+ if (response.statusCode && response.statusCode >= 400) {
282
+ let data = '';
283
+ for await (const chunk of response)
284
+ data += chunk.toString();
285
+ throw new Error(`Gemini API error ${response.statusCode}: ${data}`);
286
+ }
287
+ let buffer = '';
288
+ for await (const chunk of response) {
289
+ buffer += chunk.toString();
290
+ const lines = buffer.split('\n');
291
+ buffer = lines.pop() ?? '';
292
+ for (const line of lines) {
293
+ const trimmed = line.trim();
294
+ if (!trimmed.startsWith('data: '))
295
+ continue;
296
+ const data = trimmed.slice(6);
297
+ if (data === '[DONE]')
298
+ return;
299
+ try {
300
+ const parsed = JSON.parse(data);
301
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
302
+ if (text)
303
+ yield text;
304
+ }
305
+ catch { }
306
+ }
307
+ }
308
+ }
309
+ }
310
+ function isGeminiNative() {
311
+ const baseUrl = process.env.OPC_LLM_BASE_URL || '';
312
+ const key = getApiKey();
313
+ // Use native Gemini API when: key starts with AQ. (new format) OR base URL points to googleapis
314
+ return key.startsWith('AQ.') || (baseUrl.includes('googleapis.com') && !baseUrl.includes('/openai'));
315
+ }
186
316
  function createProvider(name = 'openai', model, baseUrl, apiKey) {
187
317
  const finalModel = model || process.env.OPC_LLM_MODEL || 'gpt-4o-mini';
188
- return new OpenAICompatibleProvider(name, finalModel, baseUrl, apiKey);
318
+ const finalKey = apiKey || getApiKey();
319
+ const finalBaseUrl = baseUrl || getBaseUrl();
320
+ // Auto-detect Gemini native when key is new format or base URL points to googleapis
321
+ if (finalKey.startsWith('AQ.') || isGeminiNative()) {
322
+ return new GeminiNativeProvider(finalModel, finalKey);
323
+ }
324
+ // Auto-detect provider name from base URL
325
+ let resolvedName = name;
326
+ if (finalBaseUrl.includes('deepseek.com')) {
327
+ resolvedName = 'deepseek';
328
+ }
329
+ else if (finalBaseUrl.includes('dashscope.aliyuncs.com')) {
330
+ resolvedName = 'qwen';
331
+ }
332
+ return new OpenAICompatibleProvider(resolvedName, finalModel, baseUrl, apiKey);
189
333
  }
190
- exports.SUPPORTED_PROVIDERS = ['openai', 'deepseek', 'qwen'];
334
+ exports.SUPPORTED_PROVIDERS = ['openai', 'deepseek', 'qwen', 'gemini', 'dashscope', 'zhipu', 'moonshot'];
191
335
  //# sourceMappingURL=index.js.map
@@ -577,6 +577,7 @@ export declare const SpecSchema: z.ZodObject<{
577
577
  config?: Record<string, unknown> | undefined;
578
578
  }[] | undefined;
579
579
  }, {
580
+ model?: string | undefined;
580
581
  auth?: {
581
582
  enabled?: boolean | undefined;
582
583
  apiKeys?: string[] | undefined;
@@ -597,7 +598,6 @@ export declare const SpecSchema: z.ZodObject<{
597
598
  default?: string | undefined;
598
599
  allowed?: string[] | undefined;
599
600
  } | undefined;
600
- model?: string | undefined;
601
601
  systemPrompt?: string | undefined;
602
602
  skills?: {
603
603
  name: string;
@@ -995,6 +995,7 @@ export declare const OADSchema: z.ZodObject<{
995
995
  config?: Record<string, unknown> | undefined;
996
996
  }[] | undefined;
997
997
  }, {
998
+ model?: string | undefined;
998
999
  auth?: {
999
1000
  enabled?: boolean | undefined;
1000
1001
  apiKeys?: string[] | undefined;
@@ -1015,7 +1016,6 @@ export declare const OADSchema: z.ZodObject<{
1015
1016
  default?: string | undefined;
1016
1017
  allowed?: string[] | undefined;
1017
1018
  } | undefined;
1018
- model?: string | undefined;
1019
1019
  systemPrompt?: string | undefined;
1020
1020
  skills?: {
1021
1021
  name: string;
@@ -1183,6 +1183,7 @@ export declare const OADSchema: z.ZodObject<{
1183
1183
  } | undefined;
1184
1184
  };
1185
1185
  spec: {
1186
+ model?: string | undefined;
1186
1187
  auth?: {
1187
1188
  enabled?: boolean | undefined;
1188
1189
  apiKeys?: string[] | undefined;
@@ -1203,7 +1204,6 @@ export declare const OADSchema: z.ZodObject<{
1203
1204
  default?: string | undefined;
1204
1205
  allowed?: string[] | undefined;
1205
1206
  } | undefined;
1206
- model?: string | undefined;
1207
1207
  systemPrompt?: string | undefined;
1208
1208
  skills?: {
1209
1209
  name: string;
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
- {
2
- "name": "opc-agent",
3
- "version": "1.1.2",
4
- "description": "Open Agent Framework — Build, test, and run AI Agents for business workstations",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "bin": {
8
- "opc": "dist/cli.js"
9
- },
10
- "scripts": {
11
- "build": "tsc",
12
- "test": "vitest run",
13
- "dev": "tsc --watch",
14
- "lint": "tsc --noEmit",
15
- "docs:dev": "vitepress dev docs",
16
- "docs:build": "vitepress build docs",
17
- "docs:preview": "vitepress preview docs"
18
- },
19
- "keywords": [
20
- "agent",
21
- "ai",
22
- "llm",
23
- "framework",
24
- "typescript",
25
- "agent-framework"
26
- ],
27
- "author": "Deepleaper",
28
- "license": "Apache-2.0",
29
- "repository": {
30
- "type": "git",
31
- "url": "https://github.com/Deepleaper/opc-agent.git"
32
- },
33
- "dependencies": {
34
- "agentkits": "^0.1.0",
35
- "commander": "^12.0.0",
36
- "express": "^4.21.0",
37
- "js-yaml": "^4.1.0",
38
- "ws": "^8.20.0",
39
- "zod": "^3.23.0"
40
- },
41
- "devDependencies": {
42
- "@types/express": "^4.17.21",
43
- "@types/js-yaml": "^4.0.9",
44
- "@types/node": "^20.11.0",
45
- "@types/ws": "^8.18.1",
46
- "typescript": "^5.5.0",
47
- "vitest": "^2.0.0",
48
- "vitepress": "^1.5.0"
49
- }
50
- }
1
+ {
2
+ "name": "opc-agent",
3
+ "version": "1.2.0",
4
+ "description": "Open Agent Framework — Build, test, and run AI Agents for business workstations",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "opc": "dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "test": "vitest run",
13
+ "dev": "tsc --watch",
14
+ "lint": "tsc --noEmit",
15
+ "docs:dev": "vitepress dev docs",
16
+ "docs:build": "vitepress build docs",
17
+ "docs:preview": "vitepress preview docs"
18
+ },
19
+ "keywords": [
20
+ "agent",
21
+ "ai",
22
+ "llm",
23
+ "framework",
24
+ "typescript",
25
+ "agent-framework"
26
+ ],
27
+ "author": "Deepleaper",
28
+ "license": "Apache-2.0",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/Deepleaper/opc-agent.git"
32
+ },
33
+ "dependencies": {
34
+ "agentkits": "^0.1.0",
35
+ "commander": "^12.0.0",
36
+ "express": "^4.21.0",
37
+ "js-yaml": "^4.1.0",
38
+ "ws": "^8.20.0",
39
+ "zod": "^3.23.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/express": "^4.17.21",
43
+ "@types/js-yaml": "^4.0.9",
44
+ "@types/node": "^20.11.0",
45
+ "@types/ws": "^8.18.1",
46
+ "typescript": "^5.5.0",
47
+ "vitest": "^2.0.0",
48
+ "vitepress": "^1.5.0"
49
+ }
50
+ }
@@ -26,14 +26,14 @@ const TEMPLATES_HTML = `<!DOCTYPE html>
26
26
  <title>Agent Templates</title>
27
27
  <style>
28
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}
29
+ body{background:#0f0f23;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
30
30
  h1{font-size:28px;margin-bottom:8px;color:#fff}
31
- .sub{color:#888;margin-bottom:32px;font-size:14px}
31
+ .sub{color:#8a8aa0;margin-bottom:32px;font-size:14px}
32
32
  nav{margin-bottom:24px}
33
33
  nav a{color:#818cf8;text-decoration:none;margin-right:16px;font-size:14px}
34
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)}
35
+ .card{background:#1a1a3a;border:1px solid #2d2d4e;border-radius:14px;padding:24px;cursor:pointer;transition:all .2s}
36
+ .card:hover{border-color:#818cf8;transform:translateY(-2px);box-shadow:0 4px 20px rgba(129,140,248,.15)}
37
37
  .card .icon{font-size:32px;margin-bottom:12px}
38
38
  .card h3{font-size:16px;color:#fff;margin-bottom:8px}
39
39
  .card p{font-size:13px;color:#888;line-height:1.5}
@@ -65,7 +65,7 @@ const CHAT_HTML = `<!DOCTYPE html>
65
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
+ :root{--bg:#0f0f23;--surface:#1a1a3a;--border:#2d2d4e;--text:#e0e0e0;--text-dim:#8a8aa0;--accent:#818cf8;--accent-hover:#6366f1;--user-bg:#667eea;--user-hover:#5a6fd6;--error-bg:#7f1d1d;--error-text:#fca5a5;--success:#22c55e;--radius:14px}
69
69
  *{margin:0;padding:0;box-sizing:border-box}
70
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
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)}
@@ -88,8 +88,8 @@ nav.header-nav a:hover{color:#fff;background:rgba(255,255,255,.06)}
88
88
  .msg-wrap.user{align-items:flex-end}
89
89
  .msg-wrap.assistant{align-items:flex-start}
90
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}
91
+ .msg.user{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border-bottom-right-radius:4px;box-shadow:0 2px 8px rgba(102,126,234,.35)}
92
+ .msg.assistant{background:#2a2a4a;color:var(--text);border:1px solid var(--border);border-bottom-left-radius:4px;box-shadow:0 2px 8px rgba(0,0,0,.25)}
93
93
  .msg.error{background:var(--error-bg);color:var(--error-text);border:1px solid rgba(239,68,68,.3)}
94
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
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}
@@ -113,7 +113,7 @@ nav.header-nav a:hover{color:#fff;background:rgba(255,255,255,.06)}
113
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
114
  #input:focus{border-color:var(--accent)}
115
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}
116
+ #send{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:var(--radius);padding:0 16px;height:42px;font-size:14px;font-weight:600;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;flex-shrink:0;letter-spacing:.5px}
117
117
  #send:hover{background:var(--user-hover);transform:scale(1.05)}
118
118
  #send:disabled{background:#334155;cursor:not-allowed;transform:none}
119
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}
@@ -132,15 +132,15 @@ nav.header-nav a:hover{color:#fff;background:rgba(255,255,255,.06)}
132
132
  <body>
133
133
  <header>
134
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>
135
+ <div class="info"><h1 id="title">OPC Agent</h1><div class="status"><span class="dot"></span>在线</div></div>
136
136
  <nav class="header-nav"><a href="/dashboard">Dashboard</a><a href="/templates">Templates</a></nav>
137
137
  </header>
138
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>
139
+ <div class="empty-state" id="empty"><div class="logo">💬</div><h2>开始对话</h2><p>在下方输入消息与 AI 助手对话。</p></div>
140
140
  </div>
141
141
  <div id="input-area">
142
- <textarea id="input" rows="1" placeholder="Type a message…" autocomplete="off"></textarea>
143
- <button id="send" aria-label="Send">↑</button>
142
+ <textarea id="input" rows="1" placeholder="输入消息…" autocomplete="off"></textarea>
143
+ <button id="send" aria-label="发送">发送</button>
144
144
  </div>
145
145
  <script>
146
146
  const msgs=document.getElementById('messages'),input=document.getElementById('input'),btn=document.getElementById('send'),empty=document.getElementById('empty');
@@ -193,7 +193,7 @@ async function send(){
193
193
  addMsg('user',text);
194
194
  const wrap=document.createElement('div');wrap.className='msg-wrap assistant';
195
195
  const d=document.createElement('div');d.className='msg assistant';
196
- d.innerHTML='<div class="typing"><span></span><span></span><span></span></div>';
196
+ d.innerHTML='<div class="typing"><span></span><span></span><span></span><small style="margin-left:6px;font-size:12px;color:#8a8aa0">思考中…</small></div>';
197
197
  wrap.appendChild(d);
198
198
  const time=document.createElement('div');time.className='msg-time';time.textContent=fmtTime();
199
199
  wrap.appendChild(time);
@@ -237,17 +237,17 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
237
237
  <title>OPC Dashboard</title>
238
238
  <style>
239
239
  *{margin:0;padding:0;box-sizing:border-box}
240
- body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
240
+ body{background:#0f0f23;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
241
241
  h1{font-size:24px;margin-bottom:24px;color:#fff}
242
242
  .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:16px;margin-bottom:32px}
243
- .card{background:#12121a;border:1px solid #1e1e2e;border-radius:12px;padding:20px}
244
- .card .label{font-size:12px;color:#888;text-transform:uppercase;letter-spacing:1px}
243
+ .card{background:#1a1a3a;border:1px solid #2d2d4e;border-radius:14px;padding:20px}
244
+ .card .label{font-size:12px;color:#8a8aa0;text-transform:uppercase;letter-spacing:1px}
245
245
  .card .value{font-size:32px;font-weight:700;color:#818cf8;margin-top:4px}
246
246
  .card .sub{font-size:12px;color:#555;margin-top:4px}
247
247
  nav{margin-bottom:24px}
248
248
  nav a{color:#818cf8;text-decoration:none;margin-right:16px;font-size:14px}
249
249
  nav a:hover{text-decoration:underline}
250
- .chart{background:#12121a;border:1px solid #1e1e2e;border-radius:12px;padding:20px;margin-bottom:16px}
250
+ .chart{background:#1a1a3a;border:1px solid #2d2d4e;border-radius:14px;padding:20px;margin-bottom:16px}
251
251
  .chart h3{font-size:14px;color:#888;margin-bottom:12px}
252
252
  </style>
253
253
  </head>
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Knowledge Base / RAG - Local vector storage with semantic search
3
+ * Supports optional DeepBrain semantic search enhancement via OPC_DEEPBRAIN_ENABLED=true
3
4
  */
4
5
  import * as fs from 'fs';
5
6
  import * as path from 'path';
6
7
  import * as crypto from 'crypto';
8
+ import { spawnSync } from 'child_process';
7
9
 
8
10
  // Simple in-memory vector store (PGlite-compatible interface for future migration)
9
11
  interface VectorEntry {
@@ -180,11 +182,54 @@ export class KnowledgeBase {
180
182
  async getContext(query: string, topK: number = 3, minScore: number = 0.1): Promise<string> {
181
183
  const results = await this.search(query, topK);
182
184
  const relevant = results.filter(r => r.score >= minScore);
183
- if (relevant.length === 0) return '';
184
185
 
185
- return `\n\n--- Relevant Knowledge ---\n${relevant.map((r, i) =>
186
- `[${i + 1}] (source: ${r.source}, relevance: ${(r.score * 100).toFixed(0)}%)\n${r.content}`
187
- ).join('\n\n')}\n--- End Knowledge ---\n`;
186
+ let context = '';
187
+ if (relevant.length > 0) {
188
+ context = `\n\n--- Relevant Knowledge ---\n${relevant.map((r, i) =>
189
+ `[${i + 1}] (source: ${r.source}, relevance: ${(r.score * 100).toFixed(0)}%)\n${r.content}`
190
+ ).join('\n\n')}\n--- End Knowledge ---\n`;
191
+ }
192
+
193
+ // Enhance with DeepBrain semantic search if enabled
194
+ const deepBrainCtx = this.queryDeepBrain(query, topK);
195
+ return context + deepBrainCtx;
196
+ }
197
+
198
+ /**
199
+ * Query DeepBrain for semantic search enhancement.
200
+ * Activated when OPC_DEEPBRAIN_ENABLED=true and deepbrain CLI is globally installed.
201
+ */
202
+ private queryDeepBrain(query: string, topK: number): string {
203
+ if (process.env.OPC_DEEPBRAIN_ENABLED !== 'true') return '';
204
+
205
+ // Verify deepbrain is installed
206
+ const check = spawnSync('deepbrain', ['--version'], { encoding: 'utf-8', timeout: 3000 });
207
+ if (check.error || check.status !== 0) return '';
208
+
209
+ try {
210
+ const result = spawnSync(
211
+ 'deepbrain',
212
+ ['query', query, '--top', String(topK), '--format', 'json'],
213
+ { encoding: 'utf-8', timeout: 5000 },
214
+ );
215
+ if (result.status !== 0 || !result.stdout?.trim()) return '';
216
+
217
+ type DeepBrainResult = { source?: string; score?: number; content?: string; text?: string };
218
+ const parsed: unknown = JSON.parse(result.stdout);
219
+ const items: DeepBrainResult[] = Array.isArray(parsed)
220
+ ? (parsed as DeepBrainResult[])
221
+ : ((parsed as { results?: DeepBrainResult[] }).results ?? []);
222
+
223
+ if (items.length === 0) return '';
224
+
225
+ return `\n\n--- DeepBrain Knowledge ---\n${items.map((r, i) => {
226
+ const relevance = r.score != null ? `${(r.score * 100).toFixed(0)}%` : 'n/a';
227
+ const text = r.content ?? r.text ?? '';
228
+ return `[${i + 1}] (source: ${r.source ?? 'deepbrain'}, relevance: ${relevance})\n${text}`;
229
+ }).join('\n\n')}\n--- End DeepBrain Knowledge ---\n`;
230
+ } catch {
231
+ return '';
232
+ }
188
233
  }
189
234
 
190
235
  getStats(): { totalEntries: number; sources: string[]; updatedAt: string } {
@@ -51,23 +51,31 @@ class OpenAICompatibleProvider implements LLMProvider {
51
51
  }
52
52
 
53
53
  const url = new URL(`${this.baseUrl}/chat/completions`);
54
+ const isGemini = url.hostname.includes('googleapis.com');
55
+ if (isGemini) {
56
+ url.searchParams.set('key', this.apiKey);
57
+ }
54
58
  const isHttps = url.protocol === 'https:';
55
59
  const lib = isHttps ? https : http;
56
60
 
57
61
  const postData = JSON.stringify(body);
58
62
 
63
+ const headers: Record<string, string> = {
64
+ 'Content-Type': 'application/json',
65
+ 'Content-Length': String(Buffer.byteLength(postData)),
66
+ };
67
+ if (!isGemini) {
68
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
69
+ }
70
+
59
71
  return new Promise((resolve, reject) => {
60
72
  const req = lib.request(
61
73
  {
62
74
  hostname: url.hostname,
63
75
  port: url.port || (isHttps ? 443 : 80),
64
- path: url.pathname,
76
+ path: url.pathname + url.search,
65
77
  method: 'POST',
66
- headers: {
67
- 'Content-Type': 'application/json',
68
- Authorization: `Bearer ${this.apiKey}`,
69
- 'Content-Length': Buffer.byteLength(postData),
70
- },
78
+ headers,
71
79
  },
72
80
  (res) => {
73
81
  let data = '';
@@ -116,6 +124,10 @@ class OpenAICompatibleProvider implements LLMProvider {
116
124
 
117
125
  const formatted = this.formatMessages(messages, systemPrompt);
118
126
  const url = new URL(`${this.baseUrl}/chat/completions`);
127
+ const isGemini = url.hostname.includes('googleapis.com');
128
+ if (isGemini) {
129
+ url.searchParams.set('key', this.apiKey);
130
+ }
119
131
  const isHttps = url.protocol === 'https:';
120
132
  const lib = isHttps ? https : http;
121
133
  const postData = JSON.stringify({
@@ -126,18 +138,22 @@ class OpenAICompatibleProvider implements LLMProvider {
126
138
  stream: true,
127
139
  });
128
140
 
141
+ const streamHeaders: Record<string, string> = {
142
+ 'Content-Type': 'application/json',
143
+ 'Content-Length': String(Buffer.byteLength(postData)),
144
+ };
145
+ if (!isGemini) {
146
+ streamHeaders['Authorization'] = `Bearer ${this.apiKey}`;
147
+ }
148
+
129
149
  const response = await new Promise<http.IncomingMessage>((resolve, reject) => {
130
150
  const req = lib.request(
131
151
  {
132
152
  hostname: url.hostname,
133
153
  port: url.port || (isHttps ? 443 : 80),
134
- path: url.pathname,
154
+ path: url.pathname + url.search,
135
155
  method: 'POST',
136
- headers: {
137
- 'Content-Type': 'application/json',
138
- Authorization: `Bearer ${this.apiKey}`,
139
- 'Content-Length': Buffer.byteLength(postData),
140
- },
156
+ headers: streamHeaders,
141
157
  },
142
158
  resolve,
143
159
  );
@@ -175,9 +191,141 @@ class OpenAICompatibleProvider implements LLMProvider {
175
191
  }
176
192
  }
177
193
 
194
+ class GeminiNativeProvider implements LLMProvider {
195
+ name = 'gemini';
196
+ private model: string;
197
+ private apiKey: string;
198
+
199
+ constructor(model: string, apiKey?: string) {
200
+ this.model = model;
201
+ this.apiKey = apiKey || getApiKey();
202
+ }
203
+
204
+ private buildUrl(stream: boolean): string {
205
+ const action = stream ? 'streamGenerateContent?alt=sse&' : 'generateContent?';
206
+ return `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:${action}key=${this.apiKey}`;
207
+ }
208
+
209
+ private formatContents(messages: Message[], systemPrompt?: string): { contents: any[]; systemInstruction?: any } {
210
+ const contents: any[] = [];
211
+ for (const m of messages) {
212
+ contents.push({ role: m.role === 'assistant' ? 'model' : 'user', parts: [{ text: m.content }] });
213
+ }
214
+ const result: any = { contents };
215
+ if (systemPrompt) {
216
+ result.systemInstruction = { parts: [{ text: systemPrompt }] };
217
+ }
218
+ return result;
219
+ }
220
+
221
+ async chat(messages: Message[], systemPrompt?: string): Promise<string> {
222
+ if (!this.apiKey) {
223
+ const last = messages[messages.length - 1];
224
+ return `[gemini/${this.model} - no API key] Echo: ${last?.content ?? ''}`;
225
+ }
226
+ const body = this.formatContents(messages, systemPrompt);
227
+ const url = this.buildUrl(false);
228
+ const postData = JSON.stringify(body);
229
+
230
+ return new Promise((resolve, reject) => {
231
+ const parsedUrl = new URL(url);
232
+ const req = https.request({
233
+ hostname: parsedUrl.hostname,
234
+ path: parsedUrl.pathname + parsedUrl.search,
235
+ method: 'POST',
236
+ headers: { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(postData)) },
237
+ }, (res) => {
238
+ let data = '';
239
+ res.on('data', (chunk: Buffer) => (data += chunk.toString()));
240
+ res.on('end', () => {
241
+ if (res.statusCode && res.statusCode >= 400) { reject(new Error(`Gemini API error ${res.statusCode}: ${data}`)); return; }
242
+ try {
243
+ const parsed = JSON.parse(data);
244
+ resolve(parsed.candidates?.[0]?.content?.parts?.[0]?.text ?? '');
245
+ } catch { reject(new Error(`Invalid Gemini response: ${data.slice(0, 200)}`)); }
246
+ });
247
+ });
248
+ req.on('error', reject);
249
+ req.write(postData);
250
+ req.end();
251
+ });
252
+ }
253
+
254
+ async *chatStream(messages: Message[], systemPrompt?: string): AsyncIterable<string> {
255
+ if (!this.apiKey) {
256
+ const last = messages[messages.length - 1];
257
+ yield `[gemini/${this.model} - no API key] Echo: ${last?.content ?? ''}`;
258
+ return;
259
+ }
260
+ const body = this.formatContents(messages, systemPrompt);
261
+ const url = this.buildUrl(true);
262
+ const postData = JSON.stringify(body);
263
+ const parsedUrl = new URL(url);
264
+
265
+ const response = await new Promise<http.IncomingMessage>((resolve, reject) => {
266
+ const req = https.request({
267
+ hostname: parsedUrl.hostname,
268
+ path: parsedUrl.pathname + parsedUrl.search,
269
+ method: 'POST',
270
+ headers: { 'Content-Type': 'application/json', 'Content-Length': String(Buffer.byteLength(postData)) },
271
+ }, resolve);
272
+ req.on('error', reject);
273
+ req.write(postData);
274
+ req.end();
275
+ });
276
+
277
+ if (response.statusCode && response.statusCode >= 400) {
278
+ let data = '';
279
+ for await (const chunk of response) data += chunk.toString();
280
+ throw new Error(`Gemini API error ${response.statusCode}: ${data}`);
281
+ }
282
+
283
+ let buffer = '';
284
+ for await (const chunk of response) {
285
+ buffer += chunk.toString();
286
+ const lines = buffer.split('\n');
287
+ buffer = lines.pop() ?? '';
288
+ for (const line of lines) {
289
+ const trimmed = line.trim();
290
+ if (!trimmed.startsWith('data: ')) continue;
291
+ const data = trimmed.slice(6);
292
+ if (data === '[DONE]') return;
293
+ try {
294
+ const parsed = JSON.parse(data);
295
+ const text = parsed.candidates?.[0]?.content?.parts?.[0]?.text;
296
+ if (text) yield text;
297
+ } catch {}
298
+ }
299
+ }
300
+ }
301
+ }
302
+
303
+ function isGeminiNative(): boolean {
304
+ const baseUrl = process.env.OPC_LLM_BASE_URL || '';
305
+ const key = getApiKey();
306
+ // Use native Gemini API when: key starts with AQ. (new format) OR base URL points to googleapis
307
+ return key.startsWith('AQ.') || (baseUrl.includes('googleapis.com') && !baseUrl.includes('/openai'));
308
+ }
309
+
178
310
  export function createProvider(name: string = 'openai', model?: string, baseUrl?: string, apiKey?: string): LLMProvider {
179
311
  const finalModel = model || process.env.OPC_LLM_MODEL || 'gpt-4o-mini';
180
- return new OpenAICompatibleProvider(name, finalModel, baseUrl, apiKey);
312
+ const finalKey = apiKey || getApiKey();
313
+ const finalBaseUrl = baseUrl || getBaseUrl();
314
+
315
+ // Auto-detect Gemini native when key is new format or base URL points to googleapis
316
+ if (finalKey.startsWith('AQ.') || isGeminiNative()) {
317
+ return new GeminiNativeProvider(finalModel, finalKey);
318
+ }
319
+
320
+ // Auto-detect provider name from base URL
321
+ let resolvedName = name;
322
+ if (finalBaseUrl.includes('deepseek.com')) {
323
+ resolvedName = 'deepseek';
324
+ } else if (finalBaseUrl.includes('dashscope.aliyuncs.com')) {
325
+ resolvedName = 'qwen';
326
+ }
327
+
328
+ return new OpenAICompatibleProvider(resolvedName, finalModel, baseUrl, apiKey);
181
329
  }
182
330
 
183
- export const SUPPORTED_PROVIDERS = ['openai', 'deepseek', 'qwen'] as const;
331
+ export const SUPPORTED_PROVIDERS = ['openai', 'deepseek', 'qwen', 'gemini', 'dashscope', 'zhipu', 'moonshot'] as const;
@@ -0,0 +1,45 @@
1
+ # Ecommerce Assistant — 电商助手工位
2
+
3
+ 专为电商平台设计的 AI 导购与售后工位,覆盖商品推荐、订单查询、退换货处理全链路。
4
+
5
+ ## 快速开始
6
+
7
+ ```bash
8
+ opc init --template ecommerce-assistant
9
+ opc start
10
+ ```
11
+
12
+ 访问 http://localhost:3000 即可使用电商助手聊天界面。
13
+
14
+ ## 功能
15
+
16
+ | 技能 | 说明 |
17
+ |------|------|
18
+ | product-search | 关键词/类目/价格区间商品搜索 |
19
+ | order-query | 订单状态与物流实时查询 |
20
+ | after-sale | 退换货受理与投诉处理 |
21
+ | promotion | 个性化优惠券与促销推送 |
22
+ | recommendation | 基于偏好的个性化商品推荐 |
23
+
24
+ ## 配置
25
+
26
+ 在 `oad.yaml` 中修改以下参数:
27
+
28
+ - `spec.provider.default` — 切换 LLM 提供商(deepseek / openai / qwen)
29
+ - `spec.model` — 指定模型版本
30
+ - `spec.systemPrompt` — 定制品牌话术、商品范围和售后规则
31
+
32
+ ### 环境变量
33
+
34
+ ```bash
35
+ OPC_LLM_API_KEY=your_key
36
+ OPC_LLM_MODEL=deepseek-chat # 可选,覆盖 oad.yaml 中的 model
37
+ OPC_DEEPBRAIN_ENABLED=true # 启用 DeepBrain 知识库增强(需全局安装 deepbrain)
38
+ ```
39
+
40
+ ## 推荐搭配
41
+
42
+ - 将商品目录、售后政策、FAQ 上传至知识库(`/api/kb/upload`)
43
+ - 开启 `OPC_DEEPBRAIN_ENABLED=true` 提升商品语义匹配精度
44
+ - 通过 Dashboard(`/dashboard`)监控转化率、客满率和退单率
45
+ - 结合 CRM 系统 Webhook 实现订单状态实时同步
@@ -0,0 +1,47 @@
1
+ apiVersion: opc/v1
2
+ kind: Agent
3
+ metadata:
4
+ name: ecommerce-assistant
5
+ version: 1.0.0
6
+ description: "电商助手工位 - 商品推荐、订单查询、售后服务一体化"
7
+ author: Deepleaper
8
+ license: Apache-2.0
9
+ marketplace:
10
+ certified: false
11
+ category: ecommerce
12
+ spec:
13
+ provider:
14
+ default: deepseek
15
+ allowed: [openai, deepseek, qwen, gemini]
16
+ model: deepseek-chat
17
+ systemPrompt: |
18
+ 你是一名专业的电商购物助手。
19
+ 你帮助用户查询商品信息、比较价格、处理订单状态和售后问题。
20
+ 根据用户需求推荐合适的商品,推送当前促销活动,并协助用户做出购买决策。
21
+ 处理退换货申请时,需先核实订单信息,再按平台规则指引用户操作。
22
+ 始终保持友好、热情、专业的服务态度。
23
+ skills:
24
+ - name: product-search
25
+ description: "搜索商品信息,支持关键词、类目、价格区间筛选"
26
+ - name: order-query
27
+ description: "查询订单状态、物流轨迹和预计送达时间"
28
+ - name: after-sale
29
+ description: "处理退换货申请、投诉受理和赔偿协商"
30
+ - name: promotion
31
+ description: "推送个性化优惠券、限时活动和会员权益"
32
+ - name: recommendation
33
+ description: "基于用户偏好和购买历史做商品推荐"
34
+ channels:
35
+ - type: web
36
+ port: 3000
37
+ memory:
38
+ shortTerm: true
39
+ longTerm: true
40
+ knowledge:
41
+ enabled: true
42
+ deepbrain: auto
43
+ dtv:
44
+ trust:
45
+ level: sandbox
46
+ value:
47
+ metrics: [conversion_rate, customer_satisfaction, avg_order_value, refund_rate]
@@ -0,0 +1,43 @@
1
+ # Tech Support Agent — 技术支持工位
2
+
3
+ 专为 IT 支持团队设计的 AI 工位,覆盖软件故障、系统配置、网络问题等场景。
4
+
5
+ ## 快速开始
6
+
7
+ ```bash
8
+ opc init --template tech-support
9
+ opc start
10
+ ```
11
+
12
+ 访问 http://localhost:3000 即可使用技术支持聊天界面。
13
+
14
+ ## 功能
15
+
16
+ | 技能 | 说明 |
17
+ |------|------|
18
+ | troubleshoot | 分步骤诊断和解决技术问题 |
19
+ | knowledge-lookup | 查询技术文档与历史解决方案 |
20
+ | ticket-create | 创建并跟踪技术支持工单 |
21
+ | escalate | 升级复杂问题至专项团队 |
22
+
23
+ ## 配置
24
+
25
+ 在 `oad.yaml` 中修改以下参数:
26
+
27
+ - `spec.provider.default` — 切换 LLM 提供商(deepseek / openai / qwen)
28
+ - `spec.model` — 指定模型版本
29
+ - `spec.systemPrompt` — 定制支持范围和话术风格
30
+
31
+ ### 环境变量
32
+
33
+ ```bash
34
+ OPC_LLM_API_KEY=your_key
35
+ OPC_LLM_MODEL=deepseek-chat # 可选,覆盖 oad.yaml 中的 model
36
+ OPC_DEEPBRAIN_ENABLED=true # 启用 DeepBrain 知识库增强(需全局安装 deepbrain)
37
+ ```
38
+
39
+ ## 推荐搭配
40
+
41
+ - 将内部技术文档、SOP、FAQ 上传至知识库(`/api/kb/upload`)
42
+ - 开启 `OPC_DEEPBRAIN_ENABLED=true` 获得更精准的语义检索
43
+ - 通过 Dashboard(`/dashboard`)监控首次解决率和平均响应时间
@@ -0,0 +1,45 @@
1
+ apiVersion: opc/v1
2
+ kind: Agent
3
+ metadata:
4
+ name: tech-support
5
+ version: 1.0.0
6
+ description: "技术支持工位 - 处理用户技术问题、故障排查和解决方案推荐"
7
+ author: Deepleaper
8
+ license: Apache-2.0
9
+ marketplace:
10
+ certified: false
11
+ category: it-support
12
+ spec:
13
+ provider:
14
+ default: deepseek
15
+ allowed: [openai, deepseek, qwen, gemini]
16
+ model: deepseek-chat
17
+ systemPrompt: |
18
+ 你是一名专业的技术支持工程师。
19
+ 你帮助用户解决技术问题,包括软件故障、系统配置、网络问题、硬件故障等。
20
+ 回答时请保持专业、耐心,并提供清晰的步骤指引。
21
+ 优先使用知识库中的已知解决方案。
22
+ 如果问题无法远程解决,请指导用户联系线下支持或提交工单。
23
+ skills:
24
+ - name: troubleshoot
25
+ description: "诊断和解决技术问题,提供分步骤操作指南"
26
+ - name: knowledge-lookup
27
+ description: "查询技术文档、FAQ 和历史解决方案库"
28
+ - name: ticket-create
29
+ description: "为复杂问题创建技术支持工单并跟踪进展"
30
+ - name: escalate
31
+ description: "将高优先级问题升级到高级工程师或专项团队"
32
+ channels:
33
+ - type: web
34
+ port: 3000
35
+ memory:
36
+ shortTerm: true
37
+ longTerm: true
38
+ knowledge:
39
+ enabled: true
40
+ deepbrain: auto
41
+ dtv:
42
+ trust:
43
+ level: internal
44
+ value:
45
+ metrics: [resolution_time, first_contact_resolution, customer_satisfaction]