Skip to content

Commit

Permalink
fix: config & startup
Browse files Browse the repository at this point in the history
  • Loading branch information
luoling8192 committed Mar 3, 2025
1 parent 562023f commit a53333d
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 153 deletions.
1 change: 1 addition & 0 deletions cspell.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ words:
- baiducloud
- bigserial
- Bitstream
- browserbasehq
- bumpp
- catppuccin
- changelogithub
Expand Down
5 changes: 4 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 6 additions & 5 deletions services/twitter-services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,19 @@
"author": "RainbowBird <rbxin2003@outlook.com>",
"license": "MIT",
"scripts": {
"build": "tsc",
"start": "tsx dist/index.js",
"dev": "tsx src/index.ts",
"dev:mcp": "tsx src/dev-server.ts"
"start": "tsx src/main.ts",
"dev": "tsx watch src/main.ts",
"dev:mcp": "tsx src/dev-server.ts",
"postinstall": "playwright install chromium"
},
"dependencies": {
"@browserbasehq/stagehand": "^1.13.1",
"@guiiai/logg": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.6.1",
"@proj-airi/server-sdk": "^0.1.0",
"@types/hast": "^3.0.1",
"dotenv": "^16.0.3",
"defu": "^6.1.4",
"dotenv": "^16.4.7",
"h3": "^1.11.0",
"hast-util-is-element": "^3.0.0",
"hast-util-select": "^6.0.4",
Expand Down
44 changes: 25 additions & 19 deletions services/twitter-services/src/adapters/browserbase-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { Buffer } from 'node:buffer'
import type { BrowserBaseClientOptions } from '../browser/browserbase'
import type { StagehandClientOptions } from '../browser/browserbase'
import type { BrowserConfig, ElementHandle, WaitOptions } from '../types/browser'
import type { BrowserAdapter } from './browser-adapter'

import { BrowserBaseClient } from '../browser/browserbase'
import { StagehandClient } from '../browser/browserbase'
import { errorToMessage } from '../utils/error'
import { logger } from '../utils/logger'

/**
* BrowserBase 元素句柄实现
* Stagehand 元素句柄实现
*/
class BrowserBaseElementHandle implements ElementHandle {
private client: BrowserBaseClient
class StagehandElementHandle implements ElementHandle {
private client: StagehandClient
private selector: string

constructor(client: BrowserBaseClient, selector: string) {
constructor(client: StagehandClient, selector: string) {
this.client = client
this.selector = selector
}
Expand All @@ -41,14 +41,14 @@ class BrowserBaseElementHandle implements ElementHandle {
}

/**
* BrowserBase 适配器实现
* 将 BrowserBase API 适配为通用浏览器接口
* Stagehand 浏览器适配器实现
* 将 Stagehand API 适配为通用浏览器接口
*/
export class BrowserBaseMCPAdapter implements BrowserAdapter {
private client: BrowserBaseClient
export class StagehandBrowserAdapter implements BrowserAdapter {
private client: StagehandClient

constructor(apiKey: string, baseUrl?: string, options: Partial<BrowserBaseClientOptions> = {}) {
this.client = new BrowserBaseClient({
constructor(apiKey: string, baseUrl?: string, options: Partial<StagehandClientOptions> = {}) {
this.client = new StagehandClient({
apiKey,
baseUrl,
...options,
Expand All @@ -61,12 +61,13 @@ export class BrowserBaseMCPAdapter implements BrowserAdapter {
headless: config.headless,
userAgent: config.userAgent,
viewport: config.viewport,
// proxyUrl: config.proxy, // TODO: Proxy
})
logger.browser.log('浏览器会话已创建', { headless: config.headless })
logger.browser.withFields({
headless: config.headless,
}).log('浏览器会话已创建')
}
catch (error) {
logger.browser.errorWithError('浏览器初始化失败', error)
logger.browser.withError(error).error('浏览器初始化失败')
throw new Error(`无法初始化浏览器: ${errorToMessage(error)}`)
}
}
Expand Down Expand Up @@ -101,14 +102,19 @@ export class BrowserBaseMCPAdapter implements BrowserAdapter {
// 获取所有匹配元素的选择器
const selectors = await this.executeScript<string[]>(`
Array.from(document.querySelectorAll('${selector}')).map((el, i) => {
const uniqueId = 'browserbase-' + Date.now() + '-' + i;
el.setAttribute('data-browserbase-id', uniqueId);
return '[data-browserbase-id="' + uniqueId + '"]';
const uniqueId = 'stagehand-' + Date.now() + '-' + i;
el.setAttribute('data-stagehand-id', uniqueId);
return '[data-stagehand-id="' + uniqueId + '"]';
})
`)

// 为每个匹配的元素创建一个 ElementHandle
return selectors.map(selector => new BrowserBaseElementHandle(this.client, selector))
return selectors.map(selector => new StagehandElementHandle(this.client, selector))
}

// 新增 Stagehand 相关方法
async act(instruction: string): Promise<void> {
await this.client.act(instruction)
}

async getScreenshot(): Promise<Buffer> {
Expand Down
2 changes: 1 addition & 1 deletion services/twitter-services/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Command } from 'commander'
import { BrowserBaseMCPAdapter } from './adapters/browserbase-adapter'
import { createDefaultConfig } from './config'
import { TwitterService } from './core/twitter-service'
import { TwitterServiceLauncher } from './index'
import { TwitterServiceLauncher } from './launcher'
import { errorToMessage } from './utils/error'

// 获取版本
Expand Down
88 changes: 57 additions & 31 deletions services/twitter-services/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,39 @@ import type { Config } from './types'
import fs from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { defu } from 'defu'
import { config as configDotenv } from 'dotenv'

import { logger } from '../utils/logger'
import { DEFAULT_CONFIG } from './types'
import { getDefaultConfig } from './types'

/**
* 加载环境变量文件
* 按优先级顺序加载
*/
function loadEnvFiles(): void {
// 加载环境变量文件
const envFiles = [
'.env.local',
]

// 从当前目录向上查找 .env 文件
for (const file of envFiles) {
const filePath = path.resolve(process.cwd(), file)
if (fs.existsSync(filePath)) {
const result = configDotenv({
path: filePath,
override: true, // 允许覆盖已存在的环境变量
})

if (result.parsed) {
logger.config.withFields({
config: result.parsed,
}).log(`已从 ${file} 加载环境变量`)
}
}
}
}

/**
* 配置管理器
Expand All @@ -19,12 +49,18 @@ export class ConfigManager {
* @param configPath 配置文件路径
*/
constructor(configPath?: string) {
this.config = { ...DEFAULT_CONFIG }
// 首先加载环境变量
loadEnvFiles()

// 设置默认配置
this.config = getDefaultConfig()

// 然后从配置文件加载(如果指定)
if (configPath) {
this.loadFromFile(configPath)
}

// 验证配置
this.validateConfig()
}

Expand All @@ -36,8 +72,9 @@ export class ConfigManager {
const configFile = fs.readFileSync(filePath, 'utf8')
const fileConfig = JSON.parse(configFile)

// 深度合并配置
this.config = this.mergeConfigs(this.config, fileConfig)
// 使用 defu 深度合并配置
// fileConfig 中的值优先于 this.config 中的值
this.config = defu(fileConfig, this.config)

logger.config.log(`配置已从 ${filePath} 加载`)
}
Expand All @@ -50,33 +87,14 @@ export class ConfigManager {
* 验证配置有效性
*/
private validateConfig(): void {
// 验证必要的 API 密钥
if (!this.config.browser.apiKey) {
console.warn('未设置 BrowserBase API 密钥!')
}

// 验证 Twitter 凭据
if (!this.config.twitter.credentials?.username || !this.config.twitter.credentials?.password) {
console.warn('未设置 Twitter 凭据!')
}
}

/**
* 递归合并配置对象
*/
private mergeConfigs(target: any, source: any): any {
const result = { ...target }

for (const key in source) {
if (source[key] instanceof Object && key in target) {
result[key] = this.mergeConfigs(target[key], source[key])
}
else {
result[key] = source[key]
}
logger.config.warn('未设置 Twitter 凭据!')
}

return result
logger.config.withFields({
config: this.config,
}).log('配置验证完成')
}

/**
Expand All @@ -90,16 +108,24 @@ export class ConfigManager {
* 更新配置
*/
updateConfig(newConfig: Partial<Config>): void {
this.config = this.mergeConfigs(this.config, newConfig)
// 使用 defu 合并新配置
this.config = defu(newConfig, this.config)
this.validateConfig()
}
}

// 单例实例
let configInstance: ConfigManager | null = null

/**
* 创建默认配置管理器
* 创建默认配置管理器 (单例)
*/
export function createDefaultConfig(): ConfigManager {
const configPath = process.env.CONFIG_PATH || path.join(process.cwd(), 'twitter-config.json')
if (configInstance) {
return configInstance
}

return new ConfigManager(fs.existsSync(configPath) ? configPath : undefined)
const configPath = process.env.CONFIG_PATH || path.join(process.cwd(), 'twitter-config.json')
configInstance = new ConfigManager(fs.existsSync(configPath) ? configPath : undefined)
return configInstance
}
76 changes: 39 additions & 37 deletions services/twitter-services/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,46 +46,48 @@ export interface Config {
/**
* 默认配置
*/
export const DEFAULT_CONFIG: Config = {
browser: {
apiKey: process.env.BROWSERBASE_API_KEY || '', // 将 apiKey 移到 browser 配置中
headless: true,
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
viewport: {
width: 1280,
height: 800,
},
timeout: 30000,
requestTimeout: 20000,
requestRetries: 2,
},
twitter: {
credentials: {
username: process.env.TWITTER_USERNAME || '',
password: process.env.TWITTER_PASSWORD || '',
export function getDefaultConfig(): Config {
return {
browser: {
apiKey: process.env.BROWSERBASE_API_KEY || '', // 将 apiKey 移到 browser 配置中
headless: true,
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
viewport: {
width: 1280,
height: 800,
},
timeout: 30000,
requestTimeout: 20000,
requestRetries: 2,
},
defaultOptions: {
timeline: {
count: 20,
includeReplies: true,
includeRetweets: true,
twitter: {
credentials: {
username: process.env.TWITTER_USERNAME || '',
password: process.env.TWITTER_PASSWORD || '',
},
defaultOptions: {
timeline: {
count: 20,
includeReplies: true,
includeRetweets: true,
},
},
},
},
adapters: {
airi: {
url: process.env.AIRI_URL || 'http://localhost:3000',
token: process.env.AIRI_TOKEN || '',
enabled: process.env.ENABLE_AIRI === 'true',
adapters: {
airi: {
url: process.env.AIRI_URL || 'http://localhost:3000',
token: process.env.AIRI_TOKEN || '',
enabled: process.env.ENABLE_AIRI === 'true',
},
mcp: {
port: Number(process.env.MCP_PORT || 8080),
enabled: process.env.ENABLE_MCP === 'true' || true,
},
},
mcp: {
port: Number(process.env.MCP_PORT || 8080),
enabled: process.env.ENABLE_MCP === 'true' || true,
system: {
logLevel: 'debug',
logFormat: 'pretty',
concurrency: Number(process.env.CONCURRENCY || 1),
},
},
system: {
logLevel: 'info',
logFormat: process.env.NODE_ENV === 'development' ? 'pretty' : 'json',
concurrency: Number(process.env.CONCURRENCY || 1),
},
}
}
10 changes: 2 additions & 8 deletions services/twitter-services/src/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createApp, createRouter, defineEventHandler, toNodeListener } from 'h3'
import { listen } from 'listhen'
import { z } from 'zod'

import { BrowserBaseMCPAdapter } from './adapters/browserbase-adapter'
import { StagehandBrowserAdapter } from './adapters/browserbase-adapter'
import { TwitterService } from './core/twitter-service'
import { errorToMessage } from './utils/error'
import { logger } from './utils/logger'
Expand All @@ -20,17 +20,11 @@ dotenv.config()
* 使用 listhen 提供开发时的便利功能
*/
async function startDevServer() {
// 基本检查环境变量
if (!process.env.BROWSERBASE_API_KEY) {
console.error('错误: 缺少环境变量 BROWSERBASE_API_KEY')
process.exit(1)
}

const app = createApp()
const router = createRouter()

// 创建浏览器和 Twitter 服务
const browser = new BrowserBaseMCPAdapter(process.env.BROWSERBASE_API_KEY)
const browser = new StagehandBrowserAdapter(process.env.BROWSERBASE_API_KEY || '')
await browser.initialize({
headless: true,
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
Expand Down
Loading

0 comments on commit a53333d

Please sign in to comment.