@kyun.si/codex-hongguo 0.0.0-stage → 0.0.21
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/.codex-plugin/plugin.json +19 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.en.md +131 -0
- package/README.md +129 -2
- package/bin/codex-hongguo.mjs +3 -0
- package/bin/run-node +29 -0
- package/bin/run-node.exe +0 -0
- package/dist/cli/main.mjs +8 -0
- package/dist/hooks/write.mjs +46 -0
- package/dist/server/index.mjs +147 -0
- package/dist/setup/check.mjs +47 -0
- package/dist/ui/player.html +2067 -0
- package/dist/vendor/openhongguo/GO_LICENSE +27 -0
- package/dist/vendor/openhongguo/LICENSE +202 -0
- package/dist/vendor/openhongguo/README.md +283 -0
- package/dist/vendor/openhongguo/dist/openhongguo.wasm +0 -0
- package/dist/vendor/openhongguo/dist/wasm_exec.js +575 -0
- package/dist/vendor/openhongguo/index.d.ts +212 -0
- package/dist/vendor/openhongguo/index.js +297 -0
- package/dist/vendor/openhongguo/package.json +46 -0
- package/dist/vendor/openhongguo/types.d.ts +887 -0
- package/docs/images/codex-hongguo-catalog.png +0 -0
- package/docs/images/codex-hongguo-player.png +0 -0
- package/hooks/hooks.json +40 -0
- package/licenses/@babel-runtime-LICENSE +22 -0
- package/licenses/@hono-node-server-LICENSE +21 -0
- package/licenses/@modelcontextprotocol-ext-apps-LICENSE +216 -0
- package/licenses/@modelcontextprotocol-sdk-LICENSE +21 -0
- package/licenses/@standard-schema-spec-LICENSE +21 -0
- package/licenses/@videojs-vhs-utils-LICENSE +19 -0
- package/licenses/@xmldom-xmldom-LICENSE +8 -0
- package/licenses/PROJECT-NOTICE.txt +1 -0
- package/licenses/accepts-LICENSE +23 -0
- package/licenses/ajv-LICENSE +22 -0
- package/licenses/ajv-formats-LICENSE +21 -0
- package/licenses/body-parser-LICENSE +23 -0
- package/licenses/body-parser-content-type-LICENSE +22 -0
- package/licenses/bytes-LICENSE +23 -0
- package/licenses/call-bind-apply-helpers-LICENSE +21 -0
- package/licenses/call-bound-LICENSE +21 -0
- package/licenses/content-disposition-LICENSE +22 -0
- package/licenses/content-type-LICENSE +22 -0
- package/licenses/cookie-LICENSE +24 -0
- package/licenses/cookie-signature-LICENSE +22 -0
- package/licenses/cors-LICENSE +22 -0
- package/licenses/cross-spawn-LICENSE +21 -0
- package/licenses/debug-LICENSE +20 -0
- package/licenses/depd-LICENSE +22 -0
- package/licenses/dom-walk-LICENCE +19 -0
- package/licenses/dunder-proto-LICENSE +21 -0
- package/licenses/ee-first-LICENSE +22 -0
- package/licenses/encodeurl-LICENSE +22 -0
- package/licenses/es-define-property-LICENSE +21 -0
- package/licenses/es-errors-LICENSE +21 -0
- package/licenses/es-object-atoms-LICENSE +21 -0
- package/licenses/escape-html-LICENSE +24 -0
- package/licenses/etag-LICENSE +22 -0
- package/licenses/eventsource-LICENSE +22 -0
- package/licenses/eventsource-parser-LICENSE +21 -0
- package/licenses/express-LICENSE +24 -0
- package/licenses/express-content-type-LICENSE +22 -0
- package/licenses/express-rate-limit-license +21 -0
- package/licenses/fast-deep-equal-LICENSE +21 -0
- package/licenses/fast-uri-LICENSE +30 -0
- package/licenses/finalhandler-LICENSE +22 -0
- package/licenses/forwarded-LICENSE +22 -0
- package/licenses/fresh-LICENSE +23 -0
- package/licenses/function-bind-LICENSE +20 -0
- package/licenses/get-intrinsic-LICENSE +21 -0
- package/licenses/get-proto-LICENSE +21 -0
- package/licenses/global-LICENSE +19 -0
- package/licenses/gopd-LICENSE +21 -0
- package/licenses/has-symbols-LICENSE +21 -0
- package/licenses/hasown-LICENSE +21 -0
- package/licenses/hls.js-LICENSE +28 -0
- package/licenses/hono-LICENSE +21 -0
- package/licenses/http-errors-LICENSE +23 -0
- package/licenses/iconv-lite-LICENSE +21 -0
- package/licenses/inherits-LICENSE +16 -0
- package/licenses/ip-address-LICENSE +19 -0
- package/licenses/ipaddr.js-LICENSE +19 -0
- package/licenses/is-promise-LICENSE +19 -0
- package/licenses/isexe-LICENSE +15 -0
- package/licenses/jose-LICENSE.md +21 -0
- package/licenses/json-schema-traverse-LICENSE +21 -0
- package/licenses/json-schema-typed-LICENSE.md +57 -0
- package/licenses/m3u8-parser-LICENSE +13 -0
- package/licenses/math-intrinsics-LICENSE +21 -0
- package/licenses/media-typer-LICENSE +22 -0
- package/licenses/merge-descriptors-license +11 -0
- package/licenses/mime-db-LICENSE +23 -0
- package/licenses/mime-types-LICENSE +23 -0
- package/licenses/min-document-LICENCE +19 -0
- package/licenses/mpd-parser-LICENSE +13 -0
- package/licenses/ms-license.md +21 -0
- package/licenses/negotiator-LICENSE +24 -0
- package/licenses/negotiator-content-type-LICENSE +22 -0
- package/licenses/object-assign-license +21 -0
- package/licenses/object-inspect-LICENSE +21 -0
- package/licenses/on-finished-LICENSE +23 -0
- package/licenses/once-LICENSE +15 -0
- package/licenses/openhongguo-LICENSE +202 -0
- package/licenses/parseurl-LICENSE +24 -0
- package/licenses/path-key-license +9 -0
- package/licenses/path-to-regexp-LICENSE +21 -0
- package/licenses/pkce-challenge-LICENSE +21 -0
- package/licenses/process-LICENSE +22 -0
- package/licenses/proxy-addr-LICENSE +22 -0
- package/licenses/qs-LICENSE.md +29 -0
- package/licenses/range-parser-LICENSE +23 -0
- package/licenses/raw-body-LICENSE +22 -0
- package/licenses/require-from-string-license +21 -0
- package/licenses/router-LICENSE +23 -0
- package/licenses/safer-buffer-LICENSE +21 -0
- package/licenses/send-LICENSE +23 -0
- package/licenses/serve-static-LICENSE +25 -0
- package/licenses/setprototypeof-LICENSE +13 -0
- package/licenses/shebang-command-license +9 -0
- package/licenses/shebang-regex-license +9 -0
- package/licenses/side-channel-LICENSE +21 -0
- package/licenses/side-channel-list-LICENSE +21 -0
- package/licenses/side-channel-map-LICENSE +21 -0
- package/licenses/side-channel-weakmap-LICENSE +21 -0
- package/licenses/statuses-LICENSE +23 -0
- package/licenses/toidentifier-LICENSE +21 -0
- package/licenses/type-is-LICENSE +23 -0
- package/licenses/type-is-content-type-LICENSE +22 -0
- package/licenses/unpipe-LICENSE +22 -0
- package/licenses/vary-LICENSE +22 -0
- package/licenses/which-LICENSE +15 -0
- package/licenses/wrappy-LICENSE +15 -0
- package/licenses/zod-LICENSE +21 -0
- package/licenses/zod-to-json-schema-LICENSE +15 -0
- package/package.json +37 -4
- package/skills/hongguo-setup/SKILL.md +16 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Album,
|
|
3
|
+
CategoryPage,
|
|
4
|
+
ContentKey,
|
|
5
|
+
Device,
|
|
6
|
+
DiscoverOptions,
|
|
7
|
+
Episode,
|
|
8
|
+
EpisodeOptions,
|
|
9
|
+
FilterRow,
|
|
10
|
+
HotSearch,
|
|
11
|
+
LandingOptions,
|
|
12
|
+
NewReleasePage,
|
|
13
|
+
NewReleaseQuery,
|
|
14
|
+
Page,
|
|
15
|
+
Person,
|
|
16
|
+
Playback,
|
|
17
|
+
RankList,
|
|
18
|
+
Ranking,
|
|
19
|
+
Recommend,
|
|
20
|
+
SearchOptions,
|
|
21
|
+
Series,
|
|
22
|
+
SeriesDetail,
|
|
23
|
+
SeriesSummary,
|
|
24
|
+
ShortFilterPage,
|
|
25
|
+
ShortFilterQuery,
|
|
26
|
+
ShortRanking,
|
|
27
|
+
ShortRankQuery,
|
|
28
|
+
Suggestion,
|
|
29
|
+
WorkPage,
|
|
30
|
+
} from './types.js';
|
|
31
|
+
|
|
32
|
+
export * from './types.js';
|
|
33
|
+
|
|
34
|
+
/** SearchFilter 是综合 tab 的搜索筛选项(SearchOptions.filters 的取值)。 */
|
|
35
|
+
export declare const SearchFilter: {
|
|
36
|
+
/** 真人剧 */
|
|
37
|
+
readonly ShortPlay: 'content_type_short_play';
|
|
38
|
+
/** 漫剧 */
|
|
39
|
+
readonly Comic: 'content_type_dynamic_comic';
|
|
40
|
+
/** AI剧 */
|
|
41
|
+
readonly AIComic: 'content_type_ai_dynamic_comic';
|
|
42
|
+
/** 影视剧 */
|
|
43
|
+
readonly Film: 'content_type_long_video';
|
|
44
|
+
/** 未看过 */
|
|
45
|
+
readonly Unwatched: 'search_range_unread';
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** 错误码,对应 Go SDK 的哨兵错误。 */
|
|
49
|
+
export type ErrorCode =
|
|
50
|
+
| 'INVALID_OPTIONS'
|
|
51
|
+
| 'PROFILE_NOT_FOUND'
|
|
52
|
+
| 'SIGNER_UNAVAILABLE'
|
|
53
|
+
| 'AUTHENTICATION_REQUIRED'
|
|
54
|
+
| 'NOT_PROTECTED'
|
|
55
|
+
| 'DRM_PROTECTED'
|
|
56
|
+
| 'ANONYMOUS_LIMITED'
|
|
57
|
+
| 'RATE_LIMITED'
|
|
58
|
+
| 'TRANSPORT'
|
|
59
|
+
| 'SIGNATURE_REJECTED'
|
|
60
|
+
| 'CONTENT_UNAVAILABLE'
|
|
61
|
+
| 'PARAM_INVALID'
|
|
62
|
+
| 'CHALLENGE_REQUIRED'
|
|
63
|
+
| 'DEVICE_REJECTED'
|
|
64
|
+
| 'END_OF_LIST'
|
|
65
|
+
| 'ALBUM_UNAVAILABLE'
|
|
66
|
+
| 'UNKNOWN';
|
|
67
|
+
|
|
68
|
+
/** OpenhongguoError 是 SDK 返回的错误。 */
|
|
69
|
+
export declare class OpenhongguoError extends Error {
|
|
70
|
+
readonly name: 'OpenhongguoError';
|
|
71
|
+
/** 错误码,可以据此判断原因,而不必解析 message。 */
|
|
72
|
+
readonly code: ErrorCode;
|
|
73
|
+
/** 原样重试是否可能成功(限流、网络错误、服务端 5xx)。 */
|
|
74
|
+
readonly retryable: boolean;
|
|
75
|
+
/** 服务端建议的等待时间,单位毫秒;0 表示没有建议。 */
|
|
76
|
+
readonly retryAfterMs: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** TransportRequest 是 SDK 要发送的一个请求。 */
|
|
80
|
+
export interface TransportRequest {
|
|
81
|
+
method: string;
|
|
82
|
+
url: string;
|
|
83
|
+
headers: Record<string, string>;
|
|
84
|
+
body: Uint8Array;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** TransportResponse 是收到的响应。body 是原始字节,不要自动解压。 */
|
|
88
|
+
export interface TransportResponse {
|
|
89
|
+
status: number;
|
|
90
|
+
headers: Record<string, string>;
|
|
91
|
+
/** Set-Cookie 解析出的名字和值。 */
|
|
92
|
+
cookies?: Record<string, string>;
|
|
93
|
+
body: Uint8Array;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Transport 发送一个请求;只有没收到响应时才 reject。 */
|
|
97
|
+
export type Transport = (request: TransportRequest) => Promise<TransportResponse>;
|
|
98
|
+
|
|
99
|
+
/** ProfileStore 读写匿名设备档案(JSON 字符串)。load 返回 null 表示还没有档案。 */
|
|
100
|
+
export interface ProfileStore {
|
|
101
|
+
load(): Promise<string | null>;
|
|
102
|
+
save(json: string): Promise<void>;
|
|
103
|
+
reset(): Promise<void>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** ClientOptions 是 createClient 的设置,全部可选。 */
|
|
107
|
+
export interface ClientOptions {
|
|
108
|
+
/**
|
|
109
|
+
* 设备档案的存放位置:文件路径,或自定义的 ProfileStore。
|
|
110
|
+
* 默认与 CLI 共用 <用户配置目录>/openhongguo/anonymous.json。
|
|
111
|
+
*/
|
|
112
|
+
profile?: string | ProfileStore;
|
|
113
|
+
/** 自定义请求发送方式;默认用 node:https。 */
|
|
114
|
+
transport?: Transport;
|
|
115
|
+
/** 默认 transport 的超时,单位毫秒,默认 30000。 */
|
|
116
|
+
timeoutMs?: number;
|
|
117
|
+
/** 接收诊断日志;日志里的 URL 已经脱敏。 */
|
|
118
|
+
log?: (line: string) => void;
|
|
119
|
+
/** 读取类接口的地址,如 "https://host";默认与 App 一致。 */
|
|
120
|
+
apiOrigin?: string;
|
|
121
|
+
/** 设备注册接口的地址;默认与 App 一致。 */
|
|
122
|
+
registerOrigin?: string;
|
|
123
|
+
/** 短剧筛选和活动榜接口的地址;默认与 App 一致。 */
|
|
124
|
+
distributionOrigin?: string;
|
|
125
|
+
/** 可重试的失败最多尝试几次,默认 3;0 或 1 表示不重试。 */
|
|
126
|
+
retryAttempts?: number;
|
|
127
|
+
/** 设备被拒或播放返回空结果时,一次调用最多换几台新的匿名设备,默认 2;0 表示关闭。 */
|
|
128
|
+
deviceResets?: number;
|
|
129
|
+
/** 新注册设备自称的机型;未填的字段取默认值(小米 14)。 */
|
|
130
|
+
device?: Partial<Device>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** PlaybackResult 是 resolvePlayback 的结果。 */
|
|
134
|
+
export interface PlaybackResult {
|
|
135
|
+
playback: Playback;
|
|
136
|
+
/** 加密剧集的内容密钥;未加密时没有这个字段。 */
|
|
137
|
+
contentKey?: ContentKey;
|
|
138
|
+
/** 加密剧集的密钥推导失败时,说明原因;此时没有 contentKey。 */
|
|
139
|
+
contentKeyError?: { code: ErrorCode; message: string };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Client 包装 Go SDK 的方法;每个方法返回 Promise,失败时抛出 OpenhongguoError。 */
|
|
143
|
+
export declare class Client {
|
|
144
|
+
private constructor();
|
|
145
|
+
/** 加载一个书城频道(默认“推荐”)。 */
|
|
146
|
+
discover(options?: DiscoverOptions): Promise<Page<SeriesSummary>>;
|
|
147
|
+
/** 搜索短剧。 */
|
|
148
|
+
search(query: string, options?: SearchOptions): Promise<Page<SeriesSummary>>;
|
|
149
|
+
/** 搜索联想词。 */
|
|
150
|
+
suggest(prefix: string): Promise<Suggestion[]>;
|
|
151
|
+
/** 搜索页的“猜你想搜”和热搜榜。 */
|
|
152
|
+
hotSearch(): Promise<HotSearch>;
|
|
153
|
+
/** 搜索框里滚动的提示词。 */
|
|
154
|
+
searchCues(): Promise<string[]>;
|
|
155
|
+
/** 上新日历的一天。 */
|
|
156
|
+
newReleases(query?: NewReleaseQuery): Promise<NewReleasePage>;
|
|
157
|
+
/** 一部剧的详情。 */
|
|
158
|
+
getSeries(seriesId: string): Promise<Series>;
|
|
159
|
+
/** 一次读取多部剧及其各集,按请求顺序返回。 */
|
|
160
|
+
getSeriesBatch(seriesIds: string[]): Promise<SeriesDetail[]>;
|
|
161
|
+
/** 一部剧的分集列表。 */
|
|
162
|
+
listEpisodes(seriesId: string, options?: EpisodeOptions): Promise<Page<Episode>>;
|
|
163
|
+
/** 多季剧的全部季。 */
|
|
164
|
+
seasons(seriesId: string): Promise<Album>;
|
|
165
|
+
/** 一部剧的相关推荐。 */
|
|
166
|
+
seriesRecommend(seriesId: string): Promise<Recommend>;
|
|
167
|
+
/** 一个合集及其各季。 */
|
|
168
|
+
getAlbum(albumId: string): Promise<Album>;
|
|
169
|
+
/** 短剧排行榜的一页。 */
|
|
170
|
+
shortRank(query?: ShortRankQuery): Promise<ShortRanking>;
|
|
171
|
+
/** 短剧筛选面板;genre 如 "comic_series"。 */
|
|
172
|
+
shortFilterPanel(genre?: string): Promise<FilterRow[]>;
|
|
173
|
+
/** 按筛选条件列出短剧。 */
|
|
174
|
+
shortFilter(query?: ShortFilterQuery): Promise<ShortFilterPage>;
|
|
175
|
+
/** 活动榜单。 */
|
|
176
|
+
ranking(activityId: string, rankListId: string): Promise<Ranking>;
|
|
177
|
+
/** 书城榜单的一页。 */
|
|
178
|
+
rankRows(cellId: string, offset?: number): Promise<Ranking>;
|
|
179
|
+
/** 书城榜单的类型。 */
|
|
180
|
+
rankGroups(): Promise<RankList[]>;
|
|
181
|
+
/** 演员或角色主页。 */
|
|
182
|
+
person(userId: string): Promise<Person>;
|
|
183
|
+
/** 演员的作品,一页。 */
|
|
184
|
+
personWorks(userId: string, cursor?: string): Promise<WorkPage>;
|
|
185
|
+
/** 书籍分类的标签页。 */
|
|
186
|
+
categories(categoryId: string): Promise<CategoryPage>;
|
|
187
|
+
/** 书籍分类下的列表。 */
|
|
188
|
+
categoryLanding(categoryId: string, options?: LandingOptions): Promise<CategoryPage>;
|
|
189
|
+
/** 一集的播放模型;加密剧集同时给出内容密钥。 */
|
|
190
|
+
resolvePlayback(episodeId: string): Promise<PlaybackResult>;
|
|
191
|
+
/** 丢弃当前匿名设备,下次请求时重新注册。 */
|
|
192
|
+
resetDevice(): Promise<void>;
|
|
193
|
+
/** 沿着游标逐部返回书城频道的剧,去掉跨页重复。 */
|
|
194
|
+
discoverAll(options?: DiscoverOptions, walk?: { maxPages?: number }): AsyncGenerator<SeriesSummary>;
|
|
195
|
+
/** 沿着游标逐部返回搜索结果,去掉跨页重复。 */
|
|
196
|
+
searchAll(query: string, options?: SearchOptions, walk?: { maxPages?: number }): AsyncGenerator<SeriesSummary>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** createClient 创建客户端。首次请求时注册一台匿名设备。 */
|
|
200
|
+
export declare function createClient(options?: ClientOptions): Promise<Client>;
|
|
201
|
+
|
|
202
|
+
/** defaultProfilePath 是默认的设备档案路径,与 CLI 共用。 */
|
|
203
|
+
export declare function defaultProfilePath(): string;
|
|
204
|
+
|
|
205
|
+
/** fileProfileStore 把设备档案存成一个 JSON 文件(权限 0600)。 */
|
|
206
|
+
export declare function fileProfileStore(path?: string): ProfileStore;
|
|
207
|
+
|
|
208
|
+
/** memoryProfileStore 只在内存里保存设备档案。 */
|
|
209
|
+
export declare function memoryProfileStore(): ProfileStore;
|
|
210
|
+
|
|
211
|
+
/** nodeTransport 是默认的请求发送方式(node:http/https)。 */
|
|
212
|
+
export declare function nodeTransport(options?: { timeoutMs?: number }): Transport;
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// openhongguo:红果短剧(Hongguo)的非官方 Node.js SDK。
|
|
2
|
+
//
|
|
3
|
+
// 底层是 Go SDK 编译成的 WebAssembly(dist/openhongguo.wasm)。本文件负责:
|
|
4
|
+
// 加载 wasm、用 node:https 发请求、读写匿名设备档案,并把 SDK 的方法
|
|
5
|
+
// 包装成 Promise 风格的 API。
|
|
6
|
+
|
|
7
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
8
|
+
import http from 'node:http';
|
|
9
|
+
import https from 'node:https';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { webcrypto } from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
// 响应体的上限,与 Go 的 HTTP 适配器一致。
|
|
19
|
+
const MAX_BODY = 8 << 20;
|
|
20
|
+
|
|
21
|
+
/** SearchFilter 是综合 tab 的搜索筛选项(SearchOptions.filters 的取值)。 */
|
|
22
|
+
export const SearchFilter = Object.freeze({
|
|
23
|
+
ShortPlay: 'content_type_short_play', // 真人剧
|
|
24
|
+
Comic: 'content_type_dynamic_comic', // 漫剧
|
|
25
|
+
AIComic: 'content_type_ai_dynamic_comic', // AI剧
|
|
26
|
+
Film: 'content_type_long_video', // 影视剧
|
|
27
|
+
Unwatched: 'search_range_unread', // 未看过
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** OpenhongguoError 是 SDK 返回的错误,code 对应 Go SDK 的哨兵错误。 */
|
|
31
|
+
export class OpenhongguoError extends Error {
|
|
32
|
+
constructor({ code = 'UNKNOWN', message = '', retryable = false, retryAfterMs = 0 } = {}) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = 'OpenhongguoError';
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.retryable = retryable;
|
|
37
|
+
this.retryAfterMs = retryAfterMs;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let runtime;
|
|
42
|
+
|
|
43
|
+
// loadRuntime 在进程内只加载一次 wasm。
|
|
44
|
+
function loadRuntime() {
|
|
45
|
+
runtime ??= (async () => {
|
|
46
|
+
globalThis.crypto ??= webcrypto;
|
|
47
|
+
const require = createRequire(import.meta.url);
|
|
48
|
+
require(join(here, 'dist', 'wasm_exec.js')); // 定义 globalThis.Go
|
|
49
|
+
const go = new globalThis.Go();
|
|
50
|
+
const bytes = await readFile(join(here, 'dist', 'openhongguo.wasm'));
|
|
51
|
+
const { instance } = await WebAssembly.instantiate(bytes, go.importObject);
|
|
52
|
+
go.run(instance); // Go 的 main 一直挂起,不会返回
|
|
53
|
+
if (!globalThis.__openhongguo) {
|
|
54
|
+
throw new OpenhongguoError({ code: 'UNKNOWN', message: 'openhongguo: wasm did not start' });
|
|
55
|
+
}
|
|
56
|
+
return globalThis.__openhongguo;
|
|
57
|
+
})();
|
|
58
|
+
return runtime;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** defaultProfilePath 与 CLI 共用设备档案:<用户配置目录>/openhongguo/anonymous.json。 */
|
|
62
|
+
export function defaultProfilePath() {
|
|
63
|
+
if (process.env.OHG_CONFIG_DIR) return join(process.env.OHG_CONFIG_DIR, 'anonymous.json');
|
|
64
|
+
let base;
|
|
65
|
+
if (process.platform === 'darwin') base = join(homedir(), 'Library', 'Application Support');
|
|
66
|
+
else if (process.platform === 'win32') base = process.env.AppData ?? join(homedir(), 'AppData', 'Roaming');
|
|
67
|
+
else base = process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config');
|
|
68
|
+
return join(base, 'openhongguo', 'anonymous.json');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** fileProfileStore 把设备档案存成一个 JSON 文件(权限 0600)。 */
|
|
72
|
+
export function fileProfileStore(path = defaultProfilePath()) {
|
|
73
|
+
return {
|
|
74
|
+
async load() {
|
|
75
|
+
try {
|
|
76
|
+
return await readFile(path, 'utf8');
|
|
77
|
+
} catch (e) {
|
|
78
|
+
if (e.code === 'ENOENT') return null;
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
async save(json) {
|
|
83
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
84
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
85
|
+
await writeFile(tmp, json, { mode: 0o600 });
|
|
86
|
+
await rename(tmp, path);
|
|
87
|
+
},
|
|
88
|
+
async reset() {
|
|
89
|
+
await rm(path, { force: true });
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** memoryProfileStore 只在内存里保存设备档案,进程退出后丢失。 */
|
|
95
|
+
export function memoryProfileStore() {
|
|
96
|
+
let saved = null;
|
|
97
|
+
return {
|
|
98
|
+
async load() {
|
|
99
|
+
return saved;
|
|
100
|
+
},
|
|
101
|
+
async save(json) {
|
|
102
|
+
saved = json;
|
|
103
|
+
},
|
|
104
|
+
async reset() {
|
|
105
|
+
saved = null;
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* nodeTransport 用 node:http/https 发送请求。
|
|
112
|
+
* 响应体保持原样,不自动解压:SDK 会按 Content-Encoding 自己处理。
|
|
113
|
+
*/
|
|
114
|
+
export function nodeTransport({ timeoutMs = 30_000 } = {}) {
|
|
115
|
+
return (req) =>
|
|
116
|
+
new Promise((resolve, reject) => {
|
|
117
|
+
const url = new URL(req.url);
|
|
118
|
+
const mod = url.protocol === 'http:' ? http : https;
|
|
119
|
+
const r = mod.request(url, { method: req.method || 'GET', headers: req.headers, timeout: timeoutMs }, (res) => {
|
|
120
|
+
const chunks = [];
|
|
121
|
+
let size = 0;
|
|
122
|
+
res.on('data', (c) => {
|
|
123
|
+
size += c.length;
|
|
124
|
+
if (size > MAX_BODY) r.destroy(new Error('response body too large'));
|
|
125
|
+
else chunks.push(c);
|
|
126
|
+
});
|
|
127
|
+
res.on('error', reject);
|
|
128
|
+
res.on('end', () => {
|
|
129
|
+
const headers = {};
|
|
130
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
131
|
+
if (k === 'set-cookie' || k === 'cookie' || v === undefined) continue;
|
|
132
|
+
headers[k] = Array.isArray(v) ? v[0] : String(v);
|
|
133
|
+
}
|
|
134
|
+
const cookies = {};
|
|
135
|
+
for (const line of res.headers['set-cookie'] ?? []) {
|
|
136
|
+
const pair = line.split(';', 1)[0];
|
|
137
|
+
const i = pair.indexOf('=');
|
|
138
|
+
if (i > 0) cookies[pair.slice(0, i).trim()] = pair.slice(i + 1).trim();
|
|
139
|
+
}
|
|
140
|
+
resolve({ status: res.statusCode, headers, cookies, body: new Uint8Array(Buffer.concat(chunks)) });
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
r.on('timeout', () => r.destroy(new Error('request timed out')));
|
|
144
|
+
r.on('error', reject);
|
|
145
|
+
if (req.body && req.body.length > 0) r.write(Buffer.from(req.body));
|
|
146
|
+
r.end();
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function toError(e) {
|
|
151
|
+
if (e instanceof Error) return e;
|
|
152
|
+
return new OpenhongguoError(e);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* createClient 创建一个客户端。首次请求时,SDK 注册一台匿名设备,
|
|
157
|
+
* 并通过 profile 保存下来。
|
|
158
|
+
*/
|
|
159
|
+
export async function createClient(options = {}) {
|
|
160
|
+
const rt = await loadRuntime();
|
|
161
|
+
const {
|
|
162
|
+
profile = fileProfileStore(),
|
|
163
|
+
transport = nodeTransport({ timeoutMs: options.timeoutMs }),
|
|
164
|
+
log,
|
|
165
|
+
apiOrigin,
|
|
166
|
+
registerOrigin,
|
|
167
|
+
distributionOrigin,
|
|
168
|
+
retryAttempts = 3,
|
|
169
|
+
deviceResets = 2,
|
|
170
|
+
device,
|
|
171
|
+
} = options;
|
|
172
|
+
const store = typeof profile === 'string' ? fileProfileStore(profile) : profile;
|
|
173
|
+
const res = rt.newClient({
|
|
174
|
+
transport,
|
|
175
|
+
profile: store,
|
|
176
|
+
log,
|
|
177
|
+
options: JSON.stringify({ apiOrigin, registerOrigin, distributionOrigin, retryAttempts, deviceResets, device }),
|
|
178
|
+
});
|
|
179
|
+
if (res.error) throw new OpenhongguoError(res.error);
|
|
180
|
+
return new Client(res.call);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Client 包装 Go SDK 的方法;每个方法返回 Promise。 */
|
|
184
|
+
export class Client {
|
|
185
|
+
#call;
|
|
186
|
+
|
|
187
|
+
constructor(call) {
|
|
188
|
+
this.#call = call;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async #invoke(name, ...args) {
|
|
192
|
+
let out;
|
|
193
|
+
try {
|
|
194
|
+
out = await this.#call(name, JSON.stringify(args));
|
|
195
|
+
} catch (e) {
|
|
196
|
+
throw toError(e);
|
|
197
|
+
}
|
|
198
|
+
return out === undefined || out === 'null' ? undefined : JSON.parse(out);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
discover(options = {}) {
|
|
202
|
+
return this.#invoke('discover', options);
|
|
203
|
+
}
|
|
204
|
+
search(query, options = {}) {
|
|
205
|
+
return this.#invoke('search', query, options);
|
|
206
|
+
}
|
|
207
|
+
suggest(prefix) {
|
|
208
|
+
return this.#invoke('suggest', prefix);
|
|
209
|
+
}
|
|
210
|
+
hotSearch() {
|
|
211
|
+
return this.#invoke('hotSearch');
|
|
212
|
+
}
|
|
213
|
+
searchCues() {
|
|
214
|
+
return this.#invoke('searchCues');
|
|
215
|
+
}
|
|
216
|
+
newReleases(query = {}) {
|
|
217
|
+
return this.#invoke('newReleases', query);
|
|
218
|
+
}
|
|
219
|
+
getSeries(seriesId) {
|
|
220
|
+
return this.#invoke('getSeries', seriesId);
|
|
221
|
+
}
|
|
222
|
+
getSeriesBatch(seriesIds) {
|
|
223
|
+
return this.#invoke('getSeriesBatch', seriesIds);
|
|
224
|
+
}
|
|
225
|
+
listEpisodes(seriesId, options = {}) {
|
|
226
|
+
return this.#invoke('listEpisodes', seriesId, options);
|
|
227
|
+
}
|
|
228
|
+
seasons(seriesId) {
|
|
229
|
+
return this.#invoke('seasons', seriesId);
|
|
230
|
+
}
|
|
231
|
+
seriesRecommend(seriesId) {
|
|
232
|
+
return this.#invoke('seriesRecommend', seriesId);
|
|
233
|
+
}
|
|
234
|
+
getAlbum(albumId) {
|
|
235
|
+
return this.#invoke('getAlbum', albumId);
|
|
236
|
+
}
|
|
237
|
+
shortRank(query = {}) {
|
|
238
|
+
return this.#invoke('shortRank', query);
|
|
239
|
+
}
|
|
240
|
+
shortFilterPanel(genre = '') {
|
|
241
|
+
return this.#invoke('shortFilterPanel', genre);
|
|
242
|
+
}
|
|
243
|
+
shortFilter(query = {}) {
|
|
244
|
+
return this.#invoke('shortFilter', query);
|
|
245
|
+
}
|
|
246
|
+
ranking(activityId, rankListId) {
|
|
247
|
+
return this.#invoke('ranking', activityId, rankListId);
|
|
248
|
+
}
|
|
249
|
+
rankRows(cellId, offset = 0) {
|
|
250
|
+
return this.#invoke('rankRows', cellId, offset);
|
|
251
|
+
}
|
|
252
|
+
rankGroups() {
|
|
253
|
+
return this.#invoke('rankGroups');
|
|
254
|
+
}
|
|
255
|
+
person(userId) {
|
|
256
|
+
return this.#invoke('person', userId);
|
|
257
|
+
}
|
|
258
|
+
personWorks(userId, cursor = '') {
|
|
259
|
+
return this.#invoke('personWorks', userId, cursor);
|
|
260
|
+
}
|
|
261
|
+
categories(categoryId) {
|
|
262
|
+
return this.#invoke('categories', categoryId);
|
|
263
|
+
}
|
|
264
|
+
categoryLanding(categoryId, options = {}) {
|
|
265
|
+
return this.#invoke('categoryLanding', categoryId, options);
|
|
266
|
+
}
|
|
267
|
+
resolvePlayback(episodeId) {
|
|
268
|
+
return this.#invoke('resolvePlayback', episodeId);
|
|
269
|
+
}
|
|
270
|
+
resetDevice() {
|
|
271
|
+
return this.#invoke('resetDevice');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** discoverAll 沿着游标逐部返回一个书城频道的剧,并去掉跨页重复的剧。 */
|
|
275
|
+
discoverAll(options = {}, { maxPages = Infinity } = {}) {
|
|
276
|
+
return this.#walk((cursor) => this.discover({ ...options, cursor }), options.cursor, maxPages);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** searchAll 沿着游标逐部返回搜索结果,并去掉跨页重复的剧。 */
|
|
280
|
+
searchAll(query, options = {}, { maxPages = Infinity } = {}) {
|
|
281
|
+
return this.#walk((cursor) => this.search(query, { ...options, cursor }), options.cursor, maxPages);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async *#walk(fetch, cursor, maxPages) {
|
|
285
|
+
const seen = new Set();
|
|
286
|
+
for (let pages = 0; pages < maxPages; pages++) {
|
|
287
|
+
const page = await fetch(cursor ?? '');
|
|
288
|
+
for (const item of page.items ?? []) {
|
|
289
|
+
if (seen.has(item.id)) continue;
|
|
290
|
+
seen.add(item.id);
|
|
291
|
+
yield item;
|
|
292
|
+
}
|
|
293
|
+
if (!page.has_more || !page.cursor) return;
|
|
294
|
+
cursor = page.cursor;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openhongguo",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "红果短剧(Hongguo)的非官方 Node.js SDK:匿名浏览、搜索与播放,基于 Go SDK 编译的 WebAssembly",
|
|
5
|
+
"author": "beilunyang",
|
|
6
|
+
"homepage": "https://github.com/beilunyang/openhongguo/tree/main/bindings/node#readme",
|
|
7
|
+
"bugs": "https://github.com/beilunyang/openhongguo/issues",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "./index.js",
|
|
10
|
+
"types": "./index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"default": "./index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"index.js",
|
|
19
|
+
"index.d.ts",
|
|
20
|
+
"types.d.ts",
|
|
21
|
+
"dist/",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"GO_LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "node scripts/build.mjs",
|
|
31
|
+
"test": "node --test"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"hongguo",
|
|
35
|
+
"红果短剧",
|
|
36
|
+
"short-drama",
|
|
37
|
+
"sdk",
|
|
38
|
+
"wasm"
|
|
39
|
+
],
|
|
40
|
+
"license": "Apache-2.0",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/beilunyang/openhongguo.git",
|
|
44
|
+
"directory": "bindings/node"
|
|
45
|
+
}
|
|
46
|
+
}
|