dsh-xueqiu 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,426 @@
1
+ return {
2
+ apply(ctx) {
3
+ const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
4
+ const BASE = 'https://stock.xueqiu.com'
5
+ const SITE = 'https://xueqiu.com'
6
+ const DEFAULT_WATCHLIST = ['SH600519', 'SZ300750', 'SZ002594', 'SH601318', '00700', 'AAPL']
7
+
8
+ const state = { cookie: '' }
9
+ let watchlist = null
10
+ let uiState = null
11
+
12
+ function enc(s) {
13
+ try { return encodeURIComponent(String(s)) } catch (e) { return String(s) }
14
+ }
15
+
16
+ function toQuery(params) {
17
+ const parts = []
18
+ for (const k in params) {
19
+ const v = params[k]
20
+ if (v === undefined || v === null || v === '') continue
21
+ parts.push(enc(k) + '=' + enc(v))
22
+ }
23
+ return parts.length ? '?' + parts.join('&') : ''
24
+ }
25
+
26
+ function getShell() { return ctx.get('shell') }
27
+
28
+ async function ensureCookie(force) {
29
+ if (state.cookie && !force) return state.cookie
30
+ const shell = getShell()
31
+ if (!shell) return ''
32
+ const cmd = "curl -s --max-time 12 -D - -o /dev/null 'https://xueqiu.com/' -H 'User-Agent: " + UA + "'"
33
+ const spec = shell.resolve({ command: cmd, timeoutMs: 15000, stdoutMaxBytes: 65536 })
34
+ const res = await shell.run(spec)
35
+ if (res.exitCode !== 0) return ''
36
+ const pairs = []
37
+ const lines = String(res.stdout.text || '').split('\n')
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const m = /^set-cookie:\s*([^=;\s]+)=([^;]*)/i.exec(lines[i].trim())
40
+ if (m) pairs.push(m[1] + '=' + m[2])
41
+ }
42
+ state.cookie = pairs.join('; ')
43
+ return state.cookie
44
+ }
45
+
46
+ async function curl(url) {
47
+ const shell = getShell()
48
+ if (!shell) throw new Error('shell 服务不可用,无法访问雪球')
49
+ const cookie = await ensureCookie(false)
50
+ let cmd = "curl -s --max-time 12 '" + url + "'"
51
+ cmd += " -H 'User-Agent: " + UA + "'"
52
+ cmd += " -H 'Referer: https://xueqiu.com/'"
53
+ cmd += " -H 'Accept: application/json'"
54
+ if (cookie) cmd += " -H 'Cookie: " + cookie + "'"
55
+ const spec = shell.resolve({ command: cmd, timeoutMs: 15000, stdoutMaxBytes: 4194304 })
56
+ const result = await shell.run(spec)
57
+ if (result.exitCode !== 0) {
58
+ const err = ((result.stderr && result.stderr.text) || '').slice(0, 200)
59
+ throw new Error('curl 失败 (' + result.exitCode + '): ' + err)
60
+ }
61
+ return String(result.stdout.text || '')
62
+ }
63
+
64
+ async function getJSON(path, params, base, depth) {
65
+ const url = (base || BASE) + path + toQuery(params || {})
66
+ let text = await curl(url)
67
+ if (!text && state.cookie) {
68
+ await ensureCookie(true)
69
+ text = await curl(url)
70
+ }
71
+ if (!text) throw new Error('雪球返回空响应(可能被风控,稍后再试)')
72
+ let data
73
+ try { data = JSON.parse(text) } catch (e) { throw new Error('雪球响应解析失败: ' + text.slice(0, 120)) }
74
+ if (data && data.error_code && data.error_code !== 0) {
75
+ const code = String(data.error_code)
76
+ const desc = (data.error_description || ('雪球错误 ' + code)).slice(0, 120)
77
+ if (code === '400016' && depth < 2) {
78
+ await ensureCookie(true)
79
+ return getJSON(path, params, base, (depth || 0) + 1)
80
+ }
81
+ throw new Error(desc)
82
+ }
83
+ return data
84
+ }
85
+
86
+ // ---- data normalizers ----
87
+ function nz(v) { return v === undefined ? null : v }
88
+
89
+ function sanitize(v) {
90
+ if (v === undefined) return null
91
+ if (v === null || typeof v !== 'object') return v
92
+ if (Array.isArray(v)) {
93
+ const out = []
94
+ for (let i = 0; i < v.length; i++) out.push(sanitize(v[i]))
95
+ return out
96
+ }
97
+ const out = {}
98
+ for (const k in v) {
99
+ if (Object.prototype.hasOwnProperty.call(v, k)) out[k] = sanitize(v[k])
100
+ }
101
+ return out
102
+ }
103
+
104
+ function pickQuote(q) {
105
+ if (!q) return null
106
+ return {
107
+ symbol: nz(q.symbol), name: nz(q.name), code: nz(q.code), exchange: nz(q.exchange), type: nz(q.type),
108
+ current: nz(q.current), percent: nz(q.percent), chg: nz(q.chg),
109
+ open: nz(q.open), last_close: nz(q.last_close), high: nz(q.high), low: nz(q.low),
110
+ volume: nz(q.volume), amount: nz(q.amount), turnover_rate: nz(q.turnover_rate),
111
+ market_capital: nz(q.market_capital), float_market_capital: nz(q.float_market_capital),
112
+ volume_ratio: nz(q.volume_ratio), amplitude: nz(q.amplitude), current_year_percent: nz(q.current_year_percent),
113
+ pe_ttm: nz(q.pe_ttm), pe_lyr: nz(q.pe_lyr), pe_forecast: nz(q.pe_forecast), pb: nz(q.pb), eps: nz(q.eps),
114
+ dividend_yield: nz(q.dividend_yield), high52w: nz(q.high52w), low52w: nz(q.low52w),
115
+ limit_up: nz(q.limit_up), limit_down: nz(q.limit_down), status: nz(q.status)
116
+ }
117
+ }
118
+
119
+ function mapKline(data) {
120
+ const d = data && data.data ? data.data : null
121
+ if (!d || !d.column || !d.item) return { period: null, rows: [] }
122
+ const idx = {}
123
+ for (let i = 0; i < d.column.length; i++) idx[d.column[i]] = i
124
+ const rows = d.item.map(function (it) {
125
+ return {
126
+ timestamp: it[idx.timestamp], open: it[idx.open], high: it[idx.high],
127
+ low: it[idx.low], close: it[idx.close], volume: it[idx.volume],
128
+ amount: it[idx.amount], percent: it[idx.percent], chg: it[idx.chg],
129
+ turnoverrate: it[idx.turnoverrate], pe: it[idx.pe], pb: it[idx.pb],
130
+ market_capital: it[idx.market_capital]
131
+ }
132
+ })
133
+ return { period: null, rows: rows }
134
+ }
135
+
136
+ function mapPosts(list) {
137
+ if (!Array.isArray(list)) return []
138
+ return list.map(function (p) {
139
+ let text = p.description || p.text || ''
140
+ text = String(text).replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').slice(0, 200)
141
+ return {
142
+ id: p.id, title: p.title || '', text: text,
143
+ created_at: p.created_at, reply_count: p.reply_count, fav_count: p.fav_count,
144
+ view_count: p.view_count, target: p.target,
145
+ user: p.user ? { id: p.user.id, screen_name: p.user.screen_name, followers_count: p.user.followers_count } : null
146
+ }
147
+ })
148
+ }
149
+
150
+ function mapKOL(arr) {
151
+ if (!Array.isArray(arr)) return []
152
+ return arr.map(function (u) {
153
+ return {
154
+ id: u.id, screen_name: u.screen_name, description: u.description || '',
155
+ followers_count: u.followers_count, status_count: u.status_count,
156
+ verified: u.verified, verified_description: u.verified_description,
157
+ profile: u.profile, profile_image_url: u.profile_image_url
158
+ }
159
+ })
160
+ }
161
+
162
+ function mapFinance(list) {
163
+ if (!Array.isArray(list)) return []
164
+ return list.map(function (r) {
165
+ function v(key) { return (r[key] && r[key][0] !== undefined) ? r[key][0] : null }
166
+ return {
167
+ report_name: r.report_name, report_date: r.report_date,
168
+ avg_roe: v('avg_roe'), basic_eps: v('basic_eps'),
169
+ gross_selling_rate: v('gross_selling_rate'), net_selling_rate: v('net_selling_rate'),
170
+ operating_income_yoy: v('operating_income_yoy'), net_profit_atsopc_yoy: v('net_profit_atsopc_yoy'),
171
+ asset_liab_ratio: v('asset_liab_ratio'), current_ratio: v('current_ratio'),
172
+ total_revenue: v('total_revenue'), net_profit_atsopc: v('net_profit_atsopc'),
173
+ np_per_share: v('np_per_share'), operate_cash_flow_ps: v('operate_cash_flow_ps')
174
+ }
175
+ })
176
+ }
177
+
178
+ // ---- actions ----
179
+ async function actQuote(args) {
180
+ const symbols = Array.isArray(args.symbols) ? args.symbols : [String(args.symbols || '')]
181
+ const list = symbols.filter(function (s) { return /^[A-Za-z0-9_.]+$/.test(s) })
182
+ if (!list.length) return { list: [], status: null }
183
+ const data = await getJSON('/v5/stock/batch/quote.json', { symbol: list.join(',') })
184
+ const items = (data && data.data && data.data.items) || []
185
+ const first = items[0]
186
+ const status = first && first.market ? first.market.status_id : null
187
+ return {
188
+ status: nz(status),
189
+ list: items.map(function (it) { return pickQuote(it.quote) }).filter(Boolean)
190
+ }
191
+ }
192
+
193
+ async function actQuoteDetail(args) {
194
+ const symbol = String(args.symbol || '')
195
+ const data = await getJSON('/v5/stock/quote.json', { symbol: symbol, extend: 'detail' })
196
+ return { quote: pickQuote(data.data && data.data.quote), market: data.data && data.data.market }
197
+ }
198
+
199
+ async function actKline(args) {
200
+ const period = String(args.period || 'day')
201
+ const count = Math.min(Math.max(parseInt(args.count, 10) || 120, 5), 500)
202
+ const symbol = String(args.symbol || '')
203
+ const data = await getJSON('/v5/stock/chart/kline.json', {
204
+ symbol: symbol, period: period, type: 'before', begin: Date.now(),
205
+ count: -count, indicator: 'kline,pe,pb,ps,pcf,market_capital'
206
+ })
207
+ return mapKline(data)
208
+ }
209
+
210
+ async function actMinute(args) {
211
+ const symbol = String(args.symbol || '')
212
+ const data = await getJSON('/v5/stock/chart/minute.json', { symbol: symbol, period: '1d' })
213
+ const d = data && data.data ? data.data : null
214
+ if (!d || !d.items) return { last_close: null, items: [] }
215
+ return {
216
+ last_close: d.last_close,
217
+ items: d.items.map(function (it) {
218
+ return { timestamp: it.timestamp, current: it.current, avg_price: it.avg_price, percent: it.percent, volume: it.volume, high: it.high, low: it.low }
219
+ })
220
+ }
221
+ }
222
+
223
+ async function actHot(args) {
224
+ const market = String(args.market || 'cn')
225
+ const typeMap = { global: 10, us: 11, cn: 12, hk: 13 }
226
+ const size = Math.min(Math.max(parseInt(args.size, 10) || 10, 1), 30)
227
+ const data = await getJSON('/v5/stock/hot_stock/list.json', { type: typeMap[market] || 12, size: size })
228
+ const items = (data && data.data && data.data.items) || []
229
+ return {
230
+ list: items.map(function (it) {
231
+ return { symbol: it.symbol, code: it.code, name: it.name, current: it.current, percent: it.percent, chg: it.chg, exchange: it.exchange, value: it.value, rank_change: it.rank_change, increment: it.increment }
232
+ })
233
+ }
234
+ }
235
+
236
+ async function actSearch(args) {
237
+ const q = String(args.q || '')
238
+ if (!q) return { list: [] }
239
+ const data = await getJSON('/query/v1/suggest_stock.json', { q: q, count: Math.min(parseInt(args.count, 10) || 8, 20) }, SITE)
240
+ const list = (data && data.data) || []
241
+ return {
242
+ list: list.map(function (it) {
243
+ return { code: it.code, name: it.query || it.name, type: it.type, stock_type: it.stock_type, label: it.label }
244
+ })
245
+ }
246
+ }
247
+
248
+ async function actSearchPosts(args) {
249
+ const q = String(args.q || '')
250
+ if (!q) return { list: [] }
251
+ const data = await getJSON('/query/v1/search/status.json', { q: q, count: Math.min(parseInt(args.count, 10) || 10, 30) }, SITE)
252
+ return { list: mapPosts(data && data.list) }
253
+ }
254
+
255
+ async function actNews(args) {
256
+ const count = Math.min(Math.max(parseInt(args.count, 10) || 20, 1), 50)
257
+ const data = await getJSON('/statuses/livenews/list.json', { since_id: -1, max_id: -1, count: count }, SITE)
258
+ const items = (data && data.items) || []
259
+ return {
260
+ items: items.map(function (it) {
261
+ return { id: it.id, text: it.text, mark: it.mark, created_at: it.created_at, target: it.target, view_count: it.view_count, reply_count: it.reply_count }
262
+ })
263
+ }
264
+ }
265
+
266
+ async function actKOL(args) {
267
+ const symbol = String(args.symbol || '')
268
+ const count = Math.min(Math.max(parseInt(args.count, 10) || 8, 1), 20)
269
+ const data = await getJSON('/recommend/user/stock_hot_user.json', { symbol: symbol, start: 0, count: count }, SITE)
270
+ return { list: mapKOL(data) }
271
+ }
272
+
273
+ async function actUser(args) {
274
+ const userId = String(args.userId || args.id || '')
275
+ if (!userId) return { user: null, posts: [] }
276
+ const data = await getJSON('/statuses/user_timeline.json', { user_id: userId, page: 1, count: Math.min(parseInt(args.count, 10) || 10, 30) }, SITE)
277
+ const statuses = (data && data.statuses) || []
278
+ const first = statuses[0]
279
+ return {
280
+ user: first && first.user ? {
281
+ id: first.user.id, screen_name: first.user.screen_name,
282
+ followers_count: first.user.followers_count, status_count: first.user.status_count,
283
+ verified: first.user.verified, description: first.user.description, profile: first.user.profile
284
+ } : null,
285
+ posts: mapPosts(statuses)
286
+ }
287
+ }
288
+
289
+ async function actFinance(args) {
290
+ const symbol = String(args.symbol || '')
291
+ const data = await getJSON('/v5/stock/finance/cn/indicator.json', { symbol: symbol, type: 'all', is_detail: 'true', count: 4 })
292
+ const d = data && data.data ? data.data : null
293
+ return {
294
+ quote_name: d ? d.quote_name : null, last_report_name: d ? d.last_report_name : null,
295
+ list: mapFinance(d ? d.list : null)
296
+ }
297
+ }
298
+
299
+ // ---- watchlist ----
300
+ async function loadWatchlist() {
301
+ if (watchlist) return watchlist
302
+ watchlist = { symbols: DEFAULT_WATCHLIST.slice() }
303
+ const fs = ctx.get('fs')
304
+ const sp = ctx.get('sandboxPolicy')
305
+ const root = sp && sp.workspaceRoot ? sp.workspaceRoot : null
306
+ if (fs && root) {
307
+ try {
308
+ const target = await fs.resolve('.xueqiu-watchlist.json', { cwd: root })
309
+ const text = await fs.readText(target)
310
+ const parsed = JSON.parse(text)
311
+ if (parsed && Array.isArray(parsed.symbols) && parsed.symbols.length) {
312
+ watchlist = { symbols: parsed.symbols.slice(0, 50) }
313
+ }
314
+ } catch (e) { /* 首次运行或无写权限时使用默认列表 */ }
315
+ }
316
+ return watchlist
317
+ }
318
+
319
+ async function saveWatchlist() {
320
+ const fs = ctx.get('fs')
321
+ const sp = ctx.get('sandboxPolicy')
322
+ const root = sp && sp.workspaceRoot ? sp.workspaceRoot : null
323
+ if (!fs || !root) return
324
+ try {
325
+ const target = await fs.resolve('.xueqiu-watchlist.json', { cwd: root })
326
+ await fs.writeText(target, JSON.stringify(watchlist))
327
+ } catch (e) { /* 保持内存态 */ }
328
+ }
329
+
330
+ async function actWatchlistGet() {
331
+ const wl = await loadWatchlist()
332
+ return { symbols: wl.symbols }
333
+ }
334
+
335
+ async function actWatchlistAdd(args) {
336
+ const symbol = String(args.symbol || '')
337
+ if (!/^[A-Za-z0-9_.]+$/.test(symbol)) throw new Error('无效代码: ' + symbol)
338
+ const wl = await loadWatchlist()
339
+ if (wl.symbols.indexOf(symbol) === -1) {
340
+ wl.symbols.push(symbol)
341
+ await saveWatchlist()
342
+ }
343
+ return { symbols: wl.symbols }
344
+ }
345
+
346
+ async function actWatchlistRemove(args) {
347
+ const symbol = String(args.symbol || '')
348
+ const wl = await loadWatchlist()
349
+ const i = wl.symbols.indexOf(symbol)
350
+ if (i !== -1) {
351
+ wl.symbols.splice(i, 1)
352
+ await saveWatchlist()
353
+ }
354
+ return { symbols: wl.symbols }
355
+ }
356
+
357
+ // ---- UI 状态持久化 ----
358
+ async function loadUiState() {
359
+ if (uiState) return uiState
360
+ uiState = { pos: null, tab: 'market', minimized: false }
361
+ const fs = ctx.get('fs')
362
+ const sp = ctx.get('sandboxPolicy')
363
+ const root = sp && sp.workspaceRoot ? sp.workspaceRoot : null
364
+ if (fs && root) {
365
+ try {
366
+ const target = await fs.resolve('.xueqiu-ui-state.json', { cwd: root })
367
+ const text = await fs.readText(target)
368
+ const parsed = JSON.parse(text)
369
+ if (parsed && typeof parsed === 'object') uiState = parsed
370
+ } catch (e) { /* 默认状态 */ }
371
+ }
372
+ return uiState
373
+ }
374
+
375
+ async function saveUiState() {
376
+ const fs = ctx.get('fs')
377
+ const sp = ctx.get('sandboxPolicy')
378
+ const root = sp && sp.workspaceRoot ? sp.workspaceRoot : null
379
+ if (!fs || !root) return
380
+ try {
381
+ const target = await fs.resolve('.xueqiu-ui-state.json', { cwd: root })
382
+ await fs.writeText(target, JSON.stringify(uiState))
383
+ } catch (e) { /* 保持内存态 */ }
384
+ }
385
+
386
+ async function actUiGet() {
387
+ const s = await loadUiState()
388
+ return { pos: s.pos || null, tab: s.tab || 'market', minimized: !!s.minimized }
389
+ }
390
+
391
+ async function actUiSet(args) {
392
+ const s = await loadUiState()
393
+ if (args.pos && typeof args.pos === 'object' && isFinite(Number(args.pos.x)) && isFinite(Number(args.pos.y))) {
394
+ s.pos = { x: Number(args.pos.x), y: Number(args.pos.y) }
395
+ }
396
+ if (args.tab !== undefined) s.tab = String(args.tab)
397
+ if (args.minimized !== undefined) s.minimized = !!args.minimized
398
+ await saveUiState()
399
+ return { ok: true }
400
+ }
401
+
402
+ // ---- RPC dispatch ----
403
+ const dispatch = {
404
+ quote: actQuote, quoteDetail: actQuoteDetail, kline: actKline, minute: actMinute,
405
+ hot: actHot, search: actSearch, searchPosts: actSearchPosts, news: actNews,
406
+ kol: actKOL, user: actUser, finance: actFinance,
407
+ 'watchlist.get': actWatchlistGet, 'watchlist.add': actWatchlistAdd, 'watchlist.remove': actWatchlistRemove,
408
+ 'ui.get': actUiGet, 'ui.set': actUiSet
409
+ }
410
+
411
+ ctx.effect(function () {
412
+ return harness.handle('xq.call', async function (req) {
413
+ const reqObj = (req && typeof req === 'object') ? req : {}
414
+ const action = reqObj.action
415
+ const args = reqObj.args || {}
416
+ if (!dispatch[action]) return { ok: false, error: '未知操作: ' + action }
417
+ try {
418
+ const data = await dispatch[action](args)
419
+ return { ok: true, data: sanitize(data) }
420
+ } catch (e) {
421
+ return { ok: false, error: String((e && e.message) || e) }
422
+ }
423
+ })
424
+ })
425
+ }
426
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "dsh-xueqiu",
3
+ "version": "1.0.0",
4
+ "description": "雪球 mini 行情面板 — DeepSeek Harness 免登录 A股/港美股实时行情、K线(蜡烛/成交量/均线)、分时图、热榜、搜索、7x24 快讯、热议用户。可拖拽悬浮面板,交易时段智能刷新。",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "deepseek-harness",
9
+ "cordis",
10
+ "xueqiu",
11
+ "雪球",
12
+ "stock",
13
+ "a-share",
14
+ "finance"
15
+ ],
16
+ "license": "MIT",
17
+ "type": "module",
18
+ "main": "src/index.js",
19
+ "exports": {
20
+ ".": "./src/index.js",
21
+ "./client": "./src/client/index.js",
22
+ "./dynamic/*": "./dynamic/*",
23
+ "./package.json": "./package.json"
24
+ },
25
+ "dsh": {
26
+ "client": {
27
+ "inject": [
28
+ "@deepseek-ai/dsh-client-connection"
29
+ ],
30
+ "platform": "web"
31
+ },
32
+ "bundle": {
33
+ "patch": "./cordis.patch.yml"
34
+ }
35
+ },
36
+ "peerDependencies": {
37
+ "@deepseek-ai/cordis": "^4.0.1",
38
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
39
+ "react": "^18.3.1"
40
+ },
41
+ "files": [
42
+ "src/index.js",
43
+ "src/client/index.js",
44
+ "dynamic/host.js",
45
+ "dynamic/client.js",
46
+ "cordis.patch.yml",
47
+ "README.md",
48
+ "LICENSE"
49
+ ]
50
+ }