@qjp88995/provider-client 0.2.0 → 0.3.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.
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/justone/client.d.ts +101 -0
- package/dist/justone/client.d.ts.map +1 -0
- package/dist/justone/client.js +272 -0
- package/dist/justone/client.js.map +1 -0
- package/dist/justone/endpoint-spec.d.ts +49 -0
- package/dist/justone/endpoint-spec.d.ts.map +1 -0
- package/dist/justone/endpoint-spec.js +2 -0
- package/dist/justone/endpoint-spec.js.map +1 -0
- package/dist/justone/endpoints.generated.d.ts +7927 -0
- package/dist/justone/endpoints.generated.d.ts.map +1 -0
- package/dist/justone/endpoints.generated.js +8508 -0
- package/dist/justone/endpoints.generated.js.map +1 -0
- package/dist/justone/errors.d.ts +52 -0
- package/dist/justone/errors.d.ts.map +1 -0
- package/dist/justone/errors.js +73 -0
- package/dist/justone/errors.js.map +1 -0
- package/dist/justone/index.d.ts +8 -0
- package/dist/justone/index.d.ts.map +1 -0
- package/dist/justone/index.js +7 -0
- package/dist/justone/index.js.map +1 -0
- package/dist/justone/params.d.ts +17 -0
- package/dist/justone/params.d.ts.map +1 -0
- package/dist/justone/params.js +72 -0
- package/dist/justone/params.js.map +1 -0
- package/dist/justone/pricing.d.ts +6 -0
- package/dist/justone/pricing.d.ts.map +1 -0
- package/dist/justone/pricing.generated.d.ts +303 -0
- package/dist/justone/pricing.generated.d.ts.map +1 -0
- package/dist/justone/pricing.generated.js +305 -0
- package/dist/justone/pricing.generated.js.map +1 -0
- package/dist/justone/pricing.js +11 -0
- package/dist/justone/pricing.js.map +1 -0
- package/dist/media/bibigpt.adapter.d.ts +43 -0
- package/dist/media/bibigpt.adapter.d.ts.map +1 -0
- package/dist/media/bibigpt.adapter.js +302 -0
- package/dist/media/bibigpt.adapter.js.map +1 -0
- package/dist/media/http.d.ts +21 -0
- package/dist/media/http.d.ts.map +1 -0
- package/dist/media/http.js +39 -0
- package/dist/media/http.js.map +1 -0
- package/dist/media/index.d.ts +44 -0
- package/dist/media/index.d.ts.map +1 -0
- package/dist/media/index.js +47 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/types.d.ts +207 -0
- package/dist/media/types.d.ts.map +1 -0
- package/dist/media/types.js +21 -0
- package/dist/media/types.js.map +1 -0
- package/dist/media/zhipu-parser-package.d.ts +37 -0
- package/dist/media/zhipu-parser-package.d.ts.map +1 -0
- package/dist/media/zhipu-parser-package.js +108 -0
- package/dist/media/zhipu-parser-package.js.map +1 -0
- package/dist/media/zhipu.adapter.d.ts +37 -0
- package/dist/media/zhipu.adapter.d.ts.map +1 -0
- package/dist/media/zhipu.adapter.js +361 -0
- package/dist/media/zhipu.adapter.js.map +1 -0
- package/dist/provider-client.d.ts +49 -0
- package/dist/provider-client.d.ts.map +1 -1
- package/dist/provider-client.js +129 -0
- package/dist/provider-client.js.map +1 -1
- package/dist/types.d.ts +5 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -2
- package/dist/types.js.map +1 -1
- package/dist/web-search/deepseek.adapter.d.ts +19 -0
- package/dist/web-search/deepseek.adapter.d.ts.map +1 -0
- package/dist/web-search/deepseek.adapter.js +82 -0
- package/dist/web-search/deepseek.adapter.js.map +1 -0
- package/dist/web-search/http.d.ts +28 -0
- package/dist/web-search/http.d.ts.map +1 -0
- package/dist/web-search/http.js +95 -0
- package/dist/web-search/http.js.map +1 -0
- package/dist/web-search/index.d.ts +27 -0
- package/dist/web-search/index.d.ts.map +1 -0
- package/dist/web-search/index.js +36 -0
- package/dist/web-search/index.js.map +1 -0
- package/dist/web-search/minimax.adapter.d.ts +13 -0
- package/dist/web-search/minimax.adapter.d.ts.map +1 -0
- package/dist/web-search/minimax.adapter.js +82 -0
- package/dist/web-search/minimax.adapter.js.map +1 -0
- package/dist/web-search/moonshotai.adapter.d.ts +19 -0
- package/dist/web-search/moonshotai.adapter.d.ts.map +1 -0
- package/dist/web-search/moonshotai.adapter.js +151 -0
- package/dist/web-search/moonshotai.adapter.js.map +1 -0
- package/dist/web-search/types.d.ts +120 -0
- package/dist/web-search/types.d.ts.map +1 -0
- package/dist/web-search/types.js +60 -0
- package/dist/web-search/types.js.map +1 -0
- package/dist/web-search/zhipu.adapter.d.ts +14 -0
- package/dist/web-search/zhipu.adapter.d.ts.map +1 -0
- package/dist/web-search/zhipu.adapter.js +98 -0
- package/dist/web-search/zhipu.adapter.js.map +1 -0
- package/package.json +4 -3
- package/src/index.ts +99 -0
- package/src/justone/client.ts +462 -0
- package/src/justone/endpoint-spec.ts +66 -0
- package/src/justone/endpoints.generated.ts +9258 -0
- package/src/justone/errors.ts +97 -0
- package/src/justone/index.ts +34 -0
- package/src/justone/params.ts +91 -0
- package/src/justone/pricing.generated.ts +308 -0
- package/src/justone/pricing.ts +11 -0
- package/src/media/bibigpt.adapter.ts +372 -0
- package/src/media/http.ts +43 -0
- package/src/media/index.ts +114 -0
- package/src/media/types.ts +233 -0
- package/src/media/zhipu-parser-package.ts +149 -0
- package/src/media/zhipu.adapter.ts +510 -0
- package/src/provider-client.ts +228 -1
- package/src/types.ts +10 -2
- package/src/web-search/deepseek.adapter.ts +149 -0
- package/src/web-search/http.ts +108 -0
- package/src/web-search/index.ts +70 -0
- package/src/web-search/minimax.adapter.ts +148 -0
- package/src/web-search/moonshotai.adapter.ts +228 -0
- package/src/web-search/types.ts +172 -0
- package/src/web-search/zhipu.adapter.ts +184 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { VendorFetch } from '../vendors/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 媒体解析的厂商适配(从 agent-base `media/adapters` 搬来)。适配器只管「这家的请求长什么样、响应怎么翻、
|
|
5
|
+
* 哪些失败值得重试」,外加只有它知道的两件事:哪些设置决定产出(缓存键)、这家单次收多大的文件。
|
|
6
|
+
* 物化图片到存储、解析缓存、审计与花费是 APP 的事。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** `audio` 语音转写;`file` 文件解析;`video-link` 按链接解析在线视频。 */
|
|
10
|
+
export const MEDIA_CAPABILITIES = ['audio', 'file', 'video-link'] as const;
|
|
11
|
+
export type MediaCapability = (typeof MEDIA_CAPABILITIES)[number];
|
|
12
|
+
|
|
13
|
+
/** 要解析的东西:一个可直接下载的地址(不经中心、不带厂商鉴权),或现成的字节。 */
|
|
14
|
+
export type MediaInput =
|
|
15
|
+
| { kind: 'url'; url: string }
|
|
16
|
+
| { kind: 'bytes'; bytes: Uint8Array; mediaType: string };
|
|
17
|
+
|
|
18
|
+
export interface TokenUsage {
|
|
19
|
+
input?: number;
|
|
20
|
+
output?: number;
|
|
21
|
+
total?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AudioRequest {
|
|
25
|
+
input: MediaInput;
|
|
26
|
+
/** 语言提示;`auto` 或缺省 = 让厂商自己判断。 */
|
|
27
|
+
languageHint?: string;
|
|
28
|
+
durationHintMs?: number;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AudioOutcome {
|
|
33
|
+
text: string;
|
|
34
|
+
rawResponse: unknown;
|
|
35
|
+
audioDurationMs?: number;
|
|
36
|
+
language?: string;
|
|
37
|
+
segments?: Array<{ start: number; end: number; text: string }>;
|
|
38
|
+
tokensUsed?: TokenUsage;
|
|
39
|
+
vendorExtra?: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FileRequest {
|
|
43
|
+
input: MediaInput;
|
|
44
|
+
/** 异步任务的轮询预算(毫秒);缺省用设置里的值。 */
|
|
45
|
+
maxWaitMs?: number;
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 厂商交回来的**一张原始图片**:字节 + 它在 `markdown` 里被引用的写法。存进哪里、把 `ref` 换成什么地址是 APP 的事
|
|
51
|
+
* (厂商的链接会过期,进缓存之前必须先物化)。
|
|
52
|
+
*/
|
|
53
|
+
export interface ParsedImage {
|
|
54
|
+
/** markdown 里引用它的原始路径,逐字(智谱:`images/<md5>-image.png`)。 */
|
|
55
|
+
ref: string;
|
|
56
|
+
bytes: Uint8Array;
|
|
57
|
+
/** 完整子类型(`image/png`)。 */
|
|
58
|
+
mediaType: string;
|
|
59
|
+
/** 1 基页码;厂商没给就缺席。 */
|
|
60
|
+
page?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface FileOutcome {
|
|
64
|
+
text: string;
|
|
65
|
+
markdown: string;
|
|
66
|
+
rawResponse: unknown;
|
|
67
|
+
pageCount?: number;
|
|
68
|
+
tokensUsed?: TokenUsage;
|
|
69
|
+
vendorExtra?: Record<string, unknown>;
|
|
70
|
+
/** 文档里的图片。缺席 = 这一档 / 这家不产出图片(智谱 lite),与「有图但一张没抠出来」(空数组)不同。 */
|
|
71
|
+
images?: ParsedImage[];
|
|
72
|
+
/**
|
|
73
|
+
* 非空 = **结果不完整**,一句给人看的原因:正文是好的,但本该有的图片没拿到(结果包下载失败、解不开……)。
|
|
74
|
+
* 调用方据此**别缓存**——缓存一份缺图的结果等于把一次瞬时故障固化成永久答案——并把这句话带给用户 / 模型。
|
|
75
|
+
*/
|
|
76
|
+
degraded?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface VideoLinkRequest {
|
|
80
|
+
/** 在线视频作品页的链接(已从分享文本里抽出来)。 */
|
|
81
|
+
url: string;
|
|
82
|
+
/** 取消信号:工具超时 / 用户停止时中断还在等的请求。 */
|
|
83
|
+
signal?: AbortSignal;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 一句字幕。时间单位是秒。 */
|
|
87
|
+
export interface VideoLinkSubtitle {
|
|
88
|
+
start: number;
|
|
89
|
+
end: number;
|
|
90
|
+
text: string;
|
|
91
|
+
/** 厂商分出来的说话人编号;没分就缺席。 */
|
|
92
|
+
speaker?: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface VideoLinkChapter {
|
|
96
|
+
start: number;
|
|
97
|
+
end: number;
|
|
98
|
+
title: string;
|
|
99
|
+
summary: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 按链接解析一条在线视频的产出。除 `subtitles` 外的内容块都可能缺席——厂商某一步失败时写进 `missing`,
|
|
104
|
+
* 而不是让整次调用失败:字幕和元数据是主体,总结 / 原文细读是附加的。
|
|
105
|
+
*/
|
|
106
|
+
export interface VideoLinkOutcome {
|
|
107
|
+
title: string;
|
|
108
|
+
author?: string;
|
|
109
|
+
durationSec?: number;
|
|
110
|
+
/** ISO 时间;厂商没给就缺席。 */
|
|
111
|
+
publishedAt?: string;
|
|
112
|
+
/** 作品正文(小红书笔记正文、话题标签……),品牌名的准确写法常在这里。 */
|
|
113
|
+
description?: string;
|
|
114
|
+
subtitles: VideoLinkSubtitle[];
|
|
115
|
+
/** 全文总结(markdown,时间戳写成 `[mm:ss]`)。 */
|
|
116
|
+
summaryMarkdown?: string;
|
|
117
|
+
/** 章节总览(一段话)。 */
|
|
118
|
+
chapterOverview?: string;
|
|
119
|
+
chapters: VideoLinkChapter[];
|
|
120
|
+
/** 原文细读(按章节整理过的字幕,markdown)。 */
|
|
121
|
+
polishedMarkdown?: string;
|
|
122
|
+
/** 缺席内容块 → 一句给人看的原因。 */
|
|
123
|
+
missing: Partial<Record<'summary' | 'chapters' | 'polished', string>>;
|
|
124
|
+
/** 本次扣费与余额(秒);厂商没给就缺席。 */
|
|
125
|
+
billing?: { chargedSeconds?: number; remainingSeconds?: number };
|
|
126
|
+
rawResponse: unknown;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 传给 `cacheKeyParts` 的调用参数——各能力参数的并集,适配器只挑自己关心的。 */
|
|
130
|
+
export interface CacheKeyOpts {
|
|
131
|
+
languageHint?: string;
|
|
132
|
+
durationHintMs?: number;
|
|
133
|
+
maxWaitMs?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 传给 `maxUploadBytes` 的提示:这次要送的是什么类型的文件。厂商的上限常按格式细分(智谱 prime 档 PDF 100MB、
|
|
138
|
+
* 表格只有 10MB)。`null` = 调用方也不知道类型,此时返回该能力的最小值。
|
|
139
|
+
*/
|
|
140
|
+
export interface UploadLimitHint {
|
|
141
|
+
mediaType: string | null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 调一次上游要的东西。
|
|
146
|
+
* - `baseUrl`:这家媒体端点的根(智谱是 `media` 端点,BibiGPT 是它唯一的 `http` 端点)。
|
|
147
|
+
* - `openTask()`:**每次调用(一个任务)取一个新的** fetch,这次调用里建任务、轮询、取结果都用它。中心模式下它记住
|
|
148
|
+
* 第一条请求落在哪把 key 上,之后的请求钉在那把上(任务属于那个账号,换 key 就查不到);直连模式就是同一个 fetch。
|
|
149
|
+
* - `download`:下载不属于厂商 API 的地址(`kind: 'url'` 的输入、智谱的结果包),不经中心、不带鉴权;缺省全局 fetch。
|
|
150
|
+
* - `apiKey`:只有直连模式给,适配器以 Bearer 发出;中心模式由中心注入。
|
|
151
|
+
*/
|
|
152
|
+
export interface MediaConnection {
|
|
153
|
+
baseUrl: string;
|
|
154
|
+
openTask(): VendorFetch;
|
|
155
|
+
download?: VendorFetch;
|
|
156
|
+
apiKey?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 智谱文件解析档位:lite 只有正文;expert 只吃 PDF、带图片;prime 全格式、带图片。 */
|
|
160
|
+
export const ZHIPU_FILE_TOOL_TYPES = ['lite', 'expert', 'prime'] as const;
|
|
161
|
+
export type ZhipuFileToolType = (typeof ZHIPU_FILE_TOOL_TYPES)[number];
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 各家的设置(APP 存、调用时传;缺省值见各适配器)。**地址不是设置**:取自目录里这家的端点。
|
|
165
|
+
* 智谱的形状照 agent-base 的配置(去掉 `baseUrl`),存量配置可以原样传。
|
|
166
|
+
*/
|
|
167
|
+
export interface MediaSettingsByKind {
|
|
168
|
+
zhipu: {
|
|
169
|
+
audio?: { model?: string };
|
|
170
|
+
file?: { toolType?: ZhipuFileToolType; defaultMaxWaitMs?: number };
|
|
171
|
+
};
|
|
172
|
+
bibigpt: Record<string, never>;
|
|
173
|
+
}
|
|
174
|
+
export type MediaKind = keyof MediaSettingsByKind;
|
|
175
|
+
export const MEDIA_KINDS = [
|
|
176
|
+
'zhipu',
|
|
177
|
+
'bibigpt',
|
|
178
|
+
] as const satisfies readonly MediaKind[];
|
|
179
|
+
export type MediaSettings<K extends MediaKind = MediaKind> =
|
|
180
|
+
MediaSettingsByKind[K];
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 方法在不在 = 支不支持这项能力(`capabilities` 与之一致,spec 钉着)。
|
|
184
|
+
*/
|
|
185
|
+
export interface MediaAdapter<K extends MediaKind> {
|
|
186
|
+
readonly kind: K;
|
|
187
|
+
/** 这家的媒体请求走哪条端点:借 LLM key 的家是 `media`,自成一家的是 `http`。 */
|
|
188
|
+
readonly endpointProtocol: 'media' | 'http';
|
|
189
|
+
readonly capabilities: readonly MediaCapability[];
|
|
190
|
+
transcribeAudio?(
|
|
191
|
+
conn: MediaConnection,
|
|
192
|
+
settings: MediaSettings<K>,
|
|
193
|
+
req: AudioRequest
|
|
194
|
+
): Promise<AudioOutcome>;
|
|
195
|
+
parseFile?(
|
|
196
|
+
conn: MediaConnection,
|
|
197
|
+
settings: MediaSettings<K>,
|
|
198
|
+
req: FileRequest
|
|
199
|
+
): Promise<FileOutcome>;
|
|
200
|
+
fetchVideoLink?(
|
|
201
|
+
conn: MediaConnection,
|
|
202
|
+
settings: MediaSettings<K>,
|
|
203
|
+
req: VideoLinkRequest
|
|
204
|
+
): Promise<VideoLinkOutcome>;
|
|
205
|
+
/**
|
|
206
|
+
* 「这次调用的产出由哪些东西决定」,给调用方算解析缓存的键。`null` = **不可缓存**。只有适配器知道设置里哪些字段
|
|
207
|
+
* 换了会换来不同的产出(智谱的文件档位会,轮询预算不会)。
|
|
208
|
+
* - 绝不放密钥类字段(调用方可能明文落库供排查)。
|
|
209
|
+
* - 不认识的能力一律 null(fail-closed);生成式产出(视频总结)不缓存。
|
|
210
|
+
*/
|
|
211
|
+
cacheKeyParts(
|
|
212
|
+
capability: MediaCapability,
|
|
213
|
+
settings: MediaSettings<K>,
|
|
214
|
+
opts: CacheKeyOpts
|
|
215
|
+
): Record<string, unknown> | null;
|
|
216
|
+
/**
|
|
217
|
+
* 这家此能力单次最多收多大的文件(字节),按厂商文档抄数;`null` = 无声明,调用方只按自己的内存保护顶兜底。
|
|
218
|
+
* 文档只写「MB」没写清进制时按十进制取下界。
|
|
219
|
+
*/
|
|
220
|
+
maxUploadBytes(
|
|
221
|
+
capability: MediaCapability,
|
|
222
|
+
settings: MediaSettings<K>,
|
|
223
|
+
hint: UploadLimitHint
|
|
224
|
+
): number | null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 媒体厂商的上游失败(非 2xx、厂商说失败了、响应不成形、连不上……)。中心自己报的错(没有可用 key、
|
|
229
|
+
* 接入方不许用这家、钉住的 key 不可用……)不是这个,是 `ProviderClientError`。
|
|
230
|
+
*/
|
|
231
|
+
export class MediaUpstreamError extends Error {
|
|
232
|
+
override readonly name: string = 'MediaUpstreamError';
|
|
233
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { unzipSync } from 'fflate';
|
|
2
|
+
|
|
3
|
+
import type { ParsedImage } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 智谱文件解析 `download_link` 结果包(zip)的读法。
|
|
7
|
+
*
|
|
8
|
+
* **文档只写了「图片 + Markdown + 版面 JSON」,目录结构与引图方式全部来自
|
|
9
|
+
* 2026-09-07 的探针**(三档各跑一遍真文件,判据记在 agent-base 的 providers.md):
|
|
10
|
+
*
|
|
11
|
+
* - prime(异步):`<hash>/<hash>_md_full.md` + `<hash>/<hash>_ocr_page<N>.json`
|
|
12
|
+
* (每页一份,`PageNumber` **0 基**)+ `<hash>/images/<md5>-image.png`;
|
|
13
|
+
* - prime-sync:同上但**没有** `<hash>/` 这层目录;
|
|
14
|
+
* - expert:`result.md` + `images/<P>_<x>_<y>_<w>_<h>_0.jpg`,没有页 JSON,
|
|
15
|
+
* 页码只能从文件名前缀 `<P>` 取(0 基,与 prime 页 JSON 对过同一份 PDF);
|
|
16
|
+
* - lite:只有 `result.md`,没有图片目录。
|
|
17
|
+
*
|
|
18
|
+
* 三档的 markdown 都用 `` 引图,路径**相对 md 所在目录**。
|
|
19
|
+
* 所以这里不按目录名硬编码:md 是包里唯一的 `.md` 条目,图片按 md 里的引用
|
|
20
|
+
* 去解析,页 JSON 按后缀认。布局变了,最先坏的是「找不到 md」这条响亮的错,
|
|
21
|
+
* 而不是静默少图。
|
|
22
|
+
*/
|
|
23
|
+
export interface ParsedPackage {
|
|
24
|
+
markdown: string;
|
|
25
|
+
images: ParsedImage[];
|
|
26
|
+
/** prime 有页 JSON 时给(= 页 JSON 份数);其余档缺席。 */
|
|
27
|
+
pageCount?: number;
|
|
28
|
+
/** markdown 引用了、包里却没有的图片(排查用;正常为空)。 */
|
|
29
|
+
missingRefs: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const UTF8 = new TextDecoder();
|
|
33
|
+
|
|
34
|
+
const PAGE_JSON_RE = /_ocr_page(\d+)\.json$/;
|
|
35
|
+
|
|
36
|
+
const MEDIA_TYPE_BY_EXT: Record<string, string> = {
|
|
37
|
+
png: 'image/png',
|
|
38
|
+
jpg: 'image/jpeg',
|
|
39
|
+
jpeg: 'image/jpeg',
|
|
40
|
+
gif: 'image/gif',
|
|
41
|
+
webp: 'image/webp',
|
|
42
|
+
bmp: 'image/bmp',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function parseParserPackage(zip: Uint8Array): ParsedPackage {
|
|
46
|
+
const entries = unzipSync(zip);
|
|
47
|
+
const names = Object.keys(entries).filter(n => !n.endsWith('/'));
|
|
48
|
+
|
|
49
|
+
const mdNames = names.filter(n => /\.md$/i.test(n));
|
|
50
|
+
if (mdNames.length !== 1) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`结果包里应有且只有一个 .md 文件,实际 ${mdNames.length} 个:` +
|
|
53
|
+
`${mdNames.join(', ') || '(无)'};全部条目:${names.slice(0, 20).join(', ')}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const mdName = mdNames[0];
|
|
57
|
+
const mdDir = mdName.slice(0, mdName.lastIndexOf('/') + 1);
|
|
58
|
+
const markdown = UTF8.decode(entries[mdName]);
|
|
59
|
+
|
|
60
|
+
// 页码:prime 的页 JSON 里 `Elements[].ImagePath` 指向 `images/<name>`,
|
|
61
|
+
// `PageNumber` 0 基。递归走 `Elements`(表格里的图挂在 table 元素下)。
|
|
62
|
+
const pageByRef = new Map<string, number>();
|
|
63
|
+
let pageJsonCount = 0;
|
|
64
|
+
for (const n of names) {
|
|
65
|
+
if (!PAGE_JSON_RE.test(n)) continue;
|
|
66
|
+
pageJsonCount += 1;
|
|
67
|
+
let page: unknown;
|
|
68
|
+
try {
|
|
69
|
+
page = JSON.parse(UTF8.decode(entries[n]));
|
|
70
|
+
} catch {
|
|
71
|
+
continue; // 一页 JSON 坏了只是少了页码,不该让整包失败。
|
|
72
|
+
}
|
|
73
|
+
const pageNumber = (page as { PageNumber?: unknown }).PageNumber;
|
|
74
|
+
if (typeof pageNumber !== 'number') continue;
|
|
75
|
+
walkElements((page as { Elements?: unknown }).Elements, el => {
|
|
76
|
+
const ref = el.ImagePath;
|
|
77
|
+
if (typeof ref === 'string' && ref !== '' && !pageByRef.has(ref)) {
|
|
78
|
+
pageByRef.set(ref, pageNumber + 1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const images: ParsedImage[] = [];
|
|
84
|
+
const missingRefs: string[] = [];
|
|
85
|
+
for (const ref of collectImageRefs(markdown)) {
|
|
86
|
+
const entryName = mdDir + ref;
|
|
87
|
+
const bytes = entries[entryName];
|
|
88
|
+
if (!bytes) {
|
|
89
|
+
missingRefs.push(ref);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const ext = ref.slice(ref.lastIndexOf('.') + 1).toLowerCase();
|
|
93
|
+
const page = pageByRef.get(ref) ?? pageFromExpertName(ref);
|
|
94
|
+
images.push({
|
|
95
|
+
ref,
|
|
96
|
+
bytes,
|
|
97
|
+
mediaType: MEDIA_TYPE_BY_EXT[ext] ?? 'application/octet-stream',
|
|
98
|
+
...(page !== undefined ? { page } : {}),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
markdown,
|
|
104
|
+
images,
|
|
105
|
+
...(pageJsonCount > 0 ? { pageCount: pageJsonCount } : {}),
|
|
106
|
+
missingRefs,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface PageElement {
|
|
111
|
+
ImagePath?: unknown;
|
|
112
|
+
Elements?: unknown;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function walkElements(els: unknown, visit: (el: PageElement) => void): void {
|
|
116
|
+
if (!Array.isArray(els)) return;
|
|
117
|
+
for (const el of els) {
|
|
118
|
+
if (!el || typeof el !== 'object') continue;
|
|
119
|
+
visit(el as PageElement);
|
|
120
|
+
walkElements((el as PageElement).Elements, visit);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* expert 档的图片名 `<P>_<x>_<y>_<w>_<h>_0.jpg`:`P` 是 0 基页码。**只在
|
|
126
|
+
* 五段数字都在时才信**——prime 的 `<md5>-image.png` 不会匹配,其它未知命名
|
|
127
|
+
* 也不会被误读成页码。
|
|
128
|
+
*/
|
|
129
|
+
export function pageFromExpertName(ref: string): number | undefined {
|
|
130
|
+
const base = ref.slice(ref.lastIndexOf('/') + 1);
|
|
131
|
+
const m = /^(\d+)_\d+_\d+_\d+_\d+_\d+\./.exec(base);
|
|
132
|
+
return m ? Number(m[1]) + 1 : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* markdown 图片语法 `` 的 URL 段。智谱三档产物都用它引图(探针 2026-09-07)。
|
|
137
|
+
* 与 agent-base `parsed-images.ts` 的同名正则一致(APP 改写引用时用它自己的那份)。
|
|
138
|
+
*/
|
|
139
|
+
const IMAGE_REF_RE = /(!\[[^\]]*\]\()([^)\s]+)((?:\s+"[^"]*")?\))/g;
|
|
140
|
+
|
|
141
|
+
/** markdown 里引用了哪些图片,按首次出现顺序去重。 */
|
|
142
|
+
export function collectImageRefs(markdown: string): string[] {
|
|
143
|
+
const seen = new Set<string>();
|
|
144
|
+
for (const m of markdown.matchAll(IMAGE_REF_RE)) {
|
|
145
|
+
const ref = m[2];
|
|
146
|
+
if (ref !== undefined && !seen.has(ref)) seen.add(ref);
|
|
147
|
+
}
|
|
148
|
+
return [...seen];
|
|
149
|
+
}
|