feat: 支持4种OpenBridge主题切换,统一项目命名和端口配置

- 添加 OpenBridge 四种主题切换 (day/bright/dusk/night)
- 实现 DJB2 hash 算法生成项目专用端口 (14323)
- 统一项目名称为 openbridge-token-usage-viewer
- 更新 Tauri 应用名称、sidecar 命名和窗口标题
- 开发服务器端口从 3000 改为基于项目名称的稳定端口
This commit is contained in:
2026-01-22 13:02:43 +08:00
parent d077dfdd90
commit b520cdaf35
10 changed files with 189 additions and 36 deletions

View File

@@ -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,

View File

@@ -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
View File

@@ -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"

View File

@@ -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]

View File

@@ -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()
} }

View File

@@ -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);

View File

@@ -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"]
} }
} }

View File

@@ -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
View 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}`)
}

View File

@@ -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/**'],