w-dwdata-hub 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.
Files changed (135) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +55 -0
  4. package/.jsdoc +25 -0
  5. package/LICENSE +21 -0
  6. package/README.md +168 -0
  7. package/SECURITY.md +5 -0
  8. package/babel.config.js +16 -0
  9. package/dist/w-dwdata-hub.umd.js +7 -0
  10. package/dist/w-dwdata-hub.umd.js.map +1 -0
  11. package/docs/WDwdataHub.mjs.html +136 -0
  12. package/docs/decodeEntities.mjs.html +129 -0
  13. package/docs/extractDivContent.mjs.html +147 -0
  14. package/docs/fetchAiNewsAggregator.mjs.html +155 -0
  15. package/docs/fetchAisixiang.mjs.html +1038 -0
  16. package/docs/fetchCnyes.mjs.html +260 -0
  17. package/docs/fetchGuancha.mjs.html +1007 -0
  18. package/docs/fetchHackerNews.mjs.html +189 -0
  19. package/docs/fetchMoneydj.mjs.html +251 -0
  20. package/docs/fetchMops.mjs.html +385 -0
  21. package/docs/fetchRSS.mjs.html +160 -0
  22. package/docs/fetchStatementdog.mjs.html +213 -0
  23. package/docs/fetchTaifex.mjs.html +513 -0
  24. package/docs/fetchTpex3insti.mjs.html +278 -0
  25. package/docs/fetchTpexMargin.mjs.html +248 -0
  26. package/docs/fetchTpexStock.mjs.html +199 -0
  27. package/docs/fetchTwDataHoliday.mjs.html +236 -0
  28. package/docs/fetchTwseMargin.mjs.html +253 -0
  29. package/docs/fetchTwseStock.mjs.html +188 -0
  30. package/docs/fetchTwseT86.mjs.html +208 -0
  31. package/docs/fetchWithRetry.mjs.html +376 -0
  32. package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  33. package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  34. package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  35. package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  36. package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  37. package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  38. package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  39. package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  40. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  41. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +978 -0
  42. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  43. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  44. package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  45. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  46. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +1049 -0
  47. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  48. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  49. package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  50. package/docs/getOptFetch.mjs.html +185 -0
  51. package/docs/global.html +22649 -0
  52. package/docs/htmlToMarkdown.mjs.html +168 -0
  53. package/docs/index.html +84 -0
  54. package/docs/isYmd.mjs.html +114 -0
  55. package/docs/parseIntComma.mjs.html +100 -0
  56. package/docs/safeFilename.mjs.html +102 -0
  57. package/docs/scripts/collapse.js +39 -0
  58. package/docs/scripts/commonNav.js +28 -0
  59. package/docs/scripts/linenumber.js +25 -0
  60. package/docs/scripts/nav.js +12 -0
  61. package/docs/scripts/polyfill.js +4 -0
  62. package/docs/scripts/prettify/Apache-License-2.0.txt +202 -0
  63. package/docs/scripts/prettify/lang-css.js +2 -0
  64. package/docs/scripts/prettify/prettify.js +28 -0
  65. package/docs/scripts/search.js +99 -0
  66. package/docs/styles/jsdoc.css +776 -0
  67. package/docs/styles/prettify.css +80 -0
  68. package/docs/toDatetimeUTC8.mjs.html +143 -0
  69. package/docs/toRocDate.mjs.html +107 -0
  70. package/g.mjs +73 -0
  71. package/package.json +31 -0
  72. package/script.txt +17 -0
  73. package/src/WDwdataHub.mjs +64 -0
  74. package/src/decodeEntities.mjs +57 -0
  75. package/src/extractDivContent.mjs +75 -0
  76. package/src/fetchAiNewsAggregator.mjs +83 -0
  77. package/src/fetchAisixiang.mjs +966 -0
  78. package/src/fetchCnyes.mjs +188 -0
  79. package/src/fetchGuancha.mjs +935 -0
  80. package/src/fetchHackerNews.mjs +117 -0
  81. package/src/fetchMoneydj.mjs +179 -0
  82. package/src/fetchMops.mjs +313 -0
  83. package/src/fetchRSS.mjs +88 -0
  84. package/src/fetchStatementdog.mjs +141 -0
  85. package/src/fetchTaifex.mjs +441 -0
  86. package/src/fetchTpex3insti.mjs +206 -0
  87. package/src/fetchTpexMargin.mjs +176 -0
  88. package/src/fetchTpexStock.mjs +127 -0
  89. package/src/fetchTwDataHoliday.mjs +164 -0
  90. package/src/fetchTwseMargin.mjs +181 -0
  91. package/src/fetchTwseStock.mjs +116 -0
  92. package/src/fetchTwseT86.mjs +136 -0
  93. package/src/fetchWithRetry.mjs +304 -0
  94. package/src/getOptFetch.mjs +113 -0
  95. package/src/htmlToMarkdown.mjs +96 -0
  96. package/src/isYmd.mjs +42 -0
  97. package/src/parseIntComma.mjs +28 -0
  98. package/src/safeFilename.mjs +30 -0
  99. package/src/toDatetimeUTC8.mjs +71 -0
  100. package/src/toRocDate.mjs +35 -0
  101. package/test/WDwdataHub.test.mjs +63 -0
  102. package/test/decodeEntities.test.mjs +42 -0
  103. package/test/extractDivContent.test.mjs +48 -0
  104. package/test/fetchAiNewsAggregator.test.mjs +79 -0
  105. package/test/fetchAisixiang.test.mjs +225 -0
  106. package/test/fetchCnyes.test.mjs +61 -0
  107. package/test/fetchGuancha.test.mjs +190 -0
  108. package/test/fetchHackerNews.test.mjs +78 -0
  109. package/test/fetchMoneydj.test.mjs +82 -0
  110. package/test/fetchMops.test.mjs +106 -0
  111. package/test/fetchRSS.test.mjs +85 -0
  112. package/test/fetchStatementdog.test.mjs +69 -0
  113. package/test/fetchTaifex.test.mjs +99 -0
  114. package/test/fetchTpex3insti.test.mjs +90 -0
  115. package/test/fetchTpexMargin.test.mjs +94 -0
  116. package/test/fetchTpexStock.test.mjs +68 -0
  117. package/test/fetchTwDataHoliday.test.mjs +100 -0
  118. package/test/fetchTwseMargin.test.mjs +81 -0
  119. package/test/fetchTwseStock.test.mjs +80 -0
  120. package/test/fetchTwseT86.test.mjs +76 -0
  121. package/test/fetchWithRetry.test.mjs +174 -0
  122. package/test/htmlToMarkdown.test.mjs +71 -0
  123. package/test/isYmd.test.mjs +56 -0
  124. package/test/parseIntComma.test.mjs +38 -0
  125. package/test/safeFilename.test.mjs +35 -0
  126. package/test/toDatetimeUTC8.test.mjs +65 -0
  127. package/test/toRocDate.test.mjs +38 -0
  128. package/test/tools/encodeBig5.mjs +80 -0
  129. package/test/tools/fixtures.mjs +539 -0
  130. package/test/tools/hasChrome.mjs +39 -0
  131. package/test/tools/serverForTest.mjs +397 -0
  132. package/toolg/addVersion.mjs +4 -0
  133. package/toolg/cleanFolder.mjs +5 -0
  134. package/toolg/gDistRollup.mjs +49 -0
  135. package/toolg/modifyReadme.mjs +4 -0
