@rivus/gateway 0.16.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -0
  3. package/dist/bootstrap/pi-feishu.d.ts +20 -0
  4. package/dist/bootstrap/pi-feishu.js +671 -0
  5. package/dist/chunks/background-session-authority.js +230 -0
  6. package/dist/chunks/background-session-control-input.js +45 -0
  7. package/dist/chunks/background-session-service.d.ts +390 -0
  8. package/dist/chunks/index.d.ts +4703 -0
  9. package/dist/chunks/node-rivus-deployment-manifest.js +1650 -0
  10. package/dist/chunks/rivus-node-entrypoint.js +4464 -0
  11. package/dist/chunks/service.js +12112 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +16 -0
  14. package/dist/index.d.ts +3 -0
  15. package/dist/index.js +1215 -0
  16. package/dist/mcp.d.ts +92 -0
  17. package/dist/mcp.js +455 -0
  18. package/package.json +64 -0
  19. package/skills/runtime-management/SKILL.md +65 -0
  20. package/templates/a-share-briefing-analysis.mjs +93 -0
  21. package/templates/a-share-briefing-renderer.mjs +257 -0
  22. package/templates/a-share-index-evidence.mjs +99 -0
  23. package/templates/a-share-market-briefing.mjs +83 -0
  24. package/templates/a-share-market-date.mjs +10 -0
  25. package/templates/a-share-overseas-evidence.mjs +86 -0
  26. package/templates/a-share-policy-evidence.mjs +145 -0
  27. package/templates/a-share-provider-response.mjs +21 -0
  28. package/templates/a-share-sector-evidence.mjs +70 -0
  29. package/templates/acp-stdio-proxy.mjs +58 -0
  30. package/templates/current-weather.mjs +117 -0
  31. package/templates/html-drive-tools.mjs +262 -0
  32. package/templates/https-response-reader.mjs +36 -0
  33. package/templates/langfuse-drive-e2e.mjs +175 -0
  34. package/templates/pi-feishu-deployment.bootstrap.ts +3 -0
  35. package/templates/pi-feishu.bootstrap.ts +242 -0
  36. package/templates/rivus-agents.plugin.mjs +290 -0
  37. package/templates/rivus-langfuse-demo.config.json +37 -0
  38. package/templates/rivus-starter.plugin.mjs +47 -0
  39. package/templates/rivus.config.json +114 -0
