@tradejs/app 1.0.4 → 1.0.6
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 +1 -1
- package/bin/tradejs-app.mjs +122 -20
- package/package.json +18 -8
- package/public/auth-bg.jpg +0 -0
- package/public/next.svg +1 -0
- package/public/og-image-source.svg +91 -0
- package/public/og-image.png +0 -0
- package/public/vercel.svg +1 -0
- package/src/app/api/ai/route.ts +26 -5
- package/src/app/api/files/screenshot/[name]/route.ts +53 -17
- package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +7 -1
- package/src/app/api/scanner/[provider]/route.ts +7 -1
- package/src/app/api/scanner/route.ts +7 -1
- package/src/app/api/user/settings/route.ts +216 -0
- package/src/app/components/Dashboard/MainChart/index.tsx +10 -2
- package/src/app/components/Shared/Sidebar/AccountSettingsDrawer.tsx +809 -0
- package/src/app/components/Shared/Sidebar/index.tsx +13 -9
- package/src/app/globals.css +11 -0
- package/src/app/layout.tsx +41 -4
- package/src/app/lib/currentUser.ts +27 -0
- package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +22 -19
- package/src/app/routes/signin/page.tsx +11 -2
- package/src/app/store/data.ts +192 -87
package/README.md
CHANGED
package/bin/tradejs-app.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawn } from 'child_process';
|
|
4
4
|
import { createRequire } from 'module';
|
|
5
|
+
import net from 'net';
|
|
5
6
|
import path from 'path';
|
|
6
7
|
import { fileURLToPath } from 'url';
|
|
7
8
|
import nextEnv from '@next/env';
|
|
@@ -11,6 +12,13 @@ const { loadEnvConfig } = nextEnv;
|
|
|
11
12
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
13
|
const __dirname = path.dirname(__filename);
|
|
13
14
|
const appDir = path.resolve(__dirname, '..');
|
|
15
|
+
const LOCAL_DEV_HOSTNAMES = new Set([
|
|
16
|
+
'localhost',
|
|
17
|
+
'127.0.0.1',
|
|
18
|
+
'0.0.0.0',
|
|
19
|
+
'::1',
|
|
20
|
+
'[::1]',
|
|
21
|
+
]);
|
|
14
22
|
|
|
15
23
|
const command = process.argv[2] || 'dev';
|
|
16
24
|
const rawArgs = process.argv.slice(3);
|
|
@@ -23,32 +31,126 @@ process.env.PROJECT_CWD = projectCwd;
|
|
|
23
31
|
const dev = command === 'dev';
|
|
24
32
|
loadEnvConfig(projectCwd, dev, console);
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
const
|
|
34
|
+
function parsePort(value) {
|
|
35
|
+
const parsed = Number.parseInt(String(value || '').trim(), 10);
|
|
36
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readArgValue(args, flags) {
|
|
40
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
41
|
+
const arg = args[index];
|
|
42
|
+
if (flags.includes(arg)) {
|
|
43
|
+
return args[index + 1] || null;
|
|
44
|
+
}
|
|
45
|
+
const matchedFlag = flags.find((flag) => arg.startsWith(`${flag}=`));
|
|
46
|
+
if (matchedFlag) {
|
|
47
|
+
return arg.slice(matchedFlag.length + 1) || null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
28
50
|
|
|
29
|
-
|
|
30
|
-
command === 'start' &&
|
|
31
|
-
!rawArgs.includes('-H') &&
|
|
32
|
-
!rawArgs.includes('--hostname')
|
|
33
|
-
) {
|
|
34
|
-
args.push('-H', '0.0.0.0');
|
|
51
|
+
return null;
|
|
35
52
|
}
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
54
|
+
async function isPortAvailable(port) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const server = net.createServer();
|
|
57
|
+
server.unref();
|
|
58
|
+
server.once('error', () => resolve(false));
|
|
59
|
+
server.listen(port, () => {
|
|
60
|
+
server.close(() => resolve(true));
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function findAvailablePort(startPort, attempts = 20) {
|
|
66
|
+
for (let offset = 0; offset < attempts; offset += 1) {
|
|
67
|
+
const port = startPort + offset;
|
|
68
|
+
if (await isPortAvailable(port)) {
|
|
69
|
+
return port;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
42
72
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function syncLocalUrlEnv(name, fromPort, toPort) {
|
|
77
|
+
const rawValue = String(process.env[name] || '').trim();
|
|
78
|
+
if (!rawValue) {
|
|
79
|
+
process.env[name] = `http://localhost:${toPort}`;
|
|
46
80
|
return;
|
|
47
81
|
}
|
|
48
|
-
process.exit(code ?? 0);
|
|
49
|
-
});
|
|
50
82
|
|
|
51
|
-
|
|
52
|
-
|
|
83
|
+
try {
|
|
84
|
+
const url = new URL(rawValue);
|
|
85
|
+
const currentPort =
|
|
86
|
+
parsePort(url.port) || (url.protocol === 'https:' ? 443 : 80);
|
|
87
|
+
if (!LOCAL_DEV_HOSTNAMES.has(url.hostname) || currentPort !== fromPort) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
url.port = String(toPort);
|
|
91
|
+
process.env[name] = url.toString();
|
|
92
|
+
} catch {
|
|
93
|
+
// Ignore invalid URLs and leave user-provided values untouched.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
const nextBin = require.resolve('next/dist/bin/next');
|
|
99
|
+
const args = [nextBin, command, ...rawArgs];
|
|
100
|
+
const explicitPort = parsePort(readArgValue(rawArgs, ['-p', '--port']));
|
|
101
|
+
|
|
102
|
+
if (dev && explicitPort === null) {
|
|
103
|
+
const requestedPort = parsePort(process.env.PORT) || 3000;
|
|
104
|
+
const resolvedPort = await findAvailablePort(requestedPort);
|
|
105
|
+
|
|
106
|
+
if (resolvedPort === null) {
|
|
107
|
+
console.error(
|
|
108
|
+
`[tradejs-app] no available dev port found starting at ${requestedPort}`,
|
|
109
|
+
);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.env.PORT = String(resolvedPort);
|
|
114
|
+
args.push('-p', String(resolvedPort));
|
|
115
|
+
|
|
116
|
+
if (resolvedPort !== requestedPort) {
|
|
117
|
+
syncLocalUrlEnv('APP_URL', requestedPort, resolvedPort);
|
|
118
|
+
syncLocalUrlEnv('NEXTAUTH_URL', requestedPort, resolvedPort);
|
|
119
|
+
console.warn(
|
|
120
|
+
`[tradejs-app] port ${requestedPort} is busy, using ${resolvedPort} instead`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
command === 'start' &&
|
|
127
|
+
!rawArgs.includes('-H') &&
|
|
128
|
+
!rawArgs.includes('--hostname')
|
|
129
|
+
) {
|
|
130
|
+
args.push('-H', '0.0.0.0');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const child = spawn(process.execPath, args, {
|
|
134
|
+
cwd: appDir,
|
|
135
|
+
env: process.env,
|
|
136
|
+
stdio: 'inherit',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
child.on('exit', (code, signal) => {
|
|
140
|
+
if (signal) {
|
|
141
|
+
process.kill(process.pid, signal);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
process.exit(code ?? 0);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
child.on('error', (error) => {
|
|
148
|
+
console.error('[tradejs-app] failed to start Next.js:', error);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main().catch((error) => {
|
|
154
|
+
console.error('[tradejs-app] failed to initialize:', error);
|
|
53
155
|
process.exit(1);
|
|
54
156
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
|
+
"description": "Installable Next.js UI for the TradeJS open-source framework: dashboards, backtests, charts, and runtime data.",
|
|
4
5
|
"keywords": [
|
|
5
6
|
"tradejs",
|
|
6
7
|
"trading",
|
|
@@ -13,6 +14,7 @@
|
|
|
13
14
|
"files": [
|
|
14
15
|
"README.md",
|
|
15
16
|
"bin",
|
|
17
|
+
"public",
|
|
16
18
|
"src",
|
|
17
19
|
"!src/**/__tests__",
|
|
18
20
|
"!src/**/*.test.ts",
|
|
@@ -27,24 +29,32 @@
|
|
|
27
29
|
"publishConfig": {
|
|
28
30
|
"access": "public"
|
|
29
31
|
},
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/tradejs-dev/tradejs.git",
|
|
35
|
+
"directory": "apps/app"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/tradejs-dev/tradejs/issues"
|
|
39
|
+
},
|
|
30
40
|
"dependencies": {
|
|
31
41
|
"@chakra-ui/charts": "^3.24.0",
|
|
32
42
|
"@chakra-ui/react": "^3.24.2",
|
|
33
43
|
"@emotion/react": "^11.14.0",
|
|
34
44
|
"@langchain/core": "^0.3.68",
|
|
35
45
|
"@langchain/openai": "^0.6.11",
|
|
36
|
-
"@tradejs/connectors": "^1.0.
|
|
37
|
-
"@tradejs/core": "^1.0.
|
|
38
|
-
"@tradejs/indicators": "^1.0.
|
|
39
|
-
"@tradejs/infra": "^1.0.
|
|
40
|
-
"@tradejs/node": "^1.0.
|
|
41
|
-
"@tradejs/types": "^1.0.
|
|
46
|
+
"@tradejs/connectors": "^1.0.6",
|
|
47
|
+
"@tradejs/core": "^1.0.6",
|
|
48
|
+
"@tradejs/indicators": "^1.0.6",
|
|
49
|
+
"@tradejs/infra": "^1.0.6",
|
|
50
|
+
"@tradejs/node": "^1.0.6",
|
|
51
|
+
"@tradejs/types": "^1.0.6",
|
|
42
52
|
"bcryptjs": "^2.4.3",
|
|
43
53
|
"date-fns": "^3.3.1",
|
|
44
54
|
"idb-keyval": "^6.2.2",
|
|
45
55
|
"klinecharts": "10.0.0-alpha9",
|
|
46
56
|
"lodash": "^4.17.21",
|
|
47
|
-
"next": "^16.1.
|
|
57
|
+
"next": "^16.1.7",
|
|
48
58
|
"next-auth": "^5.0.0-beta.26",
|
|
49
59
|
"next-themes": "^0.4.6",
|
|
50
60
|
"react": "^19.2.3",
|
|
Binary file
|
package/public/next.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<svg
|
|
2
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
3
|
+
width="1200"
|
|
4
|
+
height="630"
|
|
5
|
+
viewBox="0 0 1200 630"
|
|
6
|
+
fill="none"
|
|
7
|
+
>
|
|
8
|
+
<defs>
|
|
9
|
+
<linearGradient id="bg" x1="104" y1="44" x2="1090" y2="648" gradientUnits="userSpaceOnUse">
|
|
10
|
+
<stop offset="0" stop-color="#0C1728" />
|
|
11
|
+
<stop offset="0.46" stop-color="#0A1320" />
|
|
12
|
+
<stop offset="1" stop-color="#07101A" />
|
|
13
|
+
</linearGradient>
|
|
14
|
+
<radialGradient id="glow-left" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(168 58) rotate(56.115) scale(498.404 523.217)">
|
|
15
|
+
<stop offset="0" stop-color="#20C5BD" stop-opacity="0.23" />
|
|
16
|
+
<stop offset="0.48" stop-color="#20C5BD" stop-opacity="0.08" />
|
|
17
|
+
<stop offset="1" stop-color="#20C5BD" stop-opacity="0" />
|
|
18
|
+
</radialGradient>
|
|
19
|
+
<radialGradient id="glow-right" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(1086 30) rotate(112.105) scale(468.802 625.774)">
|
|
20
|
+
<stop offset="0" stop-color="#4CA9FF" stop-opacity="0.17" />
|
|
21
|
+
<stop offset="0.44" stop-color="#4CA9FF" stop-opacity="0.06" />
|
|
22
|
+
<stop offset="1" stop-color="#4CA9FF" stop-opacity="0" />
|
|
23
|
+
</radialGradient>
|
|
24
|
+
<linearGradient id="frame" x1="160" y1="86" x2="1084" y2="544" gradientUnits="userSpaceOnUse">
|
|
25
|
+
<stop offset="0" stop-color="#27445F" stop-opacity="0.9" />
|
|
26
|
+
<stop offset="1" stop-color="#1CCAC0" stop-opacity="0.62" />
|
|
27
|
+
</linearGradient>
|
|
28
|
+
<linearGradient id="glyph" x1="0" y1="0" x2="1" y2="1">
|
|
29
|
+
<stop offset="0" stop-color="#28EAD3" />
|
|
30
|
+
<stop offset="1" stop-color="#1CCAC0" />
|
|
31
|
+
</linearGradient>
|
|
32
|
+
<linearGradient id="chart" x1="706" y1="430" x2="1110" y2="236" gradientUnits="userSpaceOnUse">
|
|
33
|
+
<stop offset="0" stop-color="#20C5BD" stop-opacity="0.22" />
|
|
34
|
+
<stop offset="0.38" stop-color="#20C5BD" stop-opacity="0.95" />
|
|
35
|
+
<stop offset="1" stop-color="#7CE8E2" stop-opacity="0.95" />
|
|
36
|
+
</linearGradient>
|
|
37
|
+
<pattern id="grid" width="36" height="36" patternUnits="userSpaceOnUse">
|
|
38
|
+
<path d="M36 0H0V36" stroke="#203347" stroke-opacity="0.32" />
|
|
39
|
+
</pattern>
|
|
40
|
+
</defs>
|
|
41
|
+
|
|
42
|
+
<rect width="1200" height="630" fill="url(#bg)" />
|
|
43
|
+
<rect width="1200" height="630" fill="url(#glow-left)" />
|
|
44
|
+
<rect width="1200" height="630" fill="url(#glow-right)" />
|
|
45
|
+
<rect width="1200" height="630" fill="url(#grid)" opacity="0.62" />
|
|
46
|
+
<rect x="64" y="64" width="1072" height="502" rx="34" fill="#0E1726" fill-opacity="0.55" stroke="url(#frame)" />
|
|
47
|
+
<rect x="96" y="96" width="148" height="38" rx="19" fill="#112233" stroke="#27445F" />
|
|
48
|
+
<text x="129" y="121" fill="#9EEDE8" font-family="Arial, Helvetica, sans-serif" font-size="15" font-weight="700" letter-spacing="2.3">
|
|
49
|
+
APP UI
|
|
50
|
+
</text>
|
|
51
|
+
|
|
52
|
+
<g transform="translate(96 170)">
|
|
53
|
+
<rect x="0" y="0" width="84" height="84" rx="22" fill="#0A1624" stroke="#235768" stroke-opacity="0.8" stroke-width="2" />
|
|
54
|
+
<rect x="0" y="0" width="84" height="84" rx="22" fill="url(#glow-left)" opacity="0.22" />
|
|
55
|
+
<path d="M24 54L40 38L49 47L67 29" stroke="url(#glyph)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
|
|
56
|
+
<path d="M59 29H67V37" stroke="url(#glyph)" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" />
|
|
57
|
+
</g>
|
|
58
|
+
|
|
59
|
+
<text x="96" y="320" fill="#EEF6FF" font-family="Arial, Helvetica, sans-serif" font-size="104" font-weight="700" letter-spacing="-4">
|
|
60
|
+
Trade
|
|
61
|
+
</text>
|
|
62
|
+
<text x="442" y="320" fill="#20C5BD" font-family="Arial, Helvetica, sans-serif" font-size="104" font-weight="700" letter-spacing="-4">
|
|
63
|
+
JS
|
|
64
|
+
</text>
|
|
65
|
+
|
|
66
|
+
<text x="96" y="382" fill="#EEF6FF" font-family="Arial, Helvetica, sans-serif" font-size="44" font-weight="700" letter-spacing="-1.2">
|
|
67
|
+
App
|
|
68
|
+
</text>
|
|
69
|
+
<text x="96" y="436" fill="#A8BED5" font-family="Arial, Helvetica, sans-serif" font-size="30" font-weight="400">
|
|
70
|
+
Dashboards, backtests, charts, derivatives, and runtime data in one UI.
|
|
71
|
+
</text>
|
|
72
|
+
|
|
73
|
+
<rect x="96" y="474" width="188" height="46" rx="23" fill="#102133" stroke="#28516B" />
|
|
74
|
+
<text x="128" y="504" fill="#DAF6F4" font-family="Arial, Helvetica, sans-serif" font-size="21" font-weight="700" letter-spacing="0.4">
|
|
75
|
+
@tradejs/app
|
|
76
|
+
</text>
|
|
77
|
+
|
|
78
|
+
<rect x="686" y="162" width="392" height="250" rx="28" fill="#101B2A" fill-opacity="0.72" stroke="#1F364C" />
|
|
79
|
+
<path d="M728 346L806 288L880 318L970 246L1048 198" stroke="url(#chart)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
|
|
80
|
+
<path d="M1048 198H1090V240" stroke="url(#chart)" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
|
|
81
|
+
<circle cx="806" cy="288" r="10" fill="#20C5BD" />
|
|
82
|
+
<circle cx="970" cy="246" r="10" fill="#20C5BD" />
|
|
83
|
+
<circle cx="1048" cy="198" r="11" fill="#7CE8E2" />
|
|
84
|
+
<rect x="730" y="370" width="120" height="14" rx="7" fill="#173046" />
|
|
85
|
+
<rect x="730" y="404" width="208" height="14" rx="7" fill="#12283C" />
|
|
86
|
+
<rect x="730" y="438" width="178" height="14" rx="7" fill="#12283C" />
|
|
87
|
+
|
|
88
|
+
<text x="96" y="560" fill="#7F93AC" font-family="Arial, Helvetica, sans-serif" font-size="20" font-weight="600" letter-spacing="1.2">
|
|
89
|
+
NEXT.JS UI PACKAGE FOR TRADEJS WORKFLOWS
|
|
90
|
+
</text>
|
|
91
|
+
</svg>
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
package/src/app/api/ai/route.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SystemMessage,
|
|
7
7
|
} from '@langchain/core/messages';
|
|
8
8
|
import { toJson } from '@tradejs/core/data';
|
|
9
|
+
import { getOpenRouterModelKwargs } from '@tradejs/node/ai';
|
|
9
10
|
import { getConnectorCreatorByProvider } from '@tradejs/node/connectors';
|
|
10
11
|
import {
|
|
11
12
|
AIChatHistory,
|
|
@@ -15,6 +16,8 @@ import {
|
|
|
15
16
|
} from '@tradejs/types';
|
|
16
17
|
import { getFile, setFile } from '@tradejs/infra/files';
|
|
17
18
|
import { logger } from '@tradejs/infra/logger';
|
|
19
|
+
import { getUserSettings } from '@tradejs/infra/userSettings';
|
|
20
|
+
import { getCurrentUserName } from '@app/lib/currentUser';
|
|
18
21
|
|
|
19
22
|
export const dynamic = 'force-dynamic';
|
|
20
23
|
|
|
@@ -67,13 +70,21 @@ const buildMessages = (
|
|
|
67
70
|
return messages;
|
|
68
71
|
};
|
|
69
72
|
|
|
70
|
-
const invokeChatModel = async (messages: BaseMessage[]) => {
|
|
73
|
+
const invokeChatModel = async (messages: BaseMessage[], userName: string) => {
|
|
74
|
+
const settings = await getUserSettings(userName);
|
|
75
|
+
if (!settings.OPENAI_API_KEY || !settings.OPENAI_API_ENDPOINT) {
|
|
76
|
+
throw new Error(`AI settings are incomplete for user ${userName}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const modelKwargs = getOpenRouterModelKwargs(settings.OPENAI_API_ENDPOINT);
|
|
80
|
+
|
|
71
81
|
const model = new ChatOpenAI({
|
|
72
82
|
temperature: 0.7,
|
|
73
83
|
modelName: 'gpt-4o',
|
|
74
|
-
|
|
84
|
+
apiKey: settings.OPENAI_API_KEY,
|
|
85
|
+
...(Object.keys(modelKwargs).length ? { modelKwargs } : {}),
|
|
75
86
|
configuration: {
|
|
76
|
-
baseURL:
|
|
87
|
+
baseURL: settings.OPENAI_API_ENDPOINT,
|
|
77
88
|
},
|
|
78
89
|
});
|
|
79
90
|
|
|
@@ -82,6 +93,11 @@ const invokeChatModel = async (messages: BaseMessage[]) => {
|
|
|
82
93
|
|
|
83
94
|
export const GET = async (request: NextRequest) => {
|
|
84
95
|
try {
|
|
96
|
+
const userName = await getCurrentUserName();
|
|
97
|
+
if (!userName) {
|
|
98
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
99
|
+
}
|
|
100
|
+
|
|
85
101
|
const symbol = request.nextUrl.searchParams.get('symbol');
|
|
86
102
|
|
|
87
103
|
if (!symbol) {
|
|
@@ -104,6 +120,11 @@ export const GET = async (request: NextRequest) => {
|
|
|
104
120
|
|
|
105
121
|
export const POST = async (request: NextRequest) => {
|
|
106
122
|
try {
|
|
123
|
+
const userName = await getCurrentUserName();
|
|
124
|
+
if (!userName) {
|
|
125
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
126
|
+
}
|
|
127
|
+
|
|
107
128
|
const body = await request.json();
|
|
108
129
|
const { message, filters } = body as {
|
|
109
130
|
message?: AIChatMessage;
|
|
@@ -128,7 +149,7 @@ export const POST = async (request: NextRequest) => {
|
|
|
128
149
|
}
|
|
129
150
|
|
|
130
151
|
const byBitConnector = await (connectorCreator as ConnectorCreator)({
|
|
131
|
-
userName
|
|
152
|
+
userName,
|
|
132
153
|
});
|
|
133
154
|
|
|
134
155
|
const data = await byBitConnector.kline({
|
|
@@ -138,7 +159,7 @@ export const POST = async (request: NextRequest) => {
|
|
|
138
159
|
|
|
139
160
|
const chatMessages = buildMessages(filters, message, data.slice(-100));
|
|
140
161
|
|
|
141
|
-
const response = await invokeChatModel(chatMessages);
|
|
162
|
+
const response = await invokeChatModel(chatMessages, userName);
|
|
142
163
|
|
|
143
164
|
const responseMessage: AIChatMessage = {
|
|
144
165
|
from: 'ai',
|
|
@@ -3,12 +3,36 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
5
|
export const runtime = 'nodejs';
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
6
7
|
|
|
7
8
|
const getProjectRoot = (): string => {
|
|
8
9
|
const fromEnv = String(process.env.PROJECT_CWD || '').trim();
|
|
9
10
|
return fromEnv ? path.resolve(fromEnv) : process.cwd();
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
const stripPngExtension = (value: string) => value.replace(/\.png$/i, '');
|
|
14
|
+
|
|
15
|
+
const isSafeScreenshotName = (value: string) =>
|
|
16
|
+
/^[A-Za-z0-9_.-]+$/.test(value) && path.basename(value) === value;
|
|
17
|
+
|
|
18
|
+
const getScreenshotCandidates = (name: string) => {
|
|
19
|
+
const projectRoot = getProjectRoot();
|
|
20
|
+
const normalizedName = stripPngExtension(name);
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
path.join(projectRoot, 'data', 'screenshots', `${normalizedName}.png`),
|
|
24
|
+
path.join(projectRoot, 'public', 'screenshots', `${normalizedName}.png`),
|
|
25
|
+
path.join(
|
|
26
|
+
projectRoot,
|
|
27
|
+
'apps',
|
|
28
|
+
'app',
|
|
29
|
+
'public',
|
|
30
|
+
'screenshots',
|
|
31
|
+
`${normalizedName}.png`,
|
|
32
|
+
),
|
|
33
|
+
];
|
|
34
|
+
};
|
|
35
|
+
|
|
12
36
|
interface Params {
|
|
13
37
|
name: string;
|
|
14
38
|
}
|
|
@@ -17,26 +41,38 @@ export async function GET(
|
|
|
17
41
|
_req: Request,
|
|
18
42
|
{ params }: { params: Promise<Params> },
|
|
19
43
|
) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
|
|
26
|
-
`${name}.png`,
|
|
44
|
+
const { name } = await params;
|
|
45
|
+
|
|
46
|
+
if (!isSafeScreenshotName(name)) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: 'Invalid screenshot name' },
|
|
49
|
+
{ status: 400 },
|
|
27
50
|
);
|
|
28
|
-
|
|
51
|
+
}
|
|
29
52
|
|
|
30
|
-
|
|
53
|
+
try {
|
|
54
|
+
for (const filePath of getScreenshotCandidates(name)) {
|
|
55
|
+
try {
|
|
56
|
+
const file = await fs.readFile(filePath);
|
|
57
|
+
const body = new Uint8Array(file);
|
|
58
|
+
|
|
59
|
+
return new NextResponse(body, {
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'image/png',
|
|
62
|
+
'Content-Length': String(file.byteLength),
|
|
63
|
+
'Cache-Control': 'public, max-age=60, immutable',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
31
70
|
|
|
32
|
-
return new NextResponse(body, {
|
|
33
|
-
headers: {
|
|
34
|
-
'Content-Type': 'image/png',
|
|
35
|
-
'Content-Length': String(file.byteLength),
|
|
36
|
-
'Cache-Control': 'public, max-age=60, immutable',
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
} catch {
|
|
40
71
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
72
|
+
} catch {
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{ error: 'Failed to load screenshot' },
|
|
75
|
+
{ status: 500 },
|
|
76
|
+
);
|
|
41
77
|
}
|
|
42
78
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Interval,
|
|
13
13
|
ConnectorCreator,
|
|
14
14
|
} from '@tradejs/types';
|
|
15
|
+
import { getCurrentUserName } from '@app/lib/currentUser';
|
|
15
16
|
|
|
16
17
|
export const dynamic = 'force-dynamic';
|
|
17
18
|
const projectRoot =
|
|
@@ -67,6 +68,11 @@ export const POST = async (
|
|
|
67
68
|
{ params }: { params: Promise<Params> },
|
|
68
69
|
) => {
|
|
69
70
|
try {
|
|
71
|
+
const userName = await getCurrentUserName();
|
|
72
|
+
if (!userName) {
|
|
73
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
74
|
+
}
|
|
75
|
+
|
|
70
76
|
const { provider, symbol, interval } = await params;
|
|
71
77
|
const body = await request.json();
|
|
72
78
|
const options = body as
|
|
@@ -87,7 +93,7 @@ export const POST = async (
|
|
|
87
93
|
throw new Error('No connector available for provider');
|
|
88
94
|
}
|
|
89
95
|
const connector = await (connectorCreator as ConnectorCreator)({
|
|
90
|
-
userName
|
|
96
|
+
userName,
|
|
91
97
|
});
|
|
92
98
|
|
|
93
99
|
const baseData = await connector.kline({
|
|
@@ -3,6 +3,7 @@ import { ConnectorCreator } from '@tradejs/types';
|
|
|
3
3
|
import { getConnectorCreatorByProvider } from '@tradejs/node/connectors';
|
|
4
4
|
import { getTopTickers } from '@tradejs/core/tickers';
|
|
5
5
|
import { logger } from '@tradejs/infra/logger';
|
|
6
|
+
import { getCurrentUserName } from '@app/lib/currentUser';
|
|
6
7
|
|
|
7
8
|
export const dynamic = 'force-dynamic';
|
|
8
9
|
const projectRoot =
|
|
@@ -17,6 +18,11 @@ export const GET = async (
|
|
|
17
18
|
{ params }: { params: Promise<Params> },
|
|
18
19
|
) => {
|
|
19
20
|
try {
|
|
21
|
+
const userName = await getCurrentUserName();
|
|
22
|
+
if (!userName) {
|
|
23
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
24
|
+
}
|
|
25
|
+
|
|
20
26
|
const { provider } = await params;
|
|
21
27
|
const connectorCreator =
|
|
22
28
|
(await getConnectorCreatorByProvider(provider, projectRoot)) ||
|
|
@@ -26,7 +32,7 @@ export const GET = async (
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
const connector = await (connectorCreator as ConnectorCreator)({
|
|
29
|
-
userName
|
|
35
|
+
userName,
|
|
30
36
|
});
|
|
31
37
|
|
|
32
38
|
const data = await connector.getTickers();
|
|
@@ -3,6 +3,7 @@ import { getTopTickers } from '@tradejs/core/tickers';
|
|
|
3
3
|
import { getConnectorCreatorByProvider } from '@tradejs/node/connectors';
|
|
4
4
|
import { logger } from '@tradejs/infra/logger';
|
|
5
5
|
import { ConnectorCreator } from '@tradejs/types';
|
|
6
|
+
import { getCurrentUserName } from '@app/lib/currentUser';
|
|
6
7
|
|
|
7
8
|
export const dynamic = 'force-dynamic';
|
|
8
9
|
const projectRoot =
|
|
@@ -10,6 +11,11 @@ const projectRoot =
|
|
|
10
11
|
|
|
11
12
|
export const GET = async () => {
|
|
12
13
|
try {
|
|
14
|
+
const userName = await getCurrentUserName();
|
|
15
|
+
if (!userName) {
|
|
16
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
17
|
+
}
|
|
18
|
+
|
|
13
19
|
const connectorCreator = await getConnectorCreatorByProvider(
|
|
14
20
|
'bybit',
|
|
15
21
|
projectRoot,
|
|
@@ -19,7 +25,7 @@ export const GET = async () => {
|
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
const byBitConnector = await (connectorCreator as ConnectorCreator)({
|
|
22
|
-
userName
|
|
28
|
+
userName,
|
|
23
29
|
});
|
|
24
30
|
|
|
25
31
|
const data = await byBitConnector.getTickers();
|