feat: 支持4种OpenBridge主题切换,统一项目命名和端口配置
- 添加 OpenBridge 四种主题切换 (day/bright/dusk/night) - 实现 DJB2 hash 算法生成项目专用端口 (14323) - 统一项目名称为 openbridge-token-usage-viewer - 更新 Tauri 应用名称、sidecar 命名和窗口标题 - 开发服务器端口从 3000 改为基于项目名称的稳定端口
This commit is contained in:
11
build.ts
11
build.ts
@@ -16,6 +16,13 @@ import { Schema } from '@effect/schema'
|
|||||||
import { $ } from 'bun'
|
import { $ } from 'bun'
|
||||||
import { Console, Context, Data, Effect, Layer } from 'effect'
|
import { Console, Context, Data, Effect, Layer } from 'effect'
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 项目配置
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** 项目名称 - 用于生成 sidecar 文件名 */
|
||||||
|
const PROJECT_NAME = 'openbridge-token-usage-viewer'
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// 领域模型和 Schema 定义
|
// 领域模型和 Schema 定义
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -195,7 +202,7 @@ class BuildService extends Context.Tag('BuildService')<
|
|||||||
Bun.build({
|
Bun.build({
|
||||||
entrypoints: [config.entrypoint],
|
entrypoints: [config.entrypoint],
|
||||||
compile: {
|
compile: {
|
||||||
outfile: `app-${targetMap[target]}`,
|
outfile: `${PROJECT_NAME}-${targetMap[target]}`,
|
||||||
target: target,
|
target: target,
|
||||||
},
|
},
|
||||||
outdir: config.outputDir,
|
outdir: config.outputDir,
|
||||||
@@ -227,7 +234,7 @@ class BuildService extends Context.Tag('BuildService')<
|
|||||||
Bun.build({
|
Bun.build({
|
||||||
entrypoints: [config.entrypoint],
|
entrypoints: [config.entrypoint],
|
||||||
compile: {
|
compile: {
|
||||||
outfile: `app-${targetMap[target]}`,
|
outfile: `${PROJECT_NAME}-${targetMap[target]}`,
|
||||||
target: target,
|
target: target,
|
||||||
},
|
},
|
||||||
outdir: config.outputDir,
|
outdir: config.outputDir,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "fullstack-starter",
|
"name": "openbridge-token-usage-viewer",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "bun@1.3.6",
|
"packageManager": "bun@1.3.6",
|
||||||
|
|||||||
22
src-tauri/Cargo.lock
generated
22
src-tauri/Cargo.lock
generated
@@ -47,17 +47,6 @@ version = "1.0.100"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "app-desktop"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"tauri",
|
|
||||||
"tauri-build",
|
|
||||||
"tauri-plugin-shell",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atk"
|
name = "atk"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
@@ -2071,6 +2060,17 @@ dependencies = [
|
|||||||
"pathdiff",
|
"pathdiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openbridge-token-usage-viewer"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"tauri",
|
||||||
|
"tauri-build",
|
||||||
|
"tauri-plugin-shell",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app-desktop"
|
name = "openbridge-token-usage-viewer"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "A Tauri App"
|
description = "OpenBridge Token Usage Viewer"
|
||||||
authors = ["imbytecat"]
|
authors = ["imbytecat"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ edition = "2021"
|
|||||||
# The `_lib` suffix may seem redundant but it is necessary
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
name = "app_desktop_lib"
|
name = "openbridge_token_usage_viewer_lib"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
app_desktop_lib::run()
|
openbridge_token_usage_viewer_lib::run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,32 @@ use tauri::Manager;
|
|||||||
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
|
// ===== 项目配置 =====
|
||||||
|
|
||||||
|
/// 项目名称 - 用于生成稳定端口和 sidecar 命名
|
||||||
|
const PROJECT_NAME: &str = "openbridge-token-usage-viewer";
|
||||||
|
|
||||||
|
/// DJB2 Hash 算法 - 将项目名称转换为稳定端口
|
||||||
|
fn djb2_hash(s: &str) -> u32 {
|
||||||
|
let mut hash: u32 = 5381;
|
||||||
|
for c in s.bytes() {
|
||||||
|
hash = hash.wrapping_shl(5).wrapping_add(hash).wrapping_add(c as u32);
|
||||||
|
}
|
||||||
|
hash
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 计算项目专用端口 (范围: 10000-60000)
|
||||||
|
fn get_project_port() -> u16 {
|
||||||
|
const PORT_MIN: u16 = 10000;
|
||||||
|
const PORT_RANGE: u32 = 50000;
|
||||||
|
PORT_MIN + (djb2_hash(PROJECT_NAME) % PORT_RANGE) as u16
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 配置常量 =====
|
// ===== 配置常量 =====
|
||||||
|
|
||||||
/// Sidecar App 启动超时时间(秒)
|
/// Sidecar App 启动超时时间(秒)
|
||||||
const STARTUP_TIMEOUT_SECS: u64 = 30;
|
const STARTUP_TIMEOUT_SECS: u64 = 30;
|
||||||
|
|
||||||
/// 默认起始端口
|
|
||||||
const DEFAULT_PORT: u16 = 3000;
|
|
||||||
|
|
||||||
/// 端口扫描范围(从起始端口开始扫描的端口数量)
|
/// 端口扫描范围(从起始端口开始扫描的端口数量)
|
||||||
const PORT_SCAN_RANGE: u16 = 100;
|
const PORT_SCAN_RANGE: u16 = 100;
|
||||||
|
|
||||||
@@ -23,7 +41,7 @@ const DEFAULT_WINDOW_WIDTH: f64 = 1200.0;
|
|||||||
const DEFAULT_WINDOW_HEIGHT: f64 = 800.0;
|
const DEFAULT_WINDOW_HEIGHT: f64 = 800.0;
|
||||||
|
|
||||||
/// 窗口标题
|
/// 窗口标题
|
||||||
const WINDOW_TITLE: &str = "Tauri App";
|
const WINDOW_TITLE: &str = "OpenBridge Token Usage Viewer";
|
||||||
|
|
||||||
// ===== 数据结构 =====
|
// ===== 数据结构 =====
|
||||||
|
|
||||||
@@ -72,15 +90,20 @@ fn show_error_dialog(message: &str) {
|
|||||||
pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
|
pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
|
||||||
// 检测是否为开发模式
|
// 检测是否为开发模式
|
||||||
let is_dev = cfg!(debug_assertions);
|
let is_dev = cfg!(debug_assertions);
|
||||||
|
// 获取项目专用端口
|
||||||
|
let project_port = get_project_port();
|
||||||
|
|
||||||
if is_dev {
|
if is_dev {
|
||||||
// 开发模式:直接创建窗口连接到 Vite 开发服务器
|
// 开发模式:直接创建窗口连接到 Vite 开发服务器
|
||||||
println!("🔧 开发模式");
|
println!("🔧 开发模式");
|
||||||
|
println!("📌 项目: {}", PROJECT_NAME);
|
||||||
|
println!("📌 端口: {}", project_port);
|
||||||
|
|
||||||
|
let dev_url = format!("http://localhost:{}", project_port);
|
||||||
match tauri::WebviewWindowBuilder::new(
|
match tauri::WebviewWindowBuilder::new(
|
||||||
&app_handle,
|
&app_handle,
|
||||||
"main",
|
"main",
|
||||||
tauri::WebviewUrl::External("http://localhost:3000".parse().unwrap()),
|
tauri::WebviewUrl::External(dev_url.parse().unwrap()),
|
||||||
)
|
)
|
||||||
.title(WINDOW_TITLE)
|
.title(WINDOW_TITLE)
|
||||||
.inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
|
.inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)
|
||||||
@@ -99,13 +122,14 @@ pub fn spawn_sidecar(app_handle: tauri::AppHandle) {
|
|||||||
// 生产模式:启动 sidecar 二进制
|
// 生产模式:启动 sidecar 二进制
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
println!("🚀 生产模式");
|
println!("🚀 生产模式");
|
||||||
|
println!("📌 项目: {}", PROJECT_NAME);
|
||||||
|
|
||||||
// 查找可用端口
|
// 查找可用端口 (从项目端口开始扫描)
|
||||||
let port = find_available_port(DEFAULT_PORT).await;
|
let port = find_available_port(project_port).await;
|
||||||
println!("使用端口: {}", port);
|
println!("📌 端口: {}", port);
|
||||||
|
|
||||||
// 启动 sidecar
|
// 启动 sidecar (使用项目名称作为 sidecar 名称)
|
||||||
let sidecar = match app_handle.shell().sidecar("app") {
|
let sidecar = match app_handle.shell().sidecar(PROJECT_NAME) {
|
||||||
Ok(cmd) => cmd.env("PORT", port.to_string()),
|
Ok(cmd) => cmd.env("PORT", port.to_string()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("✗ 无法找到 sidecar: {}", e);
|
eprintln!("✗ 无法找到 sidecar: {}", e);
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "app-desktop",
|
"productName": "openbridge-token-usage-viewer",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.imbytecat.app-desktop",
|
"identifier": "com.imbytecat.openbridge-token-usage-viewer",
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"windows": [],
|
"windows": [],
|
||||||
@@ -20,6 +20,6 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"externalBin": ["binaries/app"]
|
"externalBin": ["binaries/openbridge-token-usage-viewer"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
* 特性:
|
* 特性:
|
||||||
* - 多账户配额可视化 (根据 API 返回的账户数量动态显示)
|
* - 多账户配额可视化 (根据 API 返回的账户数量动态显示)
|
||||||
* - 实时告警通知 (低于 20% 警告,低于 5% 紧急)
|
* - 实时告警通知 (低于 20% 警告,低于 5% 紧急)
|
||||||
* - 支持日间/夜间主题切换
|
* - 支持 OpenBridge 四种主题切换 (day/bright/dusk/night)
|
||||||
* - OpenBridge 组件懒加载以避免 SSR 问题
|
* - OpenBridge 组件懒加载以避免 SSR 问题
|
||||||
*/
|
*/
|
||||||
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-day.js'
|
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-day.js'
|
||||||
|
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-day-bright.js'
|
||||||
|
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-dusk.js'
|
||||||
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-night.js'
|
import '@oicl/openbridge-webcomponents/dist/icons/icon-palette-night.js'
|
||||||
import { AlertType } from '@oicl/openbridge-webcomponents/dist/types'
|
import { AlertType } from '@oicl/openbridge-webcomponents/dist/types'
|
||||||
import { lazy, Suspense, useCallback, useMemo, useState } from 'react'
|
import { lazy, Suspense, useCallback, useMemo, useState } from 'react'
|
||||||
@@ -291,6 +293,37 @@ export const TokenUsageDashboard = ({ data }: TokenUsageDashboardProps) => {
|
|||||||
/>
|
/>
|
||||||
</ObcNavigationItem>
|
</ObcNavigationItem>
|
||||||
|
|
||||||
|
{/* 明亮模式选项 */}
|
||||||
|
<ObcNavigationItem
|
||||||
|
label="明亮模式"
|
||||||
|
checked={theme === 'bright'}
|
||||||
|
onClick={() => handleThemeChange('bright')}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
slot="icon"
|
||||||
|
// biome-ignore lint: custom element
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html:
|
||||||
|
'<obi-palette-day-bright></obi-palette-day-bright>',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ObcNavigationItem>
|
||||||
|
|
||||||
|
{/* 黄昏模式选项 */}
|
||||||
|
<ObcNavigationItem
|
||||||
|
label="黄昏模式"
|
||||||
|
checked={theme === 'dusk'}
|
||||||
|
onClick={() => handleThemeChange('dusk')}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
slot="icon"
|
||||||
|
// biome-ignore lint: custom element
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: '<obi-palette-dusk></obi-palette-dusk>',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ObcNavigationItem>
|
||||||
|
|
||||||
{/* 夜间模式选项 */}
|
{/* 夜间模式选项 */}
|
||||||
<ObcNavigationItem
|
<ObcNavigationItem
|
||||||
label="夜间模式"
|
label="夜间模式"
|
||||||
|
|||||||
70
src/lib/project-port.ts
Normal file
70
src/lib/project-port.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* 项目名称到端口的 Hash 工具
|
||||||
|
*
|
||||||
|
* 使用 DJB2 算法将项目名称转换为稳定的端口号。
|
||||||
|
* 确保:
|
||||||
|
* - 相同项目名称总是返回相同端口
|
||||||
|
* - 不同项目名称返回不同端口 (极低碰撞率)
|
||||||
|
* - 端口范围: 10000-60000 (避开常用端口和系统保留端口)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { PROJECT_NAME, PROJECT_PORT } from '@/lib/project-port'
|
||||||
|
* console.log(PROJECT_NAME) // 'openbridge-token-usage-viewer'
|
||||||
|
* console.log(PROJECT_PORT) // 34567 (示例)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 端口范围配置 */
|
||||||
|
const PORT_MIN = 10000
|
||||||
|
const PORT_MAX = 60000
|
||||||
|
const PORT_RANGE = PORT_MAX - PORT_MIN
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目名称 (从目录名获取)
|
||||||
|
*
|
||||||
|
* 这确保项目名称与目录结构保持一致
|
||||||
|
*/
|
||||||
|
export const PROJECT_NAME = 'openbridge-token-usage-viewer'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DJB2 Hash 算法
|
||||||
|
*
|
||||||
|
* 经典的字符串哈希算法,具有良好的分布特性和较低的碰撞率。
|
||||||
|
*
|
||||||
|
* @param str - 要哈希的字符串
|
||||||
|
* @returns 32位无符号整数哈希值
|
||||||
|
*/
|
||||||
|
const djb2Hash = (str: string): number => {
|
||||||
|
let hash = 5381
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
// hash * 33 + char
|
||||||
|
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将项目名称转换为稳定的端口号
|
||||||
|
*
|
||||||
|
* @param projectName - 项目名称
|
||||||
|
* @returns 10000-60000 范围内的端口号
|
||||||
|
*/
|
||||||
|
export const getPortFromName = (projectName: string): number => {
|
||||||
|
const hash = djb2Hash(projectName)
|
||||||
|
return PORT_MIN + (hash % PORT_RANGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目专用端口
|
||||||
|
*
|
||||||
|
* 基于项目名称计算的稳定端口号。
|
||||||
|
* 用于开发服务器和 Tauri sidecar。
|
||||||
|
*/
|
||||||
|
export const PROJECT_PORT = getPortFromName(PROJECT_NAME)
|
||||||
|
|
||||||
|
// 用于调试: 在构建时输出端口信息
|
||||||
|
if (import.meta.env?.DEV) {
|
||||||
|
console.log(`📌 项目: ${PROJECT_NAME}`)
|
||||||
|
console.log(`📌 端口: ${PROJECT_PORT}`)
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
* - Bun 运行时优化 (nitro preset: 'bun')
|
* - Bun 运行时优化 (nitro preset: 'bun')
|
||||||
* - 静态资源内联 (serveStatic: 'inline')
|
* - 静态资源内联 (serveStatic: 'inline')
|
||||||
* - React Compiler 自动优化 (无需手动 memo)
|
* - React Compiler 自动优化 (无需手动 memo)
|
||||||
* - 开发时热更新 (端口 3000)
|
* - 基于项目名称的稳定端口 (使用 DJB2 hash)
|
||||||
*/
|
*/
|
||||||
import tailwindcss from '@tailwindcss/vite'
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
import { devtools as tanstackDevtools } from '@tanstack/devtools-vite'
|
import { devtools as tanstackDevtools } from '@tanstack/devtools-vite'
|
||||||
@@ -17,8 +17,27 @@ import { nitro } from 'nitro/vite'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||||
|
|
||||||
/** 开发服务器端口 */
|
// ============================================================================
|
||||||
const DEV_PORT = 3000
|
// 项目配置 (集中管理)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/** 项目名称 - 用于生成稳定端口和 sidecar 命名 */
|
||||||
|
export const PROJECT_NAME = 'openbridge-token-usage-viewer'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DJB2 Hash 算法 - 将项目名称转换为稳定端口
|
||||||
|
* 端口范围: 10000-60000
|
||||||
|
*/
|
||||||
|
const djb2Hash = (str: string): number => {
|
||||||
|
let hash = 5381
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0
|
||||||
|
}
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开发服务器端口 (基于项目名称的稳定值) */
|
||||||
|
export const DEV_PORT = 10000 + (djb2Hash(PROJECT_NAME) % 50000)
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
// 禁止清屏,方便与 Tauri 开发工具共用终端
|
// 禁止清屏,方便与 Tauri 开发工具共用终端
|
||||||
@@ -60,7 +79,7 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: DEV_PORT,
|
port: DEV_PORT,
|
||||||
// 如果端口被占用则报错,而不是自动切换端口
|
// 如果端口被占用则报错,而不是自动切换端口
|
||||||
strictPort: true,
|
strictPort: false,
|
||||||
watch: {
|
watch: {
|
||||||
// 忽略 Tauri 源码目录,避免不必要的重编译
|
// 忽略 Tauri 源码目录,避免不必要的重编译
|
||||||
ignored: ['**/src-tauri/**'],
|
ignored: ['**/src-tauri/**'],
|
||||||
|
|||||||
Reference in New Issue
Block a user