@@ -0,0 +1,539 @@
1
+ //本檔為各資料來源之測試用固定回應內容
2
+ //欄位結構與命名皆比照各官方API實際格式, 令測試斷言等同於對規格之翻譯, 而非對現況之指紋
3
+
4
+
5
+ //===== AI News Aggregator =====
6
+
7
+
8
+ //含前後空白之欄位供斷言trim, 第2筆published_at不可解析供斷言time退回''
9
+ let aiNews = {
10
+ items: [
11
+ { url: ' https://ai.example/a1 ', published_at: '2026-08-07T01:02:03Z', title: ' AI標題一 ', source: ' Example News ' },
12
+ { url: 'https://ai.example/a2', published_at: 'not-a-date', title: 'AI標題二', source: 'Other News' },
13
+ ],
14
+ }
15
+
16
+
17
+ //===== RSS =====
18
+
19
+
20
+ //第1筆有dc:creator供斷言from取creator, 第2筆無供斷言from退回channel title
21
+ let rssXml = `<?xml version="1.0" encoding="UTF-8"?>
22
+ <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
23
+ <channel>
24
+ <title>測試訂閱源</title>
25
+ <link>https://rss.example/</link>
26
+ <item>
27
+ <title>RSS標題一</title>
28
+ <link>https://rss.example/1</link>
29
+ <pubDate>Fri, 07 Aug 2026 01:02:03 GMT</pubDate>
30
+ <description>描述一</description>
31
+ <dc:creator>作者甲</dc:creator>
32
+ </item>
33
+ <item>
34
+ <title>RSS標題二</title>
35
+ <link>https://rss.example/2</link>
36
+ <pubDate>Fri, 07 Aug 2026 02:03:04 GMT</pubDate>
37
+ <description>描述二</description>
38
+ </item>
39
+ </channel>
40
+ </rss>`
41
+
42
+
43
+ //===== Hacker News =====
44
+
45
+
46
+ let hnNewStories = [101, 102, 103]
47
+
48
+
49
+ //103為comment供斷言僅保留story, 102無url供斷言退回站內討論頁網址
50
+ let hnItems = {
51
+ 101: { id: 101, type: 'story', title: 'HN標題一', url: 'https://hn.example/1', time: 1786000000 },
52
+ 102: { id: 102, type: 'story', title: 'HN標題二', time: 1786000060 },
53
+ 103: { id: 103, type: 'comment', text: '留言', time: 1786000120 },
54
+ }
55
+
56
+
57
+ //===== 鉅亨網 =====
58
+
59
+
60
+ //第2筆publishAt為null供斷言time退回''
61
+ let cnyesPage1 = {
62
+ items: {
63
+ data: [
64
+ { newsId: 5001, title: '鉅亨標題一', publishAt: 1786000000 },
65
+ { newsId: 5002, title: '鉅亨標題二', publishAt: null },
66
+ ],
67
+ },
68
+ }
69
+
70
+
71
+ let cnyesPageEmpty = { items: { data: [] } }
72
+
73
+
74
+ //===== 財報狗 =====
75
+
76
+
77
+ //第1筆為相對路徑供斷言補origin, 第2筆為絕對路徑供斷言保留, 第3筆無標題供斷言略過
78
+ let statementdogHtml = `<!DOCTYPE html><html><head><title>財報狗</title></head><body>
79
+ <div class="statementdog-news-list-item">
80
+ <div class="statementdog-news-list-item-date">2026-08-07</div>
81
+ <a class="statementdog-news-list-item-link" href="/news/n1"><div class="statementdog-news-list-item-title">財報狗標題一</div></a>
82
+ </div>
83
+ <div class="statementdog-news-list-item">
84
+ <div class="statementdog-news-list-item-date">2026-08-06</div>
85
+ <a class="statementdog-news-list-item-link" href="https://other.example/n2"><div class="statementdog-news-list-item-title">財報狗標題二</div></a>
86
+ </div>
87
+ <div class="statementdog-news-list-item">
88
+ <div class="statementdog-news-list-item-date">2026-08-05</div>
89
+ <a class="statementdog-news-list-item-link" href="/news/n3"><div class="statementdog-news-list-item-title"></div></a>
90
+ </div>
91
+ </body></html>`
92
+
93
+
94
+ //主要selector全無, 供斷言fallback selector生效
95
+ let statementdogFallbackHtml = `<!DOCTYPE html><html><head><title>財報狗</title></head><body>
96
+ <article><h2>備援標題一</h2><time>2026-08-07</time><a href="/news/f1">連結</a></article>
97
+ <article><h2>備援標題二</h2><time>2026-08-06</time><a href="/news/f2">連結</a></article>
98
+ </body></html>`
99
+
100
+
101
+ //無任何項目, 供斷言拋出0筆錯誤
102
+ let statementdogEmptyHtml = '<!DOCTYPE html><html><head><title>財報狗</title></head><body><p>維護中</p></body></html>'
103
+
104
+
105
+ //===== MoneyDJ =====
106
+
107
+
108
+ //第3列時間格式不符供斷言略過
109
+ let moneydjPage1Html = `<!DOCTYPE html><html><head><title>MoneyDJ</title></head><body><table>
110
+ <tr><td>08/07 09:02</td><td><a href="/kmdj/news/newsviewer.aspx?a=m1" title="MoneyDJ標題一">連結</a></td></tr>
111
+ <tr><td>09:05</td><td><a href="https://www.moneydj.com/m2" title="MoneyDJ標題二">連結</a></td></tr>
112
+ <tr><td>不是時間</td><td><a href="/kmdj/news/newsviewer.aspx?a=m3" title="不應出現">連結</a></td></tr>
113
+ </table></body></html>`
114
+
115
+
116
+ let moneydjPage2Html = `<!DOCTYPE html><html><head><title>MoneyDJ</title></head><body><table>
117
+ <tr><td>昨 23:59</td><td><a href="/kmdj/news/newsviewer.aspx?a=m4" title="MoneyDJ標題三">連結</a></td></tr>
118
+ </table></body></html>`
119
+
120
+
121
+ let moneydjEmptyHtml = '<!DOCTYPE html><html><head><title>MoneyDJ</title></head><body><table></table></body></html>'
122
+
123
+
124
+ //===== 證交所個股日成交資訊 =====
125
+
126
+
127
+ //回傳整月資料, 供斷言指定日過濾為單筆
128
+ let twseStockDay = {
129
+ stat: 'OK',
130
+ date: '20260801',
131
+ title: '115年08月 2330 台積電 各日成交資訊',
132
+ fields: ['日期', '成交股數', '成交金額', '開盤價', '最高價', '最低價', '收盤價', '漲跌價差', '成交筆數'],
133
+ data: [
134
+ ['115/08/06', '30,000,000', '33,000,000,000', '1,090.00', '1,105.00', '1,085.00', '1,100.00', '+10.00', '50,000'],
135
+ ['115/08/07', '28,000,000', '31,000,000,000', '1,100.00', '1,120.00', '1,095.00', '1,115.00', '+15.00', '48,000'],
136
+ ],
137
+ }
138
+
139
+
140
+ let twseMiIndex = {
141
+ stat: 'OK',
142
+ date: '20260807',
143
+ tables: [
144
+ {
145
+ title: '每日收盤行情(全部(不含權證、牛熊證、可展延牛熊證))',
146
+ fields: ['證券代號', '證券名稱', '成交股數', '收盤價'],
147
+ data: [
148
+ ['2330', '台積電', '28,000,000', '1,115.00'],
149
+ ['2317', '鴻海', '15,000,000', '215.00'],
150
+ ],
151
+ },
152
+ ],
153
+ }
154
+
155
+
156
+ //===== 證交所融資融券 =====
157
+
158
+
159
+ //tables[0]為結構不同之干擾表, 供斷言以title「融資融券彙總」精確選表
160
+ let twseMargin = {
161
+ stat: 'OK',
162
+ date: '20260807',
163
+ tables: [
164
+ {
165
+ title: '信用交易統計',
166
+ fields: ['項目', '買進'],
167
+ data: [['融資(交易單位)', '100']],
168
+ },
169
+ {
170
+ title: '融資融券彙總',
171
+ fields: [
172
+ '代號', '名稱', '買進', '賣出', '現金償還', '前日餘額', '今日餘額', '次一營業日限額',
173
+ '買進', '賣出', '現券償還', '前日餘額', '今日餘額', '次一營業日限額', '資券互抵', '註記',
174
+ ],
175
+ data: [
176
+ ['2330', '台積電', '1,000', '500', '100', '10,000', '10,400', '99,999', '200', '300', '50', '2,000', '2,250', '88,888', '10', ''],
177
+ ['2317', '鴻海', '800', '900', '0', '20,000', '19,900', '77,777', '100', '80', '0', '1,000', '980', '66,666', '5', 'X'],
178
+ ],
179
+ },
180
+ ],
181
+ }
182
+
183
+
184
+ //===== 證交所三大法人買賣超 =====
185
+
186
+
187
+ //欄位值含前後空白供斷言trim
188
+ let twseT86 = {
189
+ stat: 'OK',
190
+ date: '20260807',
191
+ fields: ['證券代號', '證券名稱', '外陸資買進股數(不含外資自營商)', '三大法人買賣超股數'],
192
+ data: [
193
+ ['2330 ', '台積電 ', ' 1,000,000', '500,000'],
194
+ ['2317', '鴻海', '800,000', '-200,000'],
195
+ ],
196
+ }
197
+
198
+
199
+ //===== 櫃買中心上櫃行情 =====
200
+
201
+
202
+ //tables[0]為干擾表, 供斷言以title「行情」優先選表
203
+ let tpexStock = {
204
+ stat: 'ok',
205
+ date: '115/08/07',
206
+ tables: [
207
+ {
208
+ title: '上櫃指數統計',
209
+ fields: ['指數', '收盤'],
210
+ data: [['櫃買指數', '230.00']],
211
+ },
212
+ {
213
+ title: '上櫃股票行情(不含權證、牛熊證)',
214
+ fields: ['代號', '名稱', '收盤', '漲跌', '開盤', '最高', '最低', '成交股數', '成交金額(元)'],
215
+ data: [
216
+ ['6488', '環球晶', '520.00', '+5.00', '515.00', '525.00', '512.00', '3,000,000', '1,560,000,000'],
217
+ ['6510', '精測', '480.00', '-2.00', '482.00', '486.00', '478.00', '1,000,000', '480,000,000'],
218
+ ],
219
+ },
220
+ ],
221
+ }
222
+
223
+
224
+ //===== 櫃買中心融資融券 =====
225
+
226
+
227
+ let tpexMargin = {
228
+ stat: 'ok',
229
+ date: '115/08/07',
230
+ tables: [
231
+ {
232
+ title: '上櫃融資融券餘額',
233
+ fields: [
234
+ '代號', '名稱', '前資餘額(張)', '資買', '資賣', '現償', '資餘額', '資屬證金', '資使用率(%)',
235
+ '資限額', '前券餘額(張)', '券賣', '券買', '券償', '券餘額', '券屬證金', '券使用率(%)', '券限額', '資券相抵(張)', '備註',
236
+ ],
237
+ data: [
238
+ ['6488', '環球晶', '5,000', '300', '200', '50', '5,050', '0', '1.20', '400,000', '1,000', '100', '80', '20', '1,000', '0', '0.30', '400,000', '30', ''],
239
+ ['6510', '精測', '2,000', '100', '150', '0', '1,950', '0', '0.80', '200,000', '500', '50', '60', '10', '480', '0', '0.20', '200,000', '10', 'X'],
240
+ ],
241
+ },
242
+ ],
243
+ }
244
+
245
+
246
+ //欄位佈局錯位, 供斷言錨點欄位檢核會fail-loud
247
+ let tpexMarginBadShape = {
248
+ stat: 'ok',
249
+ tables: [
250
+ {
251
+ title: '上櫃融資融券餘額',
252
+ fields: ['代號', '名稱', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n'],
253
+ data: [['6488', '環球晶', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14']],
254
+ },
255
+ ],
256
+ }
257
+
258
+
259
+ //===== 櫃買中心三大法人買賣超 =====
260
+
261
+
262
+ //fields為裸欄名, 買進股數/賣出股數/買賣超股數循環7次, 共24欄
263
+ let tpex3instiFields = [
264
+ '代號', '名稱',
265
+ '買進股數', '賣出股數', '買賣超股數',
266
+ '買進股數', '賣出股數', '買賣超股數',
267
+ '買進股數', '賣出股數', '買賣超股數',
268
+ '買進股數', '賣出股數', '買賣超股數',
269
+ '買進股數', '賣出股數', '買賣超股數',
270
+ '買進股數', '賣出股數', '買賣超股數',
271
+ '買進股數', '賣出股數', '買賣超股數',
272
+ '三大法人買賣超股數合計',
273
+ ]
274
+
275
+
276
+ let tpex3insti = {
277
+ stat: 'ok',
278
+ tables: [
279
+ {
280
+ title: '三大法人買賣超彙總',
281
+ fields: tpex3instiFields,
282
+ data: [
283
+ ['6488', '環球晶 ', '100', '50', '50', '10', '5', '5', '110', '55', '55', '200', '100', '100', '30', '10', '20', '40', '20', '20', '70', '30', '40', '195'],
284
+ ['6510', '精測', '10', '20', '-10', '0', '0', '0', '10', '20', '-10', '5', '0', '5', '1', '1', '0', '2', '0', '2', '3', '1', '2', '-3'],
285
+ ],
286
+ },
287
+ {},
288
+ ],
289
+ }
290
+
291
+
292
+ //欄序已變, 供斷言結構防呆會fail-loud
293
+ let tpex3instiBadShape = {
294
+ stat: 'ok',
295
+ tables: [
296
+ {
297
+ title: '三大法人買賣超彙總',
298
+ fields: ['代號', '名稱', '買進股數', '買賣超股數', '賣出股數', '三大法人買賣超股數合計'],
299
+ data: [['6488', '環球晶', '1', '2', '3', '4']],
300
+ },
301
+ ],
302
+ }
303
+
304
+
305
+ //===== 期交所 =====
306
+
307
+
308
+ //含價差合約(到期月份含'/')與次月合約, 供斷言僅取近月且排除價差
309
+ let taifexFuturesCsv = `交易日期,契約,到期月份(週別),開盤價,最高價,最低價,收盤價,成交量,結算價,交易時段
310
+ 2026/08/07,TX,202608,23000,23200,22900,23150,"80,000",23160,一般
311
+ 2026/08/07,TX,202608,23150,23300,23100,23250,20000,23260,盤後
312
+ 2026/08/07,TX,202609,22900,23000,22800,22950,5000,22960,一般
313
+ 2026/08/07,TX,202608/202609,-,-,-,-,100,-,一般`
314
+
315
+
316
+ let taifexInstitutionalCsv = `日期,商品名稱,身份別,多方交易口數,空方交易口數,多空交易口數淨額,多方未平倉口數,多方未平倉契約金額(千元),空方未平倉口數,空方未平倉契約金額(千元),多空未平倉口數淨額,多空未平倉契約金額淨額(千元)
317
+ 2026/08/07,臺股期貨,自營商,5000,4800,200,10000,4600000,9000,4140000,1000,460000
318
+ 2026/08/07,臺股期貨,投信,300,100,200,2000,920000,500,230000,1500,690000
319
+ 2026/08/07,臺股期貨,外資,20000,18000,2000,50000,23000000,30000,13800000,20000,9200000`
320
+
321
+
322
+ let taifexPcRatioCsv = `日期,賣權成交量,買權成交量,買賣權成交量比率%,賣權未平倉量,買權未平倉量,買賣權未平倉量比率%
323
+ 2026/08/07,100000,90000,111.11,50000,60000,83.33`
324
+
325
+
326
+ //===== 公開資訊觀測站 =====
327
+
328
+
329
+ //MOPS重大訊息頁, 內容僅需可載入以供page.evaluate於同源context內發送查詢
330
+ let mopsPageHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>重大訊息</title></head>
331
+ <body><div id="app">公開資訊觀測站 重大訊息 t146sb10</div></body></html>`
332
+
333
+
334
+ //各市場別之查詢結果, 興櫃回code 406(查無相符資料)供斷言其亦屬正常空結果
335
+ let mopsResults = {
336
+ sii: { code: 200, message: '查詢成功', result: [{ companyId: '2330', companyName: '台積電', typeK: 'sii' }] },
337
+ otc: { code: 200, message: '查詢成功', result: [{ companyId: '6488', companyName: '環球晶', typeK: 'otc' }] },
338
+ rotc: { code: 406, message: '查無相符資料', result: [] },
339
+ pub: { code: 200, message: '查詢成功', result: [{ companyId: '9999', companyName: '某公開發行公司', typeK: 'pub' }] },
340
+ }
341
+
342
+
343
+ /**
344
+ * 取得指定市場別之MOPS查詢結果
345
+ *
346
+ * @param {String} marketKind 輸入市場別字串,可為'sii'、'otc'、'rotc'、'pub'
347
+ * @returns {Object} 回傳MOPS查詢結果物件
348
+ */
349
+ function getMopsResult(marketKind) {
350
+ return mopsResults[marketKind] || { code: 406, message: '查無相符資料', result: [] }
351
+ }
352
+
353
+
354
+ //===== 證交所休市日期 =====
355
+
356
+
357
+ //含重複條目、交易日標記與結算作業日, 供斷言去重與排除
358
+ let holidaySchedule = [
359
+ { Date: '1150216', Name: '農曆春節', Weekday: '一', Description: '2月16日<br/>放假' },
360
+ { Date: '1150101', Name: '中華民國開國紀念日', Weekday: '四', Description: '1月1日(星期四)為假日。' },
361
+ { Date: '1150101', Name: '中華民國開國紀念日', Weekday: '四', Description: '重複條目' },
362
+ { Date: '1150102', Name: '國曆新年開始交易日', Weekday: '五', Description: '開始交易' },
363
+ { Date: '1150218', Name: '市場無交易,僅辦理結算交割作業', Weekday: '三', Description: '' },
364
+ ]
365
+
366
+
367
+ //===== 愛思想 =====
368
+
369
+
370
+ let aisixiangThinktankHtml = `<!DOCTYPE html><html><head><title>思想庫_爱思想</title></head><body>
371
+ <div class="thinktank_list">
372
+ <a href="/thinktank/gezhaoguang.html">葛兆光</a>
373
+ <a href="/thinktank/qinhui.html">秦晖,清华大学</a>
374
+ <a href="/thinktank/zhangwei.html">张维</a>
375
+ </div>
376
+ </body></html>`
377
+
378
+
379
+ let aisixiangAuthorHtml = `<!DOCTYPE html><html><head><title>葛兆光_爱思想</title></head><body>
380
+ <h3>论文</h3>
381
+ <a href="/data/146669.html">禅宗与中国文化</a>
382
+ <a href="/data/146670.html">中国思想史</a>
383
+ <h3>时评</h3>
384
+ <a href="/data/146671.html">学术与社会</a>
385
+ </body></html>`
386
+
387
+
388
+ let aisixiangEmptyAuthorHtml = `<!DOCTYPE html><html><head><title>无文章_爱思想</title></head><body>
389
+ <h3>论文</h3>
390
+ </body></html>`
391
+
392
+
393
+ let aisixiangZhuantiListHtml = `<!DOCTYPE html><html><head><title>专题_爱思想</title></head><body>
394
+ <h3>学科关键词</h3>
395
+ <a href="/zhuanti/301.html">大数据</a>
396
+ <a href="/zhuanti/302.html">政治学</a>
397
+ <h3>事件关键词</h3>
398
+ <a href="/zhuanti/401.html">中美贸易战</a>
399
+ <h3>人物关键词</h3>
400
+ <a href="/zhuanti/501.html">康德</a>
401
+ </body></html>`
402
+
403
+
404
+ let aisixiangZhuantiPage1Html = `<!DOCTYPE html><html><head><title>大数据_爱思想</title></head><body>
405
+ <a href="/data/200001.html">作者一:大数据与治理</a>
406
+ <a href="/data/200002.html">作者二:数据要素市场</a>
407
+ <div class="list_page"><a href="/zhuanti/301.html?page=1">1</a><a href="/zhuanti/301.html?page=2">2</a></div>
408
+ </body></html>`
409
+
410
+
411
+ let aisixiangZhuantiPage2Html = `<!DOCTYPE html><html><head><title>大数据_爱思想</title></head><body>
412
+ <a href="/data/200003.html">作者三:算法治理</a>
413
+ <div class="list_page"><a href="/zhuanti/301.html?page=1">1</a><a href="/zhuanti/301.html?page=2">2</a></div>
414
+ </body></html>`
415
+
416
+
417
+ let aisixiangSearchHitHtml = `<!DOCTYPE html><html><head><title>搜索_爱思想</title></head><body>
418
+ <div class="search_list">
419
+ <a href="/data/300001.html" title="作者甲:老庄之道">老庄之道</a>
420
+ <a href="/data/300002.html" title="老庄新解">老庄新解</a>
421
+ </div>
422
+ <div class="list_page"><a href="/data/search?page=1">1</a></div>
423
+ </body></html>`
424
+
425
+
426
+ //search_list區塊為空, 供斷言0結果時不誤抓側邊欄連結
427
+ let aisixiangSearchEmptyHtml = `<!DOCTYPE html><html><head><title>搜索_爱思想</title></head><body>
428
+ <div class="search_list"></div>
429
+ <div class="side_bar"><a href="/data/999999.html" title="側邊欄不應被抓">側邊欄</a></div>
430
+ </body></html>`
431
+
432
+
433
+ let aisixiangArticleHtml = `<!DOCTYPE html><html><head><title>葛兆光:禅宗与中国文化_爱思想</title></head><body>
434
+ <div class="article-content">
435
+ <p>禅宗自唐代以来,深刻影响了中国文化。</p>
436
+ <p>本文讨论<b>禅宗</b>与<a href="/data/1.html">士大夫</a>之关系,兼及&amp;思想史脉络。</p>
437
+ <div><p>嵌套段落亦應被完整切出。</p></div>
438
+ </div>
439
+ </body></html>`
440
+
441
+
442
+ //無article-content區塊, 供斷言解析失敗會拋錯
443
+ let aisixiangBadArticleHtml = '<!DOCTYPE html><html><head><title>不存在_爱思想</title></head><body><p>404</p></body></html>'
444
+
445
+
446
+ //===== 觀察者網 =====
447
+
448
+
449
+ //首頁底部A-Z作者索引, <dt>[A-Z]</dt>需達門檻(5)以供isHomepageHtml偵測
450
+ let guanchaHomeHtml = `<!DOCTYPE html><html><head><title>观察者网</title></head><body>
451
+ <div class="index-wrap">观察者网首页内容区块,此处為版面主體,長度足供curl之最短長度檢核通過。</div>
452
+ <dl class="fix"><dt>A</dt><dd><a href="/AnSheng/list_1.shtml">安生</a></dd></dl>
453
+ <dl class="fix"><dt>B</dt><dd><a href="/BaiTong/list_1.shtml">白彤东</a></dd></dl>
454
+ <dl class="fix"><dt>C</dt><dd><a href="/ChenPing/list_1.shtml">陈平</a></dd></dl>
455
+ <dl class="fix"><dt>D</dt><dd><a href="/DaiXu/list_1.shtml">戴旭</a></dd></dl>
456
+ <dl class="fix"><dt>J</dt><dd><a href="/JinCanRong/list_1.shtml">金灿荣</a></dd></dl>
457
+ <dl class="fix"><dt>Z</dt><dd><a href="/ZhangWeiWei/list_1.shtml">张维为</a></dd></dl>
458
+ </body></html>`
459
+
460
+
461
+ let guanchaListPage1Html = `<!DOCTYPE html><html><head><title>安生-观察者网</title></head><body>
462
+ <ul class="list-wrap">
463
+ <li><a href="/AnSheng/2026_08_07_800001.shtml" title="觀察者網標題一">觀察者網標題一</a></li>
464
+ <li><a href="/AnSheng/2026_08_06_800002.shtml" title="觀察者網標題二">觀察者網標題二</a></li>
465
+ </ul>
466
+ <div class="page">此區為分頁列, 內容補長以通過curl之最短長度檢核。</div>
467
+ </body></html>`
468
+
469
+
470
+ let guanchaListPage2Html = `<!DOCTYPE html><html><head><title>安生-观察者网</title></head><body>
471
+ <ul class="list-wrap">
472
+ <li><a href="/AnSheng/2026_08_05_800003.shtml" title="觀察者網標題三">觀察者網標題三</a></li>
473
+ </ul>
474
+ <div class="page">此區為分頁列, 內容補長以通過curl之最短長度檢核。</div>
475
+ </body></html>`
476
+
477
+
478
+ //無任何文章連結, 供斷言翻頁至此即停止
479
+ let guanchaListEmptyHtml = `<!DOCTYPE html><html><head><title>安生-观察者网</title></head><body>
480
+ <ul class="list-wrap"></ul>
481
+ <div class="page">此頁已無文章, 內容補長以通過curl之最短長度檢核, 供斷言翻頁在此停止。</div>
482
+ </body></html>`
483
+
484
+
485
+ let guanchaArticleHtml = `<!DOCTYPE html><html><head><title>OPEC+宣布增产_观察者网</title></head><body>
486
+ <div class="time"><span>2026-08-07 09:02:03</span></div>
487
+ <div class="content all-txt" id="cont">
488
+ <p>【文/观察者网 王五】OPEC+今日宣布增产。</p>
489
+ <p>此為第二段,含<img src="https://i.guancha.cn/p1.jpg" />圖片與<b>粗體</b>。</p>
490
+ </div>
491
+ </body></html>`
492
+
493
+
494
+ export {
495
+ aiNews,
496
+ rssXml,
497
+ hnNewStories,
498
+ hnItems,
499
+ cnyesPage1,
500
+ cnyesPageEmpty,
501
+ statementdogHtml,
502
+ statementdogFallbackHtml,
503
+ statementdogEmptyHtml,
504
+ moneydjPage1Html,
505
+ moneydjPage2Html,
506
+ moneydjEmptyHtml,
507
+ twseStockDay,
508
+ twseMiIndex,
509
+ twseMargin,
510
+ twseT86,
511
+ tpexStock,
512
+ tpexMargin,
513
+ tpexMarginBadShape,
514
+ tpex3instiFields,
515
+ tpex3insti,
516
+ tpex3instiBadShape,
517
+ taifexFuturesCsv,
518
+ taifexInstitutionalCsv,
519
+ taifexPcRatioCsv,
520
+ mopsPageHtml,
521
+ mopsResults,
522
+ getMopsResult,
523
+ holidaySchedule,
524
+ aisixiangThinktankHtml,
525
+ aisixiangAuthorHtml,
526
+ aisixiangEmptyAuthorHtml,
527
+ aisixiangZhuantiListHtml,
528
+ aisixiangZhuantiPage1Html,
529
+ aisixiangZhuantiPage2Html,
530
+ aisixiangSearchHitHtml,
531
+ aisixiangSearchEmptyHtml,
532
+ aisixiangArticleHtml,
533
+ aisixiangBadArticleHtml,
534
+ guanchaHomeHtml,
535
+ guanchaListPage1Html,
536
+ guanchaListPage2Html,
537
+ guanchaListEmptyHtml,
538
+ guanchaArticleHtml
539
+ }
@@ -0,0 +1,39 @@
1
+ import { chromium } from 'playwright'
2
+
3
+
4
+ /**
5
+ * 檢核執行環境是否可用Playwright啟動Chrome(channel為'chrome')
6
+ *
7
+ * 用於測試中判斷是否跳過須實際啟動瀏覽器之案例,避免無Chrome環境誤報失敗
8
+ *
9
+ * @returns {Promise} 回傳Promise,resolve回傳是否可用之布林值
10
+ * @example
11
+ *
12
+ * import hasChrome from './tools/hasChrome.mjs'
13
+ *
14
+ * console.log(await hasChrome())
15
+ * // => true
16
+ *
17
+ */
18
+ async function hasChrome() {
19
+ let browser = null
20
+ try {
21
+ browser = await chromium.launch({
22
+ headless: true,
23
+ executablePath: process.env.CHROME_PATH || undefined,
24
+ channel: 'chrome',
25
+ })
26
+ return true
27
+ }
28
+ catch {
29
+ return false
30
+ }
31
+ finally {
32
+ if (browser) {
33
+ await browser.close().catch(() => {})
34
+ }
35
+ }
36
+ }
37
+
38
+
39
+ export default hasChrome