@hhxg/dsh-hhxg-market 0.4.1 → 0.4.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.2 (2026-11-02)
4
+
5
+ 结果可信度与兼容性修复版(外部评估 + Codex 审阅后的批次)。工具名、数量(7 免费 + 6 VIP)不变。
6
+
7
+ - **时效显式化** `hhxg_realtime` 的结果文本首行标出帧的状态与交易日(如「【非实时·上一交易日|帧交易日 2026-10-30|采集 2026-10-30 15:35】不是此刻的实时数据,引用时注明帧日期。」);`status=live` 但 `is_live=false` 的矛盾帧按非实时处理,绝不显示成实时。web profile 结果卡的徽标改为显示同一段完整文案(host 投影统一生成)。
8
+ - **日 K 可比性语义** 服务端 `get_realtime view=kline` 响应 `schema_version` 升到 2:`live.comparable` 可为 `null`(无法判断:历史缓存落后于帧、个股最后收盘不在上一交易日、缺基准价或交易日历不可用,原因在 `live.reason`),`false` 改为「疑似除权 / 价格基准不一致」而非「确认除权」,新增响应级 `history`(缓存末日、按交易日历算的缺口)。插件文本在 `comparable !== true` 时追加提示行,卡片里帧那天的 K 不再一律标「今日·未收盘」。
9
+ - **本地预检** `view=kline` 的 `codes ≤ 5`、`bars` 1–260 整数、去重后 `codes × bars ≤ 1000`,以及 `view=quotes` 的 `codes ≤ 30`,越界在本地返回「参数有误:…(INVALID_ARG / RESULT_TOO_LARGE)」,不发请求。
10
+ - **Token 来源** 配置 `mcpToken` 留空时读环境变量 `HHXG_MCP_TOKEN`(显式配置非空时以配置为准;日志只记来源不记值)。
11
+ - **client 降级** 宿主未提供 React / jsx-runtime 或 slots 注册失败时,client 半不再抛错,只是不注册结果卡(宿主默认文本视图照常)。
12
+ - **peer 范围** `@deepseek-ai/dsh-tools` 放宽到 `>=0.1.1-rc.1 <0.3.0-0`(严格 semver 下另列出已知的 0.1.7-rc.2 / 0.2.0-rc.1 / 0.2.1-rc.5 / 0.2.2-alpha.2 预发布版本),cordis / schemastery 改为「不低于当前、不跨主版本」;契约测试新增两种预发布匹配模式的范围断言。
13
+ - npm 包 `@hhxg/dsh-hhxg-market` 自 0.4.1 起可用,README 安装段以 npm 为主。
14
+
3
15
  ## 0.4.1 (2026-11-01)
4
16
 
5
17
  文案纠错 + 日 K 参数透传。工具名、数量(7 免费 + 6 VIP)与行为不变。
package/README.md CHANGED
@@ -9,16 +9,16 @@
9
9
 
10
10
  ## 安装
11
11
 
