七牛云送 1000 万 token,基本覆盖所有国内模型
https://s.qiniu.com/zIruma
七牛云送 1000 万 token,基本覆盖所有国内模型
邀好友体验七牛云 AI 推理,立返 500 万 Token
完成首次体验,好友即得 1000 万 Token

xiaohack博客专注前沿科技动态与实用技术干货分享,涵盖 AI 代理、大模型应用、编程工具、文档解析、SEO 实战、自动化部署等内容,提供开源项目教程、科技资讯日报、工具使用指南,助力开发者、AI 爱好者获取前沿技术与实战经验。
https://s.qiniu.com/zIruma
七牛云送 1000 万 token,基本覆盖所有国内模型
邀好友体验七牛云 AI 推理,立返 500 万 Token
完成首次体验,好友即得 1000 万 Token

不想再冲会员了,网易音乐人不是永久会员么,想知道咸鱼这种帮过的有没有坑
我外星人笔记本自己拆了不好换,这个型号是倒装主板得全部拆下来才能看到 CPU 、GPU 位置,个人搞不定,把壳拆下来螺丝都还原不回去了一个哈哈。小红书上一个海淀的换液金散热要四五百,但是也有说液金是导电的,要是外部震动、掉落,液金从散热器边缘溢出,滴到主板元器件上,瞬间短路,主板直接报废的可能性大。
所以现在为了压住使用了几年的外星人的风扇声音大,起步温度都是 95 度以上了,有以下方案:
核心散热区域( CPU & GPU ):使用 霍尼韦尔 PTM7950 相变导热片。
显存与供电区域用莱尔德导热泥代替原厂硅胶垫。
所以普通电脑店估计一样搞不好外星人这种倒装主板的散热硅脂替换方案吧,另外外星人自己专利的 31 号元素散热外面没有正宗的吧。
有没有谁认识或是外星人笔记本的换过的靠谱店家推荐下啊,双休把本子拿过去搞下~

