@numa-tech/numa 1.20.3 → 1.20.4
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/README.md +8 -0
- package/dist/application-onboarding/schemas.d.ts +24 -0
- package/dist/application-onboarding/schemas.js +4 -0
- package/dist/application-onboarding/schemas.js.map +1 -1
- package/dist/application-onboarding/tui-model.d.ts +1 -0
- package/dist/application-onboarding/tui-model.js +10 -0
- package/dist/application-onboarding/tui-model.js.map +1 -1
- package/dist/application-onboarding/tui.js +1 -1
- package/dist/application-onboarding/tui.js.map +1 -1
- package/dist/authorization/application-client.d.ts +36 -1
- package/dist/authorization/application-client.js +4 -2
- package/dist/authorization/application-client.js.map +1 -1
- package/dist/authorization/application-commands.js +4 -3
- package/dist/authorization/application-commands.js.map +1 -1
- package/dist/authorization/application-schemas.d.ts +42 -7
- package/dist/authorization/application-schemas.js +19 -3
- package/dist/authorization/application-schemas.js.map +1 -1
- package/dist/business-issuer.js +8 -0
- package/dist/business-issuer.js.map +1 -1
- package/dist/cli.js +49 -31
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.js +9 -0
- package/dist/command-catalog.js.map +1 -1
- package/dist/kb/catalog.d.ts +2 -0
- package/dist/kb/catalog.js +14 -0
- package/dist/kb/catalog.js.map +1 -0
- package/dist/kb/client.d.ts +46 -0
- package/dist/kb/client.js +272 -0
- package/dist/kb/client.js.map +1 -0
- package/dist/kb/commands.d.ts +14 -0
- package/dist/kb/commands.js +121 -0
- package/dist/kb/commands.js.map +1 -0
- package/dist/kb/config.d.ts +15 -0
- package/dist/kb/config.js +76 -0
- package/dist/kb/config.js.map +1 -0
- package/dist/kb/errors.d.ts +9 -0
- package/dist/kb/errors.js +31 -0
- package/dist/kb/errors.js.map +1 -0
- package/dist/kb/transfers.d.ts +67 -0
- package/dist/kb/transfers.js +257 -0
- package/dist/kb/transfers.js.map +1 -0
- package/dist/kb/types.d.ts +81 -0
- package/dist/kb/types.js +2 -0
- package/dist/kb/types.js.map +1 -0
- package/dist/kb/upload-state.d.ts +20 -0
- package/dist/kb/upload-state.js +135 -0
- package/dist/kb/upload-state.js.map +1 -0
- package/dist/login-recovery.js +15 -1
- package/dist/login-recovery.js.map +1 -1
- package/dist/maintenance/modules.js +1 -1
- package/dist/maintenance/modules.js.map +1 -1
- package/dist/mcp/catalog.js +6 -1
- package/dist/mcp/catalog.js.map +1 -1
- package/dist/oss/signed-url-transfers.d.ts +41 -0
- package/dist/oss/signed-url-transfers.js +207 -0
- package/dist/oss/signed-url-transfers.js.map +1 -0
- package/dist/oss/transfer-pool.d.ts +2 -0
- package/dist/oss/transfer-pool.js +27 -0
- package/dist/oss/transfer-pool.js.map +1 -0
- package/dist/oss/transfer-worker.js +8 -17
- package/dist/oss/transfer-worker.js.map +1 -1
- package/dist/oss/web-page.js +4 -3
- package/dist/oss/web-page.js.map +1 -1
- package/dist/platform-profile.d.ts +1 -1
- package/dist/server.d.ts +2 -0
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/tasks/commands.d.ts +9 -0
- package/dist/tasks/commands.js +60 -0
- package/dist/tasks/commands.js.map +1 -0
- package/dist/tasks/oss-query.d.ts +56 -0
- package/dist/tasks/oss-query.js +125 -0
- package/dist/tasks/oss-query.js.map +1 -0
- package/dist/tasks/runtime.d.ts +151 -0
- package/dist/tasks/runtime.js +71 -0
- package/dist/tasks/runtime.js.map +1 -0
- package/dist/tasks/tools.d.ts +3 -0
- package/dist/tasks/tools.js +16 -0
- package/dist/tasks/tools.js.map +1 -0
- package/dist/tasks/types.d.ts +14 -0
- package/dist/tasks/types.js +4 -0
- package/dist/tasks/types.js.map +1 -0
- package/dist/tasks/wecom-notify.d.ts +26 -0
- package/dist/tasks/wecom-notify.js +150 -0
- package/dist/tasks/wecom-notify.js.map +1 -0
- package/docs/agent-tasks.md +97 -0
- package/docs/application-default-groups.md +10 -2
- package/docs/design/kb-cli.md +94 -0
- package/docs/kb-access.md +85 -0
- package/package.json +12 -6
- package/scripts/export-kb-contract.py +158 -0
- package/skills/numa-ai-registry/SKILL.md +2 -2
- package/skills/numa-cli/SKILL.md +24 -9
- package/skills/numa-cli/agents/openai.yaml +2 -2
- package/skills/numa-cli/evals/evals.json +13 -1
- package/skills/numa-cli/numa-skill.json +1 -1
- package/skills/numa-cli/references/cli-bootstrap.md +8 -8
- package/skills/numa-cli/references/cli-mcp.md +28 -20
- package/skills/numa-cli/references/pipeline-release.md +9 -9
- package/skills/numa-cli/references/skills-command-contract.md +40 -40
- package/skills/numa-cli/references/skills-updates.md +13 -13
- package/skills/numa-cli/references/skills-workflows.md +4 -4
- package/skills/numa-create-application/SKILL.md +1 -1
- package/skills/numa-create-application/numa-skill.json +2 -2
- package/skills/numa-create-application/references/cli-bootstrap.md +3 -3
- package/skills/numa-create-application/references/numa-cli.md +16 -16
- package/skills/numa-jenkins-deployment/SKILL.md +8 -8
- package/skills/numa-jenkins-deployment/numa-skill.json +2 -2
- package/skills/numa-jenkins-deployment/references/cli-bootstrap.md +3 -3
- package/skills/numa-jenkins-deployment/references/cli-contract.md +7 -7
- package/skills/numa-jenkins-deployment/references/diagnostics.md +2 -2
- package/skills/numa-local-flux-deploy/SKILL.md +1 -1
- package/skills/numa-local-flux-deploy/numa-skill.json +2 -2
- package/skills/numa-local-flux-deploy/references/cli-bootstrap.md +3 -3
- package/skills/numa-nacos/SKILL.md +4 -4
- package/skills/numa-nacos/numa-skill.json +2 -2
- package/skills/numa-nacos/references/cli-bootstrap.md +3 -3
- package/skills/numa-nacos/references/command-contract.md +17 -17
- package/skills/numa-notion/SKILL.md +4 -4
- package/skills/numa-notion/numa-skill.json +2 -2
- package/skills/numa-notion/references/agents-automation.md +2 -2
- package/skills/numa-notion/references/cli-bootstrap.md +3 -3
- package/skills/numa-notion/references/cli.md +17 -17
- package/skills/numa-oss-access/SKILL.md +27 -81
- package/skills/numa-oss-access/agents/openai.yaml +2 -2
- package/skills/numa-oss-access/evals/evals.json +12 -0
- package/skills/numa-oss-access/numa-skill.json +2 -2
- package/skills/numa-oss-access/references/cli-bootstrap.md +5 -5
- package/skills/numa-oss-access/references/object-operations.md +95 -0
- package/skills/numa-oss-access/references/search-and-mcp.md +12 -11
- package/skills/numa-wecom/SKILL.md +27 -62
- package/skills/numa-wecom/agents/openai.yaml +2 -2
- package/skills/numa-wecom/evals/evals.json +25 -0
- package/skills/numa-wecom/numa-skill.json +2 -2
- package/skills/numa-wecom/references/cli-bootstrap.md +5 -5
- package/skills/numa-wecom/references/legacy-operations.md +71 -0
- package/skills/numa-wecom/references/machine-auth.md +7 -7
- package/skills/numa-wecom/references/mcp.md +9 -8
- package/src/kb/openapi-contract.json +1432 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { KbClient } from "./client.js";
|
|
3
|
+
import { loadKbConfig } from "./config.js";
|
|
4
|
+
import { KbError, sanitizeKbOutput } from "./errors.js";
|
|
5
|
+
import { uploadToKb, downloadFromKb, waitForTask } from "./transfers.js";
|
|
6
|
+
import { KB_COMMAND_CATALOG } from "./catalog.js";
|
|
7
|
+
export { KB_COMMAND_CATALOG };
|
|
8
|
+
function integer(value, name, max) {
|
|
9
|
+
if (!/^\d+$/u.test(value) || Number(value) < 1 || Number(value) > max) {
|
|
10
|
+
throw new KbError(`${name} 必须是 1-${max} 的整数。`, "kb_input_invalid");
|
|
11
|
+
}
|
|
12
|
+
return Number(value);
|
|
13
|
+
}
|
|
14
|
+
function taskKind(id, kind) {
|
|
15
|
+
if (kind !== undefined && kind !== "archive" && kind !== "batch") {
|
|
16
|
+
throw new KbError("--kind 必须是 archive 或 batch。", "kb_input_invalid");
|
|
17
|
+
}
|
|
18
|
+
return kind ?? (id.startsWith("ext_") ? "batch" : "archive");
|
|
19
|
+
}
|
|
20
|
+
async function interruptible(work) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const abort = () => controller.abort();
|
|
23
|
+
process.once("SIGINT", abort);
|
|
24
|
+
process.once("SIGTERM", abort);
|
|
25
|
+
try {
|
|
26
|
+
return await work(controller.signal);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
process.removeListener("SIGINT", abort);
|
|
30
|
+
process.removeListener("SIGTERM", abort);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function registerKbCommands(program, dependencies = {}) {
|
|
34
|
+
const client = async () => dependencies.clientFactory?.() ?? new KbClient(await loadKbConfig());
|
|
35
|
+
const output = (value) => (dependencies.write ?? ((text) => process.stdout.write(text)))(`${JSON.stringify({ ok: true, data: sanitizeKbOutput(value) }, null, 2)}\n`);
|
|
36
|
+
// Only explicit progress fields are rendered; tickets and response payloads never reach stderr.
|
|
37
|
+
const progress = (value) => {
|
|
38
|
+
if (!value || typeof value !== "object")
|
|
39
|
+
return;
|
|
40
|
+
const item = value;
|
|
41
|
+
const parts = [];
|
|
42
|
+
if (typeof item.phase === "string" && /^[a-z_-]{1,40}$/u.test(item.phase))
|
|
43
|
+
parts.push(item.phase);
|
|
44
|
+
if (typeof item.transferred === "number" && Number.isFinite(item.transferred))
|
|
45
|
+
parts.push(`${item.transferred} bytes`);
|
|
46
|
+
if (parts.length)
|
|
47
|
+
(dependencies.writeProgress ?? ((text) => process.stderr.write(text)))(`${parts.join(" ")}\n`);
|
|
48
|
+
};
|
|
49
|
+
const kb = program.command("kb").description("知识库检索、原件传输和入库任务;通过 KB REST API 复用 OSS 传输")
|
|
50
|
+
.addHelpText("after", "\n配置:Profile services.kb 或 NUMA_KB_BASE_URL;NUMA_KB_API_KEY / NUMA_KB_API_KEY_FILE。\n默认使用 Numa Keycloak 登录,NUMA_KB_AUTH_MODE=api-key 可显式选择仅 API Key 的部署。\n");
|
|
51
|
+
const collections = kb.command("collections").description("当前身份可见的知识库");
|
|
52
|
+
collections.command("list").description("列出知识库").action(async () => output(await (await client()).listCollections()));
|
|
53
|
+
collections.command("create").description("创建私有任务知识库")
|
|
54
|
+
.requiredOption("--name <name>", "知识库名称")
|
|
55
|
+
.option("--description <text>", "知识库说明")
|
|
56
|
+
.action(async (options) => {
|
|
57
|
+
if (!options.name.trim() || options.name.length > 50)
|
|
58
|
+
throw new KbError("名称须为 1-50 个字符。", "kb_input_invalid");
|
|
59
|
+
output(await (await client()).createCollection({ name: options.name, description: options.description }));
|
|
60
|
+
});
|
|
61
|
+
kb.command("search <query>").description("检索文档内容与来源")
|
|
62
|
+
.requiredOption("--collection-id <id>", "目标知识库 ID")
|
|
63
|
+
.option("--top-k <number>", "返回片段数 1-10", "5")
|
|
64
|
+
.option("--min-score <number>", "最低相关度 0-1", "0")
|
|
65
|
+
.action(async (query, options) => {
|
|
66
|
+
const top = integer(options.topK, "--top-k", 10);
|
|
67
|
+
const score = Number(options.minScore);
|
|
68
|
+
if (!query.trim() || !options.minScore.trim() || !Number.isFinite(score) || score < 0 || score > 1) {
|
|
69
|
+
throw new KbError("查询不能为空,--min-score 须为 0-1。", "kb_input_invalid");
|
|
70
|
+
}
|
|
71
|
+
output(await (await client()).search({ query, collection_id: options.collectionId, top_k: top, min_score: score }));
|
|
72
|
+
});
|
|
73
|
+
kb.command("upload <path>").description("上传普通文件或递归目录,并提交入库;支持文件级恢复")
|
|
74
|
+
.requiredOption("--collection-id <id>", "目标知识库 ID")
|
|
75
|
+
.option("--state-file <path>", "本地私有恢复记录,自动生成时在结果中返回")
|
|
76
|
+
.option("--resume", "使用既有恢复记录继续;须同时指定 --state-file")
|
|
77
|
+
.option("--batch-size <number>", "每批文件数 1-50", "50")
|
|
78
|
+
.option("--concurrency <number>", "同时传输文件数 1-10", "3")
|
|
79
|
+
.option("--wait", "等待解析入库完成")
|
|
80
|
+
.option("--timeout <seconds>", "等待入库的最长秒数", "600")
|
|
81
|
+
.action(async (path, options) => {
|
|
82
|
+
const batchSize = integer(options.batchSize, "--batch-size", 50);
|
|
83
|
+
const concurrency = integer(options.concurrency, "--concurrency", 10);
|
|
84
|
+
const timeoutMs = integer(options.timeout, "--timeout", 86_400) * 1000;
|
|
85
|
+
if (options.resume && !options.stateFile)
|
|
86
|
+
throw new KbError("--resume 必须同时指定 --state-file。", "kb_input_invalid");
|
|
87
|
+
const api = await client();
|
|
88
|
+
output(await interruptible(signal => (dependencies.upload ?? uploadToKb)(api, {
|
|
89
|
+
path, collectionId: options.collectionId, stateFile: options.stateFile, resume: options.resume,
|
|
90
|
+
batchSize, concurrency, wait: options.wait, timeoutMs, signal, onProgress: progress,
|
|
91
|
+
})));
|
|
92
|
+
});
|
|
93
|
+
kb.command("download <document-id>").description("下载已授权原件,校验后保存;不覆盖现有文件")
|
|
94
|
+
.requiredOption("--output <path>", "保存到指定文件路径")
|
|
95
|
+
.action(async (documentId, options) => {
|
|
96
|
+
const api = await client();
|
|
97
|
+
output(await interruptible(signal => (dependencies.download ?? downloadFromKb)(api, {
|
|
98
|
+
documentId, output: options.output, signal, onProgress: progress,
|
|
99
|
+
})));
|
|
100
|
+
});
|
|
101
|
+
const tasks = kb.command("tasks").description("查看和重试解析入库任务");
|
|
102
|
+
tasks.command("status <task-id>").description("查询任务及逐文件结果")
|
|
103
|
+
.option("--kind <kind>", "archive 或 batch;ext_ 前缀默认 batch")
|
|
104
|
+
.option("--wait", "等待终态;失败、部分失败或需要处理冲突时返回非零退出码")
|
|
105
|
+
.option("--timeout <seconds>", "最长等待秒数", "600")
|
|
106
|
+
.action(async (id, options) => {
|
|
107
|
+
const kind = taskKind(id, options.kind);
|
|
108
|
+
const timeoutMs = integer(options.timeout, "--timeout", 86_400) * 1000;
|
|
109
|
+
const api = await client();
|
|
110
|
+
output(options.wait
|
|
111
|
+
? await interruptible(signal => (dependencies.wait ?? waitForTask)(api, id, { kind, timeoutMs, signal }))
|
|
112
|
+
: await api.getTask(id, kind));
|
|
113
|
+
});
|
|
114
|
+
tasks.command("retry <task-id>").description("显式重试可恢复的失败入库项")
|
|
115
|
+
.option("--kind <kind>", "archive 或 batch;ext_ 前缀默认 batch")
|
|
116
|
+
.action(async (id, options) => {
|
|
117
|
+
const kind = taskKind(id, options.kind);
|
|
118
|
+
output(await (await client()).retryTask(id, kind));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/kb/commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AuthConfig } from "../config.js";
|
|
2
|
+
export interface KbConfig {
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
authMode: "keycloak" | "api-key";
|
|
6
|
+
auth?: AuthConfig;
|
|
7
|
+
tenantHeader?: {
|
|
8
|
+
name: string;
|
|
9
|
+
value: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export declare function validateKbBaseUrl(value: string): string;
|
|
13
|
+
export declare function loadKbConfig(): Promise<KbConfig>;
|
|
14
|
+
/** Uses the same governed authority and token cache as KB requests, without requiring a KB API key. */
|
|
15
|
+
export declare function loadKbAuthConfig(): AuthConfig;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { appConfigDirectory } from "../app-config.js";
|
|
5
|
+
import { loadConfig } from "../config.js";
|
|
6
|
+
import { requireProfileService } from "../platform-profile.js";
|
|
7
|
+
import { KbError } from "./errors.js";
|
|
8
|
+
export function validateKbBaseUrl(value) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(value);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw new KbError("KB base URL is invalid.", "kb_config_invalid");
|
|
15
|
+
}
|
|
16
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
17
|
+
if ((url.protocol !== "https:" && !(url.protocol === "http:" && local)) || url.username || url.password || url.search || url.hash) {
|
|
18
|
+
throw new KbError("KB base URL must use HTTPS without credentials, query or fragment (loopback HTTP allowed).", "kb_config_invalid");
|
|
19
|
+
}
|
|
20
|
+
return url.toString().replace(/\/+$/u, "");
|
|
21
|
+
}
|
|
22
|
+
export async function loadKbConfig() {
|
|
23
|
+
const selected = requireProfileService("kb");
|
|
24
|
+
const explicit = process.env.NUMA_KB_BASE_URL?.trim();
|
|
25
|
+
const base = selected?.service.baseUrl ?? explicit;
|
|
26
|
+
if (!base)
|
|
27
|
+
throw new KbError("Configure services.kb in the platform profile or NUMA_KB_BASE_URL.", "kb_config_missing");
|
|
28
|
+
const baseUrl = validateKbBaseUrl(base);
|
|
29
|
+
if (selected && explicit && validateKbBaseUrl(explicit) !== baseUrl) {
|
|
30
|
+
throw new KbError("NUMA_KB_BASE_URL cannot override the profile KB service.", "kb_config_invalid");
|
|
31
|
+
}
|
|
32
|
+
const mode = process.env.NUMA_KB_AUTH_MODE?.trim() || "keycloak";
|
|
33
|
+
if (mode !== "keycloak" && mode !== "api-key")
|
|
34
|
+
throw new KbError("NUMA_KB_AUTH_MODE must be keycloak or api-key.", "kb_config_invalid");
|
|
35
|
+
const keyEnv = process.env.NUMA_KB_API_KEY;
|
|
36
|
+
const keyFile = process.env.NUMA_KB_API_KEY_FILE;
|
|
37
|
+
if (keyEnv !== undefined && keyFile !== undefined)
|
|
38
|
+
throw new KbError("Set only one of NUMA_KB_API_KEY and NUMA_KB_API_KEY_FILE.", "kb_config_invalid");
|
|
39
|
+
let apiKey = keyEnv?.trim();
|
|
40
|
+
if (keyFile) {
|
|
41
|
+
try {
|
|
42
|
+
apiKey = (await readFile(keyFile, "utf8")).trim();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new KbError("Cannot read NUMA_KB_API_KEY_FILE.", "kb_config_invalid");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!apiKey || /[\u0000-\u0020\u007f]/u.test(apiKey))
|
|
49
|
+
throw new KbError("A valid KB API key is required.", "kb_config_missing");
|
|
50
|
+
const auth = mode === "keycloak" ? loadKbAuthConfig() : undefined;
|
|
51
|
+
return { baseUrl, apiKey, authMode: mode, auth,
|
|
52
|
+
...(selected ? { tenantHeader: { name: selected.profile.tenant.headerName, value: selected.profile.tenant.id } } : {}) };
|
|
53
|
+
}
|
|
54
|
+
/** Uses the same governed authority and token cache as KB requests, without requiring a KB API key. */
|
|
55
|
+
export function loadKbAuthConfig() {
|
|
56
|
+
const selected = requireProfileService("kb");
|
|
57
|
+
const explicit = process.env.NUMA_KB_BASE_URL?.trim();
|
|
58
|
+
const base = selected?.service.baseUrl ?? explicit;
|
|
59
|
+
if (!base)
|
|
60
|
+
throw new KbError("Configure services.kb or NUMA_KB_BASE_URL.", "kb_config_missing");
|
|
61
|
+
const baseUrl = validateKbBaseUrl(base);
|
|
62
|
+
if (selected && explicit && validateKbBaseUrl(explicit) !== baseUrl)
|
|
63
|
+
throw new KbError("NUMA_KB_BASE_URL cannot override the profile KB service.", "kb_config_invalid");
|
|
64
|
+
let auth = loadConfig("kb");
|
|
65
|
+
if (selected) {
|
|
66
|
+
// Older profiles allow global overrides in loadConfig; KB always honors its declared authority.
|
|
67
|
+
const clientId = auth.authenticationMode === "client_credentials" ? auth.clientId : selected.authority.clientId;
|
|
68
|
+
const tokenKey = createHash("sha256").update(`${selected.authority.issuer}\0${clientId}\0${selected.profile.tenant.id}\0${selected.service.audience}`).digest("hex").slice(0, 24);
|
|
69
|
+
auth = { ...auth, discoveryUrl: selected.authority.discoveryUrl, issuerUrl: selected.authority.issuer,
|
|
70
|
+
clientId, resourceAudience: selected.service.audience, profileTenantId: selected.profile.tenant.id,
|
|
71
|
+
tokenFile: join(appConfigDirectory(), `keycloak-token-${tokenKey}.json`),
|
|
72
|
+
scope: auth.authenticationMode === "client_credentials" ? auth.scope : selected.authority.scopes.join(" ") };
|
|
73
|
+
}
|
|
74
|
+
return { ...auth, backendBaseUrl: baseUrl };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/kb/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Only locally generated messages belong here; never interpolate remote response bodies. */
|
|
2
|
+
export declare class KbError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly status?: number | undefined;
|
|
5
|
+
readonly details?: Record<string, unknown> | undefined;
|
|
6
|
+
constructor(message: string, code: string, status?: number | undefined, details?: Record<string, unknown> | undefined);
|
|
7
|
+
}
|
|
8
|
+
/** Output-only projection. Transport code must keep using the original ticket. */
|
|
9
|
+
export declare function sanitizeKbOutput(value: unknown): unknown;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Only locally generated messages belong here; never interpolate remote response bodies. */
|
|
2
|
+
export class KbError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
details;
|
|
6
|
+
constructor(message, code, status, details) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.details = details;
|
|
11
|
+
this.name = "KbError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const secretKey = /(?:authorization|api.?key|access.?token|refresh.?token|id.?token|secret|password|cookie|credential|signature|ticket|^token$|^upload$|^headers$)/iu;
|
|
15
|
+
const urlKey = /(?:url|uri)$/iu;
|
|
16
|
+
/** Output-only projection. Transport code must keep using the original ticket. */
|
|
17
|
+
export function sanitizeKbOutput(value) {
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return value.map(sanitizeKbOutput);
|
|
20
|
+
if (value && typeof value === "object") {
|
|
21
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !secretKey.test(key) && !urlKey.test(key))
|
|
22
|
+
.map(([key, item]) => [key, sanitizeKbOutput(item)]));
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === "string") {
|
|
25
|
+
return value.replace(/https?:\/\/[^\s<>"']+/giu, "[redacted URL]")
|
|
26
|
+
.replace(/\bBearer\s+[^\s,;]+/giu, "Bearer [redacted]")
|
|
27
|
+
.replace(/((?:api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|password|secret)\s*[:=]\s*)[^\s,;]+/giu, "$1[redacted]");
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/kb/errors.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,MAAM,OAAO,OAAQ,SAAQ,KAAK;IACM,IAAI;IAAmB,MAAM;IAAoB,OAAO;IAA9F,YAAY,OAAe,EAAW,IAAY,EAAW,MAAe,EAAW,OAAiC;QACtH,KAAK,CAAC,OAAO,CAAC,CAAC;oBADqB,IAAI;sBAAmB,MAAM;uBAAoB,OAAO;QAE5F,IAAI,CAAC,IAAI,GAAG,SAAS,CAAC;IACxB,CAAC;CACF;AAED,MAAM,SAAS,GAAG,mJAAmJ,CAAC;AACtK,MAAM,MAAM,GAAG,gBAAgB,CAAC;AAEhC,kFAAkF;AAClF,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAC7D,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;aACzG,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC,OAAO,CAAC,0BAA0B,EAAE,gBAAgB,CAAC;aAC/D,OAAO,CAAC,wBAAwB,EAAE,mBAAmB,CAAC;aACtD,OAAO,CAAC,8FAA8F,EAAE,cAAc,CAAC,CAAC;IAC7H,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { BeginUploadRequest, DirectUploadSession, UploadSession, DownloadTicket, ProxyUploadRequest, KbTask, TaskKind } from './types.js';
|
|
2
|
+
export interface KbTransferClient {
|
|
3
|
+
readonly binding: string;
|
|
4
|
+
beginUpload(input: BeginUploadRequest): Promise<UploadSession>;
|
|
5
|
+
getUpload(sessionId: string, renew?: boolean): Promise<DirectUploadSession>;
|
|
6
|
+
completeUpload(sessionId: string): Promise<DirectUploadSession>;
|
|
7
|
+
proxyUpload(input: ProxyUploadRequest): Promise<KbTask>;
|
|
8
|
+
downloadTicket(documentId: string): Promise<DownloadTicket>;
|
|
9
|
+
downloadProxy(ticket: DownloadTicket, signal?: AbortSignal): Promise<Response>;
|
|
10
|
+
getTask(taskId: string, kind?: TaskKind): Promise<KbTask>;
|
|
11
|
+
}
|
|
12
|
+
export interface TransferProgress {
|
|
13
|
+
phase: string;
|
|
14
|
+
filename?: string;
|
|
15
|
+
transferred?: number;
|
|
16
|
+
total?: number;
|
|
17
|
+
taskId?: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface UploadInput {
|
|
21
|
+
path: string;
|
|
22
|
+
collectionId: string;
|
|
23
|
+
stateFile?: string;
|
|
24
|
+
resume?: boolean;
|
|
25
|
+
batchSize?: number;
|
|
26
|
+
concurrency?: number;
|
|
27
|
+
wait?: boolean;
|
|
28
|
+
timeoutMs?: number;
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
onProgress?: (progress: TransferProgress) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface TransferDependencies {
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
stateRoot?: string;
|
|
35
|
+
pollIntervalMs?: number;
|
|
36
|
+
transferTimeoutMs?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare function uploadToKb(client: KbTransferClient, input: UploadInput, deps?: TransferDependencies): Promise<{
|
|
39
|
+
state_file: string;
|
|
40
|
+
collection_id: string;
|
|
41
|
+
files: number;
|
|
42
|
+
batches: {
|
|
43
|
+
batch_id: string;
|
|
44
|
+
kind: TaskKind;
|
|
45
|
+
status: string;
|
|
46
|
+
}[];
|
|
47
|
+
tasks?: KbTask[];
|
|
48
|
+
}>;
|
|
49
|
+
export interface WaitOptions {
|
|
50
|
+
kind?: TaskKind;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
pollIntervalMs?: number;
|
|
54
|
+
onProgress?: (progress: TransferProgress) => void;
|
|
55
|
+
}
|
|
56
|
+
export declare function waitForTask(client: Pick<KbTransferClient, 'getTask'>, taskId: string, options?: WaitOptions): Promise<KbTask>;
|
|
57
|
+
export declare function downloadFromKb(client: Pick<KbTransferClient, 'downloadTicket' | 'downloadProxy'>, input: {
|
|
58
|
+
documentId: string;
|
|
59
|
+
output: string;
|
|
60
|
+
signal?: AbortSignal;
|
|
61
|
+
onProgress?: (progress: TransferProgress) => void;
|
|
62
|
+
}, deps?: TransferDependencies): Promise<{
|
|
63
|
+
output: string;
|
|
64
|
+
size_bytes: number;
|
|
65
|
+
sha256: string;
|
|
66
|
+
document_id: string;
|
|
67
|
+
}>;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { lstat, readdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, join, relative, resolve, isAbsolute } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { runTransferPool } from '../oss/transfer-pool.js';
|
|
7
|
+
import { digestFile, putSignedFile, downloadSignedFile, rejectSymlinks, SignedTransferError } from '../oss/signed-url-transfers.js';
|
|
8
|
+
import { KbError } from './errors.js';
|
|
9
|
+
import { lockUploadState, readUploadState, writeUploadState } from './upload-state.js';
|
|
10
|
+
const fail = (message, code = 'kb_input_invalid', details) => new KbError(message, code, undefined, details);
|
|
11
|
+
async function scan(path, signal) {
|
|
12
|
+
const paths = [];
|
|
13
|
+
async function walk(current) {
|
|
14
|
+
signal?.throwIfAborted();
|
|
15
|
+
await rejectSymlinks(current);
|
|
16
|
+
const stat = await lstat(current);
|
|
17
|
+
if (stat.isDirectory()) {
|
|
18
|
+
for (const name of (await readdir(current)).sort())
|
|
19
|
+
await walk(join(current, name));
|
|
20
|
+
}
|
|
21
|
+
else if (stat.isFile())
|
|
22
|
+
paths.push(current);
|
|
23
|
+
else
|
|
24
|
+
throw fail('Only regular files and directories are accepted.');
|
|
25
|
+
}
|
|
26
|
+
await walk(resolve(path));
|
|
27
|
+
if (!paths.length)
|
|
28
|
+
throw fail('Upload directory has no files.');
|
|
29
|
+
const names = new Set(), result = [];
|
|
30
|
+
for (const path of paths.sort()) {
|
|
31
|
+
const filename = basename(path);
|
|
32
|
+
if (filename.length > 255 || /[\u0000-\u001f\u007f]/u.test(filename))
|
|
33
|
+
throw fail('Upload filename is invalid.');
|
|
34
|
+
if (names.has(filename))
|
|
35
|
+
throw fail('Upload contains duplicate filenames; use separate uploads for each directory.');
|
|
36
|
+
names.add(filename);
|
|
37
|
+
result.push({ ...await digestFile(path, signal), filename, content_type: 'application/octet-stream' });
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
function batches(files, batchSize) {
|
|
42
|
+
const result = [];
|
|
43
|
+
let indexes = [], bytes = 0;
|
|
44
|
+
function flush() { if (indexes.length)
|
|
45
|
+
result.push({ requestId: `numa-kb-${randomUUID()}`, fileIndexes: indexes }); indexes = []; bytes = 0; }
|
|
46
|
+
files.forEach((file, index) => { if (indexes.length >= batchSize || bytes + file.size_bytes > 1024 ** 3)
|
|
47
|
+
flush(); indexes.push(index); bytes += file.size_bytes; });
|
|
48
|
+
flush();
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
function verifySession(session, batch, state) {
|
|
52
|
+
if (session.collection_id !== state.collectionId || session.request_id !== batch.requestId || (batch.sessionId && session.session_id !== batch.sessionId) || (batch.batchId && session.batch_id !== batch.batchId) || session.files.length !== batch.fileIndexes.length)
|
|
53
|
+
throw fail('Upload session does not match the saved request.', 'kb_protocol_error');
|
|
54
|
+
const filenames = new Set();
|
|
55
|
+
for (const remote of session.files) {
|
|
56
|
+
const local = batch.fileIndexes.map(index => state.files[index]).find(file => file.filename === remote.filename);
|
|
57
|
+
if (!local || filenames.has(remote.filename) || remote.size_bytes !== local.size_bytes || remote.sha256 !== local.sha256 || remote.content_md5 !== local.content_md5 || remote.content_type !== local.content_type)
|
|
58
|
+
throw fail('Upload session content does not match the local manifest.', 'kb_protocol_error');
|
|
59
|
+
filenames.add(remote.filename);
|
|
60
|
+
}
|
|
61
|
+
if (session.status !== 'uploading' && session.status !== 'submitted')
|
|
62
|
+
throw fail('Upload session has expired or was cancelled; start a new upload with a new state file.', 'kb_session_unavailable');
|
|
63
|
+
}
|
|
64
|
+
export async function uploadToKb(client, input, deps = {}) {
|
|
65
|
+
const sourcePath = resolve(input.path), batchSize = input.batchSize ?? 50, concurrency = input.concurrency ?? 3;
|
|
66
|
+
if (!input.collectionId.trim() || !Number.isInteger(batchSize) || batchSize < 1 || batchSize > 50 || !Number.isInteger(concurrency) || concurrency < 1 || concurrency > 32)
|
|
67
|
+
throw fail('Batch size must be 1–50 and concurrency 1–32.');
|
|
68
|
+
const key = createHash('sha256').update(JSON.stringify([client.binding, input.collectionId, sourcePath])).digest('hex');
|
|
69
|
+
const stateFile = resolve(input.stateFile ?? join(deps.stateRoot ?? join(homedir(), '.numa', 'kb', 'uploads'), `${key}.json`));
|
|
70
|
+
const child = relative(sourcePath, stateFile);
|
|
71
|
+
if (!child || (!child.startsWith('..') && !isAbsolute(child)))
|
|
72
|
+
throw fail('Upload state must be outside the uploaded source path.');
|
|
73
|
+
const release = await lockUploadState(stateFile);
|
|
74
|
+
try {
|
|
75
|
+
input.signal?.throwIfAborted();
|
|
76
|
+
input.onProgress?.({ phase: 'scanning' });
|
|
77
|
+
const files = await scan(sourcePath, input.signal);
|
|
78
|
+
let state;
|
|
79
|
+
if (input.resume) {
|
|
80
|
+
state = await readUploadState(stateFile);
|
|
81
|
+
if (state.binding !== client.binding || state.collectionId !== input.collectionId || state.sourcePath !== sourcePath)
|
|
82
|
+
throw fail('Saved upload belongs to a different KB target or identity.', 'kb_state_binding_mismatch');
|
|
83
|
+
if (state.files.length !== files.length || state.files.some((saved, index) => { const current = files[index]; return saved.path !== current.path || saved.filename !== current.filename || saved.size_bytes !== current.size_bytes || saved.sha256 !== current.sha256 || saved.content_md5 !== current.content_md5 || saved.content_type !== current.content_type; }))
|
|
84
|
+
throw fail('Upload source differs from the saved manifest.', 'kb_source_changed');
|
|
85
|
+
if (state.batches.some(batch => batch.fileIndexes.reduce((sum, index) => sum + files[index].size_bytes, 0) > 1024 ** 3))
|
|
86
|
+
throw fail('Saved upload batch exceeds the size limit.', 'kb_state_invalid');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
try {
|
|
90
|
+
await lstat(stateFile);
|
|
91
|
+
throw fail('Upload state already exists; use --resume or choose a new --state-file.', 'kb_state_exists');
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code !== 'ENOENT')
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
state = { version: 1, binding: client.binding, collectionId: input.collectionId, sourcePath, files, batches: batches(files, batchSize) };
|
|
98
|
+
await writeUploadState(stateFile, state);
|
|
99
|
+
}
|
|
100
|
+
for (const batch of state.batches) {
|
|
101
|
+
input.signal?.throwIfAborted();
|
|
102
|
+
if (batch.submitted)
|
|
103
|
+
continue;
|
|
104
|
+
const locals = batch.fileIndexes.map(index => state.files[index]);
|
|
105
|
+
let session = batch.sessionId ? await client.getUpload(batch.sessionId, true) : batch.transport === 'proxy' ? { transport: 'proxy' } : await client.beginUpload({ collection_id: state.collectionId, request_id: batch.requestId, files: locals.map(({ path: _path, ...metadata }) => metadata) });
|
|
106
|
+
input.signal?.throwIfAborted();
|
|
107
|
+
if (session.transport === 'proxy') {
|
|
108
|
+
batch.transport = 'proxy';
|
|
109
|
+
await writeUploadState(stateFile, state);
|
|
110
|
+
// Verify immediately before handing files to the multipart adapter.
|
|
111
|
+
for (const file of locals)
|
|
112
|
+
if ((await digestFile(file.path, input.signal)).sha256 !== file.sha256)
|
|
113
|
+
throw fail('Upload source changed.', 'kb_source_changed');
|
|
114
|
+
const task = await client.proxyUpload({ collectionId: state.collectionId, requestId: batch.requestId, files: locals.map(file => ({ path: file.path, filename: file.filename })), signal: input.signal });
|
|
115
|
+
batch.batchId = task.batch_id;
|
|
116
|
+
batch.submitted = true;
|
|
117
|
+
await writeUploadState(stateFile, state);
|
|
118
|
+
for (const file of locals)
|
|
119
|
+
if ((await digestFile(file.path, input.signal)).sha256 !== file.sha256)
|
|
120
|
+
throw fail('Upload source changed during proxy upload.', 'kb_source_changed');
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
verifySession(session, batch, state);
|
|
124
|
+
batch.transport = 'direct';
|
|
125
|
+
batch.sessionId = session.session_id;
|
|
126
|
+
batch.batchId = session.batch_id;
|
|
127
|
+
await writeUploadState(stateFile, state);
|
|
128
|
+
if (session.status !== 'submitted') {
|
|
129
|
+
const current = session;
|
|
130
|
+
await runTransferPool(current.files, concurrency, async (remote) => {
|
|
131
|
+
const local = locals.find(file => file.filename === remote.filename);
|
|
132
|
+
if (remote.object_uploaded) {
|
|
133
|
+
input.onProgress?.({ phase: 'uploaded', filename: local.filename, transferred: local.size_bytes, total: local.size_bytes });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!remote.upload)
|
|
137
|
+
throw fail('Upload ticket is missing.', 'kb_protocol_error');
|
|
138
|
+
try {
|
|
139
|
+
await putSignedFile(remote.upload, local, { signal: input.signal, fetchImpl: deps.fetchImpl, timeoutMs: deps.transferTimeoutMs, onProgress: (transferred, total) => input.onProgress?.({ phase: 'uploading', filename: local.filename, transferred, total: total ?? undefined }) });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (input.signal?.aborted || (error instanceof SignedTransferError && error.code !== 'kb_transfer_failed'))
|
|
143
|
+
throw error;
|
|
144
|
+
// A PUT response may be lost after the object was stored. Read before retrying.
|
|
145
|
+
const renewed = await client.getUpload(current.session_id, true);
|
|
146
|
+
verifySession(renewed, batch, state);
|
|
147
|
+
const item = renewed.files.find(file => file.file_id === remote.file_id);
|
|
148
|
+
if (item?.object_uploaded || renewed.status === 'submitted')
|
|
149
|
+
return;
|
|
150
|
+
if (!item?.upload)
|
|
151
|
+
throw error;
|
|
152
|
+
await putSignedFile(item.upload, local, { signal: input.signal, fetchImpl: deps.fetchImpl, timeoutMs: deps.transferTimeoutMs });
|
|
153
|
+
}
|
|
154
|
+
}, input.signal);
|
|
155
|
+
// Covers skipped objects and changes between hashing and upload completion.
|
|
156
|
+
for (const file of locals)
|
|
157
|
+
if ((await digestFile(file.path, input.signal)).sha256 !== file.sha256)
|
|
158
|
+
throw fail('Upload source changed.', 'kb_source_changed');
|
|
159
|
+
input.signal?.throwIfAborted();
|
|
160
|
+
session = await client.completeUpload(current.session_id);
|
|
161
|
+
verifySession(session, batch, state);
|
|
162
|
+
if (session.status !== 'submitted')
|
|
163
|
+
throw fail('Upload completion did not submit a task.', 'kb_protocol_error');
|
|
164
|
+
}
|
|
165
|
+
batch.submitted = true;
|
|
166
|
+
await writeUploadState(stateFile, state);
|
|
167
|
+
}
|
|
168
|
+
const submitted = state.batches.map(batch => ({ batch_id: batch.batchId, kind: (batch.transport === 'proxy' ? 'batch' : 'archive'), status: 'submitted' }));
|
|
169
|
+
const tasks = [];
|
|
170
|
+
const waitDeadline = Date.now() + (input.timeoutMs ?? 10 * 60_000);
|
|
171
|
+
if (input.wait)
|
|
172
|
+
for (const batch of submitted) {
|
|
173
|
+
const remaining = waitDeadline - Date.now();
|
|
174
|
+
if (remaining <= 0)
|
|
175
|
+
throw fail('Timed out waiting for KB tasks; query the saved task IDs.', 'kb_task_timeout', { batches: submitted });
|
|
176
|
+
tasks.push(await waitForTask(client, batch.batch_id, { kind: batch.kind, timeoutMs: remaining, signal: input.signal, onProgress: input.onProgress, pollIntervalMs: deps.pollIntervalMs }));
|
|
177
|
+
}
|
|
178
|
+
return { state_file: stateFile, collection_id: state.collectionId, files: state.files.length, batches: submitted, ...(input.wait ? { tasks } : {}) };
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (input.signal?.aborted)
|
|
182
|
+
throw fail('Upload interrupted; submission may have completed. Resume with the saved state to reconcile it.', 'kb_aborted', { state_file: stateFile });
|
|
183
|
+
if (error instanceof KbError)
|
|
184
|
+
throw new KbError(error.message, error.code, error.status, { ...error.details, state_file: stateFile });
|
|
185
|
+
if (error instanceof SignedTransferError)
|
|
186
|
+
throw fail(error.message, error.code, { state_file: stateFile });
|
|
187
|
+
throw fail('Upload interrupted; resume with the saved state.', 'kb_transfer_failed', { state_file: stateFile });
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await release();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export async function waitForTask(client, taskId, options = {}) {
|
|
194
|
+
const timeoutMs = options.timeoutMs ?? 10 * 60_000, interval = options.pollIntervalMs ?? 1000;
|
|
195
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || !Number.isFinite(interval) || interval < 1)
|
|
196
|
+
throw fail('Task timeout and polling interval must be positive.');
|
|
197
|
+
const deadline = Date.now() + timeoutMs;
|
|
198
|
+
while (true) {
|
|
199
|
+
if (options.signal?.aborted)
|
|
200
|
+
throw fail('Task wait interrupted.', 'kb_aborted', { task_id: taskId });
|
|
201
|
+
const remaining = deadline - Date.now();
|
|
202
|
+
if (remaining <= 0)
|
|
203
|
+
throw fail('Timed out waiting for KB task; query the task again.', 'kb_task_timeout', { task_id: taskId, kind: options.kind ?? 'archive' });
|
|
204
|
+
// The control client has its own HTTP deadline; race bounds even injected/hung clients.
|
|
205
|
+
let timer;
|
|
206
|
+
let removeAbort;
|
|
207
|
+
const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(fail('Timed out waiting for KB task; query the task again.', 'kb_task_timeout', { task_id: taskId })), remaining); });
|
|
208
|
+
const abort = new Promise((_, reject) => { const handler = () => reject(fail('Task wait interrupted.', 'kb_aborted', { task_id: taskId })); options.signal?.addEventListener('abort', handler, { once: true }); removeAbort = () => options.signal?.removeEventListener('abort', handler); });
|
|
209
|
+
let task;
|
|
210
|
+
try {
|
|
211
|
+
task = await Promise.race([client.getTask(taskId, options.kind), timeout, abort]);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
if (timer)
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
removeAbort?.();
|
|
217
|
+
}
|
|
218
|
+
const raw = typeof task.task_status === 'string' ? task.task_status : task.status;
|
|
219
|
+
const fileStates = task.files?.map(file => String(file.status));
|
|
220
|
+
const active = Number(task.pending_count ?? 0) > 0 || Number(task.processing_count ?? 0) > 0 || fileStates?.some(status => ['pending', 'processing'].includes(status));
|
|
221
|
+
const blocked = raw === 'needs_resolution' || (!active && fileStates?.some(status => status === 'needs_resolution'));
|
|
222
|
+
const cancelled = raw === 'cancelled' || (!active && fileStates?.length && fileStates.some(status => status === 'cancelled'));
|
|
223
|
+
options.onProgress?.({ phase: 'waiting', taskId, status: raw });
|
|
224
|
+
if (blocked)
|
|
225
|
+
throw fail('KB task needs conflict resolution.', 'kb_task_needs_resolution', { task_id: taskId, task });
|
|
226
|
+
if (cancelled || raw === 'failed' || (raw === 'partial_failed' && !active) || (['completed', 'archived'].includes(raw) && Number(task.failed_count ?? 0) > 0))
|
|
227
|
+
throw fail('KB task did not complete successfully.', 'kb_task_failed', { task_id: taskId, task });
|
|
228
|
+
if (['completed', 'archived'].includes(raw))
|
|
229
|
+
return task;
|
|
230
|
+
try {
|
|
231
|
+
await delay(Math.min(interval, Math.max(1, deadline - Date.now())), undefined, { signal: options.signal });
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
if (options.signal?.aborted)
|
|
235
|
+
throw fail('Task wait interrupted.', 'kb_aborted', { task_id: taskId });
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export async function downloadFromKb(client, input, deps = {}) {
|
|
241
|
+
try {
|
|
242
|
+
input.signal?.throwIfAborted();
|
|
243
|
+
const ticket = await client.downloadTicket(input.documentId);
|
|
244
|
+
const result = await downloadSignedFile(ticket, input.output, { signal: input.signal, fetchImpl: deps.fetchImpl, timeoutMs: deps.transferTimeoutMs, response: ticket.transport === 'proxy' ? signal => client.downloadProxy(ticket, signal) : undefined, onProgress: (transferred, total) => input.onProgress?.({ phase: 'downloading', filename: ticket.filename, transferred, total: total ?? undefined }) });
|
|
245
|
+
return { document_id: input.documentId, ...result };
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (input.signal?.aborted)
|
|
249
|
+
throw fail('Download interrupted.', 'kb_aborted');
|
|
250
|
+
if (error instanceof KbError)
|
|
251
|
+
throw error;
|
|
252
|
+
if (error instanceof SignedTransferError)
|
|
253
|
+
throw fail(error.message, error.code);
|
|
254
|
+
throw fail('Download failed.', 'kb_transfer_failed');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=transfers.js.map
|