opc-agent 3.0.1 → 4.0.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/README.md +404 -74
- package/README.zh-CN.md +82 -0
- package/dist/channels/dingtalk.d.ts +17 -0
- package/dist/channels/dingtalk.js +38 -0
- package/dist/channels/googlechat.d.ts +14 -0
- package/dist/channels/googlechat.js +37 -0
- package/dist/channels/imessage.d.ts +13 -0
- package/dist/channels/imessage.js +28 -0
- package/dist/channels/irc.d.ts +20 -0
- package/dist/channels/irc.js +71 -0
- package/dist/channels/line.d.ts +14 -0
- package/dist/channels/line.js +28 -0
- package/dist/channels/matrix.d.ts +15 -0
- package/dist/channels/matrix.js +28 -0
- package/dist/channels/mattermost.d.ts +18 -0
- package/dist/channels/mattermost.js +49 -0
- package/dist/channels/msteams.d.ts +14 -0
- package/dist/channels/msteams.js +28 -0
- package/dist/channels/nostr.d.ts +14 -0
- package/dist/channels/nostr.js +28 -0
- package/dist/channels/qq.d.ts +15 -0
- package/dist/channels/qq.js +28 -0
- package/dist/channels/signal.d.ts +14 -0
- package/dist/channels/signal.js +28 -0
- package/dist/channels/sms.d.ts +15 -0
- package/dist/channels/sms.js +28 -0
- package/dist/channels/twitch.d.ts +17 -0
- package/dist/channels/twitch.js +59 -0
- package/dist/channels/voice-call.d.ts +27 -0
- package/dist/channels/voice-call.js +82 -0
- package/dist/channels/whatsapp.d.ts +14 -0
- package/dist/channels/whatsapp.js +28 -0
- package/dist/cli/chat.d.ts +2 -0
- package/dist/cli/chat.js +134 -0
- package/dist/cli/setup.d.ts +4 -0
- package/dist/cli/setup.js +303 -0
- package/dist/cli.js +142 -6
- package/dist/core/api-server.d.ts +25 -0
- package/dist/core/api-server.js +286 -0
- package/dist/core/audio.d.ts +50 -0
- package/dist/core/audio.js +68 -0
- package/dist/core/context-discovery.d.ts +16 -0
- package/dist/core/context-discovery.js +107 -0
- package/dist/core/context-refs.d.ts +29 -0
- package/dist/core/context-refs.js +162 -0
- package/dist/core/gateway.d.ts +53 -0
- package/dist/core/gateway.js +80 -0
- package/dist/core/heartbeat.d.ts +19 -0
- package/dist/core/heartbeat.js +50 -0
- package/dist/core/hooks.d.ts +28 -0
- package/dist/core/hooks.js +82 -0
- package/dist/core/ide-bridge.d.ts +53 -0
- package/dist/core/ide-bridge.js +97 -0
- package/dist/core/node-network.d.ts +23 -0
- package/dist/core/node-network.js +77 -0
- package/dist/core/profiles.d.ts +27 -0
- package/dist/core/profiles.js +131 -0
- package/dist/core/sandbox.d.ts +25 -0
- package/dist/core/sandbox.js +84 -1
- package/dist/core/session-manager.d.ts +33 -0
- package/dist/core/session-manager.js +157 -0
- package/dist/core/vision.d.ts +45 -0
- package/dist/core/vision.js +177 -0
- package/dist/hub/brain-seed.d.ts +14 -0
- package/dist/hub/brain-seed.js +77 -0
- package/dist/hub/client.d.ts +25 -0
- package/dist/hub/client.js +44 -0
- package/dist/index.d.ts +66 -1
- package/dist/index.js +95 -3
- package/dist/memory/context-compressor.d.ts +43 -0
- package/dist/memory/context-compressor.js +167 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +5 -1
- package/dist/memory/user-profiler.d.ts +50 -0
- package/dist/memory/user-profiler.js +201 -0
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +54 -1
- package/dist/scheduler/cron-engine.d.ts +41 -0
- package/dist/scheduler/cron-engine.js +200 -0
- package/dist/scheduler/index.d.ts +3 -0
- package/dist/scheduler/index.js +7 -0
- package/dist/schema/oad.d.ts +12 -12
- package/dist/security/approvals.d.ts +53 -0
- package/dist/security/approvals.js +115 -0
- package/dist/security/elevated.d.ts +41 -0
- package/dist/security/elevated.js +89 -0
- package/dist/security/index.d.ts +6 -0
- package/dist/security/index.js +7 -1
- package/dist/security/secrets.d.ts +34 -0
- package/dist/security/secrets.js +115 -0
- package/dist/skills/builtin/index.d.ts +6 -0
- package/dist/skills/builtin/index.js +402 -0
- package/dist/skills/marketplace.d.ts +30 -0
- package/dist/skills/marketplace.js +142 -0
- package/dist/skills/types.d.ts +34 -0
- package/dist/skills/types.js +16 -0
- package/dist/studio/server.d.ts +25 -0
- package/dist/studio/server.js +780 -0
- package/dist/studio/templates-data.d.ts +21 -0
- package/dist/studio/templates-data.js +148 -0
- package/dist/studio-ui/index.html +2502 -1073
- package/dist/tools/builtin/browser.d.ts +47 -0
- package/dist/tools/builtin/browser.js +284 -0
- package/dist/tools/builtin/home-assistant.d.ts +12 -0
- package/dist/tools/builtin/home-assistant.js +126 -0
- package/dist/tools/builtin/index.d.ts +7 -1
- package/dist/tools/builtin/index.js +23 -2
- package/dist/tools/builtin/rl-tools.d.ts +13 -0
- package/dist/tools/builtin/rl-tools.js +228 -0
- package/dist/tools/builtin/vision.d.ts +6 -0
- package/dist/tools/builtin/vision.js +61 -0
- package/dist/tools/builtin/web-search.d.ts +9 -0
- package/dist/tools/builtin/web-search.js +150 -0
- package/dist/tools/document-processor.d.ts +39 -0
- package/dist/tools/document-processor.js +188 -0
- package/dist/tools/image-generator.d.ts +42 -0
- package/dist/tools/image-generator.js +136 -0
- package/dist/tools/web-scraper.d.ts +20 -0
- package/dist/tools/web-scraper.js +148 -0
- package/dist/tools/web-search.d.ts +51 -0
- package/dist/tools/web-search.js +152 -0
- package/install.ps1 +154 -0
- package/install.sh +164 -0
- package/package.json +63 -52
- package/src/channels/dingtalk.ts +46 -0
- package/src/channels/googlechat.ts +42 -0
- package/src/channels/imessage.ts +32 -0
- package/src/channels/irc.ts +82 -0
- package/src/channels/line.ts +33 -0
- package/src/channels/matrix.ts +34 -0
- package/src/channels/mattermost.ts +57 -0
- package/src/channels/msteams.ts +33 -0
- package/src/channels/nostr.ts +33 -0
- package/src/channels/qq.ts +34 -0
- package/src/channels/signal.ts +33 -0
- package/src/channels/sms.ts +34 -0
- package/src/channels/twitch.ts +65 -0
- package/src/channels/voice-call.ts +100 -0
- package/src/channels/whatsapp.ts +33 -0
- package/src/cli/chat.ts +99 -0
- package/src/cli/setup.ts +314 -0
- package/src/cli.ts +148 -6
- package/src/core/api-server.ts +277 -0
- package/src/core/audio.ts +98 -0
- package/src/core/context-discovery.ts +85 -0
- package/src/core/context-refs.ts +140 -0
- package/src/core/gateway.ts +106 -0
- package/src/core/heartbeat.ts +51 -0
- package/src/core/hooks.ts +105 -0
- package/src/core/ide-bridge.ts +133 -0
- package/src/core/node-network.ts +86 -0
- package/src/core/profiles.ts +122 -0
- package/src/core/sandbox.ts +100 -0
- package/src/core/session-manager.ts +137 -0
- package/src/core/vision.ts +180 -0
- package/src/hub/brain-seed.ts +54 -0
- package/src/hub/client.ts +60 -0
- package/src/index.ts +86 -1
- package/src/memory/context-compressor.ts +189 -0
- package/src/memory/index.ts +4 -0
- package/src/memory/user-profiler.ts +215 -0
- package/src/providers/index.ts +64 -1
- package/src/scheduler/cron-engine.ts +191 -0
- package/src/scheduler/index.ts +2 -0
- package/src/security/approvals.ts +143 -0
- package/src/security/elevated.ts +105 -0
- package/src/security/index.ts +6 -0
- package/src/security/secrets.ts +129 -0
- package/src/skills/builtin/index.ts +408 -0
- package/src/skills/marketplace.ts +113 -0
- package/src/skills/types.ts +42 -0
- package/src/studio/server.ts +1591 -791
- package/src/studio/templates-data.ts +178 -0
- package/src/studio-ui/index.html +2502 -1073
- package/src/tools/builtin/browser.ts +299 -0
- package/src/tools/builtin/home-assistant.ts +116 -0
- package/src/tools/builtin/index.ts +37 -28
- package/src/tools/builtin/rl-tools.ts +243 -0
- package/src/tools/builtin/vision.ts +64 -0
- package/src/tools/builtin/web-search.ts +126 -0
- package/src/tools/document-processor.ts +213 -0
- package/src/tools/image-generator.ts +150 -0
- package/src/tools/web-scraper.ts +179 -0
- package/src/tools/web-search.ts +180 -0
- package/tests/api-server.test.ts +148 -0
- package/tests/approvals.test.ts +89 -0
- package/tests/audio.test.ts +40 -0
- package/tests/browser.test.ts +179 -0
- package/tests/builtin-tools.test.ts +83 -83
- package/tests/channels-extra.test.ts +45 -0
- package/tests/context-compressor.test.ts +172 -0
- package/tests/context-refs.test.ts +121 -0
- package/tests/cron-engine.test.ts +101 -0
- package/tests/document-processor.test.ts +69 -0
- package/tests/e2e-nocode.test.ts +442 -0
- package/tests/elevated.test.ts +69 -0
- package/tests/gateway.test.ts +63 -71
- package/tests/home-assistant.test.ts +40 -0
- package/tests/hooks.test.ts +79 -0
- package/tests/ide-bridge.test.ts +38 -0
- package/tests/image-generator.test.ts +84 -0
- package/tests/node-network.test.ts +74 -0
- package/tests/profiles.test.ts +61 -0
- package/tests/rl-tools.test.ts +93 -0
- package/tests/sandbox-manager.test.ts +46 -0
- package/tests/secrets.test.ts +107 -0
- package/tests/settings-api.test.ts +148 -0
- package/tests/setup.test.ts +73 -0
- package/tests/studio.test.ts +402 -229
- package/tests/tools/builtin-extended.test.ts +138 -138
- package/tests/user-profiler.test.ts +169 -0
- package/tests/v090-features.test.ts +254 -0
- package/tests/vision.test.ts +61 -0
- package/tests/voice-call.test.ts +47 -0
- package/tests/voice-interaction.test.ts +38 -0
- package/tests/web-search.test.ts +155 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image Generator — multi-backend image generation tool.
|
|
3
|
+
* Supports DALL·E (OpenAI), Stable Diffusion (local), and Replicate.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ImageGenConfig {
|
|
7
|
+
provider?: 'dalle' | 'stable-diffusion' | 'replicate';
|
|
8
|
+
openaiApiKey?: string;
|
|
9
|
+
replicateApiKey?: string;
|
|
10
|
+
sdApiUrl?: string;
|
|
11
|
+
defaultModel?: string;
|
|
12
|
+
defaultSize?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ImageGenResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
url?: string;
|
|
18
|
+
base64?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
provider: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ImageGenerator {
|
|
24
|
+
private config: ImageGenConfig;
|
|
25
|
+
|
|
26
|
+
constructor(config?: ImageGenConfig) {
|
|
27
|
+
this.config = {
|
|
28
|
+
provider: config?.provider,
|
|
29
|
+
openaiApiKey: config?.openaiApiKey || process.env.OPENAI_API_KEY,
|
|
30
|
+
replicateApiKey: config?.replicateApiKey || process.env.REPLICATE_API_TOKEN,
|
|
31
|
+
sdApiUrl: config?.sdApiUrl || process.env.SD_API_URL,
|
|
32
|
+
defaultModel: config?.defaultModel || 'dall-e-3',
|
|
33
|
+
defaultSize: config?.defaultSize || '1024x1024',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Auto-detect best available provider */
|
|
38
|
+
detectProvider(): string | null {
|
|
39
|
+
if (this.config.openaiApiKey) return 'dalle';
|
|
40
|
+
if (this.config.sdApiUrl) return 'stable-diffusion';
|
|
41
|
+
if (this.config.replicateApiKey) return 'replicate';
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Get configuration status for the settings UI */
|
|
46
|
+
getStatus(): { configured: boolean; providers: { name: string; configured: boolean }[] } {
|
|
47
|
+
return {
|
|
48
|
+
configured: !!this.detectProvider(),
|
|
49
|
+
providers: [
|
|
50
|
+
{ name: 'dalle', configured: !!this.config.openaiApiKey },
|
|
51
|
+
{ name: 'stable-diffusion', configured: !!this.config.sdApiUrl },
|
|
52
|
+
{ name: 'replicate', configured: !!this.config.replicateApiKey },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async generate(prompt: string, options?: { provider?: string; size?: string; model?: string }): Promise<ImageGenResult> {
|
|
58
|
+
const provider = options?.provider || this.config.provider || this.detectProvider();
|
|
59
|
+
if (!provider) {
|
|
60
|
+
return { success: false, error: 'No image generation provider configured. Please set OPENAI_API_KEY, SD_API_URL, or REPLICATE_API_TOKEN.', provider: 'none' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
switch (provider) {
|
|
64
|
+
case 'dalle': return this.generateDalle(prompt, options);
|
|
65
|
+
case 'stable-diffusion': return this.generateSD(prompt, options);
|
|
66
|
+
case 'replicate': return this.generateReplicate(prompt, options);
|
|
67
|
+
default: return { success: false, error: `Unknown provider: ${provider}`, provider };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async generateDalle(prompt: string, options?: { size?: string; model?: string }): Promise<ImageGenResult> {
|
|
72
|
+
const apiKey = this.config.openaiApiKey;
|
|
73
|
+
if (!apiKey) return { success: false, error: 'OPENAI_API_KEY not configured', provider: 'dalle' };
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch('https://api.openai.com/v1/images/generations', {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
model: options?.model || this.config.defaultModel || 'dall-e-3',
|
|
81
|
+
prompt,
|
|
82
|
+
size: options?.size || this.config.defaultSize || '1024x1024',
|
|
83
|
+
n: 1,
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
const data = await res.json() as { data?: Array<{ url: string }>; error?: { message: string } };
|
|
87
|
+
if (data.error) return { success: false, error: data.error.message, provider: 'dalle' };
|
|
88
|
+
const url = data.data?.[0]?.url;
|
|
89
|
+
return url ? { success: true, url, provider: 'dalle' } : { success: false, error: 'No image returned', provider: 'dalle' };
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return { success: false, error: (err as Error).message, provider: 'dalle' };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async generateSD(prompt: string, options?: { size?: string }): Promise<ImageGenResult> {
|
|
96
|
+
const apiUrl = this.config.sdApiUrl;
|
|
97
|
+
if (!apiUrl) return { success: false, error: 'SD_API_URL not configured', provider: 'stable-diffusion' };
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const [w, h] = (options?.size || '1024x1024').split('x').map(Number);
|
|
101
|
+
const res = await fetch(`${apiUrl}/sdapi/v1/txt2img`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: { 'Content-Type': 'application/json' },
|
|
104
|
+
body: JSON.stringify({ prompt, width: w || 1024, height: h || 1024 }),
|
|
105
|
+
});
|
|
106
|
+
const data = await res.json() as { images?: string[] };
|
|
107
|
+
if (data.images?.length) {
|
|
108
|
+
return { success: true, base64: data.images[0], provider: 'stable-diffusion' };
|
|
109
|
+
}
|
|
110
|
+
return { success: false, error: 'No image generated', provider: 'stable-diffusion' };
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return { success: false, error: (err as Error).message, provider: 'stable-diffusion' };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async generateReplicate(prompt: string, _options?: { model?: string }): Promise<ImageGenResult> {
|
|
117
|
+
const apiKey = this.config.replicateApiKey;
|
|
118
|
+
if (!apiKey) return { success: false, error: 'REPLICATE_API_TOKEN not configured', provider: 'replicate' };
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch('https://api.replicate.com/v1/predictions', {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'Authorization': `Token ${apiKey}`, 'Content-Type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
version: 'stability-ai/sdxl:39ed52f2a78e934b3ba6e2a89f5b1c712de7dfea535525255b1aa35c5565e08b',
|
|
126
|
+
input: { prompt },
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
const prediction = await res.json() as { id: string; urls?: { get: string }; error?: string };
|
|
130
|
+
if (prediction.error) return { success: false, error: prediction.error, provider: 'replicate' };
|
|
131
|
+
|
|
132
|
+
// Poll for completion (max 60s)
|
|
133
|
+
const getUrl = prediction.urls?.get || `https://api.replicate.com/v1/predictions/${prediction.id}`;
|
|
134
|
+
for (let i = 0; i < 30; i++) {
|
|
135
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
136
|
+
const poll = await fetch(getUrl, { headers: { 'Authorization': `Token ${apiKey}` } });
|
|
137
|
+
const result = await poll.json() as { status: string; output?: string[]; error?: string };
|
|
138
|
+
if (result.status === 'succeeded' && result.output?.length) {
|
|
139
|
+
return { success: true, url: result.output[0], provider: 'replicate' };
|
|
140
|
+
}
|
|
141
|
+
if (result.status === 'failed') {
|
|
142
|
+
return { success: false, error: result.error || 'Generation failed', provider: 'replicate' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { success: false, error: 'Timeout waiting for image generation', provider: 'replicate' };
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return { success: false, error: (err as Error).message, provider: 'replicate' };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Scraper - v0.10.0
|
|
3
|
+
* Fetch URL content and extract readable text in markdown format.
|
|
4
|
+
* Uses a simple readability-style extraction (no external dependencies).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export interface ScrapedContent {
|
|
8
|
+
title: string;
|
|
9
|
+
content: string; // markdown
|
|
10
|
+
url: string;
|
|
11
|
+
wordCount: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MAX_CONTENT_LENGTH = 5000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Fetch a URL and extract readable content as markdown.
|
|
18
|
+
*/
|
|
19
|
+
export async function scrapeUrl(url: string, maxLength = MAX_CONTENT_LENGTH): Promise<ScrapedContent> {
|
|
20
|
+
const response = await fetch(url, {
|
|
21
|
+
headers: {
|
|
22
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
23
|
+
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
24
|
+
},
|
|
25
|
+
signal: AbortSignal.timeout(15000),
|
|
26
|
+
redirect: 'follow',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const contentType = response.headers.get('content-type') || '';
|
|
30
|
+
const text = await response.text();
|
|
31
|
+
|
|
32
|
+
// If not HTML, return raw text
|
|
33
|
+
if (!contentType.includes('html')) {
|
|
34
|
+
const truncated = text.slice(0, maxLength);
|
|
35
|
+
return {
|
|
36
|
+
title: url,
|
|
37
|
+
content: truncated,
|
|
38
|
+
url,
|
|
39
|
+
wordCount: truncated.split(/\s+/).length,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return extractReadableContent(text, url, maxLength);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract readable content from HTML using simple heuristics.
|
|
48
|
+
*/
|
|
49
|
+
export function extractReadableContent(html: string, url: string, maxLength = MAX_CONTENT_LENGTH): ScrapedContent {
|
|
50
|
+
// Extract title
|
|
51
|
+
const titleMatch = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
52
|
+
const title = titleMatch ? decodeEntities(titleMatch[1]).trim() : url;
|
|
53
|
+
|
|
54
|
+
// Remove non-content elements
|
|
55
|
+
let content = html;
|
|
56
|
+
|
|
57
|
+
// Remove script, style, nav, header, footer, aside, iframe
|
|
58
|
+
const removePatterns = [
|
|
59
|
+
/<script[\s\S]*?<\/script>/gi,
|
|
60
|
+
/<style[\s\S]*?<\/style>/gi,
|
|
61
|
+
/<nav[\s\S]*?<\/nav>/gi,
|
|
62
|
+
/<footer[\s\S]*?<\/footer>/gi,
|
|
63
|
+
/<aside[\s\S]*?<\/aside>/gi,
|
|
64
|
+
/<iframe[\s\S]*?<\/iframe>/gi,
|
|
65
|
+
/<noscript[\s\S]*?<\/noscript>/gi,
|
|
66
|
+
/<!--[\s\S]*?-->/g,
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
for (const pattern of removePatterns) {
|
|
70
|
+
content = content.replace(pattern, '');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Try to find main content area
|
|
74
|
+
const mainContent = findMainContent(content);
|
|
75
|
+
content = mainContent || content;
|
|
76
|
+
|
|
77
|
+
// Convert to markdown-ish text
|
|
78
|
+
content = htmlToMarkdown(content);
|
|
79
|
+
|
|
80
|
+
// Clean up whitespace
|
|
81
|
+
content = content
|
|
82
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
83
|
+
.replace(/[ \t]+/g, ' ')
|
|
84
|
+
.trim();
|
|
85
|
+
|
|
86
|
+
// Truncate
|
|
87
|
+
if (content.length > maxLength) {
|
|
88
|
+
content = content.slice(0, maxLength) + '\n\n...[truncated]';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
title,
|
|
93
|
+
content,
|
|
94
|
+
url,
|
|
95
|
+
wordCount: content.split(/\s+/).filter(Boolean).length,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Try to find the main content area of the page.
|
|
101
|
+
*/
|
|
102
|
+
function findMainContent(html: string): string | null {
|
|
103
|
+
// Try common content selectors
|
|
104
|
+
const patterns = [
|
|
105
|
+
/<article[^>]*>([\s\S]*?)<\/article>/i,
|
|
106
|
+
/<main[^>]*>([\s\S]*?)<\/main>/i,
|
|
107
|
+
/<div[^>]*class="[^"]*(?:content|article|post|entry|main)[^"]*"[^>]*>([\s\S]*?)<\/div>/i,
|
|
108
|
+
/<div[^>]*id="[^"]*(?:content|article|post|entry|main)[^"]*"[^>]*>([\s\S]*?)<\/div>/i,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
for (const pattern of patterns) {
|
|
112
|
+
const match = html.match(pattern);
|
|
113
|
+
if (match && match[1] && match[1].length > 200) {
|
|
114
|
+
return match[1];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Fallback: find body content
|
|
119
|
+
const bodyMatch = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
120
|
+
return bodyMatch ? bodyMatch[1] : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Simple HTML to Markdown conversion.
|
|
125
|
+
*/
|
|
126
|
+
function htmlToMarkdown(html: string): string {
|
|
127
|
+
let md = html;
|
|
128
|
+
|
|
129
|
+
// Headers
|
|
130
|
+
md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, '\n# $1\n');
|
|
131
|
+
md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, '\n## $1\n');
|
|
132
|
+
md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, '\n### $1\n');
|
|
133
|
+
md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, '\n#### $1\n');
|
|
134
|
+
md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, '\n##### $1\n');
|
|
135
|
+
md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, '\n###### $1\n');
|
|
136
|
+
|
|
137
|
+
// Paragraphs and line breaks
|
|
138
|
+
md = md.replace(/<p[^>]*>/gi, '\n');
|
|
139
|
+
md = md.replace(/<\/p>/gi, '\n');
|
|
140
|
+
md = md.replace(/<br\s*\/?>/gi, '\n');
|
|
141
|
+
|
|
142
|
+
// Links
|
|
143
|
+
md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
|
|
144
|
+
|
|
145
|
+
// Bold and italic
|
|
146
|
+
md = md.replace(/<(?:strong|b)[^>]*>([\s\S]*?)<\/(?:strong|b)>/gi, '**$1**');
|
|
147
|
+
md = md.replace(/<(?:em|i)[^>]*>([\s\S]*?)<\/(?:em|i)>/gi, '*$1*');
|
|
148
|
+
|
|
149
|
+
// Code
|
|
150
|
+
md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
|
|
151
|
+
md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, '\n```\n$1\n```\n');
|
|
152
|
+
|
|
153
|
+
// Lists
|
|
154
|
+
md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '- $1\n');
|
|
155
|
+
|
|
156
|
+
// Blockquote
|
|
157
|
+
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, '\n> $1\n');
|
|
158
|
+
|
|
159
|
+
// Remove remaining HTML tags
|
|
160
|
+
md = md.replace(/<[^>]+>/g, '');
|
|
161
|
+
|
|
162
|
+
// Decode entities
|
|
163
|
+
md = decodeEntities(md);
|
|
164
|
+
|
|
165
|
+
return md;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function decodeEntities(text: string): string {
|
|
169
|
+
return text
|
|
170
|
+
.replace(/&/g, '&')
|
|
171
|
+
.replace(/</g, '<')
|
|
172
|
+
.replace(/>/g, '>')
|
|
173
|
+
.replace(/"/g, '"')
|
|
174
|
+
.replace(/'/g, "'")
|
|
175
|
+
.replace(/'/g, "'")
|
|
176
|
+
.replace(/ /g, ' ')
|
|
177
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n)))
|
|
178
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, n) => String.fromCharCode(parseInt(n, 16)));
|
|
179
|
+
}
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { APIServer } from '../src/core/api-server';
|
|
3
|
+
|
|
4
|
+
function createMockAgent(overrides: any = {}) {
|
|
5
|
+
return {
|
|
6
|
+
name: 'test-agent',
|
|
7
|
+
state: 'running',
|
|
8
|
+
config: { model: 'test-model', name: 'test-agent' },
|
|
9
|
+
chat: async (msg: string) => `echo: ${msg}`,
|
|
10
|
+
...overrides,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function request(port: number, method: string, path: string, body?: any, headers?: Record<string, string>) {
|
|
15
|
+
const res = await fetch(`http://127.0.0.1:${port}${path}`, {
|
|
16
|
+
method,
|
|
17
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
18
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
19
|
+
});
|
|
20
|
+
return res;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('APIServer', () => {
|
|
24
|
+
let server: APIServer;
|
|
25
|
+
const port = 19876;
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
server = new APIServer({ port, host: '127.0.0.1', agent: createMockAgent() });
|
|
29
|
+
await server.start();
|
|
30
|
+
});
|
|
31
|
+
afterAll(async () => { await server.stop(); });
|
|
32
|
+
|
|
33
|
+
it('GET /health returns ok', async () => {
|
|
34
|
+
const res = await request(port, 'GET', '/health');
|
|
35
|
+
expect(res.status).toBe(200);
|
|
36
|
+
const data: any = await res.json();
|
|
37
|
+
expect(data.status).toBe('ok');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('GET /v1/models lists models', async () => {
|
|
41
|
+
const res = await request(port, 'GET', '/v1/models');
|
|
42
|
+
const data: any = await res.json();
|
|
43
|
+
expect(data.object).toBe('list');
|
|
44
|
+
expect(data.data[0].id).toBe('test-model');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('GET /v1/agent/status returns agent info', async () => {
|
|
48
|
+
const res = await request(port, 'GET', '/v1/agent/status');
|
|
49
|
+
const data: any = await res.json();
|
|
50
|
+
expect(data.name).toBe('test-agent');
|
|
51
|
+
expect(data.state).toBe('running');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('POST /v1/chat/completions non-streaming', async () => {
|
|
55
|
+
const res = await request(port, 'POST', '/v1/chat/completions', {
|
|
56
|
+
model: 'test-model',
|
|
57
|
+
messages: [{ role: 'user', content: 'hello' }],
|
|
58
|
+
});
|
|
59
|
+
expect(res.status).toBe(200);
|
|
60
|
+
const data: any = await res.json();
|
|
61
|
+
expect(data.choices[0].message.content).toBe('echo: hello');
|
|
62
|
+
expect(data.choices[0].finish_reason).toBe('stop');
|
|
63
|
+
expect(data.id).toMatch(/^chatcmpl-/);
|
|
64
|
+
expect(data.object).toBe('chat.completion');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('POST /v1/chat/completions streaming', async () => {
|
|
68
|
+
const res = await request(port, 'POST', '/v1/chat/completions', {
|
|
69
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
70
|
+
stream: true,
|
|
71
|
+
});
|
|
72
|
+
expect(res.status).toBe(200);
|
|
73
|
+
expect(res.headers.get('content-type')).toBe('text/event-stream');
|
|
74
|
+
const text = await res.text();
|
|
75
|
+
expect(text).toContain('data: ');
|
|
76
|
+
expect(text).toContain('[DONE]');
|
|
77
|
+
// Parse SSE chunks
|
|
78
|
+
const chunks = text.split('\n').filter(l => l.startsWith('data: ') && !l.includes('[DONE]'));
|
|
79
|
+
expect(chunks.length).toBeGreaterThan(0);
|
|
80
|
+
const first = JSON.parse(chunks[0].slice(6));
|
|
81
|
+
expect(first.object).toBe('chat.completion.chunk');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('POST /v1/chat/completions rejects missing messages', async () => {
|
|
85
|
+
const res = await request(port, 'POST', '/v1/chat/completions', { model: 'x' });
|
|
86
|
+
expect(res.status).toBe(400);
|
|
87
|
+
const data: any = await res.json();
|
|
88
|
+
expect(data.error.type).toBe('invalid_request_error');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('POST /v1/chat/completions rejects invalid JSON', async () => {
|
|
92
|
+
const res = await fetch(`http://127.0.0.1:${port}/v1/chat/completions`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
body: 'not json',
|
|
96
|
+
});
|
|
97
|
+
expect(res.status).toBe(400);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('OPTIONS returns CORS headers', async () => {
|
|
101
|
+
const res = await fetch(`http://127.0.0.1:${port}/v1/models`, { method: 'OPTIONS' });
|
|
102
|
+
expect(res.status).toBe(204);
|
|
103
|
+
expect(res.headers.get('access-control-allow-origin')).toBe('*');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('unknown route returns 404', async () => {
|
|
107
|
+
const res = await request(port, 'GET', '/v1/unknown');
|
|
108
|
+
expect(res.status).toBe(404);
|
|
109
|
+
const data: any = await res.json();
|
|
110
|
+
expect(data.error.code).toBe(404);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('POST /v1/embeddings returns 501 without embed provider', async () => {
|
|
114
|
+
const res = await request(port, 'POST', '/v1/embeddings', { input: 'hello' });
|
|
115
|
+
expect(res.status).toBe(501);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('POST /v1/embeddings rejects missing input', async () => {
|
|
119
|
+
const res = await request(port, 'POST', '/v1/embeddings', { model: 'x' });
|
|
120
|
+
expect(res.status).toBe(400);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('APIServer with auth', () => {
|
|
125
|
+
let server: APIServer;
|
|
126
|
+
const port = 19877;
|
|
127
|
+
|
|
128
|
+
beforeAll(async () => {
|
|
129
|
+
server = new APIServer({ port, host: '127.0.0.1', apiKey: 'secret-key', agent: createMockAgent() });
|
|
130
|
+
await server.start();
|
|
131
|
+
});
|
|
132
|
+
afterAll(async () => { await server.stop(); });
|
|
133
|
+
|
|
134
|
+
it('rejects unauthenticated requests', async () => {
|
|
135
|
+
const res = await request(port, 'GET', '/v1/models');
|
|
136
|
+
expect(res.status).toBe(401);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('accepts valid Bearer token', async () => {
|
|
140
|
+
const res = await request(port, 'GET', '/v1/models', undefined, { Authorization: 'Bearer secret-key' });
|
|
141
|
+
expect(res.status).toBe(200);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('health check works without auth', async () => {
|
|
145
|
+
const res = await request(port, 'GET', '/health');
|
|
146
|
+
expect(res.status).toBe(200);
|
|
147
|
+
});
|
|
148
|
+
});
|