早上上班一开机,发现微信更新了,更新完给我整蒙了,这鲜艳的大绿色,太丑了
TradingView 是全球最专业的金融图表可视化库之一,提供了功能强大的 K 线图、指标系统和技术分析工具。在金融行情类 Web 应用中,接入 TradingView 是提升用户体验的首选方案。 本文将基于实际项目代码,系统讲解如何在 Next.js 项目中接入 TradingView Charts,包括环境配置、Datafeed 数据馈送实现、自定义指标开发、主题样式定制、以及关键的性能优化策略。 TradingView 图表库需要从官方获取授权后下载。获取后将文件放置在项目的 Datafeed 是 TradingView 与后端数据交互的核心接口,需要实现以下方法: URL 参数说明: 本文详细讲解了 Next.js 项目中接入 TradingView 图表的完整方案,涵盖了: 通过以上方案,可以在 Next.js 项目中快速构建专业的金融图表应用。如需更高级的功能(如图表保存加载、自定义交易品种等),可以参考 TradingView 官方文档。原文地址:https://feinterview.poetries.top/blog/nextjs-tradingview-inte...
导语
一、项目准备与环境配置
1.1 获取 TradingView 图表库
public/static/charting_library 目录下:public/
└── static/
└── charting_library/
├── charting_library.standalone.js
└── bundles/
├── *.js
└── *.css1.2 组件目录结构
src/components/Tradingview/
├── index.tsx # 主组件
├── datafeed.ts # 数据馈送实现
├── widgetOpts.tsx # 图表配置选项
├── widgetMethods.ts # 图表方法工具
├── theme.ts # 主题配置
├── constant.ts # 常量定义
└── customIndicators/ # 自定义指标
├── ma.ts
├── macd.ts
├── kdj.ts
└── customerRSI.ts二、核心组件实现
2.1 主组件:TradingView 图表容器
// src/components/Tradingview/index.tsx
import { useEffect, useRef, useState } from 'react'
import { widget } from 'public/static/charting_library'
import { useStores } from '@/context/mobxProvider'
import { STORAGE_GET_CHART_PROPS, STORAGE_REMOVE_CHART_PROPS, ThemeConst } from './constant'
import { ColorType, applyOverrides, createWatermarkLogo, setCSSCustomProperty, setChartStyleProperties } from './widgetMethods'
import getWidgetOpts from './widgetOpts'
import { useConfig } from '@/context/configProvider'
import { useRouter } from 'next/router'
import stores from '@/stores'
import { observer } from 'mobx-react'
import { STORAGE_SET_TRADINGVIEW_RESOLUTION } from '@/utils/storage'
const Tradingview = () => {
const chartContainerRef = useRef<HTMLDivElement>()
const { ws } = useStores()
const { isMobile, isPc } = useConfig()
const router = useRouter()
const [isChartLoading, setIsChartLoading] = useState(true)
const [loading, setLoading] = useState(true)
const query = {
...router.query,
...getInjectParams()
} as any
const datafeedParams = {
setActiveSymbolInfo: ws.setActiveSymbolInfo,
removeActiveSymbol: ws.removeActiveSymbol,
getDataFeedBarCallback: ws.getDataFeedBarCallback,
dataSourceCode: query.dataSourceCode
}
const params = {
symbol: (query.symbolName || 'BTCUSDT') as string,
locale: (query.locale || 'en') as LanguageCode,
theme: (query.theme || 'light') as ThemeName,
colorType: Number(query.colorType || 1) as ColorType,
isMobile,
bgGradientStartColor: query.bgGradientStartColor ? `#${query.bgGradientStartColor}` : '',
bgGradientEndColor: query.bgGradientEndColor ? `#${query.bgGradientEndColor}` : ''
}
useEffect(() => {
console.log('Tradingview组件初始化')
const showBottomMACD = Number(query.showBottomMACD || 1)
const chartType = (query.chartType !== '' ? Number(query.chartType || 1) : 1) as ChartStyle
const theme = params.theme
// 切换主题时清除本地缓存,避免颜色闪烁
const defaultBgColor = theme === 'dark' ? ThemeConst.black : ThemeConst.white
if (theme && defaultBgColor !== STORAGE_GET_CHART_PROPS('paneProperties.background')) {
STORAGE_REMOVE_CHART_PROPS()
}
const widgetOptions = getWidgetOpts(params, chartContainerRef.current, datafeedParams)
const tvWidget = new widget(widgetOptions)
setTimeout(() => {
setLoading(false)
}, 200)
tvWidget.onChartReady(async () => {
setIsChartLoading(false)
// 动态设置 CSS 变量
setCSSCustomProperty({ tvWidget, theme })
// 监听时间周期变化
tvWidget
.activeChart()
.onIntervalChanged()
.subscribe(null, (interval, timeframeObj) => {
// 记录当前分辨率
STORAGE_SET_TRADINGVIEW_RESOLUTION(interval)
// 日周月级别使用 UTC 时区,分钟级别使用上海时区
if (['D', 'W', 'M', 'Y'].some((item) => interval.endsWith(item))) {
tvWidget.activeChart().getTimezoneApi().setTimezone('Etc/UTC')
} else {
tvWidget.activeChart().getTimezoneApi().setTimezone('Asia/Shanghai')
}
ws.activeSymbolInfo.onResetCacheNeededCallback?.()
setTimeout(() => {
tvWidget.activeChart().resetData()
}, 100)
})
// 默认显示 MACD 指标
if (showBottomMACD === 1) {
tvWidget.activeChart().createStudy(
'MACD',
false,
false,
{ in_0: 12, in_1: 26, in_3: 'close', in_2: 9 },
{
'Histogram.color.3': 'rgba(197, 71, 71, 0.7188)',
showLabelsOnPriceScale: !!isPc
}
)
}
// 创建自定义 MA 指标
tvWidget.activeChart().createStudy('Customer Moving Average', false, false, {}, { showLabelsOnPriceScale: false })
// 动态切换主题
if (query.theme && !params.bgGradientStartColor) {
await tvWidget.changeTheme(theme)
}
// 设置 K 线柱样式(绿涨红跌 / 红涨绿跌)
setChartStyleProperties({ colorType: params.colorType, tvWidget })
// 应用覆盖样式
applyOverrides({
tvWidget,
chartType,
bgGradientStartColor: params.bgGradientStartColor,
bgGradientEndColor: params.bgGradientEndColor
})
// 添加水印 Logo
if (query.hideWatermarkLogo !== '0' && query.watermarkLogoUrl) {
createWatermarkLogo(query.watermarkLogoUrl)
}
// 记录实例
ws.setTvWidget(tvWidget)
window.tvWidget = tvWidget
})
return () => {
tvWidget.remove()
mitt.off('symbol_change')
}
}, [router.query])
return (
<div style={{ position: 'relative' }}>
<div id="tradingview" ref={chartContainerRef} style={{ height: 'calc(100vh - 60px)', opacity: loading ? 0 : 1 }} />
{isChartLoading && (
<div className="loading-container">
<div className="loading"></div>
</div>
)}
</div>
)
}
export default observer(Tradingview)2.2 Datafeed 数据馈送实现
// src/components/Tradingview/datafeed.ts
class DataFeedBase {
configuration: DatafeedConfiguration
constructor(props: Partial<ChartingLibraryWidgetOptions>) {
this.configuration = {
supports_time: true,
supports_timescale_marks: true,
supports_marks: true,
// 支持的分辨率
supported_resolutions: ['1', '5', '15', '30', '60', '240', '1D', '1W', '1M'],
intraday_multipliers: ['1', '5', '15', '30', '60', '240', '1D', '1W', '1M']
} as DatafeedConfiguration
this.setActiveSymbolInfo = props.setActiveSymbolInfo
this.removeActiveSymbol = props.removeActiveSymbol
this.getDataFeedBarCallback = props.getDataFeedBarCallback
this.isZh = props.locale === 'zh_TW'
}
// 图表初始化时调用,设置支持的配置
onReady(callback) {
setTimeout(() => {
callback(this.configuration)
}, 0)
}
// 解析品种信息
async resolveSymbol(symbolName, onSymbolResolvedCallback, onResolveErrorCallback, extension) {
const resolution = String(STORAGE_GET_TRADINGVIEW_RESOLUTION() || '')
const ENV = getEnv()
const urlPrefix = ENV.isApp ? getInjectParams().baseUrl : ''
let symbolInfo
if (!ENV.isApp) {
// HTTP 请求获取品种信息
const res = await request(`${urlPrefix}/api/trade-core/coreApi/symbols/symbol/detail?symbol=${symbolName}`)
symbolInfo = res?.data || {}
} else {
// APP 内获取 RN 传递的数据
symbolInfo = {
...(ENV?.injectParams?.symbolInfo || {}),
...(stores.global.symbolInfo || {})
}
}
const currentSymbol = {
...symbolInfo,
precision: symbolInfo?.symbolDecimal || 2,
description: symbolInfo?.remark || '',
exchange: '',
session: '24x7',
name: symbolInfo.symbol,
dataSourceCode: symbolInfo.dataSourceCode
}
const commonSymbolInfo = {
has_intraday: true,
has_daily: true,
has_weekly_and_monthly: true,
intraday_multipliers: this.configuration.intraday_multipliers,
supported_resolutions: this.configuration.supported_resolutions,
data_status: 'streaming',
format: 'price',
minmov: 1,
pricescale: Math.pow(10, currentSymbol.precision),
ticker: currentSymbol?.name
} as LibrarySymbolInfo
const currentSymbolInfo = {
...commonSymbolInfo,
...currentSymbol,
description: this.isZh ? currentSymbol.description : currentSymbol?.name,
exchange: this.isZh ? currentSymbol?.exchange : '',
session: '0000-0000|0000-0000:1234567;1',
timezone: ['D', 'W', 'M', 'Y'].some((item) => resolution.endsWith(item)) ? 'Etc/UTC' : 'Asia/Shanghai'
} as LibrarySymbolInfo
setTimeout(() => {
onSymbolResolvedCallback(currentSymbolInfo)
}, 0)
}
// 搜索品种
searchSymbols(userInput, exchange, symbolType, onResultReadyCallback) {
const keyword = userInput || ''
const resultArr = symbolInfoArr
.filter((item) => item.name.includes(keyword))
.map((item) => ({
symbol: item.name,
name: item.name,
full_name: `${item.name}`,
description: this.isZh ? item.description : item.name,
exchange: this.isZh ? item.exchange : '',
type: item.type,
ticker: item.name
}))
setTimeout(() => {
onResultReadyCallback(resultArr)
}, 0)
}
// 获取 K 线历史数据(核心方法)
getBars(symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) {
const { from, to, firstDataRequest, countBack } = periodParams
this.setActiveSymbolInfo({ symbolInfo, resolution })
this.getDataFeedBarCallback({
symbolInfo,
resolution,
from,
to,
countBack,
onHistoryCallback,
onErrorCallback,
firstDataRequest
})
}
// 订阅实时数据更新
subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) {
this.setActiveSymbolInfo({
symbolInfo,
resolution,
onRealtimeCallback,
subscriberUID,
onResetCacheNeededCallback
})
mitt.on('symbol_change', () => {
onResetCacheNeededCallback()
})
}
// 取消订阅
unsubscribeBars(subscriberUID) {
this.removeActiveSymbol(subscriberUID)
}
}
export default DataFeedBase2.3 图表配置选项
// src/components/Tradingview/widgetOpts.tsx
import ma from './customIndicators/ma'
export default function getWidgetOpts(props, containerRef: any, datafeedParams: any): ChartingLibraryWidgetOptions {
const ENV = getEnv()
const theme = props.theme
const bgColor = theme === 'dark' ? ThemeConst.black : ThemeConst.white
const toolbar_bg = theme === 'dark' ? ThemeConst.black : '#fff'
// 禁用的功能
const disabled_features: ChartingLibraryFeatureset[] = [
'header_compare',
'symbol_search_hot_key',
'study_templates',
'header_saveload',
'save_shortcut',
'header_undo_redo',
'symbol_info',
'timeframes_toolbar',
'scales_date_format',
'header_fullscreen_button',
'display_market_status'
]
// 移动端额外禁用
if (props.isMobile) {
disabled_features.push(
'header_symbol_search',
'context_menus',
'show_chart_property_page',
'header_screenshot',
'adaptive_logo',
'left_toolbar'
)
}
const widgetOptions: ChartingLibraryWidgetOptions = {
fullscreen: true,
autosize: true,
timezone: 'exchange',
library_path: `${ENV.isApp ? '.' : ''}/static/charting_library/`,
datafeed: new DataFeedBase(datafeedParams),
symbol: props.symbol,
client_id: 'tradingview.com',
user_id: 'public_user_id',
locale: props.locale as LanguageCode,
interval: isPC() ? '15' : '1',
theme,
toolbar_bg,
container: containerRef,
symbol_search_request_delay: 1000,
auto_save_delay: 5,
study_count_limit: 5,
allow_symbol_change: true,
overrides: {
'paneProperties.background': `${bgColor}`
},
disabled_features,
enabled_features: ['hide_resolution_in_legend', 'display_legend_on_all_charts'],
custom_css_url: ENV.isApp ? `./styles/index.css` : `/static/styles/index.css`,
favorites: {
intervals: ['1', '5', '15', '30', '60']
},
custom_indicators_getter: function (PineJS) {
return Promise.resolve([ma(PineJS)])
},
loading_screen: {
backgroundColor: 'transparent',
foregroundColor: 'transparent'
}
}
return widgetOptions
}三、K线数据与WebSocket实时更新
3.1 WebSocket Store 实现
// src/stores/ws.ts
class WsStore {
tvWidget = null
@observable lastbar = {}
@observable activeSymbolInfo = {}
// HTTP 获取历史 K 线数据
getHttpHistoryBars = async (symbolInfo, resolution, from, to, countBack, firstDataRequest) => {
const klineType =
{
1: '1min',
5: '5min',
15: '15min',
30: '30min',
60: '60min',
240: '4hour',
'1D': '1day',
'1W': '1week',
'1M': '1mon'
}[resolution] || '1min'
const res = await request.get(`${url}/api/trade-market/marketApi/kline/symbol/klineList`, {
params: {
symbol: symbolInfo.symbol,
first: firstDataRequest,
current: 1,
size: document.documentElement.clientWidth >= 1200 ? 500 : 200,
klineType,
klineTime: to * 1000
}
})
const list = res?.data || []
return list
.map((item) => {
const [klineTime, open, high, low, close] = (item || '').split(',')
return {
open: Number(open),
close: Number(close),
high: Number(high),
low: Number(low),
time: resolution.includes('M') ? Number(klineTime) + 8 * 60 * 60 * 1000 : Number(klineTime)
}
})
.reverse()
}
// 更新最后一条 K 线
updateBar = (socketData, currentSymbol) => {
const precision = currentSymbol.precision
const lastBar = this.lastbar
const resolution = currentSymbol.resolution
const serverTime = socketData?.priceData?.id / 1000
const bid = socketData?.priceData?.buy
let rounded = serverTime
if (!isNaN(resolution) || resolution.includes('D')) {
const coeff = (resolution.includes('D') ? 1440 : Number(resolution)) * 60
rounded = Math.floor(serverTime / coeff) * coeff
}
const lastBarSec = lastBar?.time / 1000
if (rounded > lastBarSec) {
// 新建 K 线
return {
time: rounded * 1000,
open: Number(bid),
high: Number(bid),
low: Number(bid),
close: Number(bid)
}
} else {
// 更新当前 K 线
return {
time: lastBar.time,
open: lastBar.open,
high: Math.max(lastBar.high, Number(bid)),
low: Math.min(lastBar.low, Number(bid)),
close: Number(bid)
}
}
}
// 处理 WebSocket 消息
@action
message(res) {
if (res?.header?.msgId === 'symbol') {
const quoteBody = this.parseQuoteBodyData(res?.body)
if (quoteBody?.symbol === this.activeSymbolInfo?.symbolInfo?.name) {
const newLastBar = this.updateBar(quoteBody, {
resolution: this.activeSymbolInfo.resolution,
precision: this.activeSymbolInfo.symbolInfo.precision,
symbolInfo: this.activeSymbolInfo.symbolInfo
})
if (newLastBar) {
this.activeSymbolInfo.onRealtimeCallback?.(newLastBar)
this.lastbar = newLastBar
}
}
}
}
// Datafeed 回调
@action
getDataFeedBarCallback = (obj = {}) => {
const { symbolInfo, resolution, firstDataRequest, from, to, countBack, onHistoryCallback } = obj
this.getHttpHistoryBars(symbolInfo, resolution, from, to, countBack, firstDataRequest).then((bars) => {
if (bars?.length) {
onHistoryCallback(bars, { noData: false })
this.lastbar = bars.at(-1)
} else {
onHistoryCallback(bars, { noData: true })
}
})
}
}
export default wsStore四、自定义指标开发
4.1 自定义 MA 指标示例
// src/components/Tradingview/customIndicators/ma.ts
const customerMovingAverage = (PineJS: PineJS) => {
const indicators: CustomIndicator = {
name: 'Customer Moving Average',
metainfo: {
_metainfoVersion: 51,
id: 'Customer Moving Average@tv-basicstudies-1',
name: 'Customer Moving Average',
description: 'Customer Moving Average',
shortDescription: 'MA',
is_price_study: true,
isCustomIndicator: true,
format: { type: 'price' },
defaults: {
styles: {
plot_0: { linestyle: 0, linewidth: 1, plottype: 0, trackPrice: false, transparency: 35, visible: true, color: '#FF0000' },
plot_1: { linestyle: 0, linewidth: 1, plottype: 0, trackPrice: false, transparency: 35, visible: true, color: '#00FF00' },
plot_2: { linestyle: 0, linewidth: 1, plottype: 0, trackPrice: false, transparency: 35, visible: true, color: '#00FFFF' }
},
inputs: { in_0: 5, in_1: 10, in_2: 30 },
precision: 4
},
plots: [
{ id: 'plot_0', type: 'line' },
{ id: 'plot_1', type: 'line' },
{ id: 'plot_2', type: 'line' }
],
inputs: [
{ id: 'in_0', name: 'Length', defval: 9, type: 'integer', min: 1, max: 1e4 },
{ id: 'in_1', name: 'Length1', defval: 10, type: 'integer', min: 1, max: 1e4 },
{ id: 'in_2', name: 'Length2', defval: 30, type: 'integer', min: 1, max: 1e4 }
]
},
constructor: function (this: LibraryPineStudy<IPineStudyResult>) {
this.main = function (context, inputCallback) {
const close = PineJS.Std.close(context)
const len1 = inputCallback(0)
const len2 = inputCallback(1)
const len3 = inputCallback(2)
const value1 = PineJS.Std.sma(close, len1, context)
const value2 = PineJS.Std.sma(close, len2, context)
const value3 = PineJS.Std.sma(close, len3, context)
return [
{ value: value1, offset: 0 },
{ value: value2, offset: 0 },
{ value: value3, offset: 0 }
]
}
}
}
return indicators
}
export default customerMovingAverage五、主题与样式定制
5.1 主题配置
// src/components/Tradingview/theme.ts
export const getTradingviewThemeCssVar = (theme: ThemeName) => {
const primary = ThemeConst.primary
const textPrimary = ThemeConst.textPrimary
const isDark = theme === 'dark'
return {
'--tv-color-toolbar-button-text': '#7B7E80',
'--tv-color-toolbar-button-text-active': textPrimary,
'--tv-color-toolbar-button-text-active-hover': textPrimary,
'--tv-color-toolbar-toggle-button-background-active': primary,
'--tv-color-toolbar-toggle-button-background-active-hover': primary,
'--tv-color-popup-element-text-active': '#131722',
'--tv-color-popup-element-background-active': '#f0f3fa',
...(isDark ? { '--tv-color-pane-background': ThemeConst.black } : {})
}
}5.2 K线颜色与涨跌色设置
// src/components/Tradingview/widgetMethods.ts
export type ColorType = 1 | 2 // 1绿涨红跌 2红涨绿跌
export function setChartStyleProperties(props: { colorType: ColorType; tvWidget: IChartingLibraryWidget }) {
const { colorType, tvWidget } = props
const red = ThemeConst.red // #C54747
const green = ThemeConst.green // #45A48A
let upColor = Number(colorType) === 2 ? red : green
let downColor = Number(colorType) === 2 ? green : red
// 蜡烛图样式
tvWidget.chart().getSeries().setChartStyleProperties(1, {
upColor,
downColor,
wickUpColor: upColor,
wickDownColor: downColor,
borderUpColor: upColor,
borderDownColor: downColor
})
// 空心蜡烛图样式
tvWidget.chart().getSeries().setChartStyleProperties(9, {
upColor,
downColor,
wickUpColor: upColor,
wickDownColor: downColor,
borderUpColor: upColor,
borderDownColor: downColor
})
}六、性能优化策略
6.1 数据加载优化
// 1. 按需加载历史数据
getHttpHistoryBars = async (symbolInfo, resolution, from, to, countBack, firstDataRequest) => {
const size = document.documentElement.clientWidth >= 1200 ? 500 : 200
// 根据屏幕宽度调整加载数量,移动端减少请求数据量
}
// 2. 数据缓存策略
@action
getDataFeedBarCallback = (obj = {}) => {
const { firstDataRequest } = obj
if (firstDataRequest) {
// 首次请求完整数据
this.getHttpHistoryBars(symbolInfo, resolution, from, to, countBack, true)
} else {
// 后续请求只获取增量数据
this.getHttpHistoryBars(symbolInfo, resolution, from, this.lastBarTime, countBack, false)
}
}6.2 WebSocket 连接优化
// 使用 reconnecting-websocket 实现自动重连
this.socket = new ReconnectingWebSocket(wsUrl, ['WebSocket', token], {
minReconnectionDelay: 1,
connectionTimeout: 3000,
maxEnqueuedMessages: 0,
maxRetries: 10000
})
// 心跳保活
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
this.send({}, { msgId: 'heartbeat' })
}, 20000)
}6.3 图表渲染优化
// 1. 使用 loading 状态避免闪烁
const [loading, setLoading] = useState(true)
setTimeout(() => {
setLoading(false)
}, 200)
// 2. 延迟初始化避免阻塞
useEffect(() => {
// 延迟加载图表
setTimeout(() => {
const tvWidget = new widget(widgetOptions)
}, 100)
}, [])
// 3. 缓存主题配置
const defaultBgColor = theme === 'dark' ? ThemeConst.black : ThemeConst.white
if (theme && defaultBgColor !== STORAGE_GET_CHART_PROPS('paneProperties.background')) {
STORAGE_REMOVE_CHART_PROPS()
}6.4 内存管理与清理
useEffect(() => {
return () => {
// 组件卸载时清理
tvWidget.remove() // 销毁图表实例
mitt.off('symbol_change') // 取消事件订阅
this.stopHeartbeat() // 停止心跳
this.socket?.close() // 关闭 WebSocket
}
}, [])七、常见问题与解决方案
7.1 主题切换不生效
// 问题:切换主题后图表颜色不变
// 解决:清除本地缓存 + 动态调用 changeTheme
// 1. 切换主题时清除缓存
STORAGE_REMOVE_CHART_PROPS()
// 2. 动态切换主题
tvWidget.changeTheme(theme)
// 3. 设置 CSS 变量
setCSSCustomProperty({ tvWidget, theme })7.2 数据请求重复
// 问题:多次调用 getBars
// 解决:使用 lastBarTime 缓存截止时间
this.lastBarTime = bars[0]?.time / 1000
if (this.lastBarTime === bars[0]?.time / 1000) {
this.datafeedBarCallbackObj.onHistoryCallback([], { noData: true })
}7.3 移动端适配
// 移动端禁用多余功能
if (props.isMobile) {
disabled_features.push('header_symbol_search', 'context_menus', 'show_chart_property_page', 'header_screenshot', 'left_toolbar')
}
// 禁止双指缩放
document.body.addEventListener(
'touchstart',
(e) => {
if (e.touches.length > 1) {
e.preventDefault()
}
},
{ passive: false }
)八、完整调用示例
// src/pages/index.tsx
import Tradingview from '@/components/Tradingview'
export default function ChartPage() {
return (
<div>
<Tradingview />
</div>
)
}symbolName: 交易品种,如 BTCUSDTtheme: 主题,light 或 darklocale: 语言,如 en、zh\_TWcolorType: 涨跌颜色,1 绿涨红跌,2 红涨绿跌chartType: 图表类型,1 蜡烛图、2 折线图等总结
邀请链接: https://ai.baishan.com/auth/login?referralCode=7wHVGUNz4s
支持 GLM,Qwen,DeepSeek,Kimi 等国内主流模型,大家薅起来;
想要学习或者工作使用大模型的赶紧注册起来吧,留着备用!
Docker 是一个用于在容器内部开发、发布和运行应用程序的平台。使用 Docker 时的一个常见任务是 docker cp 命令允许您在容器和本地文件系统之间复制文件或目录,基本语法如下: 在将文件复制到容器之前,先找出容器的名称或 id 例如,将 example.txt 复制到 mycontainer 容器下的 /tmp 目录 检查文件是否正确复制,运行如下命令 Overwriting docker cp 命令会在没有任何警告的情况下覆盖容器内的文件,所以一定要确保目标路径是正确的,并且不包含相同的文件或目录名称,除非您打算覆盖它们。 Permissions 文件复制到容器将保持其权限,您可能需要在复制文件后调整文件权限。 Alternatives 除了 docker cp 命令,您还可以使用 volume 在主机和容器之间共享文件。volume 是更高级的主题,如果需要持久化数据或希望在主机和容器文件系统之间实现实时同步。
在主机和容器之间传输文件。在本文中,我们将探讨如何使用将文件从主机复制到 Docker 容器内部。docker cp 命令
docker cp [OPTIONS] SRC_PATH CONTAINER:DEST_PATH复制文件到 Docker 容器
docker psdocker cp example.txt mycontainer:/tmp/docker exec mycontainer ls /tmp/注意事项
我的开源项目
白山一提起,大家可能最新想到的是 CDN ,现在他们也出了 AI 广场,可以调用 GLM-5,miniMax ,注册并认证送 150 元,这个额度不用白不用,白山怎么说也算是个正经厂了
aff: https://ai.baishan.com/auth/login?referralCode=akCUuc9eOq
邀请码:akCUuc9eOq
curl --request POST \
--url https://api.edgefn.net/v1/chat/completions \
--header 'Authorization: Bearer 你的 APIkey' \
--header 'Content-Type: application/json' \
--data '{
"model": "MiniMax-M2.5",
"messages": [{"role": "user", "content": "Hello, how are you?"}]
}'
讽刺的是,这一事件发生在Qwen用户增长最快、影响力最大的时期: 林俊旸在X平台发布告别消息: 此次事件引发了业界对以下问题的广泛讨论: 正如批评者所言:当技术天才被当成"业务的技术外包"使用,离心力便产生了。 事件仍在持续发酵中,林俊旸的下一步去向尚未公布。一、事件背景:高光时刻的突然转折

