Simford.Dong Claude commited on
Commit ·
89d226e
1
Parent(s): 4ee7a5e
Replace custom wrapper with full OpenClaw project
Browse files- Remove custom market research wrapper (src/, public/, server.js)
- Install full OpenClaw from npm (openclaw@latest)
- Configure gateway to run on port 7860 (HF default)
- Enable Control UI and WebChat access
- Set up auto-configuration script for openclaw.json
- Update README with full OpenClaw documentation
Features now available:
- Control UI (web dashboard)
- WebChat (browser-based chat)
- Multi-channel support (WhatsApp, Telegram, Slack, Discord, etc.)
- Multi-model support (Anthropic, OpenAI, Gemini, DeepSeek, etc.)
- Tools (Browser, Canvas, Nodes, Cron, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- .gitattributes +0 -35
- Dockerfile +39 -11
- README.md +104 -4
- package.json +0 -11
- public/index.html +0 -648
- server.js +0 -222
- src/index.js +0 -33
- src/llm/callLLM.js +0 -72
- src/llm/providers.js +0 -94
- src/marketResearchAgent.js +0 -266
- src/schema/dashboard.schema.js +0 -39
- src/schema/report.schema.js +0 -35
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Dockerfile
CHANGED
|
@@ -1,17 +1,45 @@
|
|
| 1 |
-
FROM node:
|
| 2 |
|
| 3 |
-
|
|
|
|
| 4 |
|
| 5 |
-
#
|
| 6 |
-
|
| 7 |
-
RUN npm install
|
| 8 |
|
| 9 |
-
#
|
| 10 |
-
|
| 11 |
-
COPY server.js .
|
| 12 |
|
| 13 |
-
#
|
| 14 |
-
|
|
|
|
| 15 |
|
|
|
|
| 16 |
EXPOSE 7860
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM node:22-slim
|
| 2 |
|
| 3 |
+
# Install system dependencies
|
| 4 |
+
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
|
| 5 |
|
| 6 |
+
# Install OpenClaw globally
|
| 7 |
+
RUN npm install -g openclaw@latest
|
|
|
|
| 8 |
|
| 9 |
+
# Create config directory
|
| 10 |
+
RUN mkdir -p /root/.openclaw
|
|
|
|
| 11 |
|
| 12 |
+
# Set up environment for Hugging Face Spaces
|
| 13 |
+
ENV PORT=7860
|
| 14 |
+
ENV GATEWAY_CONTROLUI_ALLOWINSECUREAUTH=true
|
| 15 |
|
| 16 |
+
# Expose the port (Hugging Face default)
|
| 17 |
EXPOSE 7860
|
| 18 |
+
|
| 19 |
+
# Create a startup script that generates config and starts OpenClaw
|
| 20 |
+
RUN echo '#!/bin/bash\n\
|
| 21 |
+
# Generate minimal config if not exists\n\
|
| 22 |
+
if [ ! -f /root/.openclaw/openclaw.json ]; then\n\
|
| 23 |
+
cat > /root/.openclaw/openclaw.json << EOF\n\
|
| 24 |
+
{\n\
|
| 25 |
+
"gateway": {\n\
|
| 26 |
+
"bind": "0.0.0.0",\n\
|
| 27 |
+
"port": 7860,\n\
|
| 28 |
+
"controlUi": {\n\
|
| 29 |
+
"allowInsecureAuth": true\n\
|
| 30 |
+
}\n\
|
| 31 |
+
},\n\
|
| 32 |
+
"agents": {\n\
|
| 33 |
+
"defaults": {\n\
|
| 34 |
+
"model": "anthropic/claude-sonnet-4-20250514"\n\
|
| 35 |
+
}\n\
|
| 36 |
+
}\n\
|
| 37 |
+
}\n\
|
| 38 |
+
EOF\n\
|
| 39 |
+
fi\n\
|
| 40 |
+
\n\
|
| 41 |
+
# Start OpenClaw gateway\n\
|
| 42 |
+
exec openclaw gateway --port 7860 --verbose\n\
|
| 43 |
+
' > /usr/local/bin/start-openclaw && chmod +x /usr/local/bin/start-openclaw
|
| 44 |
+
|
| 45 |
+
CMD ["/usr/local/bin/start-openclaw"]
|
README.md
CHANGED
|
@@ -1,12 +1,112 @@
|
|
| 1 |
---
|
| 2 |
-
title: OpenClaw
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: yellow
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
-
short_description:
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: OpenClaw AI Assistant
|
| 3 |
+
emoji: 🦞
|
| 4 |
colorFrom: pink
|
| 5 |
colorTo: yellow
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
+
short_description: Your own personal AI assistant
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# OpenClaw AI Assistant
|
| 13 |
+
|
| 14 |
+
> Your own personal AI assistant. Any OS. Any Platform. The lobster way. 🦞
|
| 15 |
+
|
| 16 |
+
This Hugging Face Space runs the full **OpenClaw** gateway with Control UI and WebChat support.
|
| 17 |
+
|
| 18 |
+
## Features
|
| 19 |
+
|
| 20 |
+
- 🎛️ **Control UI** - Web-based dashboard for managing your assistant
|
| 21 |
+
- 💬 **WebChat** - Browser-based chat interface
|
| 22 |
+
- 📡 **WebSocket Gateway** - Real-time communication
|
| 23 |
+
- 🔌 **Multi-Channel Support** - WhatsApp, Telegram, Slack, Discord, Google Chat, Signal, iMessage, and more
|
| 24 |
+
- 🤖 **Multi-Model Support** - Anthropic, OpenAI, Gemini, DeepSeek, OpenRouter, etc.
|
| 25 |
+
- 🛠️ **Tool Support** - Browser automation, Canvas, Nodes, Cron, and more
|
| 26 |
+
|
| 27 |
+
## Quick Start
|
| 28 |
+
|
| 29 |
+
### 1. Access the Control UI
|
| 30 |
+
|
| 31 |
+
Visit the Space URL to access the Control UI:
|
| 32 |
+
```
|
| 33 |
+
https://sim4imgbed-openclaw.hf.space/
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
### 2. Configure API Keys
|
| 37 |
+
|
| 38 |
+
Set environment variables in the Space settings:
|
| 39 |
+
|
| 40 |
+
**For Anthropic Claude:**
|
| 41 |
+
```
|
| 42 |
+
ANTHROPIC_AUTH_TOKEN=your-sk-ant-key
|
| 43 |
+
# or OAuth: ANTHROPIC_AUTH_REFRESH_TOKEN=your-refresh-token
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
**For OpenAI:**
|
| 47 |
+
```
|
| 48 |
+
OPENAI_KEY=your-sk-key
|
| 49 |
+
```
|
| 50 |
+
|
| 51 |
+
**For Gemini:**
|
| 52 |
+
```
|
| 53 |
+
GEMINI_KEY=your-api-key
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
**For DeepSeek:**
|
| 57 |
+
```
|
| 58 |
+
DEEPSEEK_KEY=your-api-key
|
| 59 |
+
```
|
| 60 |
+
|
| 61 |
+
### 3. Set Auth Token (Optional)
|
| 62 |
+
|
| 63 |
+
For secure access, set a token:
|
| 64 |
+
```
|
| 65 |
+
GATEWAY_TOKEN=your-secret-token
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
Then access with: `/?token=your-secret-token`
|
| 69 |
+
|
| 70 |
+
## Channels Setup
|
| 71 |
+
|
| 72 |
+
OpenClaw supports multiple messaging channels. Configure them in the Control UI or via environment variables:
|
| 73 |
+
|
| 74 |
+
| Channel | Environment Variable |
|
| 75 |
+
|---------|---------------------|
|
| 76 |
+
| Discord | `DISCORD_TOKEN` |
|
| 77 |
+
| Slack | `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` |
|
| 78 |
+
| Telegram | `TELEGRAM_BOT_TOKEN` |
|
| 79 |
+
| WhatsApp | Configure via Control UI (pairing required) |
|
| 80 |
+
| Google Chat | Configure via Control UI |
|
| 81 |
+
| Signal | Configure via Control UI |
|
| 82 |
+
|
| 83 |
+
## Configuration
|
| 84 |
+
|
| 85 |
+
The gateway uses `~/.openclaw/openclaw.json` for configuration. Key settings:
|
| 86 |
+
|
| 87 |
+
```json
|
| 88 |
+
{
|
| 89 |
+
"gateway": {
|
| 90 |
+
"bind": "0.0.0.0",
|
| 91 |
+
"port": 7860,
|
| 92 |
+
"controlUi": {
|
| 93 |
+
"allowInsecureAuth": true
|
| 94 |
+
}
|
| 95 |
+
},
|
| 96 |
+
"agents": {
|
| 97 |
+
"defaults": {
|
| 98 |
+
"model": "anthropic/claude-sonnet-4-20250514"
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
```
|
| 103 |
+
|
| 104 |
+
## Documentation
|
| 105 |
+
|
| 106 |
+
- [Official Docs](https://docs.openclaw.ai/)
|
| 107 |
+
- [GitHub](https://github.com/openclaw/openclaw)
|
| 108 |
+
- [Discord](https://discord.gg/openclaw)
|
| 109 |
+
|
| 110 |
+
## License
|
| 111 |
+
|
| 112 |
+
MIT
|
package.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"name": "openclaw",
|
| 3 |
-
"private": true,
|
| 4 |
-
"type": "module",
|
| 5 |
-
"scripts": {
|
| 6 |
-
"start": "node server.js"
|
| 7 |
-
},
|
| 8 |
-
"dependencies": {
|
| 9 |
-
"express": "^4.19.2"
|
| 10 |
-
}
|
| 11 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public/index.html
DELETED
|
@@ -1,648 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
-
<title>OpenClaw Market Research Agent</title>
|
| 7 |
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
| 8 |
-
<style>
|
| 9 |
-
* {
|
| 10 |
-
margin: 0;
|
| 11 |
-
padding: 0;
|
| 12 |
-
box-sizing: border-box;
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
body {
|
| 16 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
| 17 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 18 |
-
min-height: 100vh;
|
| 19 |
-
padding: 20px;
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
.container {
|
| 23 |
-
max-width: 1200px;
|
| 24 |
-
margin: 0 auto;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
.header {
|
| 28 |
-
text-align: center;
|
| 29 |
-
color: white;
|
| 30 |
-
margin-bottom: 30px;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
.header h1 {
|
| 34 |
-
font-size: 2.5rem;
|
| 35 |
-
margin-bottom: 10px;
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
.header p {
|
| 39 |
-
font-size: 1.1rem;
|
| 40 |
-
opacity: 0.9;
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
.card {
|
| 44 |
-
background: white;
|
| 45 |
-
border-radius: 16px;
|
| 46 |
-
padding: 24px;
|
| 47 |
-
margin-bottom: 20px;
|
| 48 |
-
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
| 49 |
-
}
|
| 50 |
-
|
| 51 |
-
.input-section {
|
| 52 |
-
display: flex;
|
| 53 |
-
gap: 12px;
|
| 54 |
-
align-items: center;
|
| 55 |
-
flex-wrap: wrap;
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
.input-group {
|
| 59 |
-
flex: 1;
|
| 60 |
-
min-width: 200px;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
.input-group label {
|
| 64 |
-
display: block;
|
| 65 |
-
font-size: 0.9rem;
|
| 66 |
-
color: #666;
|
| 67 |
-
margin-bottom: 6px;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
.input-group input,
|
| 71 |
-
.input-group select {
|
| 72 |
-
width: 100%;
|
| 73 |
-
padding: 12px 16px;
|
| 74 |
-
border: 2px solid #e0e0e0;
|
| 75 |
-
border-radius: 10px;
|
| 76 |
-
font-size: 1rem;
|
| 77 |
-
transition: border-color 0.2s;
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
.input-group input:focus,
|
| 81 |
-
.input-group select:focus {
|
| 82 |
-
outline: none;
|
| 83 |
-
border-color: #667eea;
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
.btn {
|
| 87 |
-
padding: 12px 24px;
|
| 88 |
-
border: none;
|
| 89 |
-
border-radius: 10px;
|
| 90 |
-
font-size: 1rem;
|
| 91 |
-
font-weight: 600;
|
| 92 |
-
cursor: pointer;
|
| 93 |
-
transition: all 0.2s;
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
.btn-primary {
|
| 97 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 98 |
-
color: white;
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
.btn-primary:hover {
|
| 102 |
-
transform: translateY(-2px);
|
| 103 |
-
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
.btn-primary:disabled {
|
| 107 |
-
opacity: 0.6;
|
| 108 |
-
cursor: not-allowed;
|
| 109 |
-
transform: none;
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
.loading {
|
| 113 |
-
display: none;
|
| 114 |
-
text-align: center;
|
| 115 |
-
padding: 40px;
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
.loading.active {
|
| 119 |
-
display: block;
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
.spinner {
|
| 123 |
-
width: 50px;
|
| 124 |
-
height: 50px;
|
| 125 |
-
border: 4px solid #f3f3f3;
|
| 126 |
-
border-top: 4px solid #667eea;
|
| 127 |
-
border-radius: 50%;
|
| 128 |
-
animation: spin 1s linear infinite;
|
| 129 |
-
margin: 0 auto 20px;
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
@keyframes spin {
|
| 133 |
-
0% { transform: rotate(0deg); }
|
| 134 |
-
100% { transform: rotate(360deg); }
|
| 135 |
-
}
|
| 136 |
-
|
| 137 |
-
.results {
|
| 138 |
-
display: none;
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
.results.active {
|
| 142 |
-
display: block;
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
.results-header {
|
| 146 |
-
display: flex;
|
| 147 |
-
justify-content: space-between;
|
| 148 |
-
align-items: center;
|
| 149 |
-
margin-bottom: 20px;
|
| 150 |
-
padding-bottom: 20px;
|
| 151 |
-
border-bottom: 1px solid #e0e0e0;
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
.results-title {
|
| 155 |
-
font-size: 1.5rem;
|
| 156 |
-
color: #333;
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
.meta-info {
|
| 160 |
-
display: flex;
|
| 161 |
-
gap: 20px;
|
| 162 |
-
font-size: 0.9rem;
|
| 163 |
-
color: #666;
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
.meta-item {
|
| 167 |
-
display: flex;
|
| 168 |
-
align-items: center;
|
| 169 |
-
gap: 6px;
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
.meta-badge {
|
| 173 |
-
background: #e8f5e9;
|
| 174 |
-
color: #2e7d32;
|
| 175 |
-
padding: 4px 12px;
|
| 176 |
-
border-radius: 20px;
|
| 177 |
-
font-size: 0.85rem;
|
| 178 |
-
font-weight: 600;
|
| 179 |
-
}
|
| 180 |
-
|
| 181 |
-
.summary-grid {
|
| 182 |
-
display: grid;
|
| 183 |
-
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 184 |
-
gap: 16px;
|
| 185 |
-
margin-bottom: 24px;
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
.summary-card {
|
| 189 |
-
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
|
| 190 |
-
border-radius: 12px;
|
| 191 |
-
padding: 20px;
|
| 192 |
-
text-align: center;
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
.summary-label {
|
| 196 |
-
font-size: 0.85rem;
|
| 197 |
-
color: #666;
|
| 198 |
-
margin-bottom: 8px;
|
| 199 |
-
}
|
| 200 |
-
|
| 201 |
-
.summary-value {
|
| 202 |
-
font-size: 1.8rem;
|
| 203 |
-
font-weight: 700;
|
| 204 |
-
color: #333;
|
| 205 |
-
}
|
| 206 |
-
|
| 207 |
-
.summary-value.highlight {
|
| 208 |
-
color: #667eea;
|
| 209 |
-
}
|
| 210 |
-
|
| 211 |
-
.chart-section {
|
| 212 |
-
margin-bottom: 24px;
|
| 213 |
-
}
|
| 214 |
-
|
| 215 |
-
.chart-title {
|
| 216 |
-
font-size: 1.1rem;
|
| 217 |
-
color: #333;
|
| 218 |
-
margin-bottom: 16px;
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
.chart-container {
|
| 222 |
-
height: 300px;
|
| 223 |
-
position: relative;
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
.data-table {
|
| 227 |
-
width: 100%;
|
| 228 |
-
border-collapse: collapse;
|
| 229 |
-
margin-top: 16px;
|
| 230 |
-
}
|
| 231 |
-
|
| 232 |
-
.data-table th,
|
| 233 |
-
.data-table td {
|
| 234 |
-
padding: 12px;
|
| 235 |
-
text-align: left;
|
| 236 |
-
border-bottom: 1px solid #e0e0e0;
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
.data-table th {
|
| 240 |
-
background: #f5f5f5;
|
| 241 |
-
font-weight: 600;
|
| 242 |
-
color: #333;
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
.data-table tr:hover {
|
| 246 |
-
background: #f9f9f9;
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
.section-title {
|
| 250 |
-
font-size: 1.2rem;
|
| 251 |
-
color: #333;
|
| 252 |
-
margin-bottom: 16px;
|
| 253 |
-
display: flex;
|
| 254 |
-
align-items: center;
|
| 255 |
-
gap: 10px;
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
.section-title::before {
|
| 259 |
-
content: '';
|
| 260 |
-
width: 4px;
|
| 261 |
-
height: 24px;
|
| 262 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 263 |
-
border-radius: 2px;
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
.insights-grid {
|
| 267 |
-
display: grid;
|
| 268 |
-
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 269 |
-
gap: 16px;
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
.insight-card {
|
| 273 |
-
background: #f9f9f9;
|
| 274 |
-
border-radius: 12px;
|
| 275 |
-
padding: 16px;
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
.insight-card h4 {
|
| 279 |
-
color: #333;
|
| 280 |
-
margin-bottom: 12px;
|
| 281 |
-
font-size: 1rem;
|
| 282 |
-
}
|
| 283 |
-
|
| 284 |
-
.insight-list {
|
| 285 |
-
list-style: none;
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
.insight-list li {
|
| 289 |
-
padding: 6px 0;
|
| 290 |
-
color: #666;
|
| 291 |
-
font-size: 0.95rem;
|
| 292 |
-
}
|
| 293 |
-
|
| 294 |
-
.insight-list li::before {
|
| 295 |
-
content: '•';
|
| 296 |
-
color: #667eea;
|
| 297 |
-
font-weight: bold;
|
| 298 |
-
margin-right: 8px;
|
| 299 |
-
}
|
| 300 |
-
|
| 301 |
-
.error {
|
| 302 |
-
display: none;
|
| 303 |
-
background: #ffebee;
|
| 304 |
-
color: #c62828;
|
| 305 |
-
padding: 16px;
|
| 306 |
-
border-radius: 10px;
|
| 307 |
-
margin-top: 20px;
|
| 308 |
-
}
|
| 309 |
-
|
| 310 |
-
.error.active {
|
| 311 |
-
display: block;
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
@media (max-width: 768px) {
|
| 315 |
-
.header h1 {
|
| 316 |
-
font-size: 1.8rem;
|
| 317 |
-
}
|
| 318 |
-
|
| 319 |
-
.input-section {
|
| 320 |
-
flex-direction: column;
|
| 321 |
-
}
|
| 322 |
-
|
| 323 |
-
.input-group {
|
| 324 |
-
width: 100%;
|
| 325 |
-
}
|
| 326 |
-
|
| 327 |
-
.btn {
|
| 328 |
-
width: 100%;
|
| 329 |
-
}
|
| 330 |
-
}
|
| 331 |
-
</style>
|
| 332 |
-
</head>
|
| 333 |
-
<body>
|
| 334 |
-
<div class="container">
|
| 335 |
-
<div class="header">
|
| 336 |
-
<h1>OpenClaw Market Research</h1>
|
| 337 |
-
<p>AI-powered market analysis and forecasting</p>
|
| 338 |
-
</div>
|
| 339 |
-
|
| 340 |
-
<div class="card">
|
| 341 |
-
<div class="input-section">
|
| 342 |
-
<div class="input-group">
|
| 343 |
-
<label for="keyword">Market Keyword</label>
|
| 344 |
-
<input type="text" id="keyword" placeholder="e.g., AI Healthcare, Electric Vehicles, Cloud Computing" value="AI Healthcare">
|
| 345 |
-
</div>
|
| 346 |
-
<div class="input-group">
|
| 347 |
-
<label for="provider">AI Provider</label>
|
| 348 |
-
<select id="provider">
|
| 349 |
-
<option value="auto">Auto Select</option>
|
| 350 |
-
<option value="gemini">Gemini</option>
|
| 351 |
-
<option value="openai">OpenAI</option>
|
| 352 |
-
<option value="claude">Claude</option>
|
| 353 |
-
<option value="deepseek">DeepSeek</option>
|
| 354 |
-
<option value="openrouter">OpenRouter</option>
|
| 355 |
-
</select>
|
| 356 |
-
</div>
|
| 357 |
-
<button class="btn btn-primary" id="searchBtn" onclick="runResearch()">
|
| 358 |
-
Generate Report
|
| 359 |
-
</button>
|
| 360 |
-
</div>
|
| 361 |
-
</div>
|
| 362 |
-
|
| 363 |
-
<div class="loading" id="loading">
|
| 364 |
-
<div class="spinner"></div>
|
| 365 |
-
<p>Analyzing market data and generating forecast...</p>
|
| 366 |
-
</div>
|
| 367 |
-
|
| 368 |
-
<div class="error" id="error"></div>
|
| 369 |
-
|
| 370 |
-
<div class="results" id="results">
|
| 371 |
-
<div class="card">
|
| 372 |
-
<div class="results-header">
|
| 373 |
-
<h2 class="results-title" id="marketTitle">Market Research Report</h2>
|
| 374 |
-
<div class="meta-info">
|
| 375 |
-
<span class="meta-badge" id="providerBadge">Gemini</span>
|
| 376 |
-
<span class="meta-item" id="timestamp"></span>
|
| 377 |
-
</div>
|
| 378 |
-
</div>
|
| 379 |
-
|
| 380 |
-
<div class="summary-grid">
|
| 381 |
-
<div class="summary-card">
|
| 382 |
-
<div class="summary-label">2023 Market Size</div>
|
| 383 |
-
<div class="summary-value" id="past2023">$0B</div>
|
| 384 |
-
</div>
|
| 385 |
-
<div class="summary-card">
|
| 386 |
-
<div class="summary-label">2025 Market Size</div>
|
| 387 |
-
<div class="summary-value highlight" id="current2025">$0B</div>
|
| 388 |
-
</div>
|
| 389 |
-
<div class="summary-card">
|
| 390 |
-
<div class="summary-label">2033 Forecast</div>
|
| 391 |
-
<div class="summary-value" id="forecast2033">$0B</div>
|
| 392 |
-
</div>
|
| 393 |
-
<div class="summary-card">
|
| 394 |
-
<div class="summary-label">CAGR</div>
|
| 395 |
-
<div class="summary-value highlight" id="cagr">0%</div>
|
| 396 |
-
</div>
|
| 397 |
-
</div>
|
| 398 |
-
|
| 399 |
-
<div class="chart-section">
|
| 400 |
-
<h3 class="chart-title">Market Forecast (2023-2033)</h3>
|
| 401 |
-
<div class="chart-container">
|
| 402 |
-
<canvas id="forecastChart"></canvas>
|
| 403 |
-
</div>
|
| 404 |
-
</div>
|
| 405 |
-
|
| 406 |
-
<div class="chart-section">
|
| 407 |
-
<h3 class="chart-title">Regional Market Distribution</h3>
|
| 408 |
-
<div class="chart-container">
|
| 409 |
-
<canvas id="regionalChart"></canvas>
|
| 410 |
-
</div>
|
| 411 |
-
</div>
|
| 412 |
-
|
| 413 |
-
<h3 class="section-title">Regional Analysis</h3>
|
| 414 |
-
<table class="data-table" id="regionalTable">
|
| 415 |
-
<thead>
|
| 416 |
-
<tr>
|
| 417 |
-
<th>Region</th>
|
| 418 |
-
<th>Market Share</th>
|
| 419 |
-
<th>CAGR</th>
|
| 420 |
-
</tr>
|
| 421 |
-
</thead>
|
| 422 |
-
<tbody id="regionalBody"></tbody>
|
| 423 |
-
</table>
|
| 424 |
-
|
| 425 |
-
<h3 class="section-title" style="margin-top: 24px;">Competitive Landscape</h3>
|
| 426 |
-
<table class="data-table">
|
| 427 |
-
<thead>
|
| 428 |
-
<tr>
|
| 429 |
-
<th>Company</th>
|
| 430 |
-
<th>Market Share</th>
|
| 431 |
-
</tr>
|
| 432 |
-
</thead>
|
| 433 |
-
<tbody id="competitiveBody"></tbody>
|
| 434 |
-
</table>
|
| 435 |
-
|
| 436 |
-
<h3 class="section-title" style="margin-top: 24px;">Key Insights</h3>
|
| 437 |
-
<div class="insights-grid">
|
| 438 |
-
<div class="insight-card">
|
| 439 |
-
<h4>Opportunities</h4>
|
| 440 |
-
<ul class="insight-list" id="opportunitiesList"></ul>
|
| 441 |
-
</div>
|
| 442 |
-
<div class="insight-card">
|
| 443 |
-
<h4>Challenges</h4>
|
| 444 |
-
<ul class="insight-list" id="challengesList"></ul>
|
| 445 |
-
</div>
|
| 446 |
-
<div class="insight-card">
|
| 447 |
-
<h4>Market Drivers</h4>
|
| 448 |
-
<ul class="insight-list" id="driversList"></ul>
|
| 449 |
-
</div>
|
| 450 |
-
</div>
|
| 451 |
-
</div>
|
| 452 |
-
</div>
|
| 453 |
-
</div>
|
| 454 |
-
|
| 455 |
-
<script>
|
| 456 |
-
let forecastChart = null;
|
| 457 |
-
let regionalChart = null;
|
| 458 |
-
|
| 459 |
-
async function runResearch() {
|
| 460 |
-
const keyword = document.getElementById('keyword').value.trim();
|
| 461 |
-
const provider = document.getElementById('provider').value;
|
| 462 |
-
|
| 463 |
-
if (!keyword) {
|
| 464 |
-
showError('Please enter a market keyword');
|
| 465 |
-
return;
|
| 466 |
-
}
|
| 467 |
-
|
| 468 |
-
const loading = document.getElementById('loading');
|
| 469 |
-
const results = document.getElementById('results');
|
| 470 |
-
const error = document.getElementById('error');
|
| 471 |
-
const btn = document.getElementById('searchBtn');
|
| 472 |
-
|
| 473 |
-
loading.classList.add('active');
|
| 474 |
-
results.classList.remove('active');
|
| 475 |
-
error.classList.remove('active');
|
| 476 |
-
btn.disabled = true;
|
| 477 |
-
|
| 478 |
-
try {
|
| 479 |
-
const response = await fetch('/run', {
|
| 480 |
-
method: 'POST',
|
| 481 |
-
headers: {
|
| 482 |
-
'Content-Type': 'application/json'
|
| 483 |
-
},
|
| 484 |
-
body: JSON.stringify({
|
| 485 |
-
task: 'market_research',
|
| 486 |
-
provider: provider === 'auto' ? undefined : provider,
|
| 487 |
-
keyword: keyword
|
| 488 |
-
})
|
| 489 |
-
});
|
| 490 |
-
|
| 491 |
-
const data = await response.json();
|
| 492 |
-
|
| 493 |
-
if (!response.ok || data.error) {
|
| 494 |
-
throw new Error(data.error || data.details || 'Request failed');
|
| 495 |
-
}
|
| 496 |
-
|
| 497 |
-
displayResults(data);
|
| 498 |
-
results.classList.add('active');
|
| 499 |
-
} catch (err) {
|
| 500 |
-
showError(err.message);
|
| 501 |
-
} finally {
|
| 502 |
-
loading.classList.remove('active');
|
| 503 |
-
btn.disabled = false;
|
| 504 |
-
}
|
| 505 |
-
}
|
| 506 |
-
|
| 507 |
-
function displayResults(data) {
|
| 508 |
-
const dashboard = data.dashboard_view;
|
| 509 |
-
|
| 510 |
-
// Header
|
| 511 |
-
document.getElementById('marketTitle').textContent = dashboard.marketTitle;
|
| 512 |
-
document.getElementById('providerBadge').textContent = data.meta.provider_used || 'Auto';
|
| 513 |
-
document.getElementById('timestamp').textContent = new Date(data.meta.timestamp).toLocaleString();
|
| 514 |
-
|
| 515 |
-
// Summary
|
| 516 |
-
document.getElementById('past2023').textContent = `$${dashboard.marketSummary.past2023}B`;
|
| 517 |
-
document.getElementById('current2025').textContent = `$${dashboard.marketSummary.current2025}B`;
|
| 518 |
-
document.getElementById('forecast2033').textContent = `$${dashboard.marketSummary.forecast2033}B`;
|
| 519 |
-
document.getElementById('cagr').textContent = `${dashboard.marketSummary.cagr}%`;
|
| 520 |
-
|
| 521 |
-
// Forecast Chart
|
| 522 |
-
updateForecastChart(dashboard.forecast);
|
| 523 |
-
|
| 524 |
-
// Regional Chart
|
| 525 |
-
updateRegionalChart(dashboard.regional);
|
| 526 |
-
|
| 527 |
-
// Regional Table
|
| 528 |
-
const regionalBody = document.getElementById('regionalBody');
|
| 529 |
-
regionalBody.innerHTML = dashboard.regional.map(r => `
|
| 530 |
-
<tr>
|
| 531 |
-
<td>${r.region}</td>
|
| 532 |
-
<td>${r.share}%</td>
|
| 533 |
-
<td>${r.cagr}%</td>
|
| 534 |
-
</tr>
|
| 535 |
-
`).join('');
|
| 536 |
-
|
| 537 |
-
// Competitive Table
|
| 538 |
-
const competitiveBody = document.getElementById('competitiveBody');
|
| 539 |
-
competitiveBody.innerHTML = dashboard.competitive.map(c => `
|
| 540 |
-
<tr>
|
| 541 |
-
<td>${c.company}</td>
|
| 542 |
-
<td>${c.share}%</td>
|
| 543 |
-
</tr>
|
| 544 |
-
`).join('');
|
| 545 |
-
|
| 546 |
-
// Insights
|
| 547 |
-
document.getElementById('opportunitiesList').innerHTML = dashboard.insights.keyOpportunities.map(o => `<li>${o}</li>`).join('');
|
| 548 |
-
document.getElementById('challengesList').innerHTML = dashboard.insights.majorChallenges.map(c => `<li>${c}</li>`).join('');
|
| 549 |
-
document.getElementById('driversList').innerHTML = dashboard.drivers.map(d => `<li>${d.driver} (Impact: ${d.impact})</li>`).join('');
|
| 550 |
-
}
|
| 551 |
-
|
| 552 |
-
function updateForecastChart(forecast) {
|
| 553 |
-
const ctx = document.getElementById('forecastChart').getContext('2d');
|
| 554 |
-
|
| 555 |
-
if (forecastChart) {
|
| 556 |
-
forecastChart.destroy();
|
| 557 |
-
}
|
| 558 |
-
|
| 559 |
-
forecastChart = new Chart(ctx, {
|
| 560 |
-
type: 'line',
|
| 561 |
-
data: {
|
| 562 |
-
labels: forecast.map(f => f.year),
|
| 563 |
-
datasets: [{
|
| 564 |
-
label: 'Market Size (Billion USD)',
|
| 565 |
-
data: forecast.map(f => f.value),
|
| 566 |
-
borderColor: '#667eea',
|
| 567 |
-
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
| 568 |
-
fill: true,
|
| 569 |
-
tension: 0.4,
|
| 570 |
-
pointBackgroundColor: '#667eea',
|
| 571 |
-
pointBorderColor: '#fff',
|
| 572 |
-
pointBorderWidth: 2,
|
| 573 |
-
pointRadius: 6
|
| 574 |
-
}]
|
| 575 |
-
},
|
| 576 |
-
options: {
|
| 577 |
-
responsive: true,
|
| 578 |
-
maintainAspectRatio: false,
|
| 579 |
-
plugins: {
|
| 580 |
-
legend: {
|
| 581 |
-
display: false
|
| 582 |
-
}
|
| 583 |
-
},
|
| 584 |
-
scales: {
|
| 585 |
-
y: {
|
| 586 |
-
beginAtZero: true,
|
| 587 |
-
grid: {
|
| 588 |
-
color: 'rgba(0,0,0,0.05)'
|
| 589 |
-
}
|
| 590 |
-
},
|
| 591 |
-
x: {
|
| 592 |
-
grid: {
|
| 593 |
-
display: false
|
| 594 |
-
}
|
| 595 |
-
}
|
| 596 |
-
}
|
| 597 |
-
}
|
| 598 |
-
});
|
| 599 |
-
}
|
| 600 |
-
|
| 601 |
-
function updateRegionalChart(regional) {
|
| 602 |
-
const ctx = document.getElementById('regionalChart').getContext('2d');
|
| 603 |
-
|
| 604 |
-
if (regionalChart) {
|
| 605 |
-
regionalChart.destroy();
|
| 606 |
-
}
|
| 607 |
-
|
| 608 |
-
regionalChart = new Chart(ctx, {
|
| 609 |
-
type: 'doughnut',
|
| 610 |
-
data: {
|
| 611 |
-
labels: regional.map(r => r.region),
|
| 612 |
-
datasets: [{
|
| 613 |
-
data: regional.map(r => r.share),
|
| 614 |
-
backgroundColor: [
|
| 615 |
-
'#667eea',
|
| 616 |
-
'#764ba2',
|
| 617 |
-
'#f093fb',
|
| 618 |
-
'#4facfe',
|
| 619 |
-
'#43e97b'
|
| 620 |
-
],
|
| 621 |
-
borderWidth: 0
|
| 622 |
-
}]
|
| 623 |
-
},
|
| 624 |
-
options: {
|
| 625 |
-
responsive: true,
|
| 626 |
-
maintainAspectRatio: false,
|
| 627 |
-
plugins: {
|
| 628 |
-
legend: {
|
| 629 |
-
position: 'right'
|
| 630 |
-
}
|
| 631 |
-
}
|
| 632 |
-
}
|
| 633 |
-
});
|
| 634 |
-
}
|
| 635 |
-
|
| 636 |
-
function showError(message) {
|
| 637 |
-
const error = document.getElementById('error');
|
| 638 |
-
error.textContent = `Error: ${message}`;
|
| 639 |
-
error.classList.add('active');
|
| 640 |
-
}
|
| 641 |
-
|
| 642 |
-
// Run on Enter key
|
| 643 |
-
document.getElementById('keyword').addEventListener('keypress', (e) => {
|
| 644 |
-
if (e.key === 'Enter') runResearch();
|
| 645 |
-
});
|
| 646 |
-
</script>
|
| 647 |
-
</body>
|
| 648 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
server.js
DELETED
|
@@ -1,222 +0,0 @@
|
|
| 1 |
-
import express from "express";
|
| 2 |
-
import { spawn } from "child_process";
|
| 3 |
-
import { fileURLToPath } from "url";
|
| 4 |
-
import { dirname, join } from "path";
|
| 5 |
-
|
| 6 |
-
const __filename = fileURLToPath(import.meta.url);
|
| 7 |
-
const __dirname = dirname(__filename);
|
| 8 |
-
|
| 9 |
-
const app = express();
|
| 10 |
-
|
| 11 |
-
// Serve static files from public directory
|
| 12 |
-
app.use(express.static(join(__dirname, "public")));
|
| 13 |
-
app.use(express.json());
|
| 14 |
-
|
| 15 |
-
/* ===============================
|
| 16 |
-
1. BASIC AUTH
|
| 17 |
-
================================ */
|
| 18 |
-
|
| 19 |
-
const GATE_KEY = process.env.OPENCLAW_GATE_KEY;
|
| 20 |
-
|
| 21 |
-
app.use((req, res, next) => {
|
| 22 |
-
if (!GATE_KEY) return next();
|
| 23 |
-
if (req.headers["x-openclaw-key"] !== GATE_KEY) {
|
| 24 |
-
return res.status(401).json({ error: "Unauthorized" });
|
| 25 |
-
}
|
| 26 |
-
next();
|
| 27 |
-
});
|
| 28 |
-
|
| 29 |
-
/* ===============================
|
| 30 |
-
2. RATE LIMITING (IP-based)
|
| 31 |
-
================================ */
|
| 32 |
-
|
| 33 |
-
const RATE_LIMIT = 30; // requests
|
| 34 |
-
const WINDOW_MS = 60_000;
|
| 35 |
-
const ipHits = new Map();
|
| 36 |
-
|
| 37 |
-
app.use((req, res, next) => {
|
| 38 |
-
const ip = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
|
| 39 |
-
const now = Date.now();
|
| 40 |
-
|
| 41 |
-
const record = ipHits.get(ip) || { count: 0, ts: now };
|
| 42 |
-
if (now - record.ts > WINDOW_MS) {
|
| 43 |
-
record.count = 0;
|
| 44 |
-
record.ts = now;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
record.count++;
|
| 48 |
-
ipHits.set(ip, record);
|
| 49 |
-
|
| 50 |
-
if (record.count > RATE_LIMIT) {
|
| 51 |
-
return res.status(429).json({ error: "Rate limit exceeded" });
|
| 52 |
-
}
|
| 53 |
-
next();
|
| 54 |
-
});
|
| 55 |
-
|
| 56 |
-
/* ===============================
|
| 57 |
-
3. LOAD KEY POOLS
|
| 58 |
-
================================ */
|
| 59 |
-
|
| 60 |
-
function loadPool(prefix) {
|
| 61 |
-
return Object.keys(process.env)
|
| 62 |
-
.filter(k => k.startsWith(prefix))
|
| 63 |
-
.map(k => process.env[k])
|
| 64 |
-
.filter(Boolean);
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
const PROVIDERS = {
|
| 68 |
-
openai: loadPool("OPENAI_API_KEY_"),
|
| 69 |
-
deepseek: loadPool("DEEPSEEK_API_KEY_"),
|
| 70 |
-
gemini: loadPool("GEMINI_API_KEY_"),
|
| 71 |
-
openrouter: loadPool("OPENROUTER_API_KEY_"),
|
| 72 |
-
dashscope: loadPool("DASHSCOPE_API_KEY_"),
|
| 73 |
-
claude: loadPool("CLAUDE_API_KEY_")
|
| 74 |
-
};
|
| 75 |
-
|
| 76 |
-
function randomKey(pool) {
|
| 77 |
-
return pool[Math.floor(Math.random() * pool.length)];
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
/* ===============================
|
| 81 |
-
4. TASK ROUTING
|
| 82 |
-
================================ */
|
| 83 |
-
|
| 84 |
-
const TASKS = new Set([
|
| 85 |
-
"market_research",
|
| 86 |
-
"summarize",
|
| 87 |
-
"classify"
|
| 88 |
-
]);
|
| 89 |
-
|
| 90 |
-
/* ===============================
|
| 91 |
-
5. RUN AGENT
|
| 92 |
-
================================ */
|
| 93 |
-
app.use((req, res, next) => {
|
| 94 |
-
if (req.method === "POST" && !req.is("application/json")) {
|
| 95 |
-
return res.status(400).json({ error: "JSON body required" });
|
| 96 |
-
}
|
| 97 |
-
next();
|
| 98 |
-
});
|
| 99 |
-
|
| 100 |
-
app.post("/run", async (req, res) => {
|
| 101 |
-
const { task, provider, model, ...payload } = req.body;
|
| 102 |
-
|
| 103 |
-
if (!TASKS.has(task)) {
|
| 104 |
-
return res.status(400).json({ error: "Unknown task" });
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
const providersToTry = provider
|
| 108 |
-
? [provider, ...Object.keys(PROVIDERS).filter(p => p !== provider)]
|
| 109 |
-
: Object.keys(PROVIDERS);
|
| 110 |
-
|
| 111 |
-
let lastError;
|
| 112 |
-
|
| 113 |
-
for (const p of providersToTry) {
|
| 114 |
-
const pool = PROVIDERS[p];
|
| 115 |
-
if (!pool || pool.length === 0) continue;
|
| 116 |
-
|
| 117 |
-
for (let i = 0; i < pool.length; i++) {
|
| 118 |
-
const apiKey = randomKey(pool);
|
| 119 |
-
|
| 120 |
-
const env = {
|
| 121 |
-
...process.env,
|
| 122 |
-
OPENCLAW_PROVIDER: p,
|
| 123 |
-
OPENCLAW_MODEL: model || "",
|
| 124 |
-
OPENCLAW_API_KEY: apiKey,
|
| 125 |
-
OPENCLAW_TASK: task
|
| 126 |
-
};
|
| 127 |
-
|
| 128 |
-
try {
|
| 129 |
-
const result = await runOpenClaw(env, payload);
|
| 130 |
-
return res.json(result);
|
| 131 |
-
} catch (err) {
|
| 132 |
-
lastError = err;
|
| 133 |
-
if (!isRateLimit(err)) break;
|
| 134 |
-
}
|
| 135 |
-
}
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
res.status(500).json({
|
| 139 |
-
error: "All providers failed",
|
| 140 |
-
details: lastError?.message
|
| 141 |
-
});
|
| 142 |
-
});
|
| 143 |
-
|
| 144 |
-
/* ===============================
|
| 145 |
-
6. EXECUTION + JSON PARSE
|
| 146 |
-
================================ */
|
| 147 |
-
|
| 148 |
-
function runOpenClaw(env, payload) {
|
| 149 |
-
return new Promise((resolve, reject) => {
|
| 150 |
-
const proc = spawn("node", ["src/index.js"], {
|
| 151 |
-
cwd: "/app",
|
| 152 |
-
env
|
| 153 |
-
});
|
| 154 |
-
|
| 155 |
-
let out = "";
|
| 156 |
-
let err = "";
|
| 157 |
-
|
| 158 |
-
const timeout = setTimeout(() => {
|
| 159 |
-
proc.kill("SIGKILL");
|
| 160 |
-
reject(new Error("Agent timeout"));
|
| 161 |
-
}, 120000); // 2 minutes
|
| 162 |
-
|
| 163 |
-
proc.stdout.on("data", d => out += d.toString());
|
| 164 |
-
proc.stderr.on("data", d => err += d.toString());
|
| 165 |
-
|
| 166 |
-
proc.on("close", code => {
|
| 167 |
-
clearTimeout(timeout);
|
| 168 |
-
|
| 169 |
-
if (code !== 0) {
|
| 170 |
-
return reject(new Error(err || "Execution failed"));
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
try {
|
| 174 |
-
const json = JSON.parse(out);
|
| 175 |
-
resolve(json);
|
| 176 |
-
} catch {
|
| 177 |
-
reject(new Error("Invalid JSON output from agent"));
|
| 178 |
-
}
|
| 179 |
-
});
|
| 180 |
-
|
| 181 |
-
proc.stdin.write(JSON.stringify(payload));
|
| 182 |
-
proc.stdin.end();
|
| 183 |
-
});
|
| 184 |
-
}
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
function isRateLimit(err) {
|
| 188 |
-
const msg = err.message?.toLowerCase() || "";
|
| 189 |
-
return (
|
| 190 |
-
msg.includes("429") ||
|
| 191 |
-
msg.includes("rate") ||
|
| 192 |
-
msg.includes("quota") ||
|
| 193 |
-
msg.includes("limit")
|
| 194 |
-
);
|
| 195 |
-
}
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
/* ===============================
|
| 199 |
-
7. HEALTH CHECK
|
| 200 |
-
================================ */
|
| 201 |
-
|
| 202 |
-
app.get("/health", (req, res) => {
|
| 203 |
-
res.json({ status: "ok", service: "openclaw-agent" });
|
| 204 |
-
});
|
| 205 |
-
|
| 206 |
-
app.get("/", (req, res) => {
|
| 207 |
-
res.json({
|
| 208 |
-
service: "OpenClaw Agent Gateway",
|
| 209 |
-
endpoints: {
|
| 210 |
-
health: "GET /health",
|
| 211 |
-
run: "POST /run"
|
| 212 |
-
}
|
| 213 |
-
});
|
| 214 |
-
});
|
| 215 |
-
|
| 216 |
-
/* ===============================
|
| 217 |
-
8. START
|
| 218 |
-
================================ */
|
| 219 |
-
|
| 220 |
-
app.listen(7860, () => {
|
| 221 |
-
console.log("🚀 OpenClaw Agent Gateway running");
|
| 222 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/index.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
| 1 |
-
import { marketResearchAgent } from "./marketResearchAgent.js";
|
| 2 |
-
|
| 3 |
-
function readStdin() {
|
| 4 |
-
return new Promise(resolve => {
|
| 5 |
-
let data = "";
|
| 6 |
-
process.stdin.on("data", chunk => data += chunk);
|
| 7 |
-
process.stdin.on("end", () => resolve(data));
|
| 8 |
-
});
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
(async () => {
|
| 12 |
-
try {
|
| 13 |
-
const input = JSON.parse(await readStdin());
|
| 14 |
-
|
| 15 |
-
const result = await marketResearchAgent({
|
| 16 |
-
input,
|
| 17 |
-
provider: process.env.OPENCLAW_PROVIDER,
|
| 18 |
-
model: process.env.OPENCLAW_MODEL
|
| 19 |
-
});
|
| 20 |
-
|
| 21 |
-
// CRITICAL: JSON ONLY
|
| 22 |
-
console.log(JSON.stringify(result));
|
| 23 |
-
} catch (e) {
|
| 24 |
-
console.log(JSON.stringify({
|
| 25 |
-
meta: {
|
| 26 |
-
status: "failed",
|
| 27 |
-
error: e.message
|
| 28 |
-
},
|
| 29 |
-
dashboard_view: {},
|
| 30 |
-
report_view: {}
|
| 31 |
-
}));
|
| 32 |
-
}
|
| 33 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/llm/callLLM.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
| 1 |
-
import { getProviderConfig } from "./providers.js";
|
| 2 |
-
|
| 3 |
-
export async function callLLM({ provider, model, prompt }) {
|
| 4 |
-
const cfg = getProviderConfig(provider, model);
|
| 5 |
-
|
| 6 |
-
let body;
|
| 7 |
-
|
| 8 |
-
switch (cfg.name) {
|
| 9 |
-
case "gemini":
|
| 10 |
-
body = {
|
| 11 |
-
contents: [{ parts: [{ text: prompt }] }]
|
| 12 |
-
};
|
| 13 |
-
break;
|
| 14 |
-
|
| 15 |
-
case "dashscope":
|
| 16 |
-
body = {
|
| 17 |
-
model: cfg.model,
|
| 18 |
-
input: { prompt }
|
| 19 |
-
};
|
| 20 |
-
break;
|
| 21 |
-
|
| 22 |
-
case "claude":
|
| 23 |
-
body = {
|
| 24 |
-
model: cfg.model,
|
| 25 |
-
max_tokens: 4096,
|
| 26 |
-
messages: [
|
| 27 |
-
{ role: "user", content: prompt }
|
| 28 |
-
],
|
| 29 |
-
system: "You are a market research analyst."
|
| 30 |
-
};
|
| 31 |
-
break;
|
| 32 |
-
|
| 33 |
-
default:
|
| 34 |
-
body = {
|
| 35 |
-
model: cfg.model,
|
| 36 |
-
messages: [
|
| 37 |
-
{ role: "system", content: "You are a market research analyst." },
|
| 38 |
-
{ role: "user", content: prompt }
|
| 39 |
-
],
|
| 40 |
-
temperature: 0.4
|
| 41 |
-
};
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
const res = await fetch(cfg.baseUrl, {
|
| 45 |
-
method: "POST",
|
| 46 |
-
headers: cfg.headers,
|
| 47 |
-
body: JSON.stringify(body)
|
| 48 |
-
});
|
| 49 |
-
|
| 50 |
-
const text = await res.text();
|
| 51 |
-
|
| 52 |
-
if (!res.ok) {
|
| 53 |
-
throw new Error(`LLM error ${res.status}: ${text}`);
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
const json = JSON.parse(text);
|
| 57 |
-
|
| 58 |
-
// Normalize output
|
| 59 |
-
if (cfg.name === "gemini") {
|
| 60 |
-
return json.candidates?.[0]?.content?.parts?.[0]?.text || "";
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
if (cfg.name === "dashscope") {
|
| 64 |
-
return json.output?.text || "";
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
if (cfg.name === "claude") {
|
| 68 |
-
return json.content?.[0]?.text || "";
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
return json.choices?.[0]?.message?.content || "";
|
| 72 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/llm/providers.js
DELETED
|
@@ -1,94 +0,0 @@
|
|
| 1 |
-
export function getProviderConfig(provider, model) {
|
| 2 |
-
const apiKey = process.env.OPENCLAW_API_KEY;
|
| 3 |
-
|
| 4 |
-
if (!apiKey) {
|
| 5 |
-
throw new Error("OPENCLAW_API_KEY not set");
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
// Support custom base URLs via environment variables
|
| 9 |
-
// Format: OPENCLAW_<PROVIDER>_BASE_URL
|
| 10 |
-
const customBaseUrl = process.env[`OPENCLAW_${provider.toUpperCase()}_BASE_URL`];
|
| 11 |
-
|
| 12 |
-
switch (provider) {
|
| 13 |
-
case "openai":
|
| 14 |
-
return {
|
| 15 |
-
name: "openai",
|
| 16 |
-
baseUrl: customBaseUrl || "https://api.openai.com/v1/chat/completions",
|
| 17 |
-
model: model || "gpt-4.1-mini",
|
| 18 |
-
headers: {
|
| 19 |
-
"Authorization": `Bearer ${apiKey}`,
|
| 20 |
-
"Content-Type": "application/json"
|
| 21 |
-
}
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
case "deepseek":
|
| 25 |
-
return {
|
| 26 |
-
name: "deepseek",
|
| 27 |
-
baseUrl: customBaseUrl || "https://api.deepseek.com/chat/completions",
|
| 28 |
-
model: model || "deepseek-chat",
|
| 29 |
-
headers: {
|
| 30 |
-
"Authorization": `Bearer ${apiKey}`,
|
| 31 |
-
"Content-Type": "application/json"
|
| 32 |
-
}
|
| 33 |
-
};
|
| 34 |
-
|
| 35 |
-
case "gemini":
|
| 36 |
-
if (customBaseUrl) {
|
| 37 |
-
return {
|
| 38 |
-
name: "gemini",
|
| 39 |
-
baseUrl: `${customBaseUrl}/${model || "gemini-1.5-pro"}:generateContent?key=${apiKey}`,
|
| 40 |
-
model,
|
| 41 |
-
headers: {
|
| 42 |
-
"Content-Type": "application/json"
|
| 43 |
-
}
|
| 44 |
-
};
|
| 45 |
-
}
|
| 46 |
-
return {
|
| 47 |
-
name: "gemini",
|
| 48 |
-
baseUrl: `https://generativelanguage.googleapis.com/v1beta/models/${model || "gemini-1.5-pro"}:generateContent?key=${apiKey}`,
|
| 49 |
-
model,
|
| 50 |
-
headers: {
|
| 51 |
-
"Content-Type": "application/json"
|
| 52 |
-
}
|
| 53 |
-
};
|
| 54 |
-
|
| 55 |
-
case "openrouter":
|
| 56 |
-
return {
|
| 57 |
-
name: "openrouter",
|
| 58 |
-
baseUrl: customBaseUrl || "https://openrouter.ai/api/v1/chat/completions",
|
| 59 |
-
model: model || "openai/gpt-4o-mini",
|
| 60 |
-
headers: {
|
| 61 |
-
"Authorization": `Bearer ${apiKey}`,
|
| 62 |
-
"Content-Type": "application/json",
|
| 63 |
-
"HTTP-Referer": "https://sim4imgbed-openclaw.hf.space",
|
| 64 |
-
"X-Title": "OpenClaw Market Research Agent"
|
| 65 |
-
}
|
| 66 |
-
};
|
| 67 |
-
|
| 68 |
-
case "dashscope":
|
| 69 |
-
return {
|
| 70 |
-
name: "dashscope",
|
| 71 |
-
baseUrl: customBaseUrl || "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation",
|
| 72 |
-
model: model || "qwen-plus",
|
| 73 |
-
headers: {
|
| 74 |
-
"Authorization": `Bearer ${apiKey}`,
|
| 75 |
-
"Content-Type": "application/json"
|
| 76 |
-
}
|
| 77 |
-
};
|
| 78 |
-
|
| 79 |
-
case "claude":
|
| 80 |
-
return {
|
| 81 |
-
name: "claude",
|
| 82 |
-
baseUrl: customBaseUrl || "https://api.anthropic.com/v1/messages",
|
| 83 |
-
model: model || "claude-sonnet-4-20250514",
|
| 84 |
-
headers: {
|
| 85 |
-
"x-api-key": apiKey,
|
| 86 |
-
"anthropic-version": "2023-06-01",
|
| 87 |
-
"Content-Type": "application/json"
|
| 88 |
-
}
|
| 89 |
-
};
|
| 90 |
-
|
| 91 |
-
default:
|
| 92 |
-
throw new Error(`Unsupported provider: ${provider}`);
|
| 93 |
-
}
|
| 94 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/marketResearchAgent.js
DELETED
|
@@ -1,266 +0,0 @@
|
|
| 1 |
-
import crypto from "crypto";
|
| 2 |
-
|
| 3 |
-
/* =========================================================
|
| 4 |
-
PUBLIC ENTRY
|
| 5 |
-
========================================================= */
|
| 6 |
-
|
| 7 |
-
export async function marketResearchAgent({ input, provider, model }) {
|
| 8 |
-
if (!input?.keyword) {
|
| 9 |
-
throw new Error("keyword is required");
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const keyword = normalizeKeyword(input.keyword);
|
| 13 |
-
const normalizedTitle = `Global ${keyword} Market and Forecast 2026-2033`;
|
| 14 |
-
|
| 15 |
-
/* =======================================================
|
| 16 |
-
1. GENERATE CORE MARKET ESTIMATES (LLM or heuristic)
|
| 17 |
-
======================================================= */
|
| 18 |
-
|
| 19 |
-
// NOTE:
|
| 20 |
-
// Replace this block with a real LLM call later.
|
| 21 |
-
// This placeholder is deterministic + schema-safe.
|
| 22 |
-
const baseMarket2023 = randomFloat(0.5, 5.0);
|
| 23 |
-
const cagr = randomFloat(8, 22);
|
| 24 |
-
const market2025 = round(baseMarket2023 * Math.pow(1 + cagr / 100, 2));
|
| 25 |
-
const market2033 = round(baseMarket2023 * Math.pow(1 + cagr / 100, 10));
|
| 26 |
-
|
| 27 |
-
/* =======================================================
|
| 28 |
-
2. BUILD DASHBOARD VIEW
|
| 29 |
-
======================================================= */
|
| 30 |
-
|
| 31 |
-
const dashboard_view = {
|
| 32 |
-
marketTitle: normalizedTitle,
|
| 33 |
-
|
| 34 |
-
marketSummary: {
|
| 35 |
-
past2023: baseMarket2023,
|
| 36 |
-
current2025: market2025,
|
| 37 |
-
forecast2033: market2033,
|
| 38 |
-
cagr
|
| 39 |
-
},
|
| 40 |
-
|
| 41 |
-
forecast: [
|
| 42 |
-
{ year: "2023", value: baseMarket2023 },
|
| 43 |
-
{ year: "2025", value: market2025 },
|
| 44 |
-
{ year: "2033", value: market2033 }
|
| 45 |
-
],
|
| 46 |
-
|
| 47 |
-
regional: buildRegions(),
|
| 48 |
-
|
| 49 |
-
competitive: buildTopPlayers(),
|
| 50 |
-
|
| 51 |
-
segments: buildSegments(),
|
| 52 |
-
|
| 53 |
-
drivers: [
|
| 54 |
-
{ driver: "Technology adoption", impact: 92 },
|
| 55 |
-
{ driver: "Rising healthcare demand", impact: 88 },
|
| 56 |
-
{ driver: "Digital transformation", impact: 85 }
|
| 57 |
-
],
|
| 58 |
-
|
| 59 |
-
insights: {
|
| 60 |
-
largestSegment2025: "Primary Segment",
|
| 61 |
-
fastestGrowingSegment: "AI-Enabled Solutions",
|
| 62 |
-
keyOpportunities: [
|
| 63 |
-
"Emerging markets",
|
| 64 |
-
"AI integration",
|
| 65 |
-
"Cost-efficient platforms"
|
| 66 |
-
],
|
| 67 |
-
majorChallenges: [
|
| 68 |
-
"Regulatory approvals",
|
| 69 |
-
"High initial investment",
|
| 70 |
-
"Skilled workforce shortage"
|
| 71 |
-
]
|
| 72 |
-
},
|
| 73 |
-
|
| 74 |
-
citation: "iHealthcareAnalyst, Inc. Internal Data Analysis",
|
| 75 |
-
citationUrl: "https://www.ihealthcareanalyst.com"
|
| 76 |
-
};
|
| 77 |
-
|
| 78 |
-
/* =======================================================
|
| 79 |
-
3. BUILD REPORT VIEW
|
| 80 |
-
======================================================= */
|
| 81 |
-
|
| 82 |
-
const report_view = {
|
| 83 |
-
marketTitle: keyword,
|
| 84 |
-
|
| 85 |
-
marketSizing: {
|
| 86 |
-
pastYear_2023: baseMarket2023,
|
| 87 |
-
currentYear_2025: market2025,
|
| 88 |
-
forecastYear_2033: market2033,
|
| 89 |
-
global_cagr_Forecast: cagr
|
| 90 |
-
},
|
| 91 |
-
|
| 92 |
-
executiveOverview:
|
| 93 |
-
`The ${keyword} market is experiencing accelerated growth driven by technological innovation, increasing healthcare demand, and expanding adoption across developed and emerging economies.`,
|
| 94 |
-
|
| 95 |
-
marketSegments: [
|
| 96 |
-
{
|
| 97 |
-
segmentName: "Primary Segment",
|
| 98 |
-
subSegments: [
|
| 99 |
-
{
|
| 100 |
-
subSegmentName: "Core Products",
|
| 101 |
-
segmet_marketShare_2023: 40,
|
| 102 |
-
segment_marketShare_2025: 45,
|
| 103 |
-
segment_marketShare_2033: 50,
|
| 104 |
-
segmentName_cagr_Forecast: round(cagr + 2)
|
| 105 |
-
}
|
| 106 |
-
]
|
| 107 |
-
}
|
| 108 |
-
],
|
| 109 |
-
|
| 110 |
-
marketDrivers: [
|
| 111 |
-
"Growing demand for advanced healthcare solutions",
|
| 112 |
-
"Increased investment in digital health",
|
| 113 |
-
"Favorable government initiatives"
|
| 114 |
-
],
|
| 115 |
-
|
| 116 |
-
emergingTrends: [
|
| 117 |
-
"AI-driven diagnostics",
|
| 118 |
-
"Cloud-based healthcare platforms",
|
| 119 |
-
"Personalized medicine"
|
| 120 |
-
],
|
| 121 |
-
|
| 122 |
-
competitiveLandscape: [
|
| 123 |
-
{
|
| 124 |
-
company: "Company A",
|
| 125 |
-
player_markerShare_2025: 18,
|
| 126 |
-
positioning: "Market leader with diversified portfolio"
|
| 127 |
-
},
|
| 128 |
-
{
|
| 129 |
-
company: "Company B",
|
| 130 |
-
player_markerShare_2025: 14,
|
| 131 |
-
positioning: "Strong innovation focus"
|
| 132 |
-
}
|
| 133 |
-
],
|
| 134 |
-
|
| 135 |
-
regulatoryEnvironment:
|
| 136 |
-
"The market is regulated by stringent healthcare and data protection regulations, with increasing emphasis on patient safety and clinical validation.",
|
| 137 |
-
|
| 138 |
-
geographicAnalysis:
|
| 139 |
-
"North America leads the market due to early adoption, while Asia-Pacific is expected to witness the fastest growth driven by expanding healthcare infrastructure.",
|
| 140 |
-
|
| 141 |
-
futureOutlook:
|
| 142 |
-
"The market is projected to grow steadily through 2033, supported by innovation, demographic trends, and increased healthcare spending.",
|
| 143 |
-
|
| 144 |
-
strategicRecommendations: [
|
| 145 |
-
"Invest in AI-enabled platforms",
|
| 146 |
-
"Expand presence in emerging markets",
|
| 147 |
-
"Strengthen regulatory compliance capabilities"
|
| 148 |
-
]
|
| 149 |
-
};
|
| 150 |
-
|
| 151 |
-
/* =======================================================
|
| 152 |
-
4. META + FINAL ASSEMBLY
|
| 153 |
-
======================================================= */
|
| 154 |
-
|
| 155 |
-
const result = {
|
| 156 |
-
meta: {
|
| 157 |
-
job_id: input.job_id || `job_${crypto.randomUUID()}`,
|
| 158 |
-
keyword,
|
| 159 |
-
normalized_title: normalizedTitle,
|
| 160 |
-
status: "completed",
|
| 161 |
-
timestamp: new Date().toISOString(),
|
| 162 |
-
provider_used: provider || "auto",
|
| 163 |
-
model_used: model || "auto",
|
| 164 |
-
confidence: "medium"
|
| 165 |
-
},
|
| 166 |
-
dashboard_view,
|
| 167 |
-
report_view
|
| 168 |
-
};
|
| 169 |
-
|
| 170 |
-
/* =======================================================
|
| 171 |
-
5. SCHEMA ENFORCEMENT (STEP A5)
|
| 172 |
-
======================================================= */
|
| 173 |
-
|
| 174 |
-
validateDashboard(result.dashboard_view);
|
| 175 |
-
validateReport(result.report_view);
|
| 176 |
-
|
| 177 |
-
return result;
|
| 178 |
-
}
|
| 179 |
-
|
| 180 |
-
/* =========================================================
|
| 181 |
-
SCHEMA VALIDATION
|
| 182 |
-
========================================================= */
|
| 183 |
-
|
| 184 |
-
function validateDashboard(d) {
|
| 185 |
-
if (!d.marketTitle) throw new Error("dashboard_view.marketTitle missing");
|
| 186 |
-
if (!d.marketSummary) throw new Error("dashboard_view.marketSummary missing");
|
| 187 |
-
|
| 188 |
-
const ms = d.marketSummary;
|
| 189 |
-
requireNumber(ms.past2023, "past2023");
|
| 190 |
-
requireNumber(ms.current2025, "current2025");
|
| 191 |
-
requireNumber(ms.forecast2033, "forecast2033");
|
| 192 |
-
requireNumber(ms.cagr, "cagr");
|
| 193 |
-
|
| 194 |
-
if (!Array.isArray(d.forecast)) throw new Error("forecast must be array");
|
| 195 |
-
if (!Array.isArray(d.segments)) throw new Error("segments must be array");
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
function validateReport(r) {
|
| 199 |
-
if (!r.marketTitle) throw new Error("report_view.marketTitle missing");
|
| 200 |
-
if (!r.marketSizing) throw new Error("marketSizing missing");
|
| 201 |
-
|
| 202 |
-
const ms = r.marketSizing;
|
| 203 |
-
requireNumber(ms.pastYear_2023, "pastYear_2023");
|
| 204 |
-
requireNumber(ms.currentYear_2025, "currentYear_2025");
|
| 205 |
-
requireNumber(ms.forecastYear_2033, "forecastYear_2033");
|
| 206 |
-
requireNumber(ms.global_cagr_Forecast, "global_cagr_Forecast");
|
| 207 |
-
|
| 208 |
-
if (!Array.isArray(r.marketSegments)) {
|
| 209 |
-
throw new Error("marketSegments must be array");
|
| 210 |
-
}
|
| 211 |
-
}
|
| 212 |
-
|
| 213 |
-
/* =========================================================
|
| 214 |
-
HELPERS
|
| 215 |
-
========================================================= */
|
| 216 |
-
|
| 217 |
-
function normalizeKeyword(k) {
|
| 218 |
-
return k.replace(/market/gi, "").trim();
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
function round(n) {
|
| 222 |
-
return Math.round(n * 100) / 100;
|
| 223 |
-
}
|
| 224 |
-
|
| 225 |
-
function randomFloat(min, max) {
|
| 226 |
-
return round(min + Math.random() * (max - min));
|
| 227 |
-
}
|
| 228 |
-
|
| 229 |
-
function requireNumber(v, name) {
|
| 230 |
-
if (typeof v !== "number" || Number.isNaN(v)) {
|
| 231 |
-
throw new Error(`${name} must be a number`);
|
| 232 |
-
}
|
| 233 |
-
}
|
| 234 |
-
|
| 235 |
-
function buildRegions() {
|
| 236 |
-
return [
|
| 237 |
-
{ region: "North America", share: 42, cagr: 14.2 },
|
| 238 |
-
{ region: "Europe", share: 28, cagr: 12.8 },
|
| 239 |
-
{ region: "Asia Pacific", share: 22, cagr: 18.6 },
|
| 240 |
-
{ region: "Latin America", share: 5, cagr: 11.3 },
|
| 241 |
-
{ region: "Middle East & Africa", share: 3, cagr: 10.9 }
|
| 242 |
-
];
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
function buildTopPlayers() {
|
| 246 |
-
return [
|
| 247 |
-
{ company: "Company A", share: 18 },
|
| 248 |
-
{ company: "Company B", share: 15 },
|
| 249 |
-
{ company: "Company C", share: 12 },
|
| 250 |
-
{ company: "Company D", share: 10 },
|
| 251 |
-
{ company: "Company E", share: 8 }
|
| 252 |
-
];
|
| 253 |
-
}
|
| 254 |
-
|
| 255 |
-
function buildSegments() {
|
| 256 |
-
return [
|
| 257 |
-
{
|
| 258 |
-
segment: "Primary Segment",
|
| 259 |
-
subSegments: [
|
| 260 |
-
{ name: "Core Products", share2025: 45, cagr: 16.5 },
|
| 261 |
-
{ name: "AI Solutions", share2025: 35, cagr: 19.2 },
|
| 262 |
-
{ name: "Services", share2025: 20, cagr: 13.1 }
|
| 263 |
-
]
|
| 264 |
-
}
|
| 265 |
-
];
|
| 266 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/schema/dashboard.schema.js
DELETED
|
@@ -1,39 +0,0 @@
|
|
| 1 |
-
export function validateDashboard(d) {
|
| 2 |
-
if (!d) throw new Error("dashboard_view missing");
|
| 3 |
-
|
| 4 |
-
if (!d.marketTitle) {
|
| 5 |
-
throw new Error("dashboard_view.marketTitle missing");
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
if (!d.marketSummary) {
|
| 9 |
-
throw new Error("dashboard_view.marketSummary missing");
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const ms = d.marketSummary;
|
| 13 |
-
requireNumber(ms.past2023, "past2023");
|
| 14 |
-
requireNumber(ms.current2025, "current2025");
|
| 15 |
-
requireNumber(ms.forecast2033, "forecast2033");
|
| 16 |
-
requireNumber(ms.cagr, "cagr");
|
| 17 |
-
|
| 18 |
-
if (!Array.isArray(d.forecast)) {
|
| 19 |
-
throw new Error("dashboard_view.forecast must be array");
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
if (!Array.isArray(d.regional)) {
|
| 23 |
-
throw new Error("dashboard_view.regional must be array");
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
if (!Array.isArray(d.segments)) {
|
| 27 |
-
throw new Error("dashboard_view.segments must be array");
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
if (!Array.isArray(d.competitive)) {
|
| 31 |
-
throw new Error("dashboard_view.competitive must be array");
|
| 32 |
-
}
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
function requireNumber(value, name) {
|
| 36 |
-
if (typeof value !== "number" || Number.isNaN(value)) {
|
| 37 |
-
throw new Error(`dashboard_view.marketSummary.${name} must be number`);
|
| 38 |
-
}
|
| 39 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/schema/report.schema.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
export function validateReport(r) {
|
| 2 |
-
if (!r) throw new Error("report_view missing");
|
| 3 |
-
|
| 4 |
-
if (!r.marketTitle) {
|
| 5 |
-
throw new Error("report_view.marketTitle missing");
|
| 6 |
-
}
|
| 7 |
-
|
| 8 |
-
if (!r.marketSizing) {
|
| 9 |
-
throw new Error("report_view.marketSizing missing");
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
const ms = r.marketSizing;
|
| 13 |
-
requireNumber(ms.pastYear_2023, "pastYear_2023");
|
| 14 |
-
requireNumber(ms.currentYear_2025, "currentYear_2025");
|
| 15 |
-
requireNumber(ms.forecastYear_2033, "forecastYear_2033");
|
| 16 |
-
requireNumber(ms.global_cagr_Forecast, "global_cagr_Forecast");
|
| 17 |
-
|
| 18 |
-
if (!Array.isArray(r.marketSegments)) {
|
| 19 |
-
throw new Error("report_view.marketSegments must be array");
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
if (!Array.isArray(r.marketDrivers)) {
|
| 23 |
-
throw new Error("report_view.marketDrivers must be array");
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
if (!Array.isArray(r.strategicRecommendations)) {
|
| 27 |
-
throw new Error("report_view.strategicRecommendations must be array");
|
| 28 |
-
}
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
function requireNumber(value, name) {
|
| 32 |
-
if (typeof value !== "number" || Number.isNaN(value)) {
|
| 33 |
-
throw new Error(`report_view.marketSizing.${name} must be number`);
|
| 34 |
-
}
|
| 35 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|