@kevisual/cnb 0.0.76 → 0.0.78
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/agent/routes/knowledge/embedding.ts +166 -0
- package/agent/routes/knowledge/index.ts +2 -1
- package/agent/routes/labels/tags.ts +43 -3
- package/agent/routes/repo/download.ts +23 -0
- package/agent/routes/repo/index.ts +2 -1
- package/dist/cli-live.js +22 -22
- package/dist/cli.js +848 -364
- package/dist/keep.js +14 -14
- package/dist/npc.js +836 -352
- package/dist/opencode.js +833 -349
- package/dist/routes.d.ts +61 -28
- package/dist/routes.js +829 -345
- package/package.json +2 -2
- package/src/cnb-core.ts +26 -11
- package/src/git/index.ts +54 -2
- package/src/knowledge/index.ts +81 -22
- package/src/labels/repo-label.ts +16 -0
- package/src/repo/download.ts +88 -1
- package/src/issue/issue-alive/issues-alive.html +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kevisual/cnb",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.78",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"basename": "/root/cnb",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"ai": "^6.0.158",
|
|
49
49
|
"commander": "^14.0.3",
|
|
50
50
|
"dayjs": "^1.11.20",
|
|
51
|
-
"dotenv": "^17.4.
|
|
51
|
+
"dotenv": "^17.4.2",
|
|
52
52
|
"fast-glob": "^3.3.3",
|
|
53
53
|
"zod": "^4.3.6"
|
|
54
54
|
},
|
package/src/cnb-core.ts
CHANGED
|
@@ -7,7 +7,8 @@ export type CNBCoreOptions<T = {}> = {
|
|
|
7
7
|
cnb?: CNBCore;
|
|
8
8
|
cors?: {
|
|
9
9
|
baseUrl?: string
|
|
10
|
-
}
|
|
10
|
+
},
|
|
11
|
+
openapi?: boolean;
|
|
11
12
|
} & T;
|
|
12
13
|
|
|
13
14
|
export type RequestOptions = {
|
|
@@ -17,20 +18,24 @@ export type RequestOptions = {
|
|
|
17
18
|
body?: any;
|
|
18
19
|
params?: Record<string, any>;
|
|
19
20
|
headers?: Record<string, any>;
|
|
21
|
+
token?: string;
|
|
20
22
|
useCookie?: boolean;
|
|
21
23
|
useOrigin?: boolean;
|
|
22
24
|
};
|
|
23
|
-
const
|
|
25
|
+
const API_BASE_URL = 'https://api.cnb.cool'
|
|
24
26
|
const API_HACK_URL = 'https://cnb.cool'
|
|
27
|
+
const API_BASE_OPEN_URL = 'https://cn.cool/openapi'
|
|
25
28
|
export class CNBCore {
|
|
26
|
-
baseURL =
|
|
29
|
+
baseURL = API_BASE_URL;
|
|
27
30
|
hackURL = API_HACK_URL;
|
|
28
31
|
public token: string;
|
|
29
32
|
public cookie?: string;
|
|
30
33
|
isCors: boolean;
|
|
34
|
+
openapi?: boolean;
|
|
31
35
|
constructor(options: CNBCoreOptions) {
|
|
32
36
|
this.token = options.token;
|
|
33
37
|
this.cookie = options.cookie;
|
|
38
|
+
this.openapi = options.openapi;
|
|
34
39
|
if (options?.cnb) {
|
|
35
40
|
if (!options.token) {
|
|
36
41
|
this.token = options.cnb.token;
|
|
@@ -39,14 +44,20 @@ export class CNBCore {
|
|
|
39
44
|
this.cookie = options.cnb.cookie;
|
|
40
45
|
}
|
|
41
46
|
}
|
|
47
|
+
if (options?.openapi) {
|
|
48
|
+
this.baseURL = API_BASE_OPEN_URL;
|
|
49
|
+
}
|
|
42
50
|
if (options?.cors?.baseUrl) {
|
|
43
|
-
this.baseURL = options.cors.baseUrl + '/' +
|
|
44
|
-
this.hackURL = options.cors.baseUrl + '/' +
|
|
51
|
+
this.baseURL = options.cors.baseUrl + '/' + this.baseURL.replace('https://', '');
|
|
52
|
+
this.hackURL = options.cors.baseUrl + '/' + this.hackURL.replace('https://', '');
|
|
45
53
|
}
|
|
46
54
|
this.isCors = !!options?.cors?.baseUrl;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
|
-
async request({ url, method = 'GET', data, params, headers, body, useCookie, useOrigin }: RequestOptions): Promise<any> {
|
|
57
|
+
async request({ url, method = 'GET', data, params, headers, body, useCookie, useOrigin, ...rest }: RequestOptions): Promise<any> {
|
|
58
|
+
if (!url.startsWith('http')) {
|
|
59
|
+
url = this.makeUrl(url);
|
|
60
|
+
}
|
|
50
61
|
const defaultHeaders: Record<string, string> = {
|
|
51
62
|
'Content-Type': 'application/json',
|
|
52
63
|
// 'Accept': 'application/json, application/vnd.cnb.api+json, application/vnd.cnb.web+json',
|
|
@@ -55,10 +66,13 @@ export class CNBCore {
|
|
|
55
66
|
if (this.token) {
|
|
56
67
|
defaultHeaders['Authorization'] = `Bearer ${this.token}`;
|
|
57
68
|
}
|
|
69
|
+
// 如果请求参数中有 token,则覆盖默认 token
|
|
70
|
+
if (rest.token) {
|
|
71
|
+
defaultHeaders['Authorization'] = `Bearer ${rest.token}`;
|
|
72
|
+
}
|
|
58
73
|
if (params) {
|
|
59
74
|
const queryString = new URLSearchParams(params).toString();
|
|
60
75
|
url += `?${queryString}`;
|
|
61
|
-
defaultHeaders['Accept'] = 'application/json';
|
|
62
76
|
}
|
|
63
77
|
const _headers = { ...defaultHeaders, ...headers };
|
|
64
78
|
let _body = undefined;
|
|
@@ -142,10 +156,11 @@ export class CNBCore {
|
|
|
142
156
|
return this.request({ url: fullUrl, method: 'PATCH', ...REST });
|
|
143
157
|
}
|
|
144
158
|
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
159
|
+
* 通过 PUT 请求上传文件内容
|
|
160
|
+
* assets上传的时候,第一次会获取到一个上传 URL 和 token,然后使用这个方法将文件内容上传到指定的 URL
|
|
161
|
+
* @param data 包含 URL、token 和文件内容
|
|
162
|
+
* @returns 上传结果
|
|
163
|
+
*/
|
|
149
164
|
async putFile(data: { url: string, token: string, content: string | Buffer }): Promise<any> {
|
|
150
165
|
return this.request({
|
|
151
166
|
url: data.url,
|
package/src/git/index.ts
CHANGED
|
@@ -307,7 +307,7 @@ export class Git extends CNBCore {
|
|
|
307
307
|
* @param params 查询参数
|
|
308
308
|
* @returns 文件或目录内容
|
|
309
309
|
*/
|
|
310
|
-
async getContent(repo: string, params?: GetContentParams): Promise<Result<
|
|
310
|
+
async getContent(repo: string, params?: GetContentParams): Promise<Result<FileListContent>> {
|
|
311
311
|
const url = `/${repo}/-/git/contents`;
|
|
312
312
|
return this.get({ url, params });
|
|
313
313
|
}
|
|
@@ -319,11 +319,46 @@ export class Git extends CNBCore {
|
|
|
319
319
|
* @param params 查询参数
|
|
320
320
|
* @returns 文件内容
|
|
321
321
|
*/
|
|
322
|
-
async getContentWithPath(repo: string, filePath: string, params?: GetContentWithPathParams): Promise<Result<
|
|
322
|
+
async getContentWithPath(repo: string, filePath: string, params?: GetContentWithPathParams): Promise<Result<FileListContent>> {
|
|
323
323
|
const url = `/${repo}/-/git/contents/${filePath}`;
|
|
324
324
|
return this.get({ url, params });
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
+
async getAllContentWithPath(repo: string, filePath: string = '', opts: { recursive?: boolean } & GetContentParams): Promise<FileListContent['entries']> {
|
|
328
|
+
try {
|
|
329
|
+
const { recursive, ...rest } = opts;
|
|
330
|
+
const res = await this.getContentWithPath(repo, filePath, rest);
|
|
331
|
+
if (res.code !== 200) {
|
|
332
|
+
console.log('获取文件列表失败', repo, filePath, res);
|
|
333
|
+
return [];
|
|
334
|
+
} else if (!res.data) {
|
|
335
|
+
console.log('列表为空', res);
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
const fileList = res.data as unknown as FileListContent;
|
|
339
|
+
const entries = fileList.entries || [];
|
|
340
|
+
console.log('文件列表', fileList);
|
|
341
|
+
const treeList = entries.filter(item => item.type === 'tree');
|
|
342
|
+
const fileListResult = entries.filter(item => item.type === 'blob');
|
|
343
|
+
if (recursive) {
|
|
344
|
+
for (const tree of treeList) {
|
|
345
|
+
let treePath = tree.path;
|
|
346
|
+
if (tree.path.includes('/')) {
|
|
347
|
+
treePath = tree.name;
|
|
348
|
+
}
|
|
349
|
+
let newPath = filePath ? `${filePath}/${treePath}` : treePath;
|
|
350
|
+
const subFileList = await this.getAllContentWithPath(`${repo}`, newPath, { recursive });
|
|
351
|
+
fileListResult.push(...subFileList);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return fileListResult;
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
console.error('获取仓库文件列表失败', error);
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
327
362
|
/**
|
|
328
363
|
* 获取原始文件内容
|
|
329
364
|
* @param repo 仓库名称,格式:组织名称/仓库名称
|
|
@@ -674,3 +709,20 @@ type PutTagAnnotationsData = {
|
|
|
674
709
|
/** 注释键值对 */
|
|
675
710
|
annotations: Record<string, string>;
|
|
676
711
|
};
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
type FileListContent = {
|
|
715
|
+
type: 'blob' | 'tree';
|
|
716
|
+
size: number;
|
|
717
|
+
path: string;
|
|
718
|
+
name: string;
|
|
719
|
+
sha: string;
|
|
720
|
+
url: string;
|
|
721
|
+
entries?: FIleEntry[];
|
|
722
|
+
}
|
|
723
|
+
type FIleEntry = {
|
|
724
|
+
type: 'blob' | 'tree';
|
|
725
|
+
sha: string;
|
|
726
|
+
path: string;
|
|
727
|
+
name: string;
|
|
728
|
+
}
|
package/src/knowledge/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CNBCore, CNBCoreOptions, Result } from "../cnb-core.ts";
|
|
1
|
+
import { CNBCore, CNBCoreOptions, RequestOptions, Result } from "../cnb-core.ts";
|
|
2
2
|
|
|
3
3
|
// https://cnb.cool/cnb/plugins/cnbcool/knowledge-base/-/blob/main/src/api.py
|
|
4
4
|
export class KnowledgeBase extends CNBCore {
|
|
@@ -27,31 +27,56 @@ export class KnowledgeBase extends CNBCore {
|
|
|
27
27
|
}
|
|
28
28
|
deleteBase(repo: string): Promise<Result<any>> {
|
|
29
29
|
const url = `/${repo}/-/knowledge/base`;
|
|
30
|
-
return this.
|
|
30
|
+
return this.delete({ url });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* 未暴露
|
|
35
|
+
*
|
|
36
|
+
* 创建知识库接口,cnb 界面操作定制模块功能依赖该接口实现
|
|
35
37
|
* @param repo
|
|
36
38
|
* @param data
|
|
37
39
|
* @returns
|
|
38
40
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
|
|
42
|
+
createKnowledgeBase(repo: string, params: {
|
|
43
|
+
/**
|
|
44
|
+
* hunyuan | bge-m3
|
|
45
|
+
*/
|
|
46
|
+
model_name: string;
|
|
41
47
|
include: string;
|
|
42
48
|
exclude: string;
|
|
49
|
+
chunk_size: number;
|
|
50
|
+
chunk_overlap: number;
|
|
51
|
+
text_separator: string;
|
|
43
52
|
issue_sync_enabled?: boolean;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
text_separator: string;
|
|
48
|
-
};
|
|
49
|
-
issue?: {
|
|
50
|
-
labels: string;
|
|
51
|
-
state: string;
|
|
52
|
-
};
|
|
53
|
+
issue_labels?: string;
|
|
54
|
+
issue_exclude_labels?: string;
|
|
55
|
+
issue_state?: string;
|
|
53
56
|
}): Promise<Result<any>> {
|
|
54
57
|
const url = `/${repo}/-/knowledge/base`;
|
|
58
|
+
const metadata: any = {
|
|
59
|
+
processing: {
|
|
60
|
+
chunk_size: params.chunk_size,
|
|
61
|
+
chunk_overlap: params.chunk_overlap,
|
|
62
|
+
text_separator: params.text_separator
|
|
63
|
+
},
|
|
64
|
+
version: "1.0"
|
|
65
|
+
};
|
|
66
|
+
if (params.issue_sync_enabled) {
|
|
67
|
+
metadata.issue = {
|
|
68
|
+
labels: params.issue_labels || "",
|
|
69
|
+
exclude_labels: params.issue_exclude_labels || "",
|
|
70
|
+
state: params.issue_state || ""
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const data = {
|
|
74
|
+
embedding_model_name: params.model_name,
|
|
75
|
+
include: params.include,
|
|
76
|
+
exclude: params.exclude,
|
|
77
|
+
issue_sync_enabled: params.issue_sync_enabled || false,
|
|
78
|
+
metadata
|
|
79
|
+
};
|
|
55
80
|
return this.post({ url, data });
|
|
56
81
|
}
|
|
57
82
|
/**
|
|
@@ -74,22 +99,27 @@ export class KnowledgeBase extends CNBCore {
|
|
|
74
99
|
* @param text
|
|
75
100
|
* @returns
|
|
76
101
|
*/
|
|
77
|
-
getEmbedding(repo: string, text: string): Promise<Result<{
|
|
102
|
+
getEmbedding(repo: string, text: string): Promise<Result<{ embeddings: number[] }>> {
|
|
78
103
|
const url = `/${repo}/-/knowledge/embedding`;
|
|
79
104
|
return this.post({ url, data: { text } });
|
|
80
105
|
}
|
|
81
106
|
/**
|
|
82
|
-
*
|
|
107
|
+
* 未暴露
|
|
108
|
+
* 只能运行在流水线,使用流水线的CNB_TOKEN去使用
|
|
109
|
+
* 否则会报错:sn not found in meta
|
|
110
|
+
* opts的token必须是流水线的CNB_TOKEN,不能是用户的token
|
|
83
111
|
* @param repo
|
|
84
112
|
* @param chunksData
|
|
85
113
|
* @returns
|
|
86
114
|
*/
|
|
87
|
-
addDocument(repo: string,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
115
|
+
addDocument(repo: string, AddDocument: AddDocument, opts?: RequestOptions): Promise<Result<null>> {
|
|
116
|
+
const url = `/${repo}/-/knowledge/documents/upsert-document-with-chunks`;
|
|
117
|
+
return this.post({ url, data: AddDocument, ...opts });
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* 未暴露
|
|
121
|
+
* @param repo
|
|
122
|
+
* @param paths
|
|
93
123
|
size: number;
|
|
94
124
|
}>;
|
|
95
125
|
}): Promise<Result<any>> {
|
|
@@ -114,7 +144,7 @@ export class KnowledgeBase extends CNBCore {
|
|
|
114
144
|
* @param page_size
|
|
115
145
|
* @returns
|
|
116
146
|
*/
|
|
117
|
-
|
|
147
|
+
listDocuments(repo: string, page: number = 1, page_size: number = 50): Promise<Result<any[]>> {
|
|
118
148
|
const url = `/${repo}/-/knowledge/documents`;
|
|
119
149
|
return this.get({ url, params: { page, page_size } });
|
|
120
150
|
}
|
|
@@ -143,4 +173,33 @@ type QueryRag = {
|
|
|
143
173
|
type: string; // code, text
|
|
144
174
|
url: string;
|
|
145
175
|
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
type AddDocument = {
|
|
179
|
+
// 文档路径,必填, 例如:docs/xxx.md
|
|
180
|
+
path: string;
|
|
181
|
+
// 文件名 xxx
|
|
182
|
+
name: string;
|
|
183
|
+
// 文件扩展名,例如:md
|
|
184
|
+
extension: string;
|
|
185
|
+
// 文件大小,单位:字节
|
|
186
|
+
size: number;
|
|
187
|
+
// 文件内容的 hash,(hashlib.md5(content.encode('utf-8')).hexdigest())
|
|
188
|
+
hash: string;
|
|
189
|
+
type: "code" | "issue"; // code, pdf
|
|
190
|
+
// 类似 https://cnb.cool/kevisual/starred-auto/-/blob/62ce3d724f6e5f2cfd4b84a1df7cf4a6eaf441d6/docs/11730342.md
|
|
191
|
+
url: string;
|
|
192
|
+
chunks: Array<{
|
|
193
|
+
// 文档的分块的内容
|
|
194
|
+
"content": string,
|
|
195
|
+
"hash": string,
|
|
196
|
+
"position": number,
|
|
197
|
+
"embedding": number[]
|
|
198
|
+
}>;
|
|
199
|
+
// 处理当前批次的索引, 从0开始
|
|
200
|
+
batch_index: number;
|
|
201
|
+
// 当前批次的总数, 必须大于0
|
|
202
|
+
batch_count: number;
|
|
203
|
+
// 是否是最后一批
|
|
204
|
+
is_last_batch: boolean;
|
|
146
205
|
}
|
package/src/labels/repo-label.ts
CHANGED
|
@@ -151,6 +151,22 @@ export class RepoLabel extends CNBCore {
|
|
|
151
151
|
const url = `/${repo}/-/labels/${encodeURIComponent(name)}`;
|
|
152
152
|
return this.delete({ url });
|
|
153
153
|
}
|
|
154
|
+
async removeLabels(opts: { repo: string, labels: string[] }): Promise<Result> {
|
|
155
|
+
const { repo, labels } = opts;
|
|
156
|
+
try {
|
|
157
|
+
for (const label of labels) {
|
|
158
|
+
const res = await this.remove(repo, label);
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // 避免请求过快导致服务器拒绝服务
|
|
160
|
+
if (res.code !== 200) {
|
|
161
|
+
console.error(`删除标签失败 ${label}:`, res.message);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { code: 200, data: labels, message: '删除标签成功' };
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return { code: 500, message: '删除标签失败', data: error };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
type ListLabelsParams = {
|
package/src/repo/download.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
type DownloadFn = (url: string, opts?: { headers?: any, hash?: string, token?: string }) => Promise<{
|
|
1
|
+
type DownloadFn = (url: string, opts?: { headers?: any, hash?: string, token?: string, stream?: boolean }) => Promise<{
|
|
2
|
+
code: number;
|
|
3
|
+
buffer: Buffer | null;
|
|
4
|
+
stream?: ReadableStream<Uint8Array> | null;
|
|
5
|
+
}>;
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* 下载文件内容,使用CNB API,支持文件未修改时返回304状态码
|
|
@@ -17,6 +21,7 @@ export const downloadByCNBApi: DownloadFn = async (url, opts) => {
|
|
|
17
21
|
const res = await fetch(url, {
|
|
18
22
|
headers: headers
|
|
19
23
|
});
|
|
24
|
+
const stream = opts?.stream || false;
|
|
20
25
|
type CNBResponse = {
|
|
21
26
|
type?: 'blob' | 'file' | 'dir' | 'lfs';
|
|
22
27
|
size?: number;
|
|
@@ -48,6 +53,15 @@ export const downloadByCNBApi: DownloadFn = async (url, opts) => {
|
|
|
48
53
|
return { code: 304, buffer: null };
|
|
49
54
|
}
|
|
50
55
|
const buffer = Buffer.from(resJson.content, 'base64');
|
|
56
|
+
if (stream) {
|
|
57
|
+
const readableStream = new ReadableStream<Uint8Array>({
|
|
58
|
+
start(controller) {
|
|
59
|
+
controller.enqueue(buffer);
|
|
60
|
+
controller.close();
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return { code: 200, buffer: null, stream: readableStream };
|
|
64
|
+
}
|
|
51
65
|
return { code: 200, buffer };
|
|
52
66
|
}
|
|
53
67
|
}
|
|
@@ -63,6 +77,10 @@ export const downloadByCNBApi: DownloadFn = async (url, opts) => {
|
|
|
63
77
|
// 文件未修改,跳过下载
|
|
64
78
|
return { code: 304, buffer: null };
|
|
65
79
|
}
|
|
80
|
+
if (stream) {
|
|
81
|
+
const readableStream = lfsRes.body;
|
|
82
|
+
return { code: 200, buffer: null, stream: readableStream };
|
|
83
|
+
}
|
|
66
84
|
const arrayBuffer = await lfsRes.arrayBuffer();
|
|
67
85
|
return { code: 200, buffer: Buffer.from(arrayBuffer) };
|
|
68
86
|
}
|
|
@@ -75,4 +93,73 @@ export const downloadByCNBApi: DownloadFn = async (url, opts) => {
|
|
|
75
93
|
console.error(`下载失败 ${url}: ${res.statusText}`, resJson);
|
|
76
94
|
return { code: res.status, buffer: null };
|
|
77
95
|
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
export const downloadResource: DownloadFn = async (url, opts) => {
|
|
100
|
+
const headers = opts?.headers || {};
|
|
101
|
+
const res = await fetch(url, { headers });
|
|
102
|
+
const stream = opts?.stream || false;
|
|
103
|
+
const etag = res.headers.get('ETag');
|
|
104
|
+
if (opts?.hash && etag === opts.hash) {
|
|
105
|
+
// 文件未修改,跳过下载
|
|
106
|
+
return { code: 304, buffer: null };
|
|
107
|
+
}
|
|
108
|
+
if (res.ok) {
|
|
109
|
+
if (stream) {
|
|
110
|
+
const readableStream = res.body;
|
|
111
|
+
return { code: 200, buffer: null, stream: readableStream };
|
|
112
|
+
}
|
|
113
|
+
const arrayBuffer = await res.arrayBuffer();
|
|
114
|
+
return { code: 200, buffer: Buffer.from(arrayBuffer) };
|
|
115
|
+
}
|
|
116
|
+
console.error(`下载失败 ${url}: ${res.statusText}`);
|
|
117
|
+
return { code: res.status, buffer: null };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const isCNBOpenApi = (url: string) => { return url.startsWith('https://api.cnb.cool') }
|
|
121
|
+
|
|
122
|
+
export const download: DownloadFn = async (url, opts) => {
|
|
123
|
+
const isCNB = isCNBOpenApi(url);
|
|
124
|
+
if (isCNB) {
|
|
125
|
+
return await downloadByCNBApi(url, opts);
|
|
126
|
+
} else {
|
|
127
|
+
return await downloadResource(url, opts);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const getLfsDownloadUrl = async (url: string, opts?: { headers?: any, token?: string }) => {
|
|
132
|
+
const headers = {
|
|
133
|
+
Accept: "application/vnd.cnb.api+json",
|
|
134
|
+
...(opts?.token ? { Authorization: `${opts.token}` } : {}),
|
|
135
|
+
...opts?.headers
|
|
136
|
+
}
|
|
137
|
+
const res = await fetch(url, {
|
|
138
|
+
headers: headers
|
|
139
|
+
});
|
|
140
|
+
type CNBResponse = {
|
|
141
|
+
type?: 'blob' | 'file' | 'dir' | 'lfs';
|
|
142
|
+
size?: number;
|
|
143
|
+
path: string;
|
|
144
|
+
name: string;
|
|
145
|
+
sha?: string;
|
|
146
|
+
encoding?: string;
|
|
147
|
+
content?: string;
|
|
148
|
+
|
|
149
|
+
lfs_oid?: string;
|
|
150
|
+
lfs_size?: number;
|
|
151
|
+
lfs_download_url?: string;
|
|
152
|
+
}
|
|
153
|
+
let resJson: CNBResponse;
|
|
154
|
+
try {
|
|
155
|
+
resJson = await res.json() as CNBResponse;
|
|
156
|
+
} catch (e) {
|
|
157
|
+
console.error(`获取LFS下载链接失败 ${url}: ${res.statusText}`, e);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
if (res.ok && resJson.type === 'lfs' && resJson.lfs_download_url) {
|
|
161
|
+
return resJson.lfs_download_url;
|
|
162
|
+
}
|
|
163
|
+
console.error(`获取LFS下载链接失败 ${url}: ${res.statusText}`, resJson);
|
|
164
|
+
return null;
|
|
78
165
|
}
|