二、核心人物:林俊旸是谁?
项目 信息 年龄 32岁 学历 2019年北京大学毕业(计算机科学与语言学) 职级 阿里巴巴最年轻的P10级高管(高级领导层级别) 贡献 阿里早期AI工作核心贡献者(M6、OFA模型),将Qwen从一个不起眼的副项目发展成为世界上最具影响力的开源LLM系列 三、事件时间线
2026年1月
2026年春节(除夕夜)
2026年3月3日(周一)下午
2026年3月4日凌晨
"me stepping down. bye my beloved qwen."(我卸任了,再见,我心爱的Qwen)

2026年3月4日白天


2026年3月4日下午
2026年3月5日上午
四、离职深层原因分析
1. 组织架构重组冲突
2. 外部空降高管带来的紧张
3. 资源分配问题
4. 错位的考核体系
5. 开源模式商业化困惑

五、阿里高管/HR回应及争议
CEO吴泳铭
首席人才官蒋芳(争议最大)
阿里云CTO周靖人
内部匿名评价

六、核心成员离职潮
姓名 职位 离职时间 去向 惠必远 Qwen Code负责人 2026年1月 加入Meta 林俊旸 Qwen技术负责人 2026年3月3日 未知 余博文 Qwen后训练负责人 2026年3月4日 未知 李凯欣 Qwen 3.5/Qwen-VL核心贡献者 2026年3月4日 未知 七、社区与外界反应
团队成员
行业人士
网友评论
八、事件本质总结
维度 问题 人才管理 "不能推上神坛"表态引发寒蝉效应 沟通机制 组织架构调整缺乏充分信息沟通 资源分配 算力、招聘名额内外不平衡 考核体系 用C端DAU指标考核基座模型团队 技术尊重 高管称Qwen 3.5为"半成品" 权力博弈 空降高管引发原有团队不安 九、事件影响与反思
等了几天,以为今晚肯定会发布了,看来没希望了
进去看了下,一进去一堆人给你发什么老公,就是一个频道 几个人挂在上面 刷礼物他们语音配料,刷三十块钱才唱首歌,这朋友之前也是,要不然干主播,要不然干那啥,反正就是类似的工作,但是听她说这玩意确实挺挣钱.. 那种大叔挺多的。我比较好奇这种一般什么人会去点啊,一百块钱的礼物陪聊一个小时,我按摩不过一百出头..
PHP 的传统执行模型是同步的,这意味着代码按照语句出现的顺序逐条执行。这本身并非问题,因为同步思维往往更为简单。 当要求 PHP 开发者实现 SQL 分页展示时,他们通常会先执行一条统计总数的查询,再执行第二条查询获取当前页的数据。总记录数对于生成分页链接(首页、下一页、末页等)是必需的。 当 SQL 服务器处理第一条计数查询时,PHP 服务器处于等待状态,收到响应后才执行第二条查询。 当然,存在一次性获取两种信息的方法,但那不是本文的主题,请保持专注。 从这个分页示例中,我们可以看到潜在的优化空间:在 SQL 服务器处理第一条查询的同时启动第二条查询。但要注意,在拿到计数结果之前我们不会显示分页链接,因此即使计数查询先完成,也需要等待另一条查询的结果。 由此可见,异步操作的管理不仅限于并行执行任务,还包括管理响应的处理顺序。 存在许多需要异步执行代码的场景,这通常与 I/O 操作相关:HTTP 请求、数据库访问、文件读写或启动外部进程。 要判断 PHP 是否"异步",首先需要理解"异步"的含义。异步指的是:不同时发生。当某项操作耗时时,与其等待完成,不如先去做其他事情,等操作完成后再回来继续。因此,异步的核心在于操作是非阻塞的。 人们常常混淆异步和并行。 打个比方:异步如同一位厨师将锅接满水放在灶台上开火,趁水烧开的工夫去切蔬菜。等蔬菜切好、水也烧开,就开始烹饪。 并行则是两位厨师:一位切蔬菜的同时,另一位负责烧水。蔬菜切好、水也烧开后,由第一位厨师负责烹饪。 并行节省了时间,因为切蔬菜与烧水准备是同时进行的。但两种模式下,水烧开的过程中都可以去做其他事情。 具体而言,我们的"厨师"就是机器的 CPU/GPU。 从 2002 年 PHP 4.3 发布起,一项重要功能被引入:Streams。通过 注意,这段示例代码刻意简化,未处理错误等情况。 在 23 年前 PHP 就已支持异步编程,然而几年前人们还说 PHP 不是异步语言,为什么? 因为实现异步不仅仅是启动非阻塞处理,还需要有机制来管理这些等待时间。 这就引入了协程的概念。协程是一种可以被挂起、之后恢复的函数。 2013 年 6 月,PHP 5.5 引入生成器(Generators)后,开发者开始将其改造为协程使用。 PHP 8.1 的发布标志着 PHP 向异步编程迈出了重要一步,引入了 Fiber 作为真正的协程技术基础。 你会发现代码与使用生成器时几乎没什么变化。 虽然 PHP 从 4.3 版本就具备底层异步能力,但 PHP 8.1 引入的 Fiber 标志着一个转折点。Fiber 提供了原生且强大的异步编程工具,使其变得更加自然。 既然我们已经知道如何中断协程并执行非阻塞处理,接下来需要管理多个并行任务,因为单个异步处理的意义不大。 谈到并行,人们常会想到线程——线程提供进程间的自然隔离,并能利用多核 CPU,这对计算密集型任务非常有吸引力。 然而,并行、特别是多线程的实现更为复杂,调试更困难,还存在死锁和内存并发访问的风险。 正是出于这些原因,Web 领域更倾向于使用另一种模式:EventLoop。Web 场景的特点是并发连接数可能非常高。 EventLoop 是一个无限循环,它监听事件队列(如结果到达),并以串行方式逐个处理。 我们将待处理的任务加入这个队列,然后启动循环。 问题是如何告知 EventLoop 如何处理任务的结果?很简单,我们指定一个回调函数,当结果可用时 EventLoop 会调用它。 注意:下面代码中的 EventLoop 是虚构的,但代表了大多数 EventLoop 的工作方式。 这段代码的预期输出: 同时读取两个文件的情况: 根据存储介质的性能,输出可能是: 当需要链式执行异步操作时,就会陷入回调地狱(或末日金字塔):回调函数层层嵌套。 如果再加上错误处理,代码会更加复杂难读。 为了改善可读性和更好地管理异步,Promise(承诺)的概念值得考虑。 Promise 的概念于 80 年代在 Multilisp 等语言中引入,但真正流行是在 2009 年,Dojo、Q、jQuery.Deferred 等 JavaScript 库率先实现了它。 Promise 是什么?它是一个包含处理结果(当前或未来)的对象。打个比方: "我不会立即给你处理结果,但我承诺稍后会在这个对象里给你。" 示例代码: 运行这段代码会看到 "启动 Promise",但 "Hello, world!" 在哪里?为什么要调用 实际上,需要使用 输出: 如果 Promise 没有被解决(resolve),什么都不会发生,只会显示启动信息。 具体来说,当 Promise 被解决时, 配合 EventLoop 的完整示例: 这段代码使用异步定时器在 1 秒后解决 Promise。输出: Promise 的价值体现在哪里?回到回调地狱的问题。使用 Promise 后,代码可以这样写: 你也可以在回调中返回值,这个值会被转换为立即解决的 Promise。如果不返回任何内容,相当于返回一个值为 NULL 的已解决 Promise。 如果需要在各阶段处理错误, 需要注意的是,如果错误回调返回了值(或没有 return),后续的 这是 在 Packagist 上搜索 "promise" 会发现有 4 个包较为突出。 如果你已经在使用 Guzzle,可能无需选择其他包,因为它已经相当完善。 但 Guzzle/Promises 最初是为处理异步 HTTP 请求设计的,使用内部不暴露的 EventLoop,这使得集成其他类型的 I/O(如 Mysqli 异步查询或进程)更加困难。 剩下的两个重要选择是 ReactPHP 提供了简单且高性能的 JavaScript Promises/A+ 标准实现(Promise 最初是 JavaScript 语言中涌现的标准,没告诉过你吧?)。 Amp 则没有完全实现 Promise:3.0 版本中没有 因此,一边是 Promise 链式管理,另一边是面向协程的管理。 如果你用过 JavaScript 的 Promise,ReactPHP 可能更容易上手;否则 Amp 的协程方式代码可读性更好,更接近我们习惯的"同步" PHP 写法。 但无论选择 ReactPHP 还是 Amp,都需要 EventLoop。 ReactPHP 提供 如果你想使用 Promise 模式,毫无疑问应该选择 另一方面,Amp 提供了一种不同的写法,对某些人来说可能更"自然",建议你两种都试试看哪个更适合。 对于 EventLoop,建议选择 Revolt,其统一生态的愿景在中期来看可能会带来回报。 还有一个参考因素:Amp v3 使用 PHP 8.1 的 Fiber,而 ReactPHP 可以在 PHP 7.1 上运行。PHP 的异步编程 该怎么选择
PHP 是异步的吗?
PHP 的异步能力
stream_set_blocking() 和 stream_select() 函数,PHP 进入了异步编程时代。$h = fopen(__FILE__, 'r');
stream_set_blocking($h, false);
$content = '';
while (!feof($h)) {
$read = array($h);
$write = $except = null;
// 检查是否有可读内容,最多等待 1000 微秒
// 永远不要设为 0,否则会导致 CPU 过度占用
$ready = stream_select($read, $write, $except, 1000);
if ($ready === 0) {
// 没有可读内容,稍作等待
// 或者去做其他事情...
usleep(1000);
continue;
}
$chunk = fgets($h, 1024);
if ($chunk !== false) {
$content .= $chunk;
}
}
fclose($h);
echo $content;usleep(1000) 的位置,可以执行其他操作,比如读取另一个文件,甚至向其他服务器发起 HTTP 请求。不过,如果你的文件系统很快,可能不会进入等待时间。这种技术更适合处理慢速文件系统或其他类型的 I/O 操作。协程与 Fiber
$generator = (function() {
$count = 3;
echo "开始\n";
while(true) {
yield; // 挂起函数(生成器)
echo "有结果了吗?\n";
$count--;
if ($count === 0) {
return; // 收到结果,停止
}
}
})();
$generator->current(); // 启动处理
do {
echo "做其他事情\n";
$generator->next(); // 恢复函数执行(从 yield 处继续)
} while ($generator->valid()); // 函数是否结束?
echo "结束\n";$fiber = new Fiber(function() {
$count = 3;
echo "开始\n";
while(true) {
Fiber::suspend(); // 挂起 fiber
echo "有结果了吗?\n";
$count--;
if ($count === 0) {
return; // 收到结果,停止
}
}
});
$fiber->start(); // 启动处理
do {
echo "做其他事情\n";
$fiber->resume(); // 恢复 fiber 执行
} while (!$fiber->isTerminated()); // fiber 是否结束?
echo "结束\n";Event Loop
$loop = EventLoop::get();
$loop->addReadStream('file.txt', function(string $data) {
echo "读取到的数据:{$data}";
});
echo "启动 EventLoop\n";
$loop->run();启动 EventLoop
读取到的数据:<file.txt 的内容>$loop = EventLoop::get();
$loop->addReadStream('/dev/cdrom/file1.txt', function(string $data) {
echo "数据 1 已读取:{$data}";
});
$loop->addReadStream('/dev/fb0/file2.txt', function(string $data) {
echo "数据 2 已读取:{$data}";
});
echo "启动 EventLoop\n";
$loop->run();启动 EventLoop
数据 2 已读取:<软盘数据>
数据 1 已读取:<光盘数据>Promise
$loop = EventLoop::get();
$loop->addReadStream('file.txt', function(string $data) {
EventLoop::get()->defer(function() use ($data) {
return compressData($data);
}, function ($compressedData) {
EventLoop::get()->addWriteStream(
'http://foo',
$compressedData,
function (Response $response) {
echo "数据已发送\n";
});
});
});
echo "启动 EventLoop\n";
$loop->run();$promise = new Promise(function ($resolve, $reject) {
echo "启动 Promise\n";
$resolve("Hello, world!");
});$resolve()?then() 方法配合回调函数:$promise = new Promise(function ($resolve, $reject) {
echo "启动 Promise\n";
$resolve("Hello, world!");
});
$promise->then(
function ($value) {
echo "Promise 结果:$value\n";
}
);启动 Promise
Promise 结果:Hello, world!then() 中的回调会被执行。这种情况可能发生在 Promise 内部包含协程时——协程经过长时间处理收到结果后调用 $resolve()。$loop = EventLoop::get();
$promise = new Promise(function ($resolve, $reject) use ($loop) {
echo "启动 Promise\n";
$loop->addTimer(1, function () use ($resolve) {
echo "解决 Promise\n";
$resolve("Hello, World!");
});
});
$promise->then(
function ($value) {
echo "结果:$value\n";
}
);
$loop->run();启动 Promise
解决 Promise
结果:Hello, World!readFileAsync('file.txt')
->then(function ($data) {
return compressDataAsync($data);
})
->then(function ($compressedData) {
return sendDataAsync('http://foo', $compressedData);
})
->catch(function ($error) {
echo "错误:{$error}\n";
});readFileAsync() 返回一个使用 EventLoop 的 Promise,在获得结果时解决。compressDataAsync() 和 sendDataAsync() 同样返回 Promise。catch() 用于处理链中任何环节的错误。现在我们不再是嵌套回调,而是回调链。then() 方法接受第二个参数作为拒绝(错误)时的回调:readFileAsync('file.txt')
->then(
function ($data) {
return compressDataAsync($data);
},
function ($error) {
echo "文件读取错误:{$error}\n";
}
)
->then(function ($compressedData) {
return sendDataAsync('http://foo', $compressedData);
})
->catch(function ($error) {
echo "错误:{$error}\n";
});then() 会收到一个已解决的 Promise。因此需要返回一个错误状态的 Promise 或抛出异常。then(onResolve, onReject) 中处理错误的常见陷阱之一——需要在后续所有 then() 中处理错误。上面的代码中,sendDataAsync() 会收到包含 NULL 的 $compressedData。包选型建议
Guzzle/promises 和 php-http/promise
guzzle/promises 的下载量遥遥领先,很大程度上是因为它被流行的 HTTP 客户端 guzzle/guzzle 直接使用。php-http/promise 情况类似,同样专注于 HTTP 请求。ReactPHP 和 Amp
react/promise 和 amphp/amp。then(),但它实现了另一种机制——Futures,设计用于在基于生成器或 Fiber 的协程中通过 await() 等待。react/event-loop 包,Amp 推荐使用 revolt/event-loop——这是 Amp 团队发起的项目,旨在围绕现代事件循环标准统一 PHP 异步生态。Revolt 可通过适配器与 ReactPHP 互操作。怎么选?
react/promise。
目地址:
大家好,我是 NextClaw 的作者。
发这个帖,主要是想把项目讲清楚,也收集一轮真实反馈。
我自己的观察是:现在很多 OpenClaw 替代品更偏学习或二次开发。真到日常使用阶段,在功能和生态完整度上,经常会和 OpenClaw 拉开差距。NextClaw 想补的是这个空位:尽量保留 OpenClaw 生态兼容,同时把易用性做上去。
先说背景。NextClaw 是受 OpenClaw 启发做的,我们一直把 OpenClaw 当作很重要的参考。两边不是替代关系,更像两种取舍。
愿景方向:
值得一试的理由:
当前能力也补充一下:
如果你想快速搭一个可长期维护的个人 AI 中枢,这个项目可能适合你。
体验方式:
npm i -g nextclaw
nextclaw start
打开 http://127.0.0.1:18791 即可。
欢迎直接回帖提意见,尤其是你觉得最难用的一步是什么。
官方发布页:Introducing GPT‑5.4
引入计算机使用与计算机视觉能力
相关新闻:New ChatGPT 5.4 Model Is 'Built for Agents.' Will It Lure Back Claude Converts?
点赞 + 关注 + 收藏 = 学会了 每次注册新账号,是不是都在“123456”和“大写字母+生日”之间反复横跳? PSWD 是一个极简、隐私、开源的随机密码生成工具。部署在自己的 NAS 上,既不用担心在线生成器的安全性,又能随时生成符合各种复杂度要求的“赛博护符”。 本次使用绿联 NAS 安装 PSWD,其他品牌的 NAS 操作步骤也是差不多的。 打开 文件管理,在 打开 Docker 应用,进入 项目 面板,点击“创建项目”。 Compose配置如下: 这里的 等项目构建成功后,切换到「容器」面板找到 PSWD,点击它旁边的小箭头,或者在浏览器输入 PSWD 不只是生成一串乱码,它支持全能型随机密码(Password:大小写、数字、特殊符号随心控)、密码短语(Passphrase:随机单词组合)、Pin(纯数字模式) 你可以根据不同的应用场景,一键配置复杂度,甚至连长度都能拉满 以上就是本文的全部内容啦~ 你有发现什么冷门但好玩的镜像吗?欢迎在评论区留言“踢”我一下! 想了解更多NAS玩法记得关注《NAS邪修》👏 往期推荐: 点赞 + 关注 + 收藏 = 学会了💡整理了一个 NAS 专属玩法专栏,感兴趣的工友可以戳这里关注 👉 《NAS邪修》

docker 文件夹下新建一个文件夹,命名为 pswd。
pswd/docker/pswd 文件夹。
services:
timesy:
image: ghcr.io/remvze/pswd
logging:
options:
max-size: 1g
restart: always
ports:
- 3337:8080 # 前面 3337 是访问端口,可自定义3337 是我指定的访问端口,如果和你现有的服务冲突,改成任意没被占用的数字即可(比如 5678)。NAS_IP:3337 就能使用 PSWD 了。