12
- 直接从 GitHub 安装:
12
+ 从 npm 安装(推荐,2026-11-01 起发布为 [`@hhxg/dsh-hhxg-market`](https://www.npmjs.com/package/@hhxg/dsh-hhxg-market)):
13
13
 
14
14
  ```bash
15
- dsh plugin --profile web add github:Niceck/dsh-hhxg-market
15
+ dsh plugin --profile web add @hhxg/dsh-hhxg-market
16
16
  ```
17
17
 
18
- 或从 npm(发布后):
18
+ 或直接从 GitHub 安装(备选,解析到仓库最新提交):
19
19
 
20
20
  ```bash
21
- dsh plugin --profile web add @hhxg/dsh-hhxg-market
21
+ dsh plugin --profile web add github:Niceck/dsh-hhxg-market
22
22
  ```
23
23
 
24
24
  > **⚠️ 仍在用 0.2.x 的请升级**:0.2.x 的 `hhxg_strategy_signals` / `hhxg_stock_alerts` 调用的服务端旧工具名已于 2026-10-27 下线(原定 2026-11-30,提前),这两个工具现在会报「工具已下线」(其余工具不受影响)。重新执行上面的 GitHub 安装命令即可升级到最新版(`dsh plugin` 把参数转给 pnpm,`add` 会重新解析到仓库最新提交)。
@@ -48,7 +48,7 @@ dsh plugin --profile web add @hhxg/dsh-hhxg-market
48
48
  | `hhxg_predict_lianban` | 连板股次日晋级率预测(题材热度 / 行业热度 / 板高强度 3 因子) |
49
49
  | `hhxg_stock_alerts` | 个股近 30 日异动历史(普通 / 临近严重 / 严重异动逐日记录) |
50
50
  | `hhxg_query_dataset` | 对数据集(JSON 与 300KB 以内的 CSV)做过滤 / 排序 / 截断查询(先用免费的 `hhxg_datasets` 查合法 id 与是否可查) |
51
- | `hhxg_realtime` | **Max 专属**盘中实时(交易日约 1 分钟一帧,盘后为当日收盘帧):`view=market` 涨跌家数 / 涨跌停与炸板 / 涨停池与连板晋级率;`view=quotes` 指定个股(最多 30 只)的最新价与日 / 周 / 月指标;`view=screen` 全市场条件筛选(最多 8 条、返回 50 行);`view=fields` 可用字段与示例;`view=kline` 指定个股(最多 5 只)的日 K 序列(已完成交易日 ≤260 根,`adj` 前复权 / 不复权)+ 今天这根未走完的 K(`live`,除权日标 `comparable=false`)。结果带 `frame.status`(盘中 / 午休 / 收盘 / 非今日 / 延迟)。分钟 K 不提供(快照差分还原不了区间高低) |
51
+ | `hhxg_realtime` | **Max 专属**盘中实时(交易日约 1 分钟一帧,盘后为当日收盘帧):`view=market` 涨跌家数 / 涨跌停与炸板 / 涨停池与连板晋级率;`view=quotes` 指定个股(最多 30 只)的最新价与日 / 周 / 月指标;`view=screen` 全市场条件筛选(最多 8 条、返回 50 行);`view=fields` 可用字段与示例;`view=kline` 指定个股(最多 5 只)的日 K 序列(已完成交易日 ≤260 根,`codes × bars ≤ 1000`,`adj` 前复权 / 不复权)+ 实时帧那天未走完的 K(`live`;`comparable` 只有 `true` 才可与历史拼接,`false` 疑似除权 / 基准不一致,`null` 无法判断——缓存滞后、个股停牌或日历不可用,原因见 `live.reason`,缓存状态见 `history`)。结果文本首行标出帧的时效与交易日,并带 `frame.status`(盘中 / 午休 / 收盘 / 非今日 / 延迟);不是 `live` 就不是此刻的数据。分钟 K 不提供(快照差分还原不了区间高低) |
52
52
 
53
53
  **解锁步骤**:
54
54
 
@@ -64,7 +64,9 @@ dsh plugin --profile web add @hhxg/dsh-hhxg-market
64
64
  mcpToken: "<你的 MCP Token>"
65
65
  ```
66
66
 
67
- 未填 Token 时 VIP 工具仍会注册为引导占位(调用返回开通指引),不影响免费工具。设 `enableVipTools: false` 可整体关闭 VIP 工具注册。
67
+ 从终端启动 dsh 的话也可以不改文件:配置留空,设环境变量 `HHXG_MCP_TOKEN=<你的 MCP Token>`(显式配置非空时以配置为准;GUI 启动的 dsh 不一定继承 shell 环境)。
68
+
69
+ 未填 Token 时 VIP 工具仍会注册为引导占位(调用返回开通指引),不影响免费工具。设 `enableVipTools: false` 可整体关闭 VIP 工具注册(此时环境变量也不会解锁)。
68
70
 
69
71
  `hhxg_strategy_signals` 的 12 个策略标识里有 3 个(`oversold_rebound` / `macd_death_cross` / `distribution`)需要列间比较或分位数,暂不能作为预设,调用会返回说明(`PRESET_NOT_SUPPORTED`)。全市场指标全表(`quant_signal_pool`)自 2026-10-23 起为会员数据、不再提供下载地址;需要自定义条件时,可在 MCP 客户端直接调 `screen_stocks(filters=...)` 按「字段 对 常量」筛选后自行比较。
70
72
 
@@ -84,7 +86,7 @@ Header: Authorization: Bearer <你的 MCP Token>
84
86
  ## 设计约定
85
87
 
86
88
  - **只读(research-only)**:本插件只提供行情与研究数据,不下单、不划转资金,也不存在任何交易执行接口。
87
- - **凭证最小暴露**:Token 仅用于请求的 `Authorization` 头,不写盘、不打日志;错误信息经脱敏处理,不回显 Token。Token 本体只存在于你本机的 profile 配置。
89
+ - **凭证最小暴露**:Token 仅用于请求的 `Authorization` 头,不写盘、不打日志(日志只记「来自配置 / 环境变量 / 未配置」);错误信息经脱敏处理,不回显 Token。Token 本体只存在于你本机的 profile 配置或你自己设置的环境变量里(环境变量对同进程插件与继承环境的子进程可见,不是保险库)。
88
90
  - **上游文本消毒**:新闻标题、题材名等第三方文本进入模型上下文前统一清洗(删不可见字符、压平控制字符、断开链接/图片语法),降低数据侧 prompt injection 风险。
89
91
  - **免责声明**:所有数据仅供研究参考,不构成投资建议。数据可能存在延迟或缺失,据此操作风险自负。
90
92
 
package/cordis.patch.yml CHANGED
@@ -12,6 +12,7 @@
12
12
  # - id: hhxg-market
13
13
  # config:
14
14
  # mcpToken: "<hhxg.top/account/mcp.html 生成的 MCP Token>"
15
+ # 从终端启动 dsh 时也可以不改文件:配置留空,设环境变量 HHXG_MCP_TOKEN。
15
16
  - insert:
16
17
  - id: hhxg-market
17
18
  name: '@hhxg/dsh-hhxg-market'
package/lib/api.js CHANGED
@@ -16,6 +16,21 @@ export const BASE_URL = 'https://hhxg.top'
16
16
  export const MCP_RPC_URL = `${BASE_URL}/mcp/rpc`
17
17
  export const UPGRADE_URL = `${BASE_URL}/vip.html`
18
18
  export const TOKEN_URL = `${BASE_URL}/account/mcp.html`
19
+ /** mcpToken 的环境变量回退来源(终端启动 dsh 时可用;GUI 启动不一定继承 shell 环境)。 */
20
+ export const TOKEN_ENV = 'HHXG_MCP_TOKEN'
21
+
22
+ /**
23
+ * 解析 MCP Token 来源:显式配置(非空)优先;为空才看环境变量 HHXG_MCP_TOKEN;两者都空 = 未配置。
24
+ * 显式配置非法(含空白 / 非 ASCII)时不回退环境变量——那是用户填错了,要报错而不是悄悄换一把。
25
+ * 返回 { token, source },source ∈ config / env / none;只在 host apply() 读一次,不写回任何地方。
26
+ */
27
+ export function resolveMcpToken(config, env = process.env) {
28
+ const configured = typeof config?.mcpToken === 'string' ? config.mcpToken.trim() : ''
29
+ if (configured) return { token: configured, source: 'config' }
30
+ const fromEnv = typeof env?.[TOKEN_ENV] === 'string' ? env[TOKEN_ENV].trim() : ''
31
+ if (fromEnv) return { token: fromEnv, source: 'env' }
32
+ return { token: '', source: 'none' }
33
+ }
19
34
 
20
35
  const require = createRequire(import.meta.url)
21
36
  const { version: PLUGIN_VERSION } = require('../package.json')
package/lib/client.js CHANGED
@@ -25,12 +25,16 @@ window.__ModuleLoader__.load({
25
25
  let react
26
26
  let jsx
27
27
  let jsxs
28
+ // 宿主没给 React(或 jsx-runtime):不抛——抛出去会让宿主把整个 client 模块判坏;
29
+ // 改成「不注册卡片」,宿主继续用默认文本视图,免费工具与其它 VIP 工具不受影响。
30
+ let unavailable = null
28
31
  try {
29
32
  react = require('react')
30
33
  ;({ jsx, jsxs } = require('react/jsx-runtime'))
34
+ if (typeof jsx !== 'function' || typeof jsxs !== 'function') throw new Error('jsx runtime missing')
31
35
  } catch (error) {
32
- console.error('[hhxg-market] client require failed; card cannot mount:', error)
33
- throw error
36
+ unavailable = error
37
+ console.warn('[hhxg-market] client dependencies unavailable; realtime card disabled, host default view kept:', error)
34
38
  }
35
39
 
36
40
  const TOOL = 'hhxg_realtime'
@@ -106,11 +110,14 @@ window.__ModuleLoader__.load({
106
110
 
107
111
  function badgeEl(frame) {
108
112
  if (!frame || typeof frame !== 'object' || typeof frame.status !== 'string') return null
109
- const label = STATUS_LABEL[frame.status] || frame.status
110
- const at = typeof frame.updated_at === 'string' ? frame.updated_at.slice(5, 16) : ''
113
+ // host 投影给的 effective_status / label 优先(is_live=false 的矛盾帧已被降为 stale,日期完整可见);
114
+ // 旧版 meta 没有这两个字段时按本地表回退
115
+ const status = typeof frame.effective_status === 'string' && frame.effective_status ? frame.effective_status : frame.status
116
+ const label = typeof frame.label === 'string' && frame.label ? frame.label : STATUS_LABEL[status] || status
117
+ const at = typeof frame.label === 'string' && frame.label ? '' : typeof frame.updated_at === 'string' ? frame.updated_at.slice(5, 16) : ''
111
118
  return jsxs('span', {
112
119
  className: 'hhxg_badge',
113
- 'data-status': frame.status,
120
+ 'data-status': status,
114
121
  title: typeof frame.status_text === 'string' ? frame.status_text : '',
115
122
  children: [label, at ? jsx('span', { className: 'hhxg_meta', children: at }) : null],
116
123
  })
@@ -169,13 +176,21 @@ window.__ModuleLoader__.load({
169
176
  const inject = ['slots']
170
177
 
171
178
  function apply(ctx) {
179
+ if (unavailable) return // 依赖缺失:不注册,宿主默认视图照常
180
+ const warn = (error) => console.warn('[hhxg-market] could not register the realtime card; host default view kept:', error)
181
+ // inject 的回调可能被宿主延后执行(slots 就绪后才调):异常要在回调里自己接住,外层 try 兜不到
182
+ const register = () => {
183
+ try {
184
+ return ctx.slots.register({ name: 'tool.call.toolview', key: TOOL }, RealtimeCard)
185
+ } catch (error) {
186
+ warn(error)
187
+ return undefined
188
+ }
189
+ }
172
190
  try {
173
- ctx.slots.inject('tool.call.toolview', () =>
174
- ctx.slots.register({ name: 'tool.call.toolview', key: TOOL }, RealtimeCard),
175
- )
191
+ ctx.slots.inject('tool.call.toolview', register)
176
192
  } catch (error) {
177
- console.error('[hhxg-market] could not register the realtime card:', error)
178
- throw error
193
+ warn(error) // slots 不可用:同样不往上抛,只少一张卡
179
194
  }
180
195
  }
181
196
 
@@ -184,6 +199,7 @@ window.__ModuleLoader__.load({
184
199
  exports.RealtimeCard = RealtimeCard
185
200
  exports.TOOL = TOOL
186
201
  exports.react = react
202
+ exports.unavailable = unavailable
187
203
  return module.exports
188
204
  },
189
205
  })
package/lib/index.js CHANGED
@@ -12,7 +12,8 @@
12
12
 
13
13
  import z from '@deepseek-ai/schemastery'
14
14
  import { defineTool } from '@deepseek-ai/dsh-tools'
15
- import { realtimePresentation } from './present.js'
15
+ import { resolveMcpToken } from './api.js'
16
+ import { realtimePresentation, renderRealtimeText } from './present.js'
16
17
  import { FREE_TOOLS, VIP_TOOLS, runVipTool, vipLockedMessage } from './tools.js'
17
18
 
18
19
  export const name = 'hhxg-market'
@@ -23,7 +24,7 @@ export const Config = z.object({
23
24
  .string()
24
25
  .role('secret')
25
26
  .default('')
26
- .description('恢恢量化会员的 MCP Token(Max 会员在 https://hhxg.top/account/mcp.html 生成)。留空时仅免费工具可用。'),
27
+ .description('恢恢量化会员的 MCP Token(Max 会员在 https://hhxg.top/account/mcp.html 生成)。留空时读环境变量 HHXG_MCP_TOKEN;都没有则仅免费工具可用。'),
27
28
  enableVipTools: z
28
29
  .boolean()
29
30
  .default(true)
@@ -37,20 +38,25 @@ const TEXT_OUTPUT = {
37
38
 
38
39
  /**
39
40
  * 桌面端卡片用的结构化投影:按工具名挑一个 presentationMeta(客户端半 lib/client.js 只认这些 kind)。
40
- * 投影从 execute 返回的同一份文本解析,模型看到的内容不变;解析不出就不给 meta,走默认文本卡。
41
+ * 投影与文本渲染都从 execute 返回的同一份原文解析(execute 本身不改写原文):
42
+ * render 给模型 / 用户的文本在首行标出帧日期与时效,presentationMeta 照旧从原 JSON 投影。
41
43
  */
42
44
  const PRESENTATIONS = {
43
- hhxg_realtime: realtimePresentation,
45
+ hhxg_realtime: { present: realtimePresentation, render: renderRealtimeText },
44
46
  }
45
47
 
46
48
  function outputFor(name) {
47
- const present = PRESENTATIONS[name]
48
- if (!present) return TEXT_OUTPUT
49
- return { ...TEXT_OUTPUT, presentationMeta: (args, value) => present(args, String(value ?? '')) }
49
+ const entry = PRESENTATIONS[name]
50
+ if (!entry) return TEXT_OUTPUT
51
+ return {
52
+ ...TEXT_OUTPUT,
53
+ render: (args, value) => [{ type: 'text', text: entry.render(args, String(value ?? '')) }],
54
+ presentationMeta: (args, value) => entry.present(args, String(value ?? '')),
55
+ }
50
56
  }
51
57
 
52
58
  export function apply(ctx, config) {
53
- const token = String(config?.mcpToken || '').trim()
59
+ const { token, source } = resolveMcpToken(config)
54
60
 
55
61
  for (const spec of FREE_TOOLS) {
56
62
  ctx.tools.register(defineTool({
@@ -82,8 +88,9 @@ export function apply(ctx, config) {
82
88
  }))
83
89
  }
84
90
 
91
+ // 只记来源,绝不记值
92
+ const sourceText = { config: 'MCP Token 来自配置', env: 'MCP Token 来自环境变量 HHXG_MCP_TOKEN', none: '未配置 Token,VIP 工具为引导占位' }[source]
85
93
  ctx.logger?.info?.(
86
- `[hhxg-market] 已注册 ${FREE_TOOLS.length} 个免费工具 + ${VIP_TOOLS.length} 个 VIP 工具` +
87
- (token ? '(MCP Token 已配置)' : '(未配置 Token,VIP 工具为引导占位)'),
94
+ `[hhxg-market] 已注册 ${FREE_TOOLS.length} 个免费工具 + ${VIP_TOOLS.length} 个 VIP 工具(${sourceText})`,
88
95
  )
89
96
  }
package/lib/present.js CHANGED
@@ -34,6 +34,54 @@ function table(columns, rows) {
34
34
  }
35
35
  }
36
36
 
37
+ /** 帧状态 → 人话;status=live 但 is_live=false 的矛盾帧按「非实时」处理,绝不显示成实时。 */
38
+ const STATUS_LABEL = {
39
+ live: '盘中实时',
40
+ lunch_break: '午间休市',
41
+ closed: '收盘帧',
42
+ previous_session: '非实时·上一交易日',
43
+ stale: '非实时·延迟',
44
+ }
45
+ const LIVE_STATUSES = new Set(['live', 'lunch_break'])
46
+
47
+ /** 进首行的服务端字段统一清洗:删控制 / 不可见字符、压平空白、限长——首行是模型最先读到的位置。 */
48
+ function safeText(v, limit) {
49
+ if (typeof v !== 'string') return ''
50
+ const s = v.replace(/[\p{Cc}\p{Cf}]/gu, '').replace(/\s+/g, ' ').trim()
51
+ return s.length > limit ? s.slice(0, limit - 1) + '…' : s
52
+ }
53
+
54
+ /** 8 位交易日 → YYYY-MM-DD;格式不对返回空串(不回显来路不明的字符串)。 */
55
+ function ymd(v) {
56
+ const s = typeof v === 'string' ? v : ''
57
+ return /^\d{8}$/.test(s) ? `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6)}` : ''
58
+ }
59
+
60
+ /**
61
+ * 帧的有效状态:只有 status=live 却 is_live=false 这种矛盾才降为 stale(不能显示绿色)。
62
+ * lunch_break 服务端本来就给 is_live=false(午休不是实时),不算矛盾。
63
+ */
64
+ function effectiveStatus(f) {
65
+ const status = typeof f?.status === 'string' ? f.status : ''
66
+ if (status === 'live' && f?.is_live === false) return 'stale'
67
+ return status
68
+ }
69
+
70
+ const UPDATED_AT_RE = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/
71
+
72
+ /**
73
+ * 徽标正文(host 统一生成,client 只显示):状态 + 帧交易日 + 采集时间,日期必须完整可见。
74
+ * 进首行的服务端字段只走白名单 / 严格格式:状态不在表里显示「状态未知」,日期时间格式不对就省略,
75
+ * 绝不把未知字符串原样提升到模型最先读到的位置。
76
+ */
77
+ function frameLabel(f) {
78
+ const status = effectiveStatus(f)
79
+ const label = STATUS_LABEL[status] || '状态未知'
80
+ const date = ymd(f?.trade_date)
81
+ const at = typeof f?.updated_at === 'string' && UPDATED_AT_RE.test(f.updated_at) ? f.updated_at.slice(0, 16) : ''
82
+ return [label, date ? `帧交易日 ${date}` : '', at ? `采集 ${at}` : ''].filter(Boolean).join('|')
83
+ }
84
+
37
85
  function frameOf(data) {
38
86
  const f = data?.frame
39
87
  if (!f || typeof f !== 'object') return null
@@ -42,6 +90,77 @@ function frameOf(data) {
42
90
  status_text: cell(f.status_text),
43
91
  trade_date: cell(f.trade_date),
44
92
  updated_at: cell(f.updated_at),
93
+ // 布尔原样保留(cell 会把它变成「是 / 否」);缺失为 null
94
+ is_live: typeof f.is_live === 'boolean' ? f.is_live : null,
95
+ effective_status: cell(effectiveStatus(f)),
96
+ // 徽标正文要完整可见(含日期与时间),不走 40 字的单元格截断;上限 80 字兜底
97
+ label: frameLabel(f).slice(0, 80),
98
+ }
99
+ }
100
+
101
+ /** live 段的可比性说明:只有 comparable===true 才能与 rows 拼接。 */
102
+ const REASON_TEXT = {
103
+ history_gap: '历史缓存落后于实时帧(中间有交易日没同步)',
104
+ last_close_not_previous_session: '该股最后一个收盘价不在上一交易日(停牌或缺数据)',
105
+ missing_reference_price: '缺少昨收 / 最后收盘价',
106
+ calendar_unavailable_or_incomplete: '交易日历不可用,无法判断缓存是否滞后',
107
+ reference_price_mismatch: '帧昨收与缓存最后收盘偏差 >0.5%,疑似除权或价格基准不一致',
108
+ }
109
+
110
+ function comparabilityNote(live) {
111
+ if (!live || typeof live !== 'object' || live.comparable === true) return null
112
+ // reason 走白名单:未知值不回显,只说「原因未说明」
113
+ const why = REASON_TEXT[live.reason] || (live.reason ? '原因未说明' : '')
114
+ const head = live.comparable === false ? '实时 K 与历史不可比' : '实时 K 与历史的可比性无法判断'
115
+ return `${head}${why ? `:${why}` : ''};不要把 live 直接拼到 rows 后面。`
116
+ }
117
+
118
+ /** 多只股票:每只有 live 且 comparable !== true 的都列出(代码走 safeText),最多列 5 只。 */
119
+ function comparabilityLines(items) {
120
+ const out = []
121
+ for (const it of items) {
122
+ const note = comparabilityNote(it?.live)
123
+ if (!note) continue
124
+ const code = safeText(typeof it.股票代码 === 'string' ? it.股票代码 : '', 12) || '(未知代码)'
125
+ out.push(`${code}:${note}`)
126
+ if (out.length >= 5) {
127
+ out.push('(其余见 JSON 的 live.reason)')
128
+ break
129
+ }
130
+ }
131
+ return out
132
+ }
133
+
134
+ /**
135
+ * 工具文本首行:把帧的时效与日期放到模型 / 用户第一眼能看到的位置(原 JSON 原样跟在后面,
136
+ * presentationMeta 仍从同一份原文解析)。非成功信封(业务答复 / 占位 / 报错)原样返回。
137
+ */
138
+ export function renderRealtimeText(args, text) {
139
+ try {
140
+ if (typeof text !== 'string' || !text.startsWith('{')) return text
141
+ const envelope = JSON.parse(text)
142
+ const data = envelope?.data
143
+ if (!data || typeof data !== 'object') return text
144
+ const view = typeof args?.view === 'string' ? args.view.trim() : ''
145
+ if (view === 'fields') return text // 字段清单本来就没有帧,不加时效行
146
+ const lines = []
147
+ if (data.frame && typeof data.frame === 'object') {
148
+ // 盘中 / 午休帧不加告警(午休是上午最后一帧,仍是今天的实时口径);其余都不是此刻的数据
149
+ const warn = LIVE_STATUSES.has(effectiveStatus(data.frame)) ? '' : '不是此刻的实时数据,引用时注明帧日期。'
150
+ lines.push(`【${frameLabel(data.frame)}】${warn}`)
151
+ } else if (view === 'kline') {
152
+ const last = ymd(data?.history?.last_date)
153
+ lines.push(`【历史日 K${last ? `|截至 ${last}` : ''}】未附实时帧。`)
154
+ } else {
155
+ lines.push('【时效未知】结果里没有帧信息。')
156
+ }
157
+ if (view === 'kline') {
158
+ const notes = comparabilityLines(dicts(data.items))
159
+ if (notes.length) lines.push(`【注意】${notes.join(';')}`)
160
+ }
161
+ return `${lines.map((l) => safeText(l, 600)).join('\n')}\n${text}`
162
+ } catch {
163
+ return text
45
164
  }
46
165
  }
47
166
 
@@ -83,12 +202,19 @@ function klineTable(data) {
83
202
  if (!first || !columns.length) return { title: '日 K', ...table(columns, []) }
84
203
  const rows = Array.isArray(first.rows) ? first.rows.filter(Array.isArray) : []
85
204
  const live = first.live && typeof first.live === 'object' && Array.isArray(first.live.bar) ? first.live.bar : null
86
- // 卡片只放最近的几十根 + 今天这根(标「今日·未收盘」),完整序列在模型侧文本里
205
+ // 卡片只放最近的几十根 + 帧那天的 K:帧是盘中才叫「未收盘」,收盘帧 / 旧帧不能这么标
206
+ const status = effectiveStatus(data?.frame)
207
+ const liveTag = status === 'live' || status === 'lunch_break' ? '帧K·未收盘' : `帧K·${STATUS_LABEL[status] || '非实时'}`
87
208
  const tail = rows.slice(-(MAX_ROWS - (live ? 1 : 0)))
88
- const shown = live ? [...tail, [`${live[0]} 今日·未收盘`, ...live.slice(1)]] : tail
209
+ const shown = live ? [...tail, [`${live[0]} ${liveTag}`, ...live.slice(1)]] : tail
89
210
  const name = [first.股票代码, first.股票名称].filter(Boolean).join(' ')
90
211
  const more = items.length > 1 ? `(另 ${items.length - 1} 只见文本)` : ''
91
- return { title: `日 K ${name}${more}`, ...table(columns, shown), truncated: rows.length > tail.length || items.length > 1 }
212
+ return {
213
+ title: `日 K ${name}${more}`,
214
+ ...table(columns, shown),
215
+ truncated: rows.length > tail.length || items.length > 1,
216
+ note: comparabilityNote(first.live),
217
+ }
92
218
  }
93
219
 
94
220
  function fieldsTable(data) {
@@ -111,7 +237,9 @@ export function realtimePresentation(args, text) {
111
237
  const envelope = JSON.parse(text)
112
238
  const data = envelope?.data
113
239
  if (!data || typeof data !== 'object') return undefined
114
- const { title, columns, rows, truncated } = build(data)
240
+ const { title, columns, rows, truncated, note: builtNote } = build(data)
241
+ // 表自带的说明(如日 K 的可比性)优先于服务端 note——它更要紧;note 不走 40 字的单元格截断
242
+ const note = safeText(builtNote || (typeof data.note === 'string' ? data.note : ''), 160)
115
243
  return {
116
244
  schema: PRESENTATION_SCHEMA,
117
245
  kind: REALTIME_KIND,
@@ -121,7 +249,7 @@ export function realtimePresentation(args, text) {
121
249
  columns,
122
250
  rows,
123
251
  truncated,
124
- note: typeof data.note === 'string' ? cell(data.note.slice(0, 120)) : null,
252
+ note: note || null,
125
253
  }
126
254
  } catch {
127
255
  return undefined
package/lib/tools.js CHANGED
@@ -382,6 +382,34 @@ function realtimeView(a) {
382
382
  return Object.hasOwn(REALTIME_PARAMS_BY_VIEW, view) ? view : ''
383
383
  }
384
384
 
385
+ // 与服务端同值的硬上限(EdgeOne agent_tools/realtime.js + VPS realtime.py):本地先拒明显越界的请求,
386
+ // 省一次往返与配额;权威校验仍在服务端。文案沿用业务答复格式「参数有误:…(CODE)」。
387
+ const REALTIME_MAX_CODES = { quotes: 30, kline: 5 }
388
+ const KLINE_BARS = { default: 120, min: 1, max: 260 }
389
+ const KLINE_MAX_TOTAL_BARS = 1000
390
+ const localArgError = (msg, code = 'INVALID_ARG') => `参数有误:${msg}(${code})`
391
+
392
+ /** 本地边界预检(view 与缺 codes 之后):越界返回说明文字,合法返回 null。 */
393
+ function realtimeBoundsRefusal(view, codes, a) {
394
+ const cap = REALTIME_MAX_CODES[view]
395
+ // 原始数组长度先判(服务端 _str_list 也是去重前查长度),全是重复项也照拒
396
+ if (cap && Array.isArray(codes) && codes.length > cap) {
397
+ return localArgError(`view=${view} 的 codes 最多 ${cap} 只(收到 ${codes.length} 只),请减少 codes`)
398
+ }
399
+ if (view !== 'kline') return null
400
+ const bars = a?.bars === undefined || a?.bars === null ? KLINE_BARS.default : a.bars
401
+ // 字符串 "60"、布尔、小数都不是合法整数:不替模型强转,让它改
402
+ if (!Number.isInteger(bars) || bars < KLINE_BARS.min || bars > KLINE_BARS.max) {
403
+ return localArgError(`bars 须为 ${KLINE_BARS.min}–${KLINE_BARS.max} 的整数(默认 ${KLINE_BARS.default})`)
404
+ }
405
+ // 乘积按服务端 _codes 的去重规则算:trim + 转大写后去重("600519" 与 "600519.SH" 仍是两个,不替它合并)
406
+ const unique = new Set(codes.map((c) => String(c).trim().toUpperCase()).filter(Boolean)).size
407
+ if (unique * bars > KLINE_MAX_TOTAL_BARS) {
408
+ return localArgError(`codes × bars 不能超过 ${KLINE_MAX_TOTAL_BARS} 根,请减少 codes 或 bars`, 'RESULT_TOO_LARGE')
409
+ }
410
+ return null
411
+ }
412
+
385
413
  export const VIP_TOOLS = [
386
414
  {
387
415
  name: 'hhxg_diagnose_stock',
@@ -468,10 +496,11 @@ export const VIP_TOOLS = [
468
496
  'view=screen:全市场按 filters [{field, op, value}] 筛选(最多 8 条,op:gt/gte/lt/lte/eq/ne/in/not_null),' +
469
497
  'sort 字段名前加 - 为降序,fields 选返回列(最多 12),limit 1-50,exclude_st 排除 ST;周 / 月指标字段名加前缀「周」「月」(如 周MACD_HIST);' +
470
498
  'view=fields:全部可用字段、取值说明与示例,不确定字段名时先调它;' +
471
- 'view=kline:codes(最多 5 只)的日 K 序列——rows 为已完成交易日(period 只支持 day,bars 1-260 默认 120,adj=qfq 前复权 / none 不复权,' +
472
- '单位 元 / 股 / 元,缺值 null),live 为今天这根未走完的 K(最新价当收盘价;live.comparable=false 表示今日除权,不要与前复权历史直接拼接)。' +
473
- '每个结果都带 frame.status(live 盘中 / lunch_break 午休 / closed 收盘帧 / previous_session 非今日 / stale 延迟),引用数据前先看它;' +
474
- '盘中价格是最新价不是收盘价。Pro 会员调用会返回「需要 Max」的说明。',
499
+ 'view=kline:codes(最多 5 只)的日 K 序列——rows 为已完成交易日(period 只支持 day,bars 1-260 默认 120,codes × bars ≤ 1000,adj=qfq 前复权 / none 不复权,' +
500
+ '单位 元 / 股 / 元,缺值 null),live 为实时帧那天未走完的 K(最新价当收盘价);live.comparable 只有 true 才可与 rows 拼接,' +
501
+ 'false = 疑似除权 / 价格基准不一致,null = 无法判断(缓存滞后 / 个股停牌 / 日历不可用,原因见 live.reason,缓存状态见 history)。' +
502
+ '结果文本首行会标出帧的时效与交易日;每个结果都带 frame.status(live 盘中 / lunch_break 午休 / closed 收盘帧 / previous_session 非今日 / stale 延迟),' +
503
+ '不是 live 就不是此刻的数据,引用时注明帧日期;盘中价格是最新价不是收盘价。Pro 会员调用会返回「需要 Max」的说明。',
475
504
  parameters: {
476
505
  view: { type: 'string', enum: REALTIME_VIEWS, required: true, description: 'market 市场概况 / quotes 指定个股 / screen 全市场筛选 / fields 字段清单 / kline 日 K 序列' },
477
506
  codes: { type: 'array', items: { type: 'string' }, description: 'view=quotes:6 位代码或 600519.SH 格式,最多 30 只;view=kline 最多 5 只' },
@@ -516,10 +545,10 @@ export const VIP_TOOLS = [
516
545
  if (!view) return REALTIME_VIEW_HINT
517
546
  const codes = realtimeCodes(a?.codes)
518
547
  if (REALTIME_VIEWS_WITH_CODES.has(view) && !(Array.isArray(codes) && codes.length)) {
519
- const cap = view === 'kline' ? 5 : 30
548
+ const cap = REALTIME_MAX_CODES[view]
520
549
  return `view=${view} 需要 codes(6 位代码数组,最多 ${cap} 只,如 ["600519", "000001"])。`
521
550
  }
522
- return null
551
+ return realtimeBoundsRefusal(view, codes, a)
523
552
  },
524
553
  buildArgs: (a) => {
525
554
  const view = realtimeView(a)
@@ -555,7 +584,7 @@ export function vipLockedMessage(toolName) {
555
584
  ` - id: hhxg-market\n` +
556
585
  ` config:\n` +
557
586
  ` mcpToken: "<你的 MCP Token>"\n` +
558
- ` 保存后重启 dsh 生效\n\n` +
587
+ ` 保存后重启 dsh 生效;从终端启动 dsh 的话也可以不改文件,设环境变量 HHXG_MCP_TOKEN=<你的 MCP Token>(配置留空时读取)\n\n` +
559
588
  `免费工具(hhxg_snapshot / hhxg_news / hhxg_margin / hhxg_calendar / hhxg_sentiment / ` +
560
589
  `hhxg_datasets / hhxg_themes)无需配置即可使用。`
561
590
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hhxg/dsh-hhxg-market",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "恢恢量化 (hhxg.top) A股量化数据插件 for DeepSeek Harness:免费提供日报快照/财经快讯/融资融券/A股日历/市场情绪/题材热度/数据集目录 7 个工具;填入会员 MCP Token 后解锁个股诊断/量化策略信号/连板晋级预测/盘中实时(Max)等 6 个进阶工具。A-share quant data plugin for DeepSeek Harness (dsh): 7 free tools + 6 VIP tools (incl. Max-only intraday realtime) bridged to the hhxg MCP server.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -31,7 +31,7 @@
31
31
  "CHANGELOG.md"
32
32
  ],
33
33
  "scripts": {
34
- "test": "node --test test/contract.test.js test/security.test.js test/tools.test.js test/present.test.js test/client.test.js"
34
+ "test": "node --test test/contract.test.js test/peers.test.js test/security.test.js test/tools.test.js test/present.test.js test/freshness.test.js test/client.test.js"
35
35
  },
36
36
  "repository": {
37
37
  "type": "git",
@@ -52,8 +52,11 @@
52
52
  }
53
53
  },
54
54
  "peerDependencies": {
55
- "@deepseek-ai/cordis": "^4.0.1",
56
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
57
- "@deepseek-ai/schemastery": "^3.18.1"
55
+ "@deepseek-ai/cordis": ">=4.0.1 <5.0.0-0",
56
+ "@deepseek-ai/dsh-tools": ">=0.1.1-rc.1 <0.3.0-0 || 0.1.7-rc.2 || 0.2.0-rc.1 || 0.2.1-rc.5 || 0.2.2-alpha.2",
57
+ "@deepseek-ai/schemastery": ">=3.18.1 <4.0.0-0"
58
+ },
59
+ "devDependencies": {
60
+ "semver": "^7.7.2"
58
61
  }
59
62
  }