@wwkit/freetoken 1.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/LICENSE +21 -0
- package/README.md +96 -0
- package/cli/helpers/args.js +48 -0
- package/cli/index.js +374 -0
- package/cli/pid-manager.js +87 -0
- package/client/index.html +25 -0
- package/client/public/favicon.svg +1 -0
- package/client/public/icons.svg +24 -0
- package/client/src/App.vue +136 -0
- package/client/src/assets/vite.svg +1 -0
- package/client/src/assets/vue.svg +1 -0
- package/client/src/components/ChatBox.vue +320 -0
- package/client/src/components/CheckList.vue +52 -0
- package/client/src/components/CodeBlock.vue +99 -0
- package/client/src/components/DetailBlock.vue +47 -0
- package/client/src/components/HelloWorld.vue +95 -0
- package/client/src/components/LangSwitch.vue +32 -0
- package/client/src/components/LinkList.vue +32 -0
- package/client/src/components/OsSwitch.vue +42 -0
- package/client/src/components/OsToggle.vue +15 -0
- package/client/src/components/PageHeader.vue +31 -0
- package/client/src/components/StepList.vue +92 -0
- package/client/src/composables/useClipboard.js +26 -0
- package/client/src/composables/useOs.js +22 -0
- package/client/src/data/docs/agent.js +1002 -0
- package/client/src/data/docs/auth.js +631 -0
- package/client/src/data/docs/builtin-tools.js +322 -0
- package/client/src/data/docs/chat.js +181 -0
- package/client/src/data/docs/index.js +9 -0
- package/client/src/data/docs/skills.js +396 -0
- package/client/src/data/docs/spec-conversion.js +1042 -0
- package/client/src/data/freeModels/bluesliu.js +97 -0
- package/client/src/data/freeModels/index.js +11 -0
- package/client/src/data/freeModels/nvidia.js +126 -0
- package/client/src/data/freeModels/openrouter.js +116 -0
- package/client/src/data/harness/claude.js +89 -0
- package/client/src/data/harness/codex.js +66 -0
- package/client/src/data/harness/dsh.js +86 -0
- package/client/src/data/harness/hermes.js +56 -0
- package/client/src/data/harness/index.js +22 -0
- package/client/src/data/harness/opencode.js +77 -0
- package/client/src/data/proxy/api-relay.js +53 -0
- package/client/src/data/proxy/builtin-proxy.js +54 -0
- package/client/src/data/proxy/cf-workers.js +67 -0
- package/client/src/data/proxy/ecs-forward.js +75 -0
- package/client/src/data/proxy/ecs-reverse.js +89 -0
- package/client/src/data/proxy/index.js +15 -0
- package/client/src/docs/claudecode.md +44 -0
- package/client/src/docs/codex.md +90 -0
- package/client/src/docs/hermesagent.md +42 -0
- package/client/src/docs/opencode.md +103 -0
- package/client/src/locales/en.js +436 -0
- package/client/src/locales/index.js +31 -0
- package/client/src/locales/zh-CN.js +461 -0
- package/client/src/main.js +16 -0
- package/client/src/router/index.js +76 -0
- package/client/src/style.css +15 -0
- package/client/src/views/AdminModelsView.vue +214 -0
- package/client/src/views/DocsView.vue +1604 -0
- package/client/src/views/FreeModelsView.vue +518 -0
- package/client/src/views/HarnessView.vue +314 -0
- package/client/src/views/HomeView.vue +147 -0
- package/client/src/views/ProxyView.vue +112 -0
- package/client/src/views/TokenMarketView.vue +26 -0
- package/client/vite.config.js +41 -0
- package/package.json +85 -0
- package/scripts/build-zip.sh +61 -0
- package/scripts/postinstall.js +7 -0
- package/server/src/config/targets.json +1 -0
- package/server/src/index.js +52 -0
- package/server/src/lib/coding-test.js +215 -0
- package/server/src/lib/database.js +353 -0
- package/server/src/lib/run-test.js +15 -0
- package/server/src/lib/scheduler.js +21 -0
- package/server/src/lib/tester.js +310 -0
- package/server/src/routes/admin.js +48 -0
- package/server/src/routes/proxy.js +64 -0
- package/server/src/routes/speed.js +87 -0
- package/src/config.js +45 -0
- package/src/config.json5 +27 -0
- package/src/index.js +10 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 bluesliu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# @wwkit/freetoken
|
|
2
|
+
|
|
3
|
+
Free LLM API token 管理工具,前后端分离 Web 架构。监控和管理免费 LLM API tokens,提供测速和代理功能。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 从 monorepo 根目录(自动检测模式 + 打开浏览器)
|
|
9
|
+
pnpm --filter freetoken start
|
|
10
|
+
|
|
11
|
+
# 或直接 CLI
|
|
12
|
+
freetoken web # 启动 + 打开浏览器
|
|
13
|
+
freetoken web --dev # 强制开发模式(Vite + server)
|
|
14
|
+
freetoken web stop # 停止
|
|
15
|
+
freetoken web restart # 重启 + 打开浏览器
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- 前端:http://localhost:7905(Vite,自动代理 `/api` 到后端)
|
|
19
|
+
- 后端:http://localhost:5905(Express + SQLite)
|
|
20
|
+
|
|
21
|
+
## 命令列表
|
|
22
|
+
|
|
23
|
+
| 命令 | 说明 |
|
|
24
|
+
|------|------|
|
|
25
|
+
| `web [start] [--dev\|--prod]` | 启动服务并打开浏览器(start 可省略) |
|
|
26
|
+
| `web stop` | 停止所有服务 |
|
|
27
|
+
| `web restart [--dev\|--prod]` | 重启服务并打开浏览器 |
|
|
28
|
+
| `status` | 查看服务状态 |
|
|
29
|
+
| `speed-test` | 手动执行测速 |
|
|
30
|
+
| `config` | 查看当前配置 |
|
|
31
|
+
| `help` / `-h` / `--help` | 显示帮助 |
|
|
32
|
+
|
|
33
|
+
> 完整命令说明见 [cli.md](cli.md)
|
|
34
|
+
|
|
35
|
+
## 配置
|
|
36
|
+
|
|
37
|
+
配置文件:`~/.config/freetoken/config.json5`(JSON5 格式,首次安装自动生成)
|
|
38
|
+
|
|
39
|
+
```json5
|
|
40
|
+
{
|
|
41
|
+
server: { port: 5905 },
|
|
42
|
+
client: { port: 7905 },
|
|
43
|
+
speedTest: { cron: '0 9 * * *', runOnStart: true },
|
|
44
|
+
web: { openBrowser: true },
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- 内置默认:`src/config.json5`
|
|
49
|
+
- 用户配置:`~/.config/freetoken/config.json5`(深合并覆盖内置)
|
|
50
|
+
- 环境注入:`FREETOKEN_CONFIG_CONTENT`(JSON5 字符串,优先级最高)
|
|
51
|
+
- 目录覆盖:`FREETOKEN_CONFIG_DIR`
|
|
52
|
+
- API Keys:通过 `server/.env`(dotenv),不写入 config
|
|
53
|
+
|
|
54
|
+
## 工程结构
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
packages/freetoken/
|
|
58
|
+
├── package.json # @wwkit/freetoken monorepo 配置
|
|
59
|
+
├── src/ # 配置层(config.json5 + config.js)
|
|
60
|
+
├── cli/ # CLI 入口(web start/stop/restart/speed-test/config)
|
|
61
|
+
├── scripts/ # postinstall + 打包脚本
|
|
62
|
+
├── client/ # 前端 Vue 3 + Vite + Element Plus
|
|
63
|
+
│ ├── vite.config.js # Vite 配置 + API 代理到 :5905
|
|
64
|
+
│ └── src/
|
|
65
|
+
├── server/ # 后端 Express + sql.js (SQLite)
|
|
66
|
+
│ ├── src/
|
|
67
|
+
│ │ ├── index.js # Express 入口
|
|
68
|
+
│ │ ├── config/ # 测速目标配置
|
|
69
|
+
│ │ ├── lib/ # 测速引擎、调度器、数据库
|
|
70
|
+
│ │ └── routes/ # API 路由(proxy/speed/admin)
|
|
71
|
+
│ └── .env.example
|
|
72
|
+
└── tests/ # jest 单元测试
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## 构建
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pnpm --filter freetoken build # 构建前端到 client/dist
|
|
79
|
+
freetoken web --prod # 生产模式启动
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 测试
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm --filter freetoken test:all
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 技术栈
|
|
89
|
+
|
|
90
|
+
| 层 | 选型 |
|
|
91
|
+
|----|------|
|
|
92
|
+
| 后端 | Express + sql.js (SQLite) + node-cron |
|
|
93
|
+
| 前端 | Vue 3 + Vue Router + Pinia + Element Plus |
|
|
94
|
+
| 构建 | Vite |
|
|
95
|
+
| 配置 | @wwkit/shared createConfigLoader (JSON5) |
|
|
96
|
+
| 项目管理 | pnpm workspaces (@wwkit) |
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 共享命令行参数解析器
|
|
3
|
+
*
|
|
4
|
+
* 支持以下格式的参数:
|
|
5
|
+
* --flag value 长标志(空格分隔)
|
|
6
|
+
* --flag=value 长标志(等号形式)
|
|
7
|
+
* -f value 短标志(空格分隔)
|
|
8
|
+
* -f=value 短标志(等号形式)
|
|
9
|
+
* positional 位置参数(不以 - 开头)
|
|
10
|
+
*
|
|
11
|
+
* @param {string[]} argv - 原始参数数组
|
|
12
|
+
* @returns {{ flags: Record<string, string>, positional: string[] }}
|
|
13
|
+
*/
|
|
14
|
+
export function parseFlags(argv) {
|
|
15
|
+
const flags = {}
|
|
16
|
+
const positional = []
|
|
17
|
+
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const arg = argv[i]
|
|
20
|
+
const next = argv[i + 1]
|
|
21
|
+
|
|
22
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
23
|
+
const idx = arg.indexOf('=')
|
|
24
|
+
flags[arg.slice(2, idx)] = arg.slice(idx + 1)
|
|
25
|
+
} else if (arg.startsWith('--')) {
|
|
26
|
+
if (next === undefined || next.startsWith('-')) {
|
|
27
|
+
flags[arg.slice(2)] = 'true'
|
|
28
|
+
} else {
|
|
29
|
+
flags[arg.slice(2)] = next
|
|
30
|
+
i++
|
|
31
|
+
}
|
|
32
|
+
} else if (arg.startsWith('-') && arg.includes('=')) {
|
|
33
|
+
const idx = arg.indexOf('=')
|
|
34
|
+
flags[arg.slice(1, idx)] = arg.slice(idx + 1)
|
|
35
|
+
} else if (arg.startsWith('-') && arg.length > 1) {
|
|
36
|
+
if (next === undefined || next.startsWith('-')) {
|
|
37
|
+
flags[arg.slice(1)] = 'true'
|
|
38
|
+
} else {
|
|
39
|
+
flags[arg.slice(1)] = next
|
|
40
|
+
i++
|
|
41
|
+
}
|
|
42
|
+
} else {
|
|
43
|
+
positional.push(arg)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { flags, positional }
|
|
48
|
+
}
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* freetoken CLI 入口
|
|
5
|
+
*
|
|
6
|
+
* 用法:
|
|
7
|
+
* freetoken web [start] [--dev|--prod] 启动服务并打开浏览器(start 可省略)
|
|
8
|
+
* freetoken web stop 停止所有服务
|
|
9
|
+
* freetoken web restart [--dev|--prod] 重启服务并打开浏览器
|
|
10
|
+
* freetoken status 查看服务状态
|
|
11
|
+
* freetoken speed-test 手动执行测速
|
|
12
|
+
* freetoken config 查看配置
|
|
13
|
+
* freetoken help 显示帮助
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { spawn } from 'node:child_process'
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
18
|
+
import { existsSync, realpathSync } from 'node:fs'
|
|
19
|
+
import { networkInterfaces } from 'node:os'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { PidManager } from './pid-manager.js'
|
|
22
|
+
import { parseFlags } from './helpers/args.js'
|
|
23
|
+
import { loadConfig, getServerPort, getClientPort, shouldOpenBrowser, getConfigPath, mergeBuiltinConfig } from '../src/config.js'
|
|
24
|
+
|
|
25
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
26
|
+
const __dirname = path.dirname(__filename)
|
|
27
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..')
|
|
28
|
+
const SERVER_ENTRY = path.join(PACKAGE_ROOT, 'server', 'src', 'index.js')
|
|
29
|
+
const CLIENT_DIR = path.join(PACKAGE_ROOT, 'client')
|
|
30
|
+
const CLIENT_DIST = path.join(CLIENT_DIR, 'dist')
|
|
31
|
+
|
|
32
|
+
const SERVER_NAME = 'server'
|
|
33
|
+
const CLIENT_NAME = 'client'
|
|
34
|
+
|
|
35
|
+
function detectMode(flags) {
|
|
36
|
+
if (flags.dev) return 'dev'
|
|
37
|
+
if (flags.prod) return 'prod'
|
|
38
|
+
if (existsSync(CLIENT_DIST)) return 'prod'
|
|
39
|
+
if (existsSync(CLIENT_DIR)) return 'dev'
|
|
40
|
+
return 'server-only'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getLocalIP() {
|
|
44
|
+
const nets = networkInterfaces()
|
|
45
|
+
for (const name of Object.keys(nets)) {
|
|
46
|
+
for (const net of nets[name]) {
|
|
47
|
+
if (net.family === 'IPv4' && !net.internal) {
|
|
48
|
+
return net.address
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return 'localhost'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function openBrowser(url) {
|
|
56
|
+
const cmd = process.platform === 'win32' ? 'cmd'
|
|
57
|
+
: process.platform === 'darwin' ? 'open'
|
|
58
|
+
: 'xdg-open'
|
|
59
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]
|
|
60
|
+
try {
|
|
61
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref()
|
|
62
|
+
console.log(`Browser opened: ${url}`)
|
|
63
|
+
} catch {
|
|
64
|
+
console.log(`Please open manually: ${url}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function startAction(argv) {
|
|
69
|
+
const { flags } = parseFlags(argv)
|
|
70
|
+
const mode = detectMode(flags)
|
|
71
|
+
const pidManager = new PidManager()
|
|
72
|
+
const serverPort = getServerPort()
|
|
73
|
+
const clientPort = getClientPort()
|
|
74
|
+
|
|
75
|
+
const serverPid = pidManager.readPid(SERVER_NAME)
|
|
76
|
+
if (serverPid && pidManager.isAlive(serverPid.pid)) {
|
|
77
|
+
console.log(`Server already running (pid ${serverPid.pid}) at http://localhost:${serverPort}`)
|
|
78
|
+
return { alreadyRunning: true, serverPort, clientPort, mode }
|
|
79
|
+
}
|
|
80
|
+
if (serverPid) pidManager.removePid(SERVER_NAME)
|
|
81
|
+
|
|
82
|
+
const clientPid = pidManager.readPid(CLIENT_NAME)
|
|
83
|
+
if (clientPid && pidManager.isAlive(clientPid.pid)) {
|
|
84
|
+
if (mode === 'dev') {
|
|
85
|
+
console.log(`Client already running (pid ${clientPid.pid}) at http://localhost:${clientPort}`)
|
|
86
|
+
} else {
|
|
87
|
+
pidManager.forceKill(clientPid.pid)
|
|
88
|
+
pidManager.removePid(CLIENT_NAME)
|
|
89
|
+
}
|
|
90
|
+
} else if (clientPid) {
|
|
91
|
+
pidManager.removePid(CLIENT_NAME)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const useWatch = mode === 'dev'
|
|
95
|
+
const serverArgs = useWatch ? ['--watch', SERVER_ENTRY] : [SERVER_ENTRY]
|
|
96
|
+
const serverLog = pidManager.getLogPath(SERVER_NAME)
|
|
97
|
+
const child = spawn(process.execPath, serverArgs, {
|
|
98
|
+
detached: true,
|
|
99
|
+
stdio: 'ignore',
|
|
100
|
+
windowsHide: true,
|
|
101
|
+
})
|
|
102
|
+
child.unref()
|
|
103
|
+
|
|
104
|
+
pidManager.writePid(SERVER_NAME, {
|
|
105
|
+
pid: child.pid,
|
|
106
|
+
port: serverPort,
|
|
107
|
+
mode,
|
|
108
|
+
startTime: new Date().toISOString(),
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const ip = getLocalIP()
|
|
112
|
+
|
|
113
|
+
console.log(`Mode: ${mode}`)
|
|
114
|
+
console.log()
|
|
115
|
+
console.log(`Server started (pid ${child.pid})`)
|
|
116
|
+
console.log(` Local: http://localhost:${serverPort}`)
|
|
117
|
+
console.log(` Network: http://${ip}:${serverPort}`)
|
|
118
|
+
if (serverLog) console.log(` Log: ${serverLog}`)
|
|
119
|
+
|
|
120
|
+
if (mode === 'dev') {
|
|
121
|
+
const clientLog = pidManager.getLogPath(CLIENT_NAME)
|
|
122
|
+
const viteBin = path.join(PACKAGE_ROOT, 'node_modules', 'vite', 'bin', 'vite.js')
|
|
123
|
+
const viteChild = spawn(process.execPath, [viteBin], {
|
|
124
|
+
cwd: CLIENT_DIR,
|
|
125
|
+
detached: true,
|
|
126
|
+
stdio: 'ignore',
|
|
127
|
+
windowsHide: true,
|
|
128
|
+
})
|
|
129
|
+
viteChild.unref()
|
|
130
|
+
|
|
131
|
+
pidManager.writePid(CLIENT_NAME, {
|
|
132
|
+
pid: viteChild.pid,
|
|
133
|
+
port: clientPort,
|
|
134
|
+
mode,
|
|
135
|
+
startTime: new Date().toISOString(),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
console.log()
|
|
139
|
+
console.log(`Client started (pid ${viteChild.pid})`)
|
|
140
|
+
console.log(` Local: http://localhost:${clientPort}`)
|
|
141
|
+
console.log(` Network: http://${ip}:${clientPort}`)
|
|
142
|
+
console.log(` Proxy: /api -> http://localhost:${serverPort}`)
|
|
143
|
+
if (clientLog) console.log(` Log: ${clientLog}`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log()
|
|
147
|
+
if (mode === 'prod') {
|
|
148
|
+
console.log(`Frontend: served from client/dist/`)
|
|
149
|
+
} else if (mode === 'server-only') {
|
|
150
|
+
console.log(`Frontend: not available (no client/ directory)`)
|
|
151
|
+
} else {
|
|
152
|
+
console.log(`Frontend: Vite dev server (hot reload)`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { alreadyRunning: false, serverPort, clientPort, mode }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function stopAction() {
|
|
159
|
+
const pidManager = new PidManager()
|
|
160
|
+
let count = 0
|
|
161
|
+
|
|
162
|
+
for (const name of [CLIENT_NAME, SERVER_NAME]) {
|
|
163
|
+
const info = pidManager.readPid(name)
|
|
164
|
+
if (!info) continue
|
|
165
|
+
if (pidManager.isAlive(info.pid)) {
|
|
166
|
+
pidManager.forceKill(info.pid)
|
|
167
|
+
console.log(`Stopped ${name} (pid ${info.pid})`)
|
|
168
|
+
} else {
|
|
169
|
+
console.log(`${name} was not running (stale PID removed)`)
|
|
170
|
+
}
|
|
171
|
+
pidManager.removePid(name)
|
|
172
|
+
count++
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (count === 0) {
|
|
176
|
+
console.log('No services running.')
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function statusAction() {
|
|
181
|
+
const pidManager = new PidManager()
|
|
182
|
+
const ip = getLocalIP()
|
|
183
|
+
const serverPort = getServerPort()
|
|
184
|
+
const clientPort = getClientPort()
|
|
185
|
+
|
|
186
|
+
console.log('Service Status')
|
|
187
|
+
console.log('='.repeat(50))
|
|
188
|
+
|
|
189
|
+
const serverInfo = pidManager.readPid(SERVER_NAME)
|
|
190
|
+
if (serverInfo && pidManager.isAlive(serverInfo.pid)) {
|
|
191
|
+
const uptime = formatUptime(serverInfo.startTime)
|
|
192
|
+
console.log(`Server: running (pid ${serverInfo.pid}, port ${serverPort}, mode ${serverInfo.mode}, uptime ${uptime})`)
|
|
193
|
+
console.log(` Local: http://localhost:${serverPort}`)
|
|
194
|
+
console.log(` Network: http://${ip}:${serverPort}`)
|
|
195
|
+
} else {
|
|
196
|
+
console.log('Server: stopped')
|
|
197
|
+
if (serverInfo) pidManager.removePid(SERVER_NAME)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
console.log()
|
|
201
|
+
|
|
202
|
+
const clientInfo = pidManager.readPid(CLIENT_NAME)
|
|
203
|
+
if (clientInfo && pidManager.isAlive(clientInfo.pid)) {
|
|
204
|
+
const uptime = formatUptime(clientInfo.startTime)
|
|
205
|
+
console.log(`Client: running (pid ${clientInfo.pid}, port ${clientPort}, mode ${clientInfo.mode}, uptime ${uptime})`)
|
|
206
|
+
console.log(` Local: http://localhost:${clientPort}`)
|
|
207
|
+
console.log(` Network: http://${ip}:${clientPort}`)
|
|
208
|
+
} else {
|
|
209
|
+
console.log('Client: stopped')
|
|
210
|
+
if (clientInfo) pidManager.removePid(CLIENT_NAME)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log('='.repeat(50))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function webStartAction(argv) {
|
|
217
|
+
const result = await startAction(argv)
|
|
218
|
+
if (!result.alreadyRunning && shouldOpenBrowser()) {
|
|
219
|
+
const port = result.mode === 'dev' ? result.clientPort : result.serverPort
|
|
220
|
+
setTimeout(() => openBrowser(`http://localhost:${port}`), 1500)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function webRestartAction(argv) {
|
|
225
|
+
console.log('Restarting services...')
|
|
226
|
+
await stopAction()
|
|
227
|
+
console.log()
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
229
|
+
await webStartAction(argv)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function speedTestAction() {
|
|
233
|
+
const { runSpeedTest } = await import('../server/src/lib/tester.js')
|
|
234
|
+
console.log('Starting manual speed test...')
|
|
235
|
+
const results = await runSpeedTest()
|
|
236
|
+
console.log(`\nSpeed test completed. ${results.length} models tested.`)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function configAction() {
|
|
240
|
+
const cfg = loadConfig()
|
|
241
|
+
console.log(`Config file: ${getConfigPath()}`)
|
|
242
|
+
console.log('='.repeat(50))
|
|
243
|
+
console.log(JSON.stringify(cfg, null, 2))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function formatUptime(startTime) {
|
|
247
|
+
const start = new Date(startTime).getTime()
|
|
248
|
+
const elapsed = Date.now() - start
|
|
249
|
+
const seconds = Math.floor(elapsed / 1000)
|
|
250
|
+
const minutes = Math.floor(seconds / 60)
|
|
251
|
+
const hours = Math.floor(minutes / 60)
|
|
252
|
+
const days = Math.floor(hours / 24)
|
|
253
|
+
|
|
254
|
+
if (days > 0) return `${days}d ${hours % 24}h ${minutes % 60}m`
|
|
255
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
|
256
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`
|
|
257
|
+
return `${seconds}s`
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const COMMANDS = {
|
|
261
|
+
web: { desc: 'Manage web service: web [start] | web stop | web restart [--dev|--prod]' },
|
|
262
|
+
status: { fn: statusAction, desc: 'Show service status' },
|
|
263
|
+
'speed-test': { fn: speedTestAction, desc: 'Run manual speed test' },
|
|
264
|
+
config: { fn: configAction, desc: 'Show current configuration' },
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function handleWeb(subArgs) {
|
|
268
|
+
const sub = subArgs[0] ?? 'start'
|
|
269
|
+
if (sub === 'stop') {
|
|
270
|
+
await stopAction()
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
if (sub === 'restart') {
|
|
274
|
+
await webRestartAction(subArgs.slice(1))
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
// 'start' or omitted → start
|
|
278
|
+
const rest = sub === 'start' ? subArgs.slice(1) : subArgs
|
|
279
|
+
await webStartAction(rest)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
class CLI {
|
|
283
|
+
async run(argv) {
|
|
284
|
+
const [action, ...rest] = argv
|
|
285
|
+
|
|
286
|
+
if (!action || action === '-h' || action === '--help' || action === 'help') {
|
|
287
|
+
this.printHelp()
|
|
288
|
+
return
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (action === 'web') {
|
|
292
|
+
await handleWeb(rest)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Legacy aliases: start/stop/restart → web start/stop/restart
|
|
297
|
+
if (action === 'start') {
|
|
298
|
+
await webStartAction(rest)
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
if (action === 'stop') {
|
|
302
|
+
await stopAction()
|
|
303
|
+
return
|
|
304
|
+
}
|
|
305
|
+
if (action === 'restart') {
|
|
306
|
+
await webRestartAction(rest)
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const cmd = COMMANDS[action]
|
|
311
|
+
if (!cmd || !cmd.fn) {
|
|
312
|
+
console.error(`Unknown command: ${action}`)
|
|
313
|
+
this.printHelp()
|
|
314
|
+
process.exit(1)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await cmd.fn(rest)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
printHelp() {
|
|
321
|
+
console.log(`
|
|
322
|
+
Usage: freetoken <command> [options]
|
|
323
|
+
|
|
324
|
+
Commands:
|
|
325
|
+
web [start] [--dev|--prod] Start services and open browser (start can be omitted)
|
|
326
|
+
web stop Stop all services
|
|
327
|
+
web restart [--dev|--prod] Restart services and open browser
|
|
328
|
+
status Show service status
|
|
329
|
+
speed-test Run manual speed test
|
|
330
|
+
config Show current configuration
|
|
331
|
+
help Show this help
|
|
332
|
+
|
|
333
|
+
Options:
|
|
334
|
+
--dev Force development mode (Vite + node --watch)
|
|
335
|
+
--prod Force production mode (node only, serve client/dist/)
|
|
336
|
+
|
|
337
|
+
Mode Detection (for web start/web restart):
|
|
338
|
+
client/dist/ exists -> production mode
|
|
339
|
+
client/ exists -> development mode
|
|
340
|
+
neither exists -> server-only mode
|
|
341
|
+
|
|
342
|
+
Examples:
|
|
343
|
+
freetoken web Auto-detect mode, start + open browser
|
|
344
|
+
freetoken web --dev Force dev mode (Vite + server)
|
|
345
|
+
freetoken web --prod Force prod mode (server only)
|
|
346
|
+
freetoken web stop Stop all services
|
|
347
|
+
freetoken web restart Restart with auto-detected mode + open browser
|
|
348
|
+
freetoken status Show running status
|
|
349
|
+
freetoken speed-test Run manual speed test
|
|
350
|
+
freetoken config Show configuration
|
|
351
|
+
|
|
352
|
+
Config: ~/.config/freetoken/config.json5
|
|
353
|
+
`)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (process.argv[1]) {
|
|
358
|
+
try {
|
|
359
|
+
const resolved = pathToFileURL(realpathSync(process.argv[1])).href
|
|
360
|
+
if (import.meta.url === resolved || import.meta.url === resolved + '/index.js' || import.meta.url === resolved + 'index.js') {
|
|
361
|
+
// Lazy-init: if user config doesn't exist, create it now
|
|
362
|
+
if (!existsSync(getConfigPath())) {
|
|
363
|
+
mergeBuiltinConfig()
|
|
364
|
+
}
|
|
365
|
+
const cli = new CLI()
|
|
366
|
+
cli.run(process.argv.slice(2)).catch((err) => {
|
|
367
|
+
console.error(`[freetoken] Error: ${err.message}`)
|
|
368
|
+
process.exit(1)
|
|
369
|
+
})
|
|
370
|
+
}
|
|
371
|
+
} catch {}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export { CLI, parseFlags, detectMode, formatUptime, getLocalIP, openBrowser }
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID 文件管理
|
|
3
|
+
*
|
|
4
|
+
* 管理 ~/.config/freetoken/pids/ 目录下的 PID 文件(XDG 规范)。
|
|
5
|
+
* 支持 server 和 client 两个服务的进程管理。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import { homedir } from 'node:os'
|
|
11
|
+
|
|
12
|
+
const XDG_CONFIG_HOME = process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config')
|
|
13
|
+
const FREETOKEN_DIR = path.join(XDG_CONFIG_HOME, 'freetoken')
|
|
14
|
+
const PID_DIR = path.join(FREETOKEN_DIR, 'pids')
|
|
15
|
+
const LOG_DIR = path.join(FREETOKEN_DIR, 'logs')
|
|
16
|
+
|
|
17
|
+
export class PidManager {
|
|
18
|
+
constructor(pidDir) {
|
|
19
|
+
this.pidDir = pidDir || PID_DIR
|
|
20
|
+
this.logDir = LOG_DIR
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ensureDir(dir) {
|
|
24
|
+
const target = dir || this.pidDir
|
|
25
|
+
if (!fs.existsSync(target)) {
|
|
26
|
+
fs.mkdirSync(target, { recursive: true })
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
writePid(name, info) {
|
|
31
|
+
this.ensureDir()
|
|
32
|
+
fs.writeFileSync(path.join(this.pidDir, `${name}.pid`), JSON.stringify(info, null, 2))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
readPid(name) {
|
|
36
|
+
const filePath = path.join(this.pidDir, `${name}.pid`)
|
|
37
|
+
if (!fs.existsSync(filePath)) return null
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
|
40
|
+
} catch {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
removePid(name) {
|
|
46
|
+
const filePath = path.join(this.pidDir, `${name}.pid`)
|
|
47
|
+
try {
|
|
48
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath)
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
listPids() {
|
|
53
|
+
this.ensureDir()
|
|
54
|
+
const files = fs.readdirSync(this.pidDir).filter((f) => f.endsWith('.pid'))
|
|
55
|
+
return files
|
|
56
|
+
.map((f) => {
|
|
57
|
+
const name = f.replace('.pid', '')
|
|
58
|
+
const info = this.readPid(name)
|
|
59
|
+
return info ? { name, ...info } : null
|
|
60
|
+
})
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isAlive(pid) {
|
|
65
|
+
try {
|
|
66
|
+
process.kill(pid, 0)
|
|
67
|
+
return true
|
|
68
|
+
} catch {
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
forceKill(pid) {
|
|
74
|
+
try {
|
|
75
|
+
if (process.platform === 'win32') {
|
|
76
|
+
process.kill(pid)
|
|
77
|
+
} else {
|
|
78
|
+
process.kill(pid, 'SIGKILL')
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getLogPath(name) {
|
|
84
|
+
this.ensureDir(this.logDir)
|
|
85
|
+
return path.join(this.logDir, `${name}.log`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<meta name="description" content="AI Harness 学习站 - 学习 AI 编程工具(OpenCode、Codex、Claude Code),获取免费大模型,国内代理方案" />
|
|
8
|
+
<title>FreeToken</title>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<script>
|
|
12
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
13
|
+
document.documentElement.classList.add('dark')
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
var savedLocale = localStorage.getItem('locale')
|
|
17
|
+
if (savedLocale) {
|
|
18
|
+
document.documentElement.lang = savedLocale === 'zh-CN' ? 'zh-CN' : 'en'
|
|
19
|
+
}
|
|
20
|
+
} catch (e) {}
|
|
21
|
+
</script>
|
|
22
|
+
<div id="app"></div>
|
|
23
|
+
<script type="module" src="/src/main.js"></script>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
|