@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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/cli/helpers/args.js +48 -0
  4. package/cli/index.js +374 -0
  5. package/cli/pid-manager.js +87 -0
  6. package/client/index.html +25 -0
  7. package/client/public/favicon.svg +1 -0
  8. package/client/public/icons.svg +24 -0
  9. package/client/src/App.vue +136 -0
  10. package/client/src/assets/vite.svg +1 -0
  11. package/client/src/assets/vue.svg +1 -0
  12. package/client/src/components/ChatBox.vue +320 -0
  13. package/client/src/components/CheckList.vue +52 -0
  14. package/client/src/components/CodeBlock.vue +99 -0
  15. package/client/src/components/DetailBlock.vue +47 -0
  16. package/client/src/components/HelloWorld.vue +95 -0
  17. package/client/src/components/LangSwitch.vue +32 -0
  18. package/client/src/components/LinkList.vue +32 -0
  19. package/client/src/components/OsSwitch.vue +42 -0
  20. package/client/src/components/OsToggle.vue +15 -0
  21. package/client/src/components/PageHeader.vue +31 -0
  22. package/client/src/components/StepList.vue +92 -0
  23. package/client/src/composables/useClipboard.js +26 -0
  24. package/client/src/composables/useOs.js +22 -0
  25. package/client/src/data/docs/agent.js +1002 -0
  26. package/client/src/data/docs/auth.js +631 -0
  27. package/client/src/data/docs/builtin-tools.js +322 -0
  28. package/client/src/data/docs/chat.js +181 -0
  29. package/client/src/data/docs/index.js +9 -0
  30. package/client/src/data/docs/skills.js +396 -0
  31. package/client/src/data/docs/spec-conversion.js +1042 -0
  32. package/client/src/data/freeModels/bluesliu.js +97 -0
  33. package/client/src/data/freeModels/index.js +11 -0
  34. package/client/src/data/freeModels/nvidia.js +126 -0
  35. package/client/src/data/freeModels/openrouter.js +116 -0
  36. package/client/src/data/harness/claude.js +89 -0
  37. package/client/src/data/harness/codex.js +66 -0
  38. package/client/src/data/harness/dsh.js +86 -0
  39. package/client/src/data/harness/hermes.js +56 -0
  40. package/client/src/data/harness/index.js +22 -0
  41. package/client/src/data/harness/opencode.js +77 -0
  42. package/client/src/data/proxy/api-relay.js +53 -0
  43. package/client/src/data/proxy/builtin-proxy.js +54 -0
  44. package/client/src/data/proxy/cf-workers.js +67 -0
  45. package/client/src/data/proxy/ecs-forward.js +75 -0
  46. package/client/src/data/proxy/ecs-reverse.js +89 -0
  47. package/client/src/data/proxy/index.js +15 -0
  48. package/client/src/docs/claudecode.md +44 -0
  49. package/client/src/docs/codex.md +90 -0
  50. package/client/src/docs/hermesagent.md +42 -0
  51. package/client/src/docs/opencode.md +103 -0
  52. package/client/src/locales/en.js +436 -0
  53. package/client/src/locales/index.js +31 -0
  54. package/client/src/locales/zh-CN.js +461 -0
  55. package/client/src/main.js +16 -0
  56. package/client/src/router/index.js +76 -0
  57. package/client/src/style.css +15 -0
  58. package/client/src/views/AdminModelsView.vue +214 -0
  59. package/client/src/views/DocsView.vue +1604 -0
  60. package/client/src/views/FreeModelsView.vue +518 -0
  61. package/client/src/views/HarnessView.vue +314 -0
  62. package/client/src/views/HomeView.vue +147 -0
  63. package/client/src/views/ProxyView.vue +112 -0
  64. package/client/src/views/TokenMarketView.vue +26 -0
  65. package/client/vite.config.js +41 -0
  66. package/package.json +85 -0
  67. package/scripts/build-zip.sh +61 -0
  68. package/scripts/postinstall.js +7 -0
  69. package/server/src/config/targets.json +1 -0
  70. package/server/src/index.js +52 -0
  71. package/server/src/lib/coding-test.js +215 -0
  72. package/server/src/lib/database.js +353 -0
  73. package/server/src/lib/run-test.js +15 -0
  74. package/server/src/lib/scheduler.js +21 -0
  75. package/server/src/lib/tester.js +310 -0
  76. package/server/src/routes/admin.js +48 -0
  77. package/server/src/routes/proxy.js +64 -0
  78. package/server/src/routes/speed.js +87 -0
  79. package/src/config.js +45 -0
  80. package/src/config.json5 +27 -0
  81. 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>