diff --git a/demo/xueqiu.test.js b/demo/xueqiu.test.js new file mode 100644 index 00000000..51d3010c --- /dev/null +++ b/demo/xueqiu.test.js @@ -0,0 +1,16 @@ +const Axios = require('axios'); +Axios.get(`http://xueqiu.com/`, {headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/3.0 Safari/536.11', +}},).then((response) => { + const cookiesHeader = response.headers['set-cookie']; + console.log("🚀 ~ Axios.get ~ response:", response) + this.cookies += + cookiesHeader + .map((h) => { + let content = h.split(';')[0]; + return content.endsWith('=') ? '' : content; + }) + .filter((h) => h !== '') + .join(';') + ';'; +}); \ No newline at end of file diff --git a/src/explorer/newsService.ts b/src/explorer/newsService.ts index da479c76..501048ba 100644 --- a/src/explorer/newsService.ts +++ b/src/explorer/newsService.ts @@ -1,25 +1,10 @@ import Axios from 'axios'; import { TreeItem, Uri } from 'vscode'; import { randHeader } from '../shared/utils'; +import { defaultXueQiuHeaders, getXueQiuToken } from '../shared/xueqiu-helper'; -const defaultHeaders = { - Accept: - 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'en-US,en;q=0.9', - 'Cache-Control': 'max-age=0', - Connection: 'keep-alive', - Host: 'xueqiu.com', // 股票的话这里写 stock.xueqiu.com - 'Sec-Fetch-Dest': 'document', - 'Sec-Fetch-Mode': 'navigate', - 'Sec-Fetch-Site': 'none', - 'Sec-Fetch-User': '?1', - 'Upgrade-Insecure-Requests': 1, - 'User-Agent': - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36};', -}; -export class NewsTreeItem extends TreeItem {} +export class NewsTreeItem extends TreeItem { } export class NewsService { private cookies = `device_id=${Math.random().toString(36).substring(2, 15)}`; @@ -29,24 +14,15 @@ export class NewsService { get headers() { return { - ...defaultHeaders, + ...defaultXueQiuHeaders, ...randHeader(), Cookie: this.cookies, }; } - init() { - Axios.get(`https://xueqiu.com/`).then((response) => { - const cookiesHeader = response.headers['set-cookie']; - this.cookies += - cookiesHeader - .map((h: string) => { - let content = h.split(';')[0]; - return content.endsWith('=') ? '' : content; - }) - .filter((h: string) => h !== '') - .join(';') + ';'; - }); + async init() { + const c = await getXueQiuToken(); + this.cookies = c; } async getNewsUserList(userIds: string[]): Promise { diff --git a/src/explorer/stockProvider.ts b/src/explorer/stockProvider.ts index 52e6f720..30083642 100644 --- a/src/explorer/stockProvider.ts +++ b/src/explorer/stockProvider.ts @@ -1,5 +1,5 @@ import { Event, EventEmitter, TreeDataProvider, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { compact, flattenDeep, uniq } from 'lodash'; +// import { compact, flattenDeep, uniq } from 'lodash'; import globalState from '../globalState'; import { LeekTreeItem } from '../shared/leekTreeItem'; import { defaultFundInfo, SortType, StockCategory } from '../shared/typed'; @@ -37,8 +37,8 @@ export class StockProvider implements TreeDataProvider { if (!element) { // Root view const stockCodes = LeekFundConfig.getConfig('leek-fund.stocks') || []; - const stockList: string[] = uniq(compact(flattenDeep(stockCodes))); - return this.service.getData(stockList, this.order).then(() => { + // const stockList: string[] = uniq(compact(flattenDeep(stockCodes))); + return this.service.getData(stockCodes, this.order).then(() => { return this.getRootNodes(); }); } else { diff --git a/src/explorer/stockService.ts b/src/explorer/stockService.ts index 85d7380e..d01fdd38 100644 --- a/src/explorer/stockService.ts +++ b/src/explorer/stockService.ts @@ -3,11 +3,13 @@ import { decode } from 'iconv-lite'; import { ExtensionContext, QuickPickItem, window } from 'vscode'; import globalState from '../globalState'; import { LeekTreeItem } from '../shared/leekTreeItem'; -import { HeldData } from '../shared/typed'; import { executeStocksRemind } from '../shared/remindNotification'; +import { HeldData } from '../shared/typed'; import { calcFixedPriceNumber, events, formatNumber, randHeader, sortData } from '../shared/utils'; +import { getXueQiuToken } from '../shared/xueqiu-helper'; import { LeekService } from './leekService'; import moment = require('moment'); +import Log from '../shared/log'; export default class StockService extends LeekService { public stockList: Array = []; @@ -28,21 +30,16 @@ export default class StockService extends LeekService { const maps = s.split(','); return this.stockList.filter((item) => !maps.includes(item.info.code)); } + async getToken(): Promise { if (this.token !== '') return this.token; - - const res = await Axios.get('https://xueqiu.com/'); - const cookies: string[] = res.headers['set-cookie']; - - const param: string = cookies.filter((key) => key.includes('xq_a_token'))[0] || ''; - this.token = param.split(';')[0] || ''; - console.log("🚀 ~ StockService ~ getToken ~ this.token:", this.token); - + const res = await getXueQiuToken(); + this.token = res; return this.token; } async getData(codes: Array, order: number): Promise> { - // console.log('fetching stock data…'); + // Log.info('fetching stock data…'); if ((codes && codes.length === 0) || !codes) { return []; } @@ -421,9 +418,7 @@ export default class StockService extends LeekService { headers: { ...randHeader(), Referer: 'https://stock.xueqiu.com/', - // 雪球token规则变化,临时解决 - // Cookie: await this.getToken(), - "cookie": "acw_tc=2760826017254576052523235e0b5e24ba6432d19252e786e33ed3e0ee2db8; acw_sc__v2=66d864c5c68f742d150271b6da3a371072df5bc6; xq_a_token=49c5e355d2fc1b871fde601c659cf9ae1457a889; xqat=49c5e355d2fc1b871fde601c659cf9ae1457a889; xq_r_token=250d5a132310b89c6cf1193e084989736506a297; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTcyNzkxNjc3OCwiY3RtIjoxNzI1NDU3NTg4MjU3LCJjaWQiOiJkOWQwbjRBWnVwIn0.ggqWWe7vvq1GcgRDS6e6abMOSiu6EqfW84QXQjeYQrLuja8zFA-KGrQBlcH6vV74bL6_NR1qROWqKp5fF7acOYagqLEUIXH4xG9M_Pf_EvscNzcp9IitW-0a5CEivezAIoms_ajKpEOp-toXQq7aOG4KfNw6Paktzr4nlrBXtmuXb9V0eaIFXnQAhjVK2_2-X351YGYfUesT0io22BYJUus037if8O_H3GmSF9xWTFcBWZ3seQl2wG_w1fwVhzP1SzcjiIzEEIU8QXttFMMUmUkG8SWsFV18JFKYEu618ZTItxE6-ijxL5Dk0oKt8MV58uz9chQ4o4vlukd_-UguTA; cookiesu=411725457605875; u=411725457605875; Hm_lvt_1db88642e346389874251b5a1eded6e3=1725457610; HMACCOUNT=2546BEF12550F4D4; device_id=a0814ba472ea428d8488ca48833b7db8; smidV2=2024090421465125ea22186efc27f3de9c33bd7df8002b00f1792dfda7b5c90; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1725457619; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=gyo2b8VGfAxa5SfTipuQx8ezwddhs93f7FW74dnOzyirEbx849B+Ff3Z7VnhDgFgOEPO8rw+tAuePz+dXrbBXQ%3D%3D; ssxmod_itna=eqRxyDciDQG=Dtmx0dGQDHFySC7fYDnG7nD8eK9cx0yDReGzDAxn40iDt=a5/j7OxAPYi0424Dun2Dh3UKWOhLINTInBw5jDbxiTD4q07Db4GkDAqiOD7kRwoD435GwD0eG+DD4DWtXI=D7rXgUkNXWq=07TNDmb=uDGQcDiU3xi5Z/L=/8eGWnqGfDDoDYbNSAeQDGkKDbTQDITXKb8hxqcj7Ha=2CPDuCa6YcqDLfQFHxB=ulmPSWFODtw=cnnQ2UAX=OuTd4hp3WxA3GuAxb8GY4rEKWhZxf6AifG+3fB3ST3e4Gw+UI04DDWDEd4D===; ssxmod_itna2=eqRxyDciDQG=Dtmx0dGQDHFySC7fYDnG7nD8eK9xn9S4DsLDwxqjKG7d4D==", + Cookie: await this.getToken() }, }); const { data, error_code, error_description } = resp.data; @@ -523,7 +518,7 @@ export default class StockService extends LeekService { searchText )}`; try { - console.log('getFutureSuggestList: getting...'); + Log.info('getFutureSuggestList: getting...'); const futureResponse = await Axios.get(futureUrl, { responseType: 'arraybuffer', transformResponse: [ @@ -539,7 +534,7 @@ export default class StockService extends LeekService { return result; } const tempArr = text.split(';'); - console.log(tempArr); + Log.info(tempArr); tempArr.forEach((item: string) => { const arr = item.split(','); @@ -576,7 +571,7 @@ export default class StockService extends LeekService { }); return result; } catch (err) { - console.log(futureUrl); + Log.info(futureUrl); console.error(err); return [{ label: '期货查询失败,请重试' }]; } @@ -586,7 +581,7 @@ export default class StockService extends LeekService { searchText )}`; try { - console.log('getStockSuggestList: getting...'); + Log.info('getStockSuggestList: getting...'); const stockResponse = await Axios.get(stockUrl, { responseType: 'text', transformResponse: [ @@ -600,8 +595,11 @@ export default class StockService extends LeekService { Referer: 'https://stock.xueqiu.com/', Cookie: await this.getToken(), }, + }).catch(() => { + this.token = ''; + return { data: {} }; }); - const stocks = stockResponse.data.stocks || []; + const stocks = stockResponse.data?.stocks || []; stocks.forEach((item: any) => { const { code, name } = item; if (code.startsWith('SH') || code.startsWith('SZ') || code.startsWith('BJ')) { @@ -627,7 +625,7 @@ export default class StockService extends LeekService { }); return result; } catch (err) { - console.log(stockUrl); + Log.info(stockUrl); console.error(err); return [{ label: '股票查询失败,请重试' }]; } diff --git a/src/extension.ts b/src/extension.ts index 6cdc5f93..d165c159 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,9 +4,7 @@ * Github: https://github.com/giscafer *-------------------------------------------------------------*/ -import { compare } from 'compare-versions'; -import { compact, flattenDeep, uniq } from 'lodash'; -import { ConfigurationChangeEvent, ExtensionContext, extensions, TreeView, window, workspace } from 'vscode'; +import { ConfigurationChangeEvent, ExtensionContext, TreeView, window, workspace } from 'vscode'; import { BinanceProvider } from './explorer/binanceProvider'; import BinanceService from './explorer/binanceService'; import { ForexProvider } from './explorer/forexProvider'; @@ -22,6 +20,7 @@ import FlashNewsOutputServer from './output/flash-news/FlashNewsOutputServer'; import { registerCommandPaletteEvent, registerViewEvent } from './registerCommand'; import { HolidayHelper } from './shared/holidayHelper'; import { LeekFundConfig } from './shared/leekConfig'; +import Log from './shared/log'; import { Telemetry } from './shared/telemetry'; import { SortType } from './shared/typed'; import { events, formatDate, isStockTime } from './shared/utils'; @@ -134,7 +133,7 @@ export function activate(context: ExtensionContext) { manualRequest(); } } else { - console.log('StockMarket Closed! Polling closed!'); + Log.info('StockMarket Closed! Polling closed!'); // 闭市时增加轮询间隔时长 if (intervalTime === intervalTimeConfig) { intervalTime = intervalTimeConfig * 100; @@ -184,8 +183,9 @@ export function activate(context: ExtensionContext) { setIntervalTime(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars workspace.onDidChangeConfiguration((e: ConfigurationChangeEvent) => { - console.log('🐥>>>Configuration changed', e); + Log.info('Configuration changed'); intervalTimeConfig = LeekFundConfig.getConfig('leek-fund.interval'); setIntervalTime(); setGlobalVariable(); @@ -256,18 +256,23 @@ function setGlobalVariable() { globalState.fundLists = fundLists; } // 临时解决3.10.1~3.10.3 pr产生的分组bug - const leekFundExt = extensions.getExtension('giscafer.leek-fund'); - const currentVersion = leekFundExt?.packageJSON?.version; + // const leekFundExt = extensions.getExtension('giscafer.leek-fund'); + // const currentVersion = leekFundExt?.packageJSON?.version; // if (compare(currentVersion, '3.9.2', '>=')) { - const arr = LeekFundConfig.getConfig('leek-fund.stocks') || []; - const stockList = uniq(compact(flattenDeep(arr))); - LeekFundConfig.setConfig('leek-fund.stocks', stockList); + // const arr = LeekFundConfig.getConfig('leek-fund.stocks') || []; + // const flag = arr.some((a: any) => Array.isArray(a)); + // if (flag) { + // const stockList = uniq(compact(flattenDeep(arr))); + // Log.info(" ~ setGlobalVariable ~ stockList:", stockList); + // LeekFundConfig.setConfig('leek-fund.stocks', stockList); + // } + // } } // this method is called when your extension is deactivated export function deactivate() { - console.log('🐥deactivate'); + Log.info('deactivate'); FlashNewsDaemon.KillAllServer(); profitBar?.destroy(); if (loopTimer) { diff --git a/src/shared/holidayHelper.ts b/src/shared/holidayHelper.ts index f6933f6f..b09bc120 100644 --- a/src/shared/holidayHelper.ts +++ b/src/shared/holidayHelper.ts @@ -1,5 +1,6 @@ import axios from 'axios'; -import { formatDate } from './utils'; +import { formatDate, randHeader } from './utils'; +import Log from './log'; export class HolidayHelper { /** @@ -11,7 +12,12 @@ export class HolidayHelper { // https://timor.tech/api/holiday/year/2020 const url = `https://timor.tech/api/holiday/info/${year}`; try { - const response = await axios.get(url); + const response = await axios.get(url, { + headers: { + ...randHeader(), + Referer: 'https://timor.tech/', + } + }); const data = response.data; // 返回的结构体如下: // 解释: @@ -53,8 +59,14 @@ export class HolidayHelper { // https://timor.tech/api/holiday/info/2020-09-18 const url = `https://timor.tech/api/holiday/info/${formatDate(date)}`; try { - const response = await axios.get(url); + const response = await axios.get(url, { + headers: { + ...randHeader(), + Referer: 'https://timor.tech/', + } + }); const data = response.data; + // 返回的结构体如下: // 实例: {"code":0,"type":{"type":0,"name":"周五","week":5},"holiday":null} // 解释: @@ -91,7 +103,8 @@ export class HolidayHelper { let dataObj = await HolidayHelper.getHolidayDataByDate(date); if (dataObj) { - tof = dataObj.type.type === 2; + tof = dataObj.type?.type === 2; + Log.info("HolidayHelper ~ 节假日= ~ :", dataObj?.type?.name); } return tof; diff --git a/src/shared/leekConfig.ts b/src/shared/leekConfig.ts index d07c0e0f..747ed9d8 100644 --- a/src/shared/leekConfig.ts +++ b/src/shared/leekConfig.ts @@ -7,7 +7,7 @@ import { window, workspace } from 'vscode'; import globalState from '../globalState'; import { clean, uniq, events } from './utils'; -import { flattenDeep } from 'lodash'; +import { compact, flattenDeep } from 'lodash'; export class BaseConfig { static getConfig(key: string, defaultValue?: any): any { @@ -22,12 +22,13 @@ export class BaseConfig { return config.update(cfgKey, cfgValue, true); } - static updateConfig(cfgKey: string, codes: Array) { + static async updateConfig(cfgKey: string, codes: Array) { const config = workspace.getConfiguration(); - const updatedCfg = [...config.get(cfgKey, []), ...codes]; - let newCodes = clean(updatedCfg); - newCodes = uniq(newCodes); - return config.update(cfgKey, newCodes, true); + const origin: string[] = config.get(cfgKey, []); + let newCodes = uniq(compact(origin.concat(codes))); + console.log(`🚀 ~ BaseConfig ~ updateConfig ~ ${cfgKey}:`, newCodes); + await config.update(cfgKey, newCodes, true); + return newCodes; } static removeConfig(cfgKey: string, code: string) { @@ -147,11 +148,16 @@ export class LeekFundConfig extends BaseConfig { // Fund End // Stock Begin - static updateStockCfg(codes: string, cb?: Function) { - this.updateConfig('leek-fund.stocks', codes.split(',')).then(() => { + static updateStockCfg(list: string, cb?: Function) { + const cfgKey = 'leek-fund.stocks'; + const config = workspace.getConfiguration(); + const origin: string[] = config.get(cfgKey, []); + let codes = typeof list === 'string' ? list.split(',') : list; + let newCodes = uniq(compact(flattenDeep(origin).concat(codes))); + config.update(cfgKey, newCodes, true).then(() => { window.showInformationMessage(`Stock Successfully add.`); if (cb && typeof cb === 'function') { - cb(codes); + cb(codes, newCodes); } }); } diff --git a/src/shared/log.ts b/src/shared/log.ts new file mode 100644 index 00000000..1651220c --- /dev/null +++ b/src/shared/log.ts @@ -0,0 +1,21 @@ +let verbose = false; + + +function log(level = 'info', ...msg: any[]) { + const date = new Date(); + console.log( + ` 🚀 「LeekFund」${date.toLocaleDateString()} ${date.toLocaleTimeString()} [${level || "info" + }] `, ...msg + ); +} + + +const Log = { + info: (...rest: any[]) => log("info", ...rest), + warn: (msg: string) => log("warn", msg), + error: (msg: string) => log("error", msg), + debug: (msg: string) => verbose && log("debug", msg), + log, +}; + +export default Log diff --git a/src/shared/xueqiu-helper.ts b/src/shared/xueqiu-helper.ts new file mode 100644 index 00000000..cbd50759 --- /dev/null +++ b/src/shared/xueqiu-helper.ts @@ -0,0 +1,41 @@ +import Axios from "axios"; + + +export const defaultXueQiuHeaders = { + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'en-US,en;q=0.9', + 'Cache-Control': 'max-age=0', + Connection: 'keep-alive', + Host: 'xueqiu.com', // 股票的话这里写 stock.xueqiu.com + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + 'Sec-Fetch-Site': 'none', + 'Sec-Fetch-User': '?1', + 'Upgrade-Insecure-Requests': 1, + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36};', +}; + +export async function getXueQiuToken() { + let cookies = ''; + const response = await Axios.get(`http://xueqiu.com/`, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.11 TaoBrowser/3.0 Safari/536.11', + } + }); + const cookiesHeader = response.headers['set-cookie']; + cookies += + cookiesHeader + .map((h: string) => { + let content = h.split(';')[0]; + return content.endsWith('=') ? '' : content; + }) + .filter((h: string) => h !== '') + .join(';') + ';'; + + + return cookies; +}