@@ -0,0 +1,93 @@
1
+ export function analyzeAShareBriefing({ overseasEvidence, quotes, sectorEvidence }) {
2
+ return Object.freeze({
3
+ index: Object.freeze(analyzeIndexStructure(quotes)),
4
+ overseas: Object.freeze(analyzeOverseasStructure(overseasEvidence)),
5
+ sector: analyzeSectorStructure(sectorEvidence),
6
+ turnover: quotes
7
+ .filter(({ symbol }) => symbol === "sh000001" || symbol === "sz399001")
8
+ .reduce((sum, quote) => sum + quote.amount, 0)
9
+ });
10
+ }
11
+
12
+ function analyzeIndexStructure(quotes) {
13
+ const direction = readDirection(quotes);
14
+ const ranked = [...quotes].sort((left, right) => right.changePercent - left.changePercent);
15
+ const strongest = ranked[0];
16
+ const weakest = ranked.at(-1);
17
+ const spread = strongest.changePercent - weakest.changePercent;
18
+ const dispersion = spread >= 1.5 ? "结构分化较大" : spread >= 0.75 ? "存在一定分化" : "表现相对同步";
19
+ const average = quotes.reduce((sum, quote) => sum + quote.changePercent, 0) / quotes.length;
20
+ const positive = quotes.filter(({ changePercent }) => changePercent > 0).length;
21
+ const negative = quotes.filter(({ changePercent }) => changePercent < 0).length;
22
+ const strength =
23
+ positive === quotes.length
24
+ ? "四个指数一致收涨,指数层面的风险偏好偏强。"
25
+ : negative === quotes.length
26
+ ? "四个指数一致收跌,指数层面的风险偏好偏弱。"
27
+ : average >= 0.5
28
+ ? "多数指数收涨且平均涨幅偏强,但方向并不完全一致。"
29
+ : average <= -0.5
30
+ ? "多数指数承压且平均跌幅偏弱,仍需防范弱势扩散。"
31
+ : "指数涨跌互现,尚未形成一致的市场方向。";
32
+ const growth = quotes.find(({ name }) => name === "创业板指");
33
+ const largeCap = quotes.find(({ name }) => name === "沪深 300");
34
+ const styleGap = growth.changePercent - largeCap.changePercent;
35
+ const style =
36
+ styleGap >= 0.5
37
+ ? `创业板指较沪深 300 高 ${formatPercentagePoint(styleGap)},成长风格相对占优。`
38
+ : styleGap <= -0.5
39
+ ? `沪深 300 较创业板指高 ${formatPercentagePoint(-styleGap)},大盘风格相对占优。`
40
+ : "创业板指与沪深 300 表现接近,暂未形成清晰的成长/大盘偏向。";
41
+ const risk =
42
+ spread >= 1.5
43
+ ? `最强与最弱指数相差 ${formatPercentagePoint(spread)},单看综合指数可能掩盖明显分化。`
44
+ : `最强与最弱指数相差 ${formatPercentagePoint(spread)},指数间分化暂时可控。`;
45
+ return { direction: `四个宽基指数 ${direction}`, dispersion, risk, strength, strongest, style, weakest };
46
+ }
47
+
48
+ function analyzeOverseasStructure(evidence) {
49
+ if (!evidence) {
50
+ return {
51
+ conclusion: "隔夜海外指数数据不可用,外部风险背景待开盘数据确认",
52
+ direction: undefined,
53
+ inference: "海外指数数据不可用,不补写外部市场判断。"
54
+ };
55
+ }
56
+ const direction = readDirection(evidence.quotes);
57
+ const average = evidence.quotes.reduce((sum, quote) => sum + quote.changePercent, 0) / evidence.quotes.length;
58
+ const conclusion = `隔夜美股 ${direction},外部风险背景${average >= 0.3 ? "偏积极" : average <= -0.3 ? "偏谨慎" : "偏中性"}`;
59
+ const nasdaq = evidence.quotes.find(({ symbol }) => symbol === "usIXIC");
60
+ const sp500 = evidence.quotes.find(({ symbol }) => symbol === "usINX");
61
+ const gap = nasdaq.changePercent - sp500.changePercent;
62
+ const relative =
63
+ gap >= 0.3
64
+ ? `纳斯达克较标普 500 高 ${formatPercentagePoint(gap)},海外科技风险偏好相对更强。`
65
+ : gap <= -0.3
66
+ ? `纳斯达克较标普 500 低 ${formatPercentagePoint(-gap)},海外科技风险偏好相对更弱。`
67
+ : "纳斯达克与标普 500 表现接近,海外科技风格未显示明显偏离。";
68
+ return { conclusion, direction, inference: `${conclusion}。${relative}` };
69
+ }
70
+
71
+ function analyzeSectorStructure(evidence) {
72
+ if (!evidence) return "行业数据不可用,不补写行业主线判断。";
73
+ const leaders = evidence.leaders
74
+ .slice(0, 2)
75
+ .map(({ name }) => name)
76
+ .join("、");
77
+ const laggards = evidence.laggards
78
+ .slice(0, 2)
79
+ .map(({ name }) => name)
80
+ .join("、");
81
+ return `领涨端集中在${leaders},领跌端集中在${laggards};这只描述行业横截面强弱,不构成市场涨跌的驱动归因。`;
82
+ }
83
+
84
+ function readDirection(quotes) {
85
+ const positive = quotes.filter(({ changePercent }) => changePercent > 0).length;
86
+ const negative = quotes.filter(({ changePercent }) => changePercent < 0).length;
87
+ const flat = quotes.length - positive - negative;
88
+ return `${positive} 涨 ${negative} 跌${flat > 0 ? ` ${flat} 平` : ""}`;
89
+ }
90
+
91
+ function formatPercentagePoint(value) {
92
+ return `${value.toFixed(2)} 个百分点`;
93
+ }
@@ -0,0 +1,257 @@
1
+ import { analyzeAShareBriefing } from "./a-share-briefing-analysis.mjs";
2
+
3
+ const TIME_ZONE = "Asia/Shanghai";
4
+
5
+ export function renderAShareMarketBriefing(input) {
6
+ if (input.status === "ready") return renderReadyBriefing(input);
7
+ if (input.status === "snapshot-unavailable") return renderUnavailablePreMarket(input);
8
+ return renderUnavailablePostMarket(input);
9
+ }
10
+
11
+ function renderReadyBriefing({
12
+ marketDate,
13
+ occurrenceDate,
14
+ overseasEvidence,
15
+ policyEvidence,
16
+ quotes,
17
+ sectorEvidence,
18
+ session,
19
+ source
20
+ }) {
21
+ if (session === "pre-market") {
22
+ return renderPreMarketBriefing({
23
+ marketDate,
24
+ occurrenceDate,
25
+ overseasEvidence,
26
+ policyEvidence,
27
+ quotes,
28
+ sectorEvidence,
29
+ source
30
+ });
31
+ }
32
+ const analysis = analyzeAShareBriefing({ quotes, sectorEvidence });
33
+ return [
34
+ `## A 股盘后分析简报 | ${occurrenceDate}`,
35
+ "",
36
+ "### 30 秒结论",
37
+ "",
38
+ `**${analysis.index.direction},${analysis.index.dispersion}**;${analysis.index.strongest.name}领涨,${analysis.index.weakest.name}相对最弱。`,
39
+ "",
40
+ "### 市场事实",
41
+ "",
42
+ `**行情基准**:今日收盘(行情日期 ${marketDate})`,
43
+ `**沪深两市成交额**:${formatTrillionYuan(analysis.turnover)}`,
44
+ "",
45
+ "#### 主要指数",
46
+ renderIndexDetails(quotes, true),
47
+ "",
48
+ "#### 行业表现",
49
+ ...renderSectorEvidence(sectorEvidence),
50
+ "",
51
+ "#### 政策资讯线索(近 3 天)",
52
+ ...renderPolicyEvidence(policyEvidence),
53
+ "",
54
+ "### 分析判断(基于上列事实)",
55
+ "",
56
+ `- **市场强弱**:${analysis.index.strength}`,
57
+ `- **风格结构**:${analysis.index.style}`,
58
+ `- **行业主线线索**:${analysis.sector}`,
59
+ `- **结构风险**:${analysis.index.risk}`,
60
+ "- **资讯边界**:官方标题只作为同期背景,不据此认定市场涨跌原因。",
61
+ "",
62
+ "### 下一时点观察",
63
+ "",
64
+ `- ${formatPossessive(analysis.index.strongest.name)}相对强势能否延续,${analysis.index.weakest.name}能否收窄差距。`,
65
+ "- 创业板指与沪深 300 的相对表现是否继续确认风格方向。",
66
+ "- 后续行业与成交数据是否支持当前指数结构。",
67
+ "",
68
+ `> 来源:${formatSources({ policyEvidence, sectorEvidence, source })};行情可能存在延迟,请以交易所最终数据为准。`,
69
+ "> 分析判断只由本简报展示的事实推导;同期线索不等于已证明的涨跌原因。",
70
+ "> 仅供信息参考,不构成投资建议。"
71
+ ].join("\n");
72
+ }
73
+
74
+ function renderPreMarketBriefing({
75
+ marketDate,
76
+ occurrenceDate,
77
+ overseasEvidence,
78
+ policyEvidence,
79
+ quotes,
80
+ sectorEvidence,
81
+ source
82
+ }) {
83
+ const analysis = analyzeAShareBriefing({ overseasEvidence, quotes, sectorEvidence });
84
+ return [
85
+ `## A 股盘前分析简报 | ${occurrenceDate}`,
86
+ "",
87
+ "### 30 秒结论",
88
+ "",
89
+ `**上一交易日 A 股:${analysis.index.direction.replace("四个宽基指数 ", "")}${analysis.index.dispersion === "表现相对同步" ? ",指数表现相对同步" : `,${analysis.index.dispersion}`};${analysis.overseas.conclusion}**`,
90
+ "",
91
+ "### 市场事实",
92
+ "",
93
+ `**行情基准**:上一可用交易日(${marketDate})收盘`,
94
+ `**上一交易日沪深两市成交额**:${formatTrillionYuan(analysis.turnover)}`,
95
+ "",
96
+ "#### 主要指数",
97
+ renderIndexDetails(quotes, false),
98
+ "",
99
+ "#### 隔夜海外指数",
100
+ ...renderOverseasEvidence(overseasEvidence, analysis.overseas.direction),
101
+ "",
102
+ "#### 上一交易日行业表现",
103
+ ...renderSectorEvidence(sectorEvidence),
104
+ "",
105
+ "#### 政策资讯线索(近 3 天)",
106
+ ...renderPolicyEvidence(policyEvidence),
107
+ "",
108
+ "### 分析判断(基于上列事实)",
109
+ "",
110
+ `- **A 股前序结构**:${analysis.index.strength}`,
111
+ `- **前序风格**:${analysis.index.style}`,
112
+ `- **前序行业线索**:${analysis.sector}`,
113
+ `- **外部风险背景**:${analysis.overseas.inference}`,
114
+ `- **结构风险**:${analysis.index.risk}`,
115
+ "- **边界**:隔夜美股只能作为外部风险背景,不能直接推导今日 A 股方向;官方标题与行业排名也不构成涨跌因果证明。",
116
+ "",
117
+ "### 今日观察清单",
118
+ "",
119
+ "- 集合竞价与开盘后首小时的方向、成交是否确认或否定前序指数结构。",
120
+ `- ${formatPossessive(analysis.index.strongest.name)}相对强势能否延续,${analysis.index.weakest.name}能否收窄差距。`,
121
+ "- 创业板指与沪深 300 的相对表现是否继续确认风格方向。",
122
+ "- 领涨与领跌行业是否出现延续、轮动或快速反转。",
123
+ "",
124
+ `> 来源:${formatSources({ overseasEvidence, policyEvidence, sectorEvidence, source })};行情可能存在延迟,请以交易所最终数据为准。`,
125
+ "> 分析判断只由本简报展示的事实推导;同期线索不等于已证明的涨跌原因。",
126
+ "> 仅供信息参考,不构成投资建议。"
127
+ ].join("\n");
128
+ }
129
+
130
+ function renderPolicyEvidence(evidence) {
131
+ if (!evidence) return ["- **官方资讯**:当前不可用,本次不补写政策或事件驱动。"];
132
+ const unavailable =
133
+ evidence.unavailableSources.length > 0
134
+ ? [`- **官方资讯缺失**:${evidence.unavailableSources.join("、")}当前不可用;其余标题仍按计划发生时刻截断。`]
135
+ : [];
136
+ if (evidence.items.length === 0) {
137
+ return [...unavailable, "- **官方资讯**:近 3 天未筛得新标题;这不代表没有其他市场信息。"];
138
+ }
139
+ return [
140
+ ...unavailable,
141
+ ...evidence.items.map(
142
+ ({ publishedAt, source, title, url }) =>
143
+ `- [${escapeMarkdownLabel(title)}](${url})(${source},${formatSourceTime(publishedAt)})`
144
+ )
145
+ ];
146
+ }
147
+
148
+ function renderSectorEvidence(evidence) {
149
+ if (!evidence) return ["- **行业数据**:当前不可用,本次不据此判断板块驱动。"];
150
+ return [
151
+ `- **领涨行业**:${formatSectorRanking(evidence.leaders)}`,
152
+ `- **领跌行业**:${formatSectorRanking(evidence.laggards)}`,
153
+ `- **行业数据时间(上海)**:${formatShanghaiTime(evidence.asOf)}`
154
+ ];
155
+ }
156
+
157
+ function renderOverseasEvidence(evidence, direction) {
158
+ if (!evidence) return ["- **海外指数**:当前不可用,本次不据此判断外部风险背景。"];
159
+ return [
160
+ `- **隔夜美股方向**:${direction}`,
161
+ ...evidence.quotes.map(
162
+ ({ changePercent, name, price }) => `- **${name}** ${formatNumber(price)}(${formatPercent(changePercent)})`
163
+ ),
164
+ `- **行情源标注时间**:${evidence.asOf}`
165
+ ];
166
+ }
167
+
168
+ function renderIndexDetails(quotes, includeRange) {
169
+ return quotes
170
+ .map(
171
+ (quote) =>
172
+ `- **${quote.name}** ${formatNumber(quote.price)}(${formatPercent(quote.changePercent)})` +
173
+ (includeRange ? `,日内 ${formatNumber(quote.low)}–${formatNumber(quote.high)}` : "")
174
+ )
175
+ .join("\n");
176
+ }
177
+
178
+ function renderUnavailablePostMarket({ marketDate, occurrenceDate, source }) {
179
+ return [
180
+ `## A 股盘后简报 | ${occurrenceDate}`,
181
+ "",
182
+ "> 今日休市或行情尚未更新,本次不生成当日涨跌数据。",
183
+ "",
184
+ `**最近可用行情日期**:${marketDate}`,
185
+ "",
186
+ `> 来源:${source};行情可能存在延迟,请以交易所最终数据为准。`,
187
+ "> 仅供信息参考,不构成投资建议。"
188
+ ].join("\n");
189
+ }
190
+
191
+ function renderUnavailablePreMarket({ asOf, occurrence, occurrenceDate, source }) {
192
+ const occurrenceTime = new Intl.DateTimeFormat("zh-CN", {
193
+ hour: "2-digit",
194
+ hourCycle: "h23",
195
+ minute: "2-digit",
196
+ timeZone: TIME_ZONE
197
+ }).format(new Date(occurrence));
198
+ return [
199
+ `## A 股盘前简报 | ${occurrenceDate}`,
200
+ "",
201
+ `> 行情源已经产生计划时间之后的数据,无法可靠还原 ${occurrenceTime} 的盘前快照;本次不展示指数涨跌。`,
202
+ "",
203
+ `最近可用行情时间:${asOf}`,
204
+ "",
205
+ `> 来源:${source};行情可能存在延迟,请以交易所最终数据为准。`,
206
+ "> 仅供信息参考,不构成投资建议。"
207
+ ].join("\n");
208
+ }
209
+
210
+ function formatSources({ overseasEvidence, policyEvidence, sectorEvidence, source }) {
211
+ return [source, overseasEvidence?.source, sectorEvidence?.source, ...(policyEvidence?.sources ?? [])]
212
+ .filter(Boolean)
213
+ .join(";");
214
+ }
215
+
216
+ function formatSectorRanking(sectors) {
217
+ return sectors.map(({ changePercent, name }) => `${name}(${formatPercent(changePercent)})`).join("、");
218
+ }
219
+
220
+ function escapeMarkdownLabel(value) {
221
+ return value.replaceAll("[", "\\[").replaceAll("]", "\\]");
222
+ }
223
+
224
+ function formatSourceTime(value) {
225
+ return value.slice(11, 19) === "00:00:00" ? value.slice(0, 10) : value.slice(0, 16).replace("T", " ");
226
+ }
227
+
228
+ function formatShanghaiTime(value) {
229
+ const parts = new Intl.DateTimeFormat("en-CA", {
230
+ day: "2-digit",
231
+ hour: "2-digit",
232
+ hourCycle: "h23",
233
+ minute: "2-digit",
234
+ month: "2-digit",
235
+ second: "2-digit",
236
+ timeZone: TIME_ZONE,
237
+ year: "numeric"
238
+ }).formatToParts(new Date(value));
239
+ const read = (type) => parts.find((part) => part.type === type)?.value;
240
+ return `${read("year")}-${read("month")}-${read("day")} ${read("hour")}:${read("minute")}:${read("second")}`;
241
+ }
242
+
243
+ function formatPossessive(name) {
244
+ return /\d$/u.test(name) ? `${name} 的` : `${name}的`;
245
+ }
246
+
247
+ function formatNumber(value) {
248
+ return new Intl.NumberFormat("zh-CN", { maximumFractionDigits: 2, minimumFractionDigits: 2 }).format(value);
249
+ }
250
+
251
+ function formatPercent(value) {
252
+ return `${value >= 0 ? "+" : ""}${value.toFixed(2)}%`;
253
+ }
254
+
255
+ function formatTrillionYuan(value) {
256
+ return `${(value / 1_000_000_000_000).toFixed(2)} 万亿元`;
257
+ }
@@ -0,0 +1,99 @@
1
+ import { readAShareProviderResponse } from "./a-share-provider-response.mjs";
2
+
3
+ const SOURCE = "腾讯证券行情中心公开行情";
4
+ const INDEXES = Object.freeze([
5
+ { name: "上证指数", symbol: "sh000001" },
6
+ { name: "深证成指", symbol: "sz399001" },
7
+ { name: "创业板指", symbol: "sz399006" },
8
+ { name: "沪深 300", symbol: "sh000300" }
9
+ ]);
10
+
11
+ export async function readAShareIndexEvidence() {
12
+ const quotes = await Promise.all(INDEXES.map(readQuote));
13
+ const marketDates = new Set(quotes.map(({ marketDate }) => marketDate));
14
+ if (marketDates.size !== 1) throw new Error("a-share market quote dates are inconsistent");
15
+ const asOf = quotes
16
+ .map(({ asOf }) => asOf)
17
+ .sort((left, right) => left.localeCompare(right))
18
+ .at(-1);
19
+ return Object.freeze({ asOf, marketDate: quotes[0].marketDate, quotes: Object.freeze(quotes), source: SOURCE });
20
+ }
21
+
22
+ async function readQuote(index) {
23
+ const url = new URL("https://qt.gtimg.cn/");
24
+ url.searchParams.set("q", index.symbol);
25
+ const body = await readAShareProviderResponse({
26
+ accept: "text/plain",
27
+ maxBytes: 64 * 1024,
28
+ referer: "https://finance.qq.com/",
29
+ source: "a-share market quote",
30
+ url
31
+ });
32
+ return parseQuote(index, new TextDecoder("gbk").decode(body));
33
+ }
34
+
35
+ function parseQuote(index, body) {
36
+ const start = body.indexOf('"');
37
+ const end = body.lastIndexOf('"');
38
+ if (start < 0 || end <= start) throw new Error(`a-share market quote is invalid for ${index.symbol}`);
39
+ const fields = body.slice(start + 1, end).split("~");
40
+ const timestamp = fields[30];
41
+ if (!/^\d{14}$/.test(timestamp ?? "")) {
42
+ throw new Error(`a-share market quote is missing a timestamp for ${index.symbol}`);
43
+ }
44
+ if (!isValidCompactTimestamp(timestamp)) {
45
+ throw new Error(`a-share market quote has an invalid timestamp for ${index.symbol}`);
46
+ }
47
+ return Object.freeze({
48
+ amount: readQuoteAmount(fields[35], index.symbol),
49
+ asOf: formatShanghaiTimestamp(timestamp),
50
+ changePercent: readFiniteNumber(fields[32], index.symbol, "change percent"),
51
+ high: readFiniteNumber(fields[33], index.symbol, "high"),
52
+ low: readFiniteNumber(fields[34], index.symbol, "low"),
53
+ marketDate: `${timestamp.slice(0, 4)}-${timestamp.slice(4, 6)}-${timestamp.slice(6, 8)}`,
54
+ name: index.name,
55
+ price: readFiniteNumber(fields[3], index.symbol, "price"),
56
+ symbol: index.symbol
57
+ });
58
+ }
59
+
60
+ function isValidCompactTimestamp(timestamp) {
61
+ const values = [
62
+ Number(timestamp.slice(0, 4)),
63
+ Number(timestamp.slice(4, 6)),
64
+ Number(timestamp.slice(6, 8)),
65
+ Number(timestamp.slice(8, 10)),
66
+ Number(timestamp.slice(10, 12)),
67
+ Number(timestamp.slice(12, 14))
68
+ ];
69
+ const instant = new Date(Date.UTC(values[0], values[1] - 1, values[2], values[3], values[4], values[5]));
70
+ return (
71
+ instant.getUTCFullYear() === values[0] &&
72
+ instant.getUTCMonth() === values[1] - 1 &&
73
+ instant.getUTCDate() === values[2] &&
74
+ instant.getUTCHours() === values[3] &&
75
+ instant.getUTCMinutes() === values[4] &&
76
+ instant.getUTCSeconds() === values[5]
77
+ );
78
+ }
79
+
80
+ function readQuoteAmount(value, symbol) {
81
+ const amount = Number(value?.split("/")[2]);
82
+ if (!Number.isFinite(amount) || amount <= 0) {
83
+ throw new Error(`a-share market quote is missing amount for ${symbol}`);
84
+ }
85
+ return amount;
86
+ }
87
+
88
+ function readFiniteNumber(value, symbol, field) {
89
+ const number = Number(value);
90
+ if (!Number.isFinite(number)) throw new Error(`a-share market quote is missing ${field} for ${symbol}`);
91
+ return number;
92
+ }
93
+
94
+ function formatShanghaiTimestamp(timestamp) {
95
+ return `${timestamp.slice(0, 4)}-${timestamp.slice(4, 6)}-${timestamp.slice(6, 8)}T${timestamp.slice(
96
+ 8,
97
+ 10
98
+ )}:${timestamp.slice(10, 12)}:${timestamp.slice(12, 14)}+08:00`;
99
+ }
@@ -0,0 +1,83 @@
1
+ import { renderAShareMarketBriefing } from "./a-share-briefing-renderer.mjs";
2
+ import { readAShareIndexEvidence } from "./a-share-index-evidence.mjs";
3
+ import { dateInShanghai } from "./a-share-market-date.mjs";
4
+ import { readAShareOverseasEvidence } from "./a-share-overseas-evidence.mjs";
5
+ import { readASharePolicyEvidence } from "./a-share-policy-evidence.mjs";
6
+ import { readAShareSectorEvidence } from "./a-share-sector-evidence.mjs";
7
+
8
+ export async function readAShareMarketBriefing(input) {
9
+ const { occurrence, session } = validateInput(input);
10
+ const occurrenceDate = dateInShanghai(new Date(occurrence));
11
+ const { asOf, marketDate, quotes, source } = await readAShareIndexEvidence();
12
+ if (marketDate > occurrenceDate) throw new Error("a-share market quote date is later than the scheduled occurrence");
13
+ const preMarketSnapshotUnavailable =
14
+ session === "pre-market" && Date.parse(asOf) > Date.parse(occurrence) + 5 * 60_000;
15
+ const status = preMarketSnapshotUnavailable
16
+ ? "snapshot-unavailable"
17
+ : session === "post-market" && marketDate !== occurrenceDate
18
+ ? "market-closed-or-not-updated"
19
+ : "ready";
20
+ const [overseasEvidence, policyEvidence, sectorEvidence] =
21
+ status === "ready"
22
+ ? await Promise.all([
23
+ session === "pre-market" ? readOptionalOverseasEvidence(occurrence) : undefined,
24
+ readOptionalPolicyEvidence(occurrence),
25
+ readOptionalSectorEvidence(marketDate)
26
+ ])
27
+ : [undefined, undefined, undefined];
28
+ const markdown = renderAShareMarketBriefing({
29
+ asOf,
30
+ marketDate,
31
+ occurrence,
32
+ occurrenceDate,
33
+ overseasEvidence,
34
+ policyEvidence,
35
+ quotes,
36
+ sectorEvidence,
37
+ session,
38
+ source,
39
+ status
40
+ });
41
+ return Object.freeze({ asOf, markdown, marketDate, session, source, status });
42
+ }
43
+
44
+ async function readOptionalOverseasEvidence(occurrence) {
45
+ try {
46
+ return await readAShareOverseasEvidence({ occurrence });
47
+ } catch {
48
+ return undefined;
49
+ }
50
+ }
51
+
52
+ async function readOptionalPolicyEvidence(occurrence) {
53
+ try {
54
+ return await readASharePolicyEvidence({ occurrence });
55
+ } catch {
56
+ return undefined;
57
+ }
58
+ }
59
+
60
+ async function readOptionalSectorEvidence(marketDate) {
61
+ try {
62
+ return await readAShareSectorEvidence({ marketDate });
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ }
67
+
68
+ function validateInput(input) {
69
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
70
+ throw new Error("a-share-market-briefing input must be an object");
71
+ }
72
+ if (input.session !== "pre-market" && input.session !== "post-market") {
73
+ throw new Error("a-share-market-briefing session must be pre-market or post-market");
74
+ }
75
+ if (typeof input.occurrence !== "string" || !Number.isFinite(Date.parse(input.occurrence))) {
76
+ throw new Error("a-share-market-briefing occurrence must be an ISO date-time");
77
+ }
78
+ const keys = Object.keys(input);
79
+ if (keys.length !== 2 || !keys.includes("occurrence") || !keys.includes("session")) {
80
+ throw new Error("a-share-market-briefing input contains unsupported fields");
81
+ }
82
+ return { occurrence: input.occurrence, session: input.session };
83
+ }
@@ -0,0 +1,10 @@
1
+ export function dateInShanghai(date) {
2
+ const parts = new Intl.DateTimeFormat("en-CA", {
3
+ day: "2-digit",
4
+ month: "2-digit",
5
+ timeZone: "Asia/Shanghai",
6
+ year: "numeric"
7
+ }).formatToParts(date);
8
+ const read = (type) => parts.find((part) => part.type === type)?.value;
9
+ return `${read("year")}-${read("month")}-${read("day")}`;
10
+ }
@@ -0,0 +1,86 @@
1
+ import { readAShareProviderResponse } from "./a-share-provider-response.mjs";
2
+
3
+ const SOURCE = "腾讯证券海外指数公开行情";
4
+ const MAX_AGE_MS = 4 * 24 * 60 * 60 * 1000;
5
+ const INDEXES = Object.freeze([
6
+ { name: "道琼斯", symbol: "usDJI" },
7
+ { name: "纳斯达克", symbol: "usIXIC" },
8
+ { name: "标普 500", symbol: "usINX" }
9
+ ]);
10
+
11
+ export async function readAShareOverseasEvidence({ occurrence }) {
12
+ const url = new URL("https://qt.gtimg.cn/");
13
+ url.searchParams.set("q", INDEXES.map(({ symbol }) => symbol).join(","));
14
+ const responseBody = await readAShareProviderResponse({
15
+ accept: "text/plain",
16
+ maxBytes: 64 * 1024,
17
+ referer: "https://finance.qq.com/",
18
+ source: "a-share overseas evidence",
19
+ url
20
+ });
21
+ const body = new TextDecoder("gbk").decode(responseBody);
22
+ const quotes = INDEXES.map((index) => parseQuote(index, body));
23
+ const asOf = quotes
24
+ .map(({ providerTime }) => providerTime)
25
+ .sort((left, right) => left.localeCompare(right))
26
+ .at(-1);
27
+ if (new Set(quotes.map(({ providerTime }) => providerTime)).size !== 1) {
28
+ throw new Error("a-share overseas evidence timestamps are inconsistent");
29
+ }
30
+ const age = providerClockTime(occurrence) - providerClockTime(asOf);
31
+ if (age < 0 || age > MAX_AGE_MS) throw new Error("a-share overseas evidence is stale for the occurrence");
32
+ return Object.freeze({ asOf, quotes: Object.freeze(quotes), source: SOURCE });
33
+ }
34
+
35
+ function providerClockTime(value) {
36
+ if (value.includes("T")) {
37
+ const parts = new Intl.DateTimeFormat("en-CA", {
38
+ day: "2-digit",
39
+ hour: "2-digit",
40
+ hourCycle: "h23",
41
+ minute: "2-digit",
42
+ month: "2-digit",
43
+ second: "2-digit",
44
+ timeZone: "America/New_York",
45
+ year: "numeric"
46
+ }).formatToParts(new Date(value));
47
+ const read = (type) => parts.find((part) => part.type === type)?.value;
48
+ return Date.parse(
49
+ `${read("year")}-${read("month")}-${read("day")}T${read("hour")}:${read("minute")}:${read("second")}Z`
50
+ );
51
+ }
52
+ return Date.parse(`${value.replace(" ", "T")}Z`);
53
+ }
54
+
55
+ function parseQuote(index, body) {
56
+ const match = body.match(new RegExp(`(?:^|\\n)v_${index.symbol}="([^"]+)";`, "u"));
57
+ if (!match) throw new Error(`a-share overseas evidence is missing ${index.symbol}`);
58
+ const fields = match[1].split("~");
59
+ const providerTime = fields[30];
60
+ if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(providerTime ?? "")) {
61
+ throw new Error(`a-share overseas evidence is missing a timestamp for ${index.symbol}`);
62
+ }
63
+ if (!isValidProviderTimestamp(providerTime)) {
64
+ throw new Error(`a-share overseas evidence has an invalid timestamp for ${index.symbol}`);
65
+ }
66
+ if (fields[35] !== "USD") throw new Error(`a-share overseas evidence has an unexpected currency for ${index.symbol}`);
67
+ return Object.freeze({
68
+ changePercent: readFiniteNumber(fields[32], index.symbol, "change percent"),
69
+ name: index.name,
70
+ price: readFiniteNumber(fields[3], index.symbol, "price"),
71
+ providerTime,
72
+ symbol: index.symbol
73
+ });
74
+ }
75
+
76
+ function isValidProviderTimestamp(value) {
77
+ const normalized = value.replace(" ", "T");
78
+ const timestamp = Date.parse(`${normalized}Z`);
79
+ return Number.isFinite(timestamp) && new Date(timestamp).toISOString().slice(0, 19) === normalized;
80
+ }
81
+
82
+ function readFiniteNumber(value, symbol, field) {
83
+ const number = Number(value);
84
+ if (!Number.isFinite(number)) throw new Error(`a-share overseas evidence is missing ${field} for ${symbol}`);
85
+ return number;
86
+ }