opc-agent 4.1.0 → 4.1.1
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/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
- package/CHANGELOG.md +48 -48
- package/CONTRIBUTING.md +36 -36
- package/README.zh-CN.md +497 -497
- package/dist/channels/wechat.js +6 -6
- package/dist/deploy/index.js +56 -56
- package/dist/studio/server.js +30 -1
- package/dist/studio-ui/index.html +230 -10
- package/dist/ui/components.js +105 -105
- package/examples/README.md +22 -22
- package/examples/basic-agent.ts +90 -90
- package/examples/brain-integration.ts +71 -71
- package/examples/multi-channel.ts +74 -74
- package/fix-sidebar.mjs +188 -188
- package/install.ps1 +154 -154
- package/install.sh +164 -164
- package/package.json +1 -1
- package/scripts/install.ps1 +31 -31
- package/scripts/install.sh +40 -40
- package/serve-studio.js +13 -13
- package/serve-test.js +25 -25
- package/src/channels/dingtalk.ts +46 -46
- package/src/channels/email.ts +351 -351
- package/src/channels/feishu.ts +349 -349
- package/src/channels/googlechat.ts +42 -42
- package/src/channels/imessage.ts +31 -31
- package/src/channels/irc.ts +82 -82
- package/src/channels/line.ts +32 -32
- package/src/channels/matrix.ts +33 -33
- package/src/channels/mattermost.ts +57 -57
- package/src/channels/msteams.ts +32 -32
- package/src/channels/nostr.ts +32 -32
- package/src/channels/qq.ts +33 -33
- package/src/channels/signal.ts +32 -32
- package/src/channels/sms.ts +33 -33
- package/src/channels/telegram.ts +616 -616
- package/src/channels/twitch.ts +65 -65
- package/src/channels/voice-call.ts +100 -100
- package/src/channels/websocket.ts +399 -399
- package/src/channels/wechat.ts +329 -329
- package/src/channels/whatsapp.ts +32 -32
- package/src/cli/chat.ts +99 -99
- package/src/cli/setup.ts +314 -314
- package/src/core/agent.ts +476 -476
- package/src/core/api-server.ts +277 -277
- package/src/core/audio.ts +98 -98
- package/src/core/collaboration.ts +275 -275
- package/src/core/context-discovery.ts +85 -85
- package/src/core/context-refs.ts +140 -140
- package/src/core/gateway.ts +106 -106
- package/src/core/heartbeat.ts +51 -51
- package/src/core/hooks.ts +105 -105
- package/src/core/ide-bridge.ts +133 -133
- package/src/core/node-network.ts +86 -86
- package/src/core/profiles.ts +122 -122
- package/src/core/scheduler.ts +187 -187
- package/src/core/session-manager.ts +137 -137
- package/src/core/subagent.ts +98 -98
- package/src/core/vision.ts +180 -180
- package/src/core/workflow-graph.ts +365 -365
- package/src/daemon.ts +96 -96
- package/src/deploy/index.ts +255 -255
- package/src/doctor.ts +156 -156
- package/src/eval/index.ts +211 -211
- package/src/eval/suites/basic.json +16 -16
- package/src/eval/suites/memory.json +12 -12
- package/src/eval/suites/safety.json +14 -14
- package/src/hub/brain-seed.ts +54 -54
- package/src/hub/client.ts +60 -60
- package/src/mcp/servers/calculator-mcp.ts +65 -65
- package/src/mcp/servers/crypto-mcp.ts +73 -73
- package/src/mcp/servers/database-mcp.ts +72 -72
- package/src/mcp/servers/datetime-mcp.ts +69 -69
- package/src/mcp/servers/filesystem.ts +66 -66
- package/src/mcp/servers/github-mcp.ts +58 -58
- package/src/mcp/servers/index.ts +63 -63
- package/src/mcp/servers/json-mcp.ts +102 -102
- package/src/mcp/servers/memory-mcp.ts +56 -56
- package/src/mcp/servers/regex-mcp.ts +53 -53
- package/src/mcp/servers/web-mcp.ts +49 -49
- package/src/memory/context-compressor.ts +189 -189
- package/src/memory/seed-loader.ts +212 -212
- package/src/memory/user-profiler.ts +215 -215
- package/src/plugins/content-filter.ts +23 -23
- package/src/plugins/logger.ts +18 -18
- package/src/plugins/rate-limiter.ts +38 -38
- package/src/protocols/a2a/client.ts +132 -132
- package/src/protocols/a2a/index.ts +8 -8
- package/src/protocols/a2a/server.ts +333 -333
- package/src/protocols/a2a/types.ts +88 -88
- package/src/protocols/a2a/utils.ts +50 -50
- package/src/protocols/agui/client.ts +83 -83
- package/src/protocols/agui/index.ts +4 -4
- package/src/protocols/agui/server.ts +218 -218
- package/src/protocols/agui/types.ts +153 -153
- package/src/protocols/index.ts +2 -2
- package/src/protocols/mcp/agent-tools.ts +134 -134
- package/src/protocols/mcp/index.ts +8 -8
- package/src/protocols/mcp/server.ts +262 -262
- package/src/protocols/mcp/types.ts +69 -69
- package/src/providers/index.ts +632 -632
- package/src/publish/index.ts +376 -376
- package/src/scheduler/cron-engine.ts +191 -191
- package/src/scheduler/index.ts +2 -2
- package/src/schema/oad.ts +217 -217
- package/src/security/approval.ts +131 -131
- package/src/security/approvals.ts +143 -143
- package/src/security/elevated.ts +105 -105
- package/src/security/guardrails.ts +248 -248
- package/src/security/index.ts +9 -9
- package/src/security/keys.ts +87 -87
- package/src/security/secrets.ts +129 -129
- package/src/skills/builtin/index.ts +408 -408
- package/src/skills/marketplace.ts +113 -113
- package/src/skills/types.ts +42 -42
- package/src/studio/server.ts +31 -1
- package/src/studio/templates-data.ts +178 -178
- package/src/studio-ui/index.html +230 -10
- package/src/telemetry/index.ts +324 -324
- package/src/tools/builtin/browser.ts +299 -299
- package/src/tools/builtin/datetime.ts +41 -41
- package/src/tools/builtin/file.ts +107 -107
- package/src/tools/builtin/home-assistant.ts +116 -116
- package/src/tools/builtin/rl-tools.ts +243 -243
- package/src/tools/builtin/shell.ts +43 -43
- package/src/tools/builtin/vision.ts +64 -64
- package/src/tools/builtin/web-search.ts +126 -126
- package/src/tools/builtin/web.ts +35 -35
- package/src/tools/document-processor.ts +213 -213
- package/src/tools/image-generator.ts +150 -150
- package/src/tools/integrations/calendar.ts +73 -73
- package/src/tools/integrations/code-exec.ts +39 -39
- package/src/tools/integrations/csv-analyzer.ts +92 -92
- package/src/tools/integrations/database.ts +44 -44
- package/src/tools/integrations/email-send.ts +76 -76
- package/src/tools/integrations/git-tool.ts +42 -42
- package/src/tools/integrations/github-tool.ts +76 -76
- package/src/tools/integrations/image-gen.ts +56 -56
- package/src/tools/integrations/index.ts +92 -92
- package/src/tools/integrations/jira.ts +83 -83
- package/src/tools/integrations/notion.ts +71 -71
- package/src/tools/integrations/npm-tool.ts +48 -48
- package/src/tools/integrations/pdf-reader.ts +58 -58
- package/src/tools/integrations/slack.ts +65 -65
- package/src/tools/integrations/summarizer.ts +49 -49
- package/src/tools/integrations/translator.ts +48 -48
- package/src/tools/integrations/trello.ts +60 -60
- package/src/tools/integrations/vector-search.ts +42 -42
- package/src/tools/integrations/web-scraper.ts +47 -47
- package/src/tools/integrations/web-search.ts +58 -58
- package/src/tools/integrations/webhook.ts +38 -38
- package/src/tools/mcp-client.ts +131 -131
- package/src/tools/web-scraper.ts +179 -179
- package/src/tools/web-search.ts +180 -180
- package/src/ui/components.ts +127 -127
- package/srv-out.txt +1 -1
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -9
- package/test-agent/README.md +50 -50
- package/test-agent/agent.yaml +23 -23
- package/test-agent/docker-compose.yml +11 -11
- package/test-agent/oad.yaml +31 -31
- package/test-agent/package-lock.json +1492 -1492
- package/test-agent/package.json +17 -17
- package/test-agent/src/index.ts +24 -24
- package/test-agent/src/skills/echo.ts +15 -15
- package/test-agent/tsconfig.json +24 -24
- package/test-full.js +43 -43
- package/test-sidebar.js +22 -22
- package/test-studio3.js +75 -75
- package/test-studio4.js +41 -41
- package/tests/a2a-protocol.test.ts +285 -285
- package/tests/agui-protocol.test.ts +246 -246
- package/tests/api-server.test.ts +148 -148
- package/tests/approvals.test.ts +89 -89
- package/tests/audio.test.ts +40 -40
- package/tests/brain-seed-extended.test.ts +490 -490
- package/tests/brain-seed.test.ts +239 -239
- package/tests/browser.test.ts +179 -179
- package/tests/channels/discord.test.ts +79 -79
- package/tests/channels/email.test.ts +148 -148
- package/tests/channels/feishu.test.ts +123 -123
- package/tests/channels/telegram.test.ts +129 -129
- package/tests/channels/websocket.test.ts +53 -53
- package/tests/channels/wechat.test.ts +170 -170
- package/tests/channels-extra.test.ts +45 -45
- package/tests/chat-cli.test.ts +160 -160
- package/tests/cli.test.ts +46 -46
- package/tests/context-compressor.test.ts +172 -172
- package/tests/context-refs.test.ts +121 -121
- package/tests/cron-engine.test.ts +101 -101
- package/tests/daemon.test.ts +135 -135
- package/tests/deepbrain-wire.test.ts +234 -234
- package/tests/deploy-and-dag.test.ts +196 -196
- package/tests/doctor.test.ts +38 -38
- package/tests/document-processor.test.ts +69 -69
- package/tests/e2e-nocode.test.ts +442 -442
- package/tests/elevated.test.ts +69 -69
- package/tests/eval.test.ts +173 -173
- package/tests/gateway.test.ts +63 -63
- package/tests/guardrails.test.ts +177 -177
- package/tests/home-assistant.test.ts +40 -40
- package/tests/hooks.test.ts +79 -79
- package/tests/ide-bridge.test.ts +38 -38
- package/tests/image-generator.test.ts +84 -84
- package/tests/init-role.test.ts +124 -124
- package/tests/integrations.test.ts +249 -249
- package/tests/mcp-client.test.ts +92 -92
- package/tests/mcp-server.test.ts +178 -178
- package/tests/mcp-servers.test.ts +260 -260
- package/tests/node-network.test.ts +74 -74
- package/tests/plugin-a2a-enhanced.test.ts +230 -230
- package/tests/profiles.test.ts +61 -61
- package/tests/publish.test.ts +231 -231
- package/tests/rl-tools.test.ts +93 -93
- package/tests/sandbox-manager.test.ts +46 -46
- package/tests/scheduler.test.ts +200 -200
- package/tests/secrets.test.ts +107 -107
- package/tests/security-enhanced.test.ts +233 -233
- package/tests/settings-api.test.ts +148 -148
- package/tests/setup.test.ts +73 -73
- package/tests/subagent.test.ts +193 -193
- package/tests/telegram-discord.test.ts +60 -60
- package/tests/telemetry.test.ts +186 -186
- package/tests/user-profiler.test.ts +169 -169
- package/tests/v090-features.test.ts +254 -254
- package/tests/vision.test.ts +61 -61
- package/tests/voice-call.test.ts +47 -47
- package/tests/voice-enhanced.test.ts +169 -169
- package/tests/voice-interaction.test.ts +38 -38
- package/tests/web-search.test.ts +155 -155
- package/tests/workflow-graph.test.ts +279 -279
- package/tutorial/customer-service-agent/README.md +612 -612
- package/tutorial/customer-service-agent/SOUL.md +26 -26
- package/tutorial/customer-service-agent/agent.yaml +63 -63
- package/tutorial/customer-service-agent/package.json +19 -19
- package/tutorial/customer-service-agent/src/index.ts +69 -69
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
- package/tutorial/customer-service-agent/tsconfig.json +14 -14
package/src/tools/web-search.ts
CHANGED
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Web Search Engine Manager - v0.10.0
|
|
3
|
-
* Supports multiple search backends with automatic fallback.
|
|
4
|
-
* Default: DuckDuckGo (free, no API key required).
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface SearchResult {
|
|
8
|
-
title: string;
|
|
9
|
-
url: string;
|
|
10
|
-
snippet: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface SearchOptions {
|
|
14
|
-
maxResults?: number;
|
|
15
|
-
engine?: SearchEngine;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type SearchEngine = 'duckduckgo' | 'brave' | 'searxng' | 'google';
|
|
19
|
-
|
|
20
|
-
export interface SearchEngineConfig {
|
|
21
|
-
enabled: boolean;
|
|
22
|
-
apiKey?: string;
|
|
23
|
-
baseUrl?: string; // For SearXNG self-hosted
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface WebSearchConfig {
|
|
27
|
-
defaultEngine: SearchEngine;
|
|
28
|
-
enabled: boolean;
|
|
29
|
-
engines: Partial<Record<SearchEngine, SearchEngineConfig>>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const DEFAULT_SEARCH_CONFIG: WebSearchConfig = {
|
|
33
|
-
defaultEngine: 'duckduckgo',
|
|
34
|
-
enabled: true,
|
|
35
|
-
engines: {
|
|
36
|
-
duckduckgo: { enabled: true },
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Parse DuckDuckGo HTML search results.
|
|
42
|
-
*/
|
|
43
|
-
export function parseDuckDuckGoHTML(html: string): SearchResult[] {
|
|
44
|
-
const results: SearchResult[] = [];
|
|
45
|
-
// Match result blocks: <a class="result__a" href="...">title</a> ... <a class="result__snippet">snippet</a>
|
|
46
|
-
const resultBlocks = html.split(/class="result__body"/);
|
|
47
|
-
|
|
48
|
-
for (let i = 1; i < resultBlocks.length && results.length < 10; i++) {
|
|
49
|
-
const block = resultBlocks[i];
|
|
50
|
-
|
|
51
|
-
// Extract URL and title from result__a
|
|
52
|
-
const linkMatch = block.match(/class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/);
|
|
53
|
-
if (!linkMatch) continue;
|
|
54
|
-
|
|
55
|
-
let url = linkMatch[1];
|
|
56
|
-
const title = stripHTML(linkMatch[2]).trim();
|
|
57
|
-
|
|
58
|
-
// DuckDuckGo wraps URLs in redirect, extract actual URL
|
|
59
|
-
const uddgMatch = url.match(/[?&]uddg=([^&]+)/);
|
|
60
|
-
if (uddgMatch) {
|
|
61
|
-
url = decodeURIComponent(uddgMatch[1]);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Extract snippet
|
|
65
|
-
const snippetMatch = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/);
|
|
66
|
-
const snippet = snippetMatch ? stripHTML(snippetMatch[1]).trim() : '';
|
|
67
|
-
|
|
68
|
-
if (title && url) {
|
|
69
|
-
results.push({ title, url, snippet });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return results;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Search using DuckDuckGo HTML interface (no API key needed).
|
|
78
|
-
*/
|
|
79
|
-
export async function searchDuckDuckGo(query: string, maxResults = 5): Promise<SearchResult[]> {
|
|
80
|
-
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
81
|
-
const response = await fetch(url, {
|
|
82
|
-
headers: {
|
|
83
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
84
|
-
},
|
|
85
|
-
signal: AbortSignal.timeout(15000),
|
|
86
|
-
});
|
|
87
|
-
const html = await response.text();
|
|
88
|
-
return parseDuckDuckGoHTML(html).slice(0, maxResults);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Search using Brave Search API.
|
|
93
|
-
*/
|
|
94
|
-
export async function searchBrave(query: string, apiKey: string, maxResults = 5): Promise<SearchResult[]> {
|
|
95
|
-
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}`;
|
|
96
|
-
const response = await fetch(url, {
|
|
97
|
-
headers: { 'X-Subscription-Token': apiKey, Accept: 'application/json' },
|
|
98
|
-
signal: AbortSignal.timeout(15000),
|
|
99
|
-
});
|
|
100
|
-
const data = await response.json() as any;
|
|
101
|
-
return (data.web?.results || []).slice(0, maxResults).map((r: any) => ({
|
|
102
|
-
title: r.title || '',
|
|
103
|
-
url: r.url || '',
|
|
104
|
-
snippet: r.description || '',
|
|
105
|
-
}));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Search using SearXNG instance.
|
|
110
|
-
*/
|
|
111
|
-
export async function searchSearXNG(query: string, baseUrl: string, maxResults = 5): Promise<SearchResult[]> {
|
|
112
|
-
const url = `${baseUrl.replace(/\/$/, '')}/search?q=${encodeURIComponent(query)}&format=json`;
|
|
113
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
114
|
-
const data = await response.json() as any;
|
|
115
|
-
return (data.results || []).slice(0, maxResults).map((r: any) => ({
|
|
116
|
-
title: r.title || '',
|
|
117
|
-
url: r.url || '',
|
|
118
|
-
snippet: r.content || '',
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Search using Google Custom Search API.
|
|
124
|
-
*/
|
|
125
|
-
export async function searchGoogle(query: string, apiKey: string, maxResults = 5): Promise<SearchResult[]> {
|
|
126
|
-
// apiKey format: "key:cx" (API key and Custom Search Engine ID)
|
|
127
|
-
const [key, cx] = apiKey.split(':');
|
|
128
|
-
const url = `https://www.googleapis.com/customsearch/v1?q=${encodeURIComponent(query)}&key=${key}&cx=${cx}&num=${maxResults}`;
|
|
129
|
-
const response = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
130
|
-
const data = await response.json() as any;
|
|
131
|
-
return (data.items || []).slice(0, maxResults).map((r: any) => ({
|
|
132
|
-
title: r.title || '',
|
|
133
|
-
url: r.link || '',
|
|
134
|
-
snippet: r.snippet || '',
|
|
135
|
-
}));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Unified search function with fallback.
|
|
140
|
-
*/
|
|
141
|
-
export async function webSearch(query: string, config?: WebSearchConfig, options?: SearchOptions): Promise<SearchResult[]> {
|
|
142
|
-
const cfg = config || DEFAULT_SEARCH_CONFIG;
|
|
143
|
-
if (!cfg.enabled) return [];
|
|
144
|
-
|
|
145
|
-
const maxResults = options?.maxResults || 5;
|
|
146
|
-
const engine = options?.engine || cfg.defaultEngine;
|
|
147
|
-
|
|
148
|
-
// Try requested engine first, then fallback chain
|
|
149
|
-
const fallbackOrder: SearchEngine[] = [engine, 'duckduckgo', 'brave', 'searxng', 'google']
|
|
150
|
-
.filter((e, i, arr) => arr.indexOf(e) === i) as SearchEngine[];
|
|
151
|
-
|
|
152
|
-
for (const eng of fallbackOrder) {
|
|
153
|
-
const engCfg = cfg.engines[eng];
|
|
154
|
-
if (engCfg && !engCfg.enabled) continue;
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
switch (eng) {
|
|
158
|
-
case 'duckduckgo':
|
|
159
|
-
return await searchDuckDuckGo(query, maxResults);
|
|
160
|
-
case 'brave':
|
|
161
|
-
if (engCfg?.apiKey) return await searchBrave(query, engCfg.apiKey, maxResults);
|
|
162
|
-
continue;
|
|
163
|
-
case 'searxng':
|
|
164
|
-
if (engCfg?.baseUrl) return await searchSearXNG(query, engCfg.baseUrl, maxResults);
|
|
165
|
-
continue;
|
|
166
|
-
case 'google':
|
|
167
|
-
if (engCfg?.apiKey) return await searchGoogle(query, engCfg.apiKey, maxResults);
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
} catch {
|
|
171
|
-
continue; // Fallback to next engine
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return [];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function stripHTML(html: string): string {
|
|
179
|
-
return html.replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ');
|
|
180
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Web Search Engine Manager - v0.10.0
|
|
3
|
+
* Supports multiple search backends with automatic fallback.
|
|
4
|
+
* Default: DuckDuckGo (free, no API key required).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface SearchResult {
|
|
8
|
+
title: string;
|
|
9
|
+
url: string;
|
|
10
|
+
snippet: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SearchOptions {
|
|
14
|
+
maxResults?: number;
|
|
15
|
+
engine?: SearchEngine;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type SearchEngine = 'duckduckgo' | 'brave' | 'searxng' | 'google';
|
|
19
|
+
|
|
20
|
+
export interface SearchEngineConfig {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
baseUrl?: string; // For SearXNG self-hosted
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WebSearchConfig {
|
|
27
|
+
defaultEngine: SearchEngine;
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
engines: Partial<Record<SearchEngine, SearchEngineConfig>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_SEARCH_CONFIG: WebSearchConfig = {
|
|
33
|
+
defaultEngine: 'duckduckgo',
|
|
34
|
+
enabled: true,
|
|
35
|
+
engines: {
|
|
36
|
+
duckduckgo: { enabled: true },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse DuckDuckGo HTML search results.
|
|
42
|
+
*/
|
|
43
|
+
export function parseDuckDuckGoHTML(html: string): SearchResult[] {
|
|
44
|
+
const results: SearchResult[] = [];
|
|
45
|
+
// Match result blocks: <a class="result__a" href="...">title</a> ... <a class="result__snippet">snippet</a>
|
|
46
|
+
const resultBlocks = html.split(/class="result__body"/);
|
|
47
|
+
|
|
48
|
+
for (let i = 1; i < resultBlocks.length && results.length < 10; i++) {
|
|
49
|
+
const block = resultBlocks[i];
|
|
50
|
+
|
|
51
|
+
// Extract URL and title from result__a
|
|
52
|
+
const linkMatch = block.match(/class="result__a"[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/);
|
|
53
|
+
if (!linkMatch) continue;
|
|
54
|
+
|
|
55
|
+
let url = linkMatch[1];
|
|
56
|
+
const title = stripHTML(linkMatch[2]).trim();
|
|
57
|
+
|
|
58
|
+
// DuckDuckGo wraps URLs in redirect, extract actual URL
|
|
59
|
+
const uddgMatch = url.match(/[?&]uddg=([^&]+)/);
|
|
60
|
+
if (uddgMatch) {
|
|
61
|
+
url = decodeURIComponent(uddgMatch[1]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Extract snippet
|
|
65
|
+
const snippetMatch = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/);
|
|
66
|
+
const snippet = snippetMatch ? stripHTML(snippetMatch[1]).trim() : '';
|
|
67
|
+
|
|
68
|
+
if (title && url) {
|
|
69
|
+
results.push({ title, url, snippet });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return results;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Search using DuckDuckGo HTML interface (no API key needed).
|
|
78
|
+
*/
|
|
79
|
+
export async function searchDuckDuckGo(query: string, maxResults = 5): Promise<SearchResult[]> {
|
|
80
|
+
const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
headers: {
|
|
83
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
84
|
+
},
|
|
85
|
+
signal: AbortSignal.timeout(15000),
|
|
86
|
+
});
|
|
87
|
+
const html = await response.text();
|
|
88
|
+
return parseDuckDuckGoHTML(html).slice(0, maxResults);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Search using Brave Search API.
|
|
93
|
+
*/
|
|
94
|
+
export async function searchBrave(query: string, apiKey: string, maxResults = 5): Promise<SearchResult[]> {
|
|
95
|
+
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${maxResults}`;
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
headers: { 'X-Subscription-Token': apiKey, Accept: 'application/json' },
|
|
98
|
+
signal: AbortSignal.timeout(15000),
|
|
99
|
+
});
|
|
100
|
+
const data = await response.json() as any;
|
|
101
|
+
return (data.web?.results || []).slice(0, maxResults).map((r: any) => ({
|
|
102
|
+
title: r.title || '',
|
|
103
|
+
url: r.url || '',
|
|
104
|
+
snippet: r.description || '',
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Search using SearXNG instance.
|
|
110
|
+
*/
|
|
111
|
+
export async function searchSearXNG(query: string, baseUrl: string, maxResults = 5): Promise<SearchResult[]> {
|
|
112
|
+
const url = `${baseUrl.replace(/\/$/, '')}/search?q=${encodeURIComponent(query)}&format=json`;
|
|
113
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
114
|
+
const data = await response.json() as any;
|
|
115
|
+
return (data.results || []).slice(0, maxResults).map((r: any) => ({
|
|
116
|
+
title: r.title || '',
|
|
117
|
+
url: r.url || '',
|
|
118
|
+
snippet: r.content || '',
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Search using Google Custom Search API.
|
|
124
|
+
*/
|
|
125
|
+
export async function searchGoogle(query: string, apiKey: string, maxResults = 5): Promise<SearchResult[]> {
|
|
126
|
+
// apiKey format: "key:cx" (API key and Custom Search Engine ID)
|
|
127
|
+
const [key, cx] = apiKey.split(':');
|
|
128
|
+
const url = `https://www.googleapis.com/customsearch/v1?q=${encodeURIComponent(query)}&key=${key}&cx=${cx}&num=${maxResults}`;
|
|
129
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(15000) });
|
|
130
|
+
const data = await response.json() as any;
|
|
131
|
+
return (data.items || []).slice(0, maxResults).map((r: any) => ({
|
|
132
|
+
title: r.title || '',
|
|
133
|
+
url: r.link || '',
|
|
134
|
+
snippet: r.snippet || '',
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Unified search function with fallback.
|
|
140
|
+
*/
|
|
141
|
+
export async function webSearch(query: string, config?: WebSearchConfig, options?: SearchOptions): Promise<SearchResult[]> {
|
|
142
|
+
const cfg = config || DEFAULT_SEARCH_CONFIG;
|
|
143
|
+
if (!cfg.enabled) return [];
|
|
144
|
+
|
|
145
|
+
const maxResults = options?.maxResults || 5;
|
|
146
|
+
const engine = options?.engine || cfg.defaultEngine;
|
|
147
|
+
|
|
148
|
+
// Try requested engine first, then fallback chain
|
|
149
|
+
const fallbackOrder: SearchEngine[] = [engine, 'duckduckgo', 'brave', 'searxng', 'google']
|
|
150
|
+
.filter((e, i, arr) => arr.indexOf(e) === i) as SearchEngine[];
|
|
151
|
+
|
|
152
|
+
for (const eng of fallbackOrder) {
|
|
153
|
+
const engCfg = cfg.engines[eng];
|
|
154
|
+
if (engCfg && !engCfg.enabled) continue;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
switch (eng) {
|
|
158
|
+
case 'duckduckgo':
|
|
159
|
+
return await searchDuckDuckGo(query, maxResults);
|
|
160
|
+
case 'brave':
|
|
161
|
+
if (engCfg?.apiKey) return await searchBrave(query, engCfg.apiKey, maxResults);
|
|
162
|
+
continue;
|
|
163
|
+
case 'searxng':
|
|
164
|
+
if (engCfg?.baseUrl) return await searchSearXNG(query, engCfg.baseUrl, maxResults);
|
|
165
|
+
continue;
|
|
166
|
+
case 'google':
|
|
167
|
+
if (engCfg?.apiKey) return await searchGoogle(query, engCfg.apiKey, maxResults);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
continue; // Fallback to next engine
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stripHTML(html: string): string {
|
|
179
|
+
return html.replace(/<[^>]+>/g, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ');
|
|
180
|
+
}
|
package/src/ui/components.ts
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chat Widget — self-contained HTML+CSS+JS for embedding
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
export interface ChatWidgetConfig {
|
|
6
|
-
endpoint: string;
|
|
7
|
-
theme?: 'dark' | 'light';
|
|
8
|
-
title?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function generateChatWidget(config: ChatWidgetConfig): string {
|
|
12
|
-
const { endpoint, theme = 'dark', title = 'OPC Chat' } = config;
|
|
13
|
-
const isDark = theme === 'dark';
|
|
14
|
-
const bg = isDark ? '#1a1a2e' : '#ffffff';
|
|
15
|
-
const fg = isDark ? '#e0e0e0' : '#1a1a2e';
|
|
16
|
-
const inputBg = isDark ? '#16213e' : '#f0f0f0';
|
|
17
|
-
const msgUser = isDark ? '#0f3460' : '#e3f2fd';
|
|
18
|
-
const msgBot = isDark ? '#1a1a2e' : '#f5f5f5';
|
|
19
|
-
const accent = '#00d2ff';
|
|
20
|
-
|
|
21
|
-
return `<!DOCTYPE html>
|
|
22
|
-
<html lang="en">
|
|
23
|
-
<head>
|
|
24
|
-
<meta charset="UTF-8">
|
|
25
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
26
|
-
<title>${title}</title>
|
|
27
|
-
<style>
|
|
28
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
29
|
-
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: ${bg}; color: ${fg}; height: 100vh; display: flex; flex-direction: column; }
|
|
30
|
-
.chat-header { padding: 16px 20px; background: ${isDark ? '#16213e' : '#fafafa'}; border-bottom: 1px solid ${isDark ? '#2a2a4a' : '#e0e0e0'}; font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }
|
|
31
|
-
.chat-header::before { content: '💬'; }
|
|
32
|
-
.chat-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
|
|
33
|
-
.msg { max-width: 80%; padding: 10px 14px; border-radius: 12px; line-height: 1.5; font-size: 14px; white-space: pre-wrap; word-wrap: break-word; }
|
|
34
|
-
.msg.user { align-self: flex-end; background: ${msgUser}; border-bottom-right-radius: 4px; }
|
|
35
|
-
.msg.assistant { align-self: flex-start; background: ${msgBot}; border: 1px solid ${isDark ? '#2a2a4a' : '#e0e0e0'}; border-bottom-left-radius: 4px; }
|
|
36
|
-
.msg.streaming::after { content: '▊'; animation: blink 0.7s infinite; }
|
|
37
|
-
@keyframes blink { 50% { opacity: 0; } }
|
|
38
|
-
.chat-input { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid ${isDark ? '#2a2a4a' : '#e0e0e0'}; background: ${isDark ? '#16213e' : '#fafafa'}; }
|
|
39
|
-
.chat-input textarea { flex: 1; resize: none; border: 1px solid ${isDark ? '#2a2a4a' : '#ccc'}; border-radius: 8px; padding: 10px; font-size: 14px; background: ${inputBg}; color: ${fg}; outline: none; font-family: inherit; min-height: 42px; max-height: 120px; }
|
|
40
|
-
.chat-input textarea:focus { border-color: ${accent}; }
|
|
41
|
-
.chat-input button { background: ${accent}; color: #000; border: none; border-radius: 8px; padding: 0 20px; cursor: pointer; font-weight: 600; font-size: 14px; }
|
|
42
|
-
.chat-input button:hover { opacity: 0.85; }
|
|
43
|
-
.chat-input button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
44
|
-
</style>
|
|
45
|
-
</head>
|
|
46
|
-
<body>
|
|
47
|
-
<div class="chat-header">${title}</div>
|
|
48
|
-
<div class="chat-messages" id="messages"></div>
|
|
49
|
-
<div class="chat-input">
|
|
50
|
-
<textarea id="input" rows="1" placeholder="Type a message..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage()}"></textarea>
|
|
51
|
-
<button id="sendBtn" onclick="sendMessage()">Send</button>
|
|
52
|
-
</div>
|
|
53
|
-
<script>
|
|
54
|
-
const ENDPOINT = ${JSON.stringify(endpoint)};
|
|
55
|
-
const messages = [];
|
|
56
|
-
const $msgs = document.getElementById('messages');
|
|
57
|
-
const $input = document.getElementById('input');
|
|
58
|
-
const $btn = document.getElementById('sendBtn');
|
|
59
|
-
|
|
60
|
-
function addMsg(role, text) {
|
|
61
|
-
const div = document.createElement('div');
|
|
62
|
-
div.className = 'msg ' + role;
|
|
63
|
-
div.textContent = text;
|
|
64
|
-
$msgs.appendChild(div);
|
|
65
|
-
$msgs.scrollTop = $msgs.scrollHeight;
|
|
66
|
-
return div;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function sendMessage() {
|
|
70
|
-
const text = $input.value.trim();
|
|
71
|
-
if (!text) return;
|
|
72
|
-
$input.value = '';
|
|
73
|
-
$btn.disabled = true;
|
|
74
|
-
messages.push({ role: 'user', content: text });
|
|
75
|
-
addMsg('user', text);
|
|
76
|
-
|
|
77
|
-
const div = addMsg('assistant', '');
|
|
78
|
-
div.classList.add('streaming');
|
|
79
|
-
let full = '';
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const res = await fetch(ENDPOINT, {
|
|
83
|
-
method: 'POST',
|
|
84
|
-
headers: { 'Content-Type': 'application/json' },
|
|
85
|
-
body: JSON.stringify({ messages }),
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
if (res.headers.get('content-type')?.includes('text/event-stream')) {
|
|
89
|
-
const reader = res.body.getReader();
|
|
90
|
-
const decoder = new TextDecoder();
|
|
91
|
-
let buf = '';
|
|
92
|
-
while (true) {
|
|
93
|
-
const { done, value } = await reader.read();
|
|
94
|
-
if (done) break;
|
|
95
|
-
buf += decoder.decode(value, { stream: true });
|
|
96
|
-
const lines = buf.split('\\n');
|
|
97
|
-
buf = lines.pop() || '';
|
|
98
|
-
for (const line of lines) {
|
|
99
|
-
if (line.startsWith('data: ')) {
|
|
100
|
-
const data = line.slice(6);
|
|
101
|
-
if (data === '[DONE]') break;
|
|
102
|
-
try { const j = JSON.parse(data); full += j.content || j.delta || ''; div.textContent = full; } catch {}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
$msgs.scrollTop = $msgs.scrollHeight;
|
|
106
|
-
}
|
|
107
|
-
} else {
|
|
108
|
-
const data = await res.json();
|
|
109
|
-
full = data.content || data.message || JSON.stringify(data);
|
|
110
|
-
div.textContent = full;
|
|
111
|
-
}
|
|
112
|
-
} catch (e) {
|
|
113
|
-
full = 'Error: ' + e.message;
|
|
114
|
-
div.textContent = full;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
div.classList.remove('streaming');
|
|
118
|
-
messages.push({ role: 'assistant', content: full });
|
|
119
|
-
$btn.disabled = false;
|
|
120
|
-
$input.focus();
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
$input.focus();
|
|
124
|
-
</script>
|
|
125
|
-
</body>
|
|
126
|
-
</html>`;
|
|
127
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Chat Widget — self-contained HTML+CSS+JS for embedding
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ChatWidgetConfig {
|
|
6
|
+
endpoint: string;
|
|
7
|
+
theme?: 'dark' | 'light';
|
|
8
|
+
title?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function generateChatWidget(config: ChatWidgetConfig): string {
|
|
12
|
+
const { endpoint, theme = 'dark', title = 'OPC Chat' } = config;
|
|
13
|
+
const isDark = theme === 'dark';
|
|
14
|
+
const bg = isDark ? '#1a1a2e' : '#ffffff';
|
|
15
|
+
const fg = isDark ? '#e0e0e0' : '#1a1a2e';
|
|
16
|
+
const inputBg = isDark ? '#16213e' : '#f0f0f0';
|
|
17
|
+
const msgUser = isDark ? '#0f3460' : '#e3f2fd';
|
|
18
|
+
const msgBot = isDark ? '#1a1a2e' : '#f5f5f5';
|
|
19
|
+
const accent = '#00d2ff';
|
|
20
|
+
|
|
21
|
+
return `<!DOCTYPE html>
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="UTF-8">
|
|
25
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
26
|
+
<title>${title}</title>
|
|
27
|
+
<style>
|
|
28
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
29
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: ${bg}; color: ${fg}; height: 100vh; display: flex; flex-direction: column; }
|
|
30
|
+
.chat-header { padding: 16px 20px; background: ${isDark ? '#16213e' : '#fafafa'}; border-bottom: 1px solid ${isDark ? '#2a2a4a' : '#e0e0e0'}; font-weight: 600; font-size: 16px; display: flex; align-items: center; gap: 8px; }
|
|
31
|
+
.chat-header::before { content: '💬'; }
|
|
32
|
+
.chat-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
|
|
33
|
+
.msg { max-width: 80%; padding: 10px 14px; border-radius: 12px; line-height: 1.5; font-size: 14px; white-space: pre-wrap; word-wrap: break-word; }
|
|
34
|
+
.msg.user { align-self: flex-end; background: ${msgUser}; border-bottom-right-radius: 4px; }
|
|
35
|
+
.msg.assistant { align-self: flex-start; background: ${msgBot}; border: 1px solid ${isDark ? '#2a2a4a' : '#e0e0e0'}; border-bottom-left-radius: 4px; }
|
|
36
|
+
.msg.streaming::after { content: '▊'; animation: blink 0.7s infinite; }
|
|
37
|
+
@keyframes blink { 50% { opacity: 0; } }
|
|
38
|
+
.chat-input { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid ${isDark ? '#2a2a4a' : '#e0e0e0'}; background: ${isDark ? '#16213e' : '#fafafa'}; }
|
|
39
|
+
.chat-input textarea { flex: 1; resize: none; border: 1px solid ${isDark ? '#2a2a4a' : '#ccc'}; border-radius: 8px; padding: 10px; font-size: 14px; background: ${inputBg}; color: ${fg}; outline: none; font-family: inherit; min-height: 42px; max-height: 120px; }
|
|
40
|
+
.chat-input textarea:focus { border-color: ${accent}; }
|
|
41
|
+
.chat-input button { background: ${accent}; color: #000; border: none; border-radius: 8px; padding: 0 20px; cursor: pointer; font-weight: 600; font-size: 14px; }
|
|
42
|
+
.chat-input button:hover { opacity: 0.85; }
|
|
43
|
+
.chat-input button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
44
|
+
</style>
|
|
45
|
+
</head>
|
|
46
|
+
<body>
|
|
47
|
+
<div class="chat-header">${title}</div>
|
|
48
|
+
<div class="chat-messages" id="messages"></div>
|
|
49
|
+
<div class="chat-input">
|
|
50
|
+
<textarea id="input" rows="1" placeholder="Type a message..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMessage()}"></textarea>
|
|
51
|
+
<button id="sendBtn" onclick="sendMessage()">Send</button>
|
|
52
|
+
</div>
|
|
53
|
+
<script>
|
|
54
|
+
const ENDPOINT = ${JSON.stringify(endpoint)};
|
|
55
|
+
const messages = [];
|
|
56
|
+
const $msgs = document.getElementById('messages');
|
|
57
|
+
const $input = document.getElementById('input');
|
|
58
|
+
const $btn = document.getElementById('sendBtn');
|
|
59
|
+
|
|
60
|
+
function addMsg(role, text) {
|
|
61
|
+
const div = document.createElement('div');
|
|
62
|
+
div.className = 'msg ' + role;
|
|
63
|
+
div.textContent = text;
|
|
64
|
+
$msgs.appendChild(div);
|
|
65
|
+
$msgs.scrollTop = $msgs.scrollHeight;
|
|
66
|
+
return div;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function sendMessage() {
|
|
70
|
+
const text = $input.value.trim();
|
|
71
|
+
if (!text) return;
|
|
72
|
+
$input.value = '';
|
|
73
|
+
$btn.disabled = true;
|
|
74
|
+
messages.push({ role: 'user', content: text });
|
|
75
|
+
addMsg('user', text);
|
|
76
|
+
|
|
77
|
+
const div = addMsg('assistant', '');
|
|
78
|
+
div.classList.add('streaming');
|
|
79
|
+
let full = '';
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(ENDPOINT, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ messages }),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (res.headers.get('content-type')?.includes('text/event-stream')) {
|
|
89
|
+
const reader = res.body.getReader();
|
|
90
|
+
const decoder = new TextDecoder();
|
|
91
|
+
let buf = '';
|
|
92
|
+
while (true) {
|
|
93
|
+
const { done, value } = await reader.read();
|
|
94
|
+
if (done) break;
|
|
95
|
+
buf += decoder.decode(value, { stream: true });
|
|
96
|
+
const lines = buf.split('\\n');
|
|
97
|
+
buf = lines.pop() || '';
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
if (line.startsWith('data: ')) {
|
|
100
|
+
const data = line.slice(6);
|
|
101
|
+
if (data === '[DONE]') break;
|
|
102
|
+
try { const j = JSON.parse(data); full += j.content || j.delta || ''; div.textContent = full; } catch {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
$msgs.scrollTop = $msgs.scrollHeight;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
const data = await res.json();
|
|
109
|
+
full = data.content || data.message || JSON.stringify(data);
|
|
110
|
+
div.textContent = full;
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
full = 'Error: ' + e.message;
|
|
114
|
+
div.textContent = full;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
div.classList.remove('streaming');
|
|
118
|
+
messages.push({ role: 'assistant', content: full });
|
|
119
|
+
$btn.disabled = false;
|
|
120
|
+
$input.focus();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
$input.focus();
|
|
124
|
+
</script>
|
|
125
|
+
</body>
|
|
126
|
+
</html>`;
|
|
127
|
+
}
|
package/srv-out.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Studio test server ready: http://localhost:4449
|
|
1
|
+
Studio test server ready: http://localhost:4449
|