@tiangong-ai/cli 0.0.13 → 0.0.15
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/AGENTS.md +4 -2
- package/README.md +59 -2
- package/dist/args.d.ts +13 -0
- package/dist/args.js +72 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.d.ts +5 -21
- package/dist/cli.js +27 -445
- package/dist/cli.js.map +1 -1
- package/dist/data.d.ts +3 -0
- package/dist/data.js +16 -0
- package/dist/data.js.map +1 -0
- package/dist/edge-search.d.ts +54 -0
- package/dist/edge-search.js +108 -0
- package/dist/edge-search.js.map +1 -0
- package/dist/education/commands.d.ts +2 -0
- package/dist/education/commands.js +144 -0
- package/dist/education/commands.js.map +1 -0
- package/dist/education/config.d.ts +24 -0
- package/dist/education/config.js +127 -0
- package/dist/education/config.js.map +1 -0
- package/dist/education/types.d.ts +1 -0
- package/dist/education/types.js +2 -0
- package/dist/education/types.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.js +42 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.js +53 -0
- package/dist/errors.js.map +1 -0
- package/dist/http.d.ts +28 -0
- package/dist/http.js +80 -0
- package/dist/http.js.map +1 -0
- package/dist/io.d.ts +11 -0
- package/dist/io.js +33 -0
- package/dist/io.js.map +1 -0
- package/dist/kb/client.d.ts +19 -0
- package/dist/kb/client.js +71 -0
- package/dist/kb/client.js.map +1 -0
- package/dist/kb/config.d.ts +12 -0
- package/dist/kb/config.js +26 -0
- package/dist/kb/config.js.map +1 -0
- package/dist/kb/pipeline-health.d.ts +10 -0
- package/dist/kb/pipeline-health.js +68 -0
- package/dist/kb/pipeline-health.js.map +1 -0
- package/dist/kb/selector.d.ts +3 -0
- package/dist/kb/selector.js +40 -0
- package/dist/kb/selector.js.map +1 -0
- package/dist/kb/status.d.ts +19 -0
- package/dist/kb/status.js +74 -0
- package/dist/kb/status.js.map +1 -0
- package/dist/research/commands.d.ts +2 -0
- package/dist/research/commands.js +147 -0
- package/dist/research/commands.js.map +1 -0
- package/dist/research/config.d.ts +23 -0
- package/dist/research/config.js +120 -0
- package/dist/research/config.js.map +1 -0
- package/dist/research/types.d.ts +1 -0
- package/dist/research/types.js +2 -0
- package/dist/research/types.js.map +1 -0
- package/dist/strict-args.d.ts +8 -0
- package/dist/strict-args.js +63 -0
- package/dist/strict-args.js.map +1 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { createReadStream, openAsBlob,
|
|
2
|
+
import { createReadStream, openAsBlob, statSync } from "node:fs";
|
|
3
3
|
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir, platform } from "node:os";
|
|
5
5
|
import { basename, dirname, extname, isAbsolute, join, posix as pathPosix, relative, resolve, sep, } from "node:path";
|
|
6
6
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
7
|
import { deflateRawSync, inflateRawSync } from "node:zlib";
|
|
9
8
|
import sharp from "sharp";
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import { getBoolean, getNonNegativeInteger, getPositiveInteger, getString, nonNegativeIntegerValue, parseArgs, positiveIntegerValue, positiveNumberValue, } from "./args.js";
|
|
10
|
+
import { isObject, responseData, stringField } from "./data.js";
|
|
11
|
+
import { firstEnv, loadDotenv } from "./env.js";
|
|
12
|
+
import { CliError, HttpError } from "./errors.js";
|
|
13
|
+
import { jsonRequest } from "./http.js";
|
|
14
|
+
import { runEducationCommand } from "./education/commands.js";
|
|
15
|
+
import { collectionKey, collectionPath, listCollections, resolveCollection, resolveSelectorFields, } from "./kb/client.js";
|
|
16
|
+
import { resolveKbConfig as resolveConfig } from "./kb/config.js";
|
|
17
|
+
import { readBulkPipelineHealth } from "./kb/pipeline-health.js";
|
|
18
|
+
import { resolveCollectionSelector } from "./kb/selector.js";
|
|
19
|
+
import { batchDocumentStatuses, getDocumentStatus } from "./kb/status.js";
|
|
20
|
+
import { write } from "./io.js";
|
|
21
|
+
import { runResearchCommand } from "./research/commands.js";
|
|
22
|
+
export { parseArgs } from "./args.js";
|
|
23
|
+
export { DEFAULT_API_BASE_URL, DEFAULT_API_PATH_PREFIX } from "./kb/config.js";
|
|
24
|
+
export { resolveCollectionSelector } from "./kb/selector.js";
|
|
12
25
|
const DEFAULT_RETRIES = 3;
|
|
13
26
|
const DEFAULT_BULK_WINDOW_SIZE = 100;
|
|
14
27
|
const DEFAULT_BULK_TOP_UP_MAX = 50;
|
|
@@ -17,11 +30,9 @@ const DEFAULT_BULK_POLL_INTERVAL_SECONDS = 30;
|
|
|
17
30
|
const DEFAULT_BULK_MAX_POLLS = 0;
|
|
18
31
|
const DEFAULT_BULK_MAX_UPLOAD_BYTES = 200 * 1024 * 1024;
|
|
19
32
|
const DEFAULT_BULK_DERIVED_DIR = ".tiangong-kb-ingest-derived";
|
|
20
|
-
const KB_DOCUMENT_STATUS_BATCH_LIMIT = 100;
|
|
21
33
|
const DOCX_TARGET_IMAGE_DPI = 300;
|
|
22
34
|
const DOCX_NORMALIZE_MIN_BYTES = 10 * 1024 * 1024;
|
|
23
35
|
const EMUS_PER_INCH = 914400;
|
|
24
|
-
const RETRYABLE_HTTP_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
25
36
|
const BULK_FAILED_STATUSES = new Set(["failed", "deleted", "dead", "timeout"]);
|
|
26
37
|
const BULK_IN_FLIGHT_STATUSES = new Set(["uploaded", "waiting_for_index_flags", "uploading"]);
|
|
27
38
|
const BULK_SUPPORTED_EXTENSIONS = new Set([
|
|
@@ -40,24 +51,6 @@ const BULK_SUPPORTED_EXTENSIONS = new Set([
|
|
|
40
51
|
".xls",
|
|
41
52
|
".xlsx",
|
|
42
53
|
]);
|
|
43
|
-
class CliError extends Error {
|
|
44
|
-
exitCode;
|
|
45
|
-
constructor(message, exitCode = 1) {
|
|
46
|
-
super(message);
|
|
47
|
-
this.exitCode = exitCode;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
class HttpError extends CliError {
|
|
51
|
-
status;
|
|
52
|
-
retryAfterSeconds;
|
|
53
|
-
retryable;
|
|
54
|
-
constructor(message, status, retryAfterSeconds, retryable) {
|
|
55
|
-
super(message);
|
|
56
|
-
this.status = status;
|
|
57
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
58
|
-
this.retryable = retryable;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
54
|
export async function runCli(argv, io) {
|
|
62
55
|
try {
|
|
63
56
|
loadDotenv(io.env);
|
|
@@ -120,6 +113,12 @@ export async function runCli(argv, io) {
|
|
|
120
113
|
write(io.stdout, kbHelp());
|
|
121
114
|
return 1;
|
|
122
115
|
}
|
|
116
|
+
if (command === "research") {
|
|
117
|
+
return await runResearchCommand(subcommand ? [subcommand, ...rest] : rest, io);
|
|
118
|
+
}
|
|
119
|
+
if (command === "education") {
|
|
120
|
+
return await runEducationCommand(subcommand ? [subcommand, ...rest] : rest, io);
|
|
121
|
+
}
|
|
123
122
|
throw new CliError(`Unknown command: ${command}`);
|
|
124
123
|
}
|
|
125
124
|
catch (error) {
|
|
@@ -130,63 +129,6 @@ export async function runCli(argv, io) {
|
|
|
130
129
|
throw error;
|
|
131
130
|
}
|
|
132
131
|
}
|
|
133
|
-
export function parseArgs(argv) {
|
|
134
|
-
const positionals = [];
|
|
135
|
-
const flags = new Map();
|
|
136
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
137
|
-
const arg = argv[index];
|
|
138
|
-
if (!arg)
|
|
139
|
-
continue;
|
|
140
|
-
if (!arg.startsWith("--")) {
|
|
141
|
-
positionals.push(arg);
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
const withoutPrefix = arg.slice(2);
|
|
145
|
-
const equalsIndex = withoutPrefix.indexOf("=");
|
|
146
|
-
if (equalsIndex >= 0) {
|
|
147
|
-
flags.set(withoutPrefix.slice(0, equalsIndex), withoutPrefix.slice(equalsIndex + 1));
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
const next = argv[index + 1];
|
|
151
|
-
if (next && !next.startsWith("--")) {
|
|
152
|
-
flags.set(withoutPrefix, next);
|
|
153
|
-
index += 1;
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
flags.set(withoutPrefix, true);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return { positionals, flags };
|
|
160
|
-
}
|
|
161
|
-
export function resolveCollectionSelector(args, env) {
|
|
162
|
-
const selectors = [];
|
|
163
|
-
addSelector(selectors, "primary_collection_id", getString(args, "collection-id"));
|
|
164
|
-
addSelector(selectors, "collection_path", getString(args, "collection-path"));
|
|
165
|
-
addSelector(selectors, "collection_key", getString(args, "collection-key"));
|
|
166
|
-
addSelector(selectors, "collection_name", getString(args, "collection-name"));
|
|
167
|
-
if (selectors.length > 1) {
|
|
168
|
-
throw new CliError("Provide exactly one collection selector.");
|
|
169
|
-
}
|
|
170
|
-
if (selectors.length === 1)
|
|
171
|
-
return selectors[0];
|
|
172
|
-
const envName = firstEnv(env, "TIANGONG_KB_DEFAULT_COLLECTION_NAME");
|
|
173
|
-
if (envName)
|
|
174
|
-
return { field: "collection_name", value: envName };
|
|
175
|
-
const legacyName = firstEnv(env, "TIANGONG_KB_DEFAULT_COLLECTION_ID");
|
|
176
|
-
if (legacyName) {
|
|
177
|
-
if (isUuid(legacyName)) {
|
|
178
|
-
throw new CliError("TIANGONG_KB_DEFAULT_COLLECTION_ID is treated as a collection name. Use --collection-id for UUID uploads.");
|
|
179
|
-
}
|
|
180
|
-
return { field: "collection_name", value: legacyName };
|
|
181
|
-
}
|
|
182
|
-
const envPath = firstEnv(env, "TIANGONG_KB_DEFAULT_COLLECTION_PATH");
|
|
183
|
-
if (envPath)
|
|
184
|
-
return { field: "collection_path", value: envPath };
|
|
185
|
-
const envKey = firstEnv(env, "TIANGONG_KB_DEFAULT_COLLECTION_KEY");
|
|
186
|
-
if (envKey)
|
|
187
|
-
return { field: "collection_key", value: envKey };
|
|
188
|
-
throw new CliError("Missing collection selector. Provide --collection-name, --collection-key, --collection-path, --collection-id, or set TIANGONG_KB_DEFAULT_COLLECTION_NAME.");
|
|
189
|
-
}
|
|
190
132
|
async function doctor(argv, io) {
|
|
191
133
|
const args = parseArgs(argv);
|
|
192
134
|
const config = resolveConfig(args, io.env, { requireKey: false });
|
|
@@ -818,120 +760,6 @@ function judgeBulkStatus(item) {
|
|
|
818
760
|
}
|
|
819
761
|
return { status: "uploaded" };
|
|
820
762
|
}
|
|
821
|
-
async function batchDocumentStatuses(config, documentIds) {
|
|
822
|
-
const statuses = [];
|
|
823
|
-
for (let index = 0; index < documentIds.length; index += KB_DOCUMENT_STATUS_BATCH_LIMIT) {
|
|
824
|
-
const chunk = documentIds.slice(index, index + KB_DOCUMENT_STATUS_BATCH_LIMIT);
|
|
825
|
-
try {
|
|
826
|
-
const payload = await jsonRequest(config, "documents/status:batch", {
|
|
827
|
-
method: "POST",
|
|
828
|
-
body: JSON.stringify({ documentIds: chunk }),
|
|
829
|
-
});
|
|
830
|
-
statuses.push(...batchStatusItems(payload));
|
|
831
|
-
}
|
|
832
|
-
catch (error) {
|
|
833
|
-
if (error instanceof HttpError && error.status && [404, 405, 501].includes(error.status)) {
|
|
834
|
-
statuses.push(...(await Promise.all(chunk.map(async (documentId) => statusItemFromPayload(await getDocumentStatus(config, documentId), documentId)))));
|
|
835
|
-
continue;
|
|
836
|
-
}
|
|
837
|
-
throw error;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
return statuses;
|
|
841
|
-
}
|
|
842
|
-
async function getDocumentStatus(config, documentId) {
|
|
843
|
-
return jsonRequest(config, `documents/${encodeURIComponent(documentId)}/status`);
|
|
844
|
-
}
|
|
845
|
-
function batchStatusItems(payload) {
|
|
846
|
-
const data = responseData(payload);
|
|
847
|
-
const items = isObject(data) && Array.isArray(data.documents)
|
|
848
|
-
? data.documents
|
|
849
|
-
: isObject(data) && Array.isArray(data.results)
|
|
850
|
-
? data.results
|
|
851
|
-
: Array.isArray(data)
|
|
852
|
-
? data
|
|
853
|
-
: [];
|
|
854
|
-
return items.filter(isObject).map((item) => batchStatusItemFromPayload(item));
|
|
855
|
-
}
|
|
856
|
-
function statusItemFromPayload(payload, fallbackDocumentId = "") {
|
|
857
|
-
const data = responseData(payload);
|
|
858
|
-
const item = isObject(data) ? data : {};
|
|
859
|
-
return {
|
|
860
|
-
documentId: stringField(item, "documentId") ?? stringField(item, "document_id") ?? fallbackDocumentId,
|
|
861
|
-
status: stringField(item, "status"),
|
|
862
|
-
terminal: typeof item.terminal === "boolean" ? item.terminal : undefined,
|
|
863
|
-
opensearchIndexed: typeof item.opensearchIndexed === "boolean" ? item.opensearchIndexed : undefined,
|
|
864
|
-
pineconeIndexed: typeof item.pineconeIndexed === "boolean" ? item.pineconeIndexed : undefined,
|
|
865
|
-
indexRecordCount: typeof item.indexRecordCount === "number" ? item.indexRecordCount : undefined,
|
|
866
|
-
lastError: typeof item.lastError === "string" ? item.lastError : undefined,
|
|
867
|
-
lastErrorStage: typeof item.lastErrorStage === "string" ? item.lastErrorStage : undefined,
|
|
868
|
-
raw: payload,
|
|
869
|
-
};
|
|
870
|
-
}
|
|
871
|
-
async function readBulkPipelineHealth(config, fallbackPollAfterSeconds, selectorFields = {}) {
|
|
872
|
-
try {
|
|
873
|
-
const payload = await jsonRequest(config, pipelineHealthPath(selectorFields));
|
|
874
|
-
return pipelineHealthFromPayload(payload, fallbackPollAfterSeconds);
|
|
875
|
-
}
|
|
876
|
-
catch (error) {
|
|
877
|
-
if (error instanceof HttpError && error.status && [404, 405, 501].includes(error.status)) {
|
|
878
|
-
return {
|
|
879
|
-
healthy: true,
|
|
880
|
-
pressure: "unknown",
|
|
881
|
-
recommendedAction: "continue",
|
|
882
|
-
recommendedPollAfterSeconds: fallbackPollAfterSeconds,
|
|
883
|
-
message: "Pipeline health endpoint is unavailable; continuing without backpressure.",
|
|
884
|
-
};
|
|
885
|
-
}
|
|
886
|
-
if (error instanceof HttpError) {
|
|
887
|
-
return {
|
|
888
|
-
healthy: false,
|
|
889
|
-
pressure: "paused",
|
|
890
|
-
recommendedAction: "pause_top_up",
|
|
891
|
-
recommendedPollAfterSeconds: error.retryAfterSeconds ?? Math.max(fallbackPollAfterSeconds, 30),
|
|
892
|
-
message: error.message,
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
return {
|
|
896
|
-
healthy: false,
|
|
897
|
-
pressure: "paused",
|
|
898
|
-
recommendedAction: "pause_top_up",
|
|
899
|
-
recommendedPollAfterSeconds: Math.max(fallbackPollAfterSeconds, 30),
|
|
900
|
-
message: error instanceof Error ? error.message : String(error),
|
|
901
|
-
};
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
function pipelineHealthPath(selectorFields) {
|
|
905
|
-
const params = new URLSearchParams();
|
|
906
|
-
for (const key of ["primary_collection_id", "collection_path", "collection_key"]) {
|
|
907
|
-
const value = selectorFields[key];
|
|
908
|
-
if (value)
|
|
909
|
-
params.set(key, value);
|
|
910
|
-
}
|
|
911
|
-
const query = params.toString();
|
|
912
|
-
return query ? `pipeline/health?${query}` : "pipeline/health";
|
|
913
|
-
}
|
|
914
|
-
function pipelineHealthFromPayload(payload, fallbackPollAfterSeconds) {
|
|
915
|
-
const data = responseData(payload);
|
|
916
|
-
if (!isObject(data)) {
|
|
917
|
-
throw new CliError("Pipeline health response did not contain an object payload.");
|
|
918
|
-
}
|
|
919
|
-
const action = stringField(data, "recommendedAction");
|
|
920
|
-
if (action !== "continue" && action !== "slow_down" && action !== "pause_top_up") {
|
|
921
|
-
throw new CliError("Pipeline health response did not contain a valid recommendedAction.");
|
|
922
|
-
}
|
|
923
|
-
const pressure = stringField(data, "pressure");
|
|
924
|
-
const pollAfter = Number(data.recommendedPollAfterSeconds);
|
|
925
|
-
return {
|
|
926
|
-
healthy: typeof data.healthy === "boolean" ? data.healthy : action === "continue",
|
|
927
|
-
pressure: pressure === "ok" || pressure === "degraded" || pressure === "paused" ? pressure : "unknown",
|
|
928
|
-
recommendedAction: action,
|
|
929
|
-
recommendedPollAfterSeconds: Number.isFinite(pollAfter) && pollAfter > 0 ? pollAfter : fallbackPollAfterSeconds,
|
|
930
|
-
checkedAt: stringField(data, "checkedAt"),
|
|
931
|
-
message: stringField(data, "message") ??
|
|
932
|
-
(isObject(data.indexPreflight) ? stringField(data.indexPreflight, "message") : undefined),
|
|
933
|
-
};
|
|
934
|
-
}
|
|
935
763
|
async function saveBulkPipelineHealth(statePath, health) {
|
|
936
764
|
const db = await openSqlite(statePath);
|
|
937
765
|
const now = new Date().toISOString();
|
|
@@ -943,25 +771,6 @@ async function saveBulkPipelineHealth(statePath, health) {
|
|
|
943
771
|
db.close();
|
|
944
772
|
}
|
|
945
773
|
}
|
|
946
|
-
function batchStatusItemFromPayload(item) {
|
|
947
|
-
const documentId = stringField(item, "documentId") ?? stringField(item, "document_id") ?? "";
|
|
948
|
-
if (item.ok === true && isObject(item.status)) {
|
|
949
|
-
return statusItemFromPayload(item.status, documentId);
|
|
950
|
-
}
|
|
951
|
-
if (item.ok === false && isObject(item.error)) {
|
|
952
|
-
const error = item.error;
|
|
953
|
-
return {
|
|
954
|
-
documentId,
|
|
955
|
-
itemError: {
|
|
956
|
-
code: stringField(error, "code") ?? "STATUS_ITEM_ERROR",
|
|
957
|
-
message: stringField(error, "message") ?? "Document status lookup failed.",
|
|
958
|
-
retryable: typeof error.retryable === "boolean" ? error.retryable : false,
|
|
959
|
-
},
|
|
960
|
-
raw: item,
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
return statusItemFromPayload(item, documentId);
|
|
964
|
-
}
|
|
965
774
|
async function initializeBulkJob(input) {
|
|
966
775
|
await mkdir(dirname(input.statePath), { recursive: true });
|
|
967
776
|
const db = await openSqlite(input.statePath);
|
|
@@ -2295,13 +2104,6 @@ async function loadOptionalSchemaSnapshot(args, env) {
|
|
|
2295
2104
|
throw error;
|
|
2296
2105
|
}
|
|
2297
2106
|
}
|
|
2298
|
-
async function resolveCollection(config, selector, options) {
|
|
2299
|
-
const params = new URLSearchParams({ action: "upload" });
|
|
2300
|
-
params.set(selector.field, selector.value);
|
|
2301
|
-
if (options.includeSchema)
|
|
2302
|
-
params.set("include_schema", "true");
|
|
2303
|
-
return jsonRequest(config, `collections/resolve?${params.toString()}`);
|
|
2304
|
-
}
|
|
2305
2107
|
async function loadMetadataMap(args) {
|
|
2306
2108
|
const metadataMapFile = getString(args, "metadata-map");
|
|
2307
2109
|
if (!metadataMapFile) {
|
|
@@ -2748,93 +2550,6 @@ async function uploadOne(input) {
|
|
|
2748
2550
|
},
|
|
2749
2551
|
});
|
|
2750
2552
|
}
|
|
2751
|
-
async function resolveSelectorFields(config, selector) {
|
|
2752
|
-
if (selector.field !== "collection_name") {
|
|
2753
|
-
return { [selector.field]: selector.value };
|
|
2754
|
-
}
|
|
2755
|
-
const matches = (await listCollections(config, "upload", 100)).filter((item) => item.name === selector.value);
|
|
2756
|
-
if (matches.length === 0)
|
|
2757
|
-
throw new CliError(`No uploadable collection matched name: ${selector.value}`);
|
|
2758
|
-
if (matches.length > 1) {
|
|
2759
|
-
const choices = matches.map((item) => item.key ?? item.path ?? item.id ?? "").join(", ");
|
|
2760
|
-
throw new CliError(`Collection name is not unique: ${selector.value}. Use --collection-key or --collection-path. Matches: ${choices}`);
|
|
2761
|
-
}
|
|
2762
|
-
const match = matches[0];
|
|
2763
|
-
const key = collectionKey(match);
|
|
2764
|
-
if (key)
|
|
2765
|
-
return { collection_key: key };
|
|
2766
|
-
const path = collectionPath(match);
|
|
2767
|
-
if (path)
|
|
2768
|
-
return { collection_path: path };
|
|
2769
|
-
if (typeof match.id === "string" && match.id)
|
|
2770
|
-
return { primary_collection_id: match.id };
|
|
2771
|
-
throw new CliError(`Collection matched name ${selector.value}, but response had no key, path, or id.`);
|
|
2772
|
-
}
|
|
2773
|
-
async function listCollections(config, capability, limit) {
|
|
2774
|
-
const collections = [];
|
|
2775
|
-
let offset = 0;
|
|
2776
|
-
while (true) {
|
|
2777
|
-
const payload = await jsonRequest(config, `collections?${new URLSearchParams({
|
|
2778
|
-
capability,
|
|
2779
|
-
limit: String(limit),
|
|
2780
|
-
offset: String(offset),
|
|
2781
|
-
}).toString()}`);
|
|
2782
|
-
const page = collectionItems(payload);
|
|
2783
|
-
collections.push(...page);
|
|
2784
|
-
if (page.length < limit)
|
|
2785
|
-
return collections;
|
|
2786
|
-
offset += limit;
|
|
2787
|
-
}
|
|
2788
|
-
}
|
|
2789
|
-
async function jsonRequest(config, path, init = {}) {
|
|
2790
|
-
const url = `${config.apiBaseUrl}${config.apiPathPrefix}/${path.replace(/^\/+/, "")}`;
|
|
2791
|
-
const headers = new Headers(init.headers);
|
|
2792
|
-
headers.set("Authorization", `Bearer ${config.apiKey}`);
|
|
2793
|
-
headers.set("Accept", "application/json");
|
|
2794
|
-
if (init.body && !(init.body instanceof FormData)) {
|
|
2795
|
-
headers.set("Content-Type", "application/json");
|
|
2796
|
-
}
|
|
2797
|
-
let response;
|
|
2798
|
-
try {
|
|
2799
|
-
response = await fetch(url, {
|
|
2800
|
-
...init,
|
|
2801
|
-
headers,
|
|
2802
|
-
signal: AbortSignal.timeout(config.timeoutSeconds * 1000),
|
|
2803
|
-
});
|
|
2804
|
-
}
|
|
2805
|
-
catch (error) {
|
|
2806
|
-
throw new HttpError(`Request failed for ${url}: ${error instanceof Error ? error.message : String(error)}`, undefined, undefined, true);
|
|
2807
|
-
}
|
|
2808
|
-
const text = await response.text();
|
|
2809
|
-
if (!response.ok) {
|
|
2810
|
-
throw new HttpError(`HTTP ${response.status} from ${url}: ${text}`, response.status, retryAfterSeconds(response.headers), RETRYABLE_HTTP_STATUSES.has(response.status));
|
|
2811
|
-
}
|
|
2812
|
-
if (!text.trim())
|
|
2813
|
-
return {};
|
|
2814
|
-
try {
|
|
2815
|
-
return JSON.parse(text);
|
|
2816
|
-
}
|
|
2817
|
-
catch {
|
|
2818
|
-
throw new CliError(`Expected JSON from ${url}, got: ${text.slice(0, 200)}`);
|
|
2819
|
-
}
|
|
2820
|
-
}
|
|
2821
|
-
function resolveConfig(args, env, options = {}) {
|
|
2822
|
-
const requireKey = options.requireKey ?? true;
|
|
2823
|
-
const apiKey = getString(args, "api-key") ?? firstEnv(env, "TIANGONG_AI_API_KEY", "TIANGONG_KB_API_KEY") ?? "";
|
|
2824
|
-
if (requireKey && !apiKey) {
|
|
2825
|
-
throw new CliError("Missing API key. Provide --api-key or set TIANGONG_AI_API_KEY.");
|
|
2826
|
-
}
|
|
2827
|
-
return {
|
|
2828
|
-
apiBaseUrl: (getString(args, "api-base-url") ??
|
|
2829
|
-
firstEnv(env, "TIANGONG_KB_API_BASE_URL") ??
|
|
2830
|
-
DEFAULT_API_BASE_URL).replace(/\/+$/, ""),
|
|
2831
|
-
apiPathPrefix: normalizePrefix(getString(args, "api-path-prefix") ??
|
|
2832
|
-
firstEnv(env, "TIANGONG_KB_API_PATH_PREFIX") ??
|
|
2833
|
-
DEFAULT_API_PATH_PREFIX),
|
|
2834
|
-
apiKey,
|
|
2835
|
-
timeoutSeconds: positiveNumberValue(getString(args, "timeout") ?? firstEnv(env, "TIANGONG_KB_TIMEOUT"), 120, "--timeout"),
|
|
2836
|
-
};
|
|
2837
|
-
}
|
|
2838
2553
|
async function collectFiles(path, recursive, options = {}) {
|
|
2839
2554
|
const item = await stat(path).catch(() => undefined);
|
|
2840
2555
|
if (!item)
|
|
@@ -2897,36 +2612,6 @@ async function runPool(items, concurrency, worker) {
|
|
|
2897
2612
|
await Promise.all(workers);
|
|
2898
2613
|
return results;
|
|
2899
2614
|
}
|
|
2900
|
-
function collectionItems(payload) {
|
|
2901
|
-
const data = responseData(payload);
|
|
2902
|
-
if (Array.isArray(data))
|
|
2903
|
-
return data.filter(isObject);
|
|
2904
|
-
if (isObject(data) && Array.isArray(data.collections))
|
|
2905
|
-
return data.collections.filter(isObject);
|
|
2906
|
-
if (isObject(data) && Array.isArray(data.data))
|
|
2907
|
-
return data.data.filter(isObject);
|
|
2908
|
-
if (isObject(payload) && Array.isArray(payload.data))
|
|
2909
|
-
return payload.data.filter(isObject);
|
|
2910
|
-
throw new CliError("Collection list response did not contain a data array.");
|
|
2911
|
-
}
|
|
2912
|
-
function collectionKey(item) {
|
|
2913
|
-
return (stringField(item, "key") ??
|
|
2914
|
-
stringField(item, "collectionKey") ??
|
|
2915
|
-
stringField(item, "collection_key"));
|
|
2916
|
-
}
|
|
2917
|
-
function collectionPath(item) {
|
|
2918
|
-
return (stringField(item, "path") ??
|
|
2919
|
-
stringField(item, "collectionPath") ??
|
|
2920
|
-
stringField(item, "collection_path"));
|
|
2921
|
-
}
|
|
2922
|
-
function responseData(payload) {
|
|
2923
|
-
if (isObject(payload) &&
|
|
2924
|
-
"data" in payload &&
|
|
2925
|
-
("api_version" in payload || "request_id" in payload || isObject(payload.data))) {
|
|
2926
|
-
return payload.data;
|
|
2927
|
-
}
|
|
2928
|
-
return payload;
|
|
2929
|
-
}
|
|
2930
2615
|
function documentIdFromUpload(payload) {
|
|
2931
2616
|
const data = responseData(payload);
|
|
2932
2617
|
if (!isObject(data))
|
|
@@ -2958,70 +2643,6 @@ function formatUploadLine(result) {
|
|
|
2958
2643
|
const existing = result.existingDocumentId ? ` existing=${result.existingDocumentId}` : "";
|
|
2959
2644
|
return `OK ${result.path} document=${result.documentId ?? ""}${duplicate}${existing}\n`;
|
|
2960
2645
|
}
|
|
2961
|
-
function addSelector(selectors, field, value) {
|
|
2962
|
-
if (value)
|
|
2963
|
-
selectors.push({ field, value });
|
|
2964
|
-
}
|
|
2965
|
-
function getString(args, name) {
|
|
2966
|
-
const value = args.flags.get(name);
|
|
2967
|
-
if (typeof value !== "string" || value.trim() === "")
|
|
2968
|
-
return undefined;
|
|
2969
|
-
return value.trim();
|
|
2970
|
-
}
|
|
2971
|
-
function getBoolean(args, name) {
|
|
2972
|
-
return args.flags.get(name) === true || args.flags.get(name) === "true";
|
|
2973
|
-
}
|
|
2974
|
-
function getPositiveInteger(args, name, defaultValue) {
|
|
2975
|
-
return positiveIntegerValue(getString(args, name), defaultValue, `--${name}`);
|
|
2976
|
-
}
|
|
2977
|
-
function positiveIntegerValue(value, defaultValue, label) {
|
|
2978
|
-
if (!value)
|
|
2979
|
-
return defaultValue;
|
|
2980
|
-
const parsed = Number(value);
|
|
2981
|
-
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
2982
|
-
throw new CliError(`${label} must be a positive integer.`);
|
|
2983
|
-
return parsed;
|
|
2984
|
-
}
|
|
2985
|
-
function getNonNegativeInteger(args, name, defaultValue) {
|
|
2986
|
-
return nonNegativeIntegerValue(getString(args, name), defaultValue, `--${name}`);
|
|
2987
|
-
}
|
|
2988
|
-
function nonNegativeIntegerValue(value, defaultValue, label) {
|
|
2989
|
-
if (!value)
|
|
2990
|
-
return defaultValue;
|
|
2991
|
-
const parsed = Number(value);
|
|
2992
|
-
if (!Number.isInteger(parsed) || parsed < 0)
|
|
2993
|
-
throw new CliError(`${label} must be a non-negative integer.`);
|
|
2994
|
-
return parsed;
|
|
2995
|
-
}
|
|
2996
|
-
function getPositiveNumber(args, name, defaultValue) {
|
|
2997
|
-
return positiveNumberValue(getString(args, name), defaultValue, `--${name}`);
|
|
2998
|
-
}
|
|
2999
|
-
function positiveNumberValue(value, defaultValue, label) {
|
|
3000
|
-
if (!value)
|
|
3001
|
-
return defaultValue;
|
|
3002
|
-
const parsed = Number(value);
|
|
3003
|
-
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
3004
|
-
throw new CliError(`${label} must be a positive number.`);
|
|
3005
|
-
return parsed;
|
|
3006
|
-
}
|
|
3007
|
-
function firstEnv(env, ...names) {
|
|
3008
|
-
for (const name of names) {
|
|
3009
|
-
const value = env[name]?.trim();
|
|
3010
|
-
if (value)
|
|
3011
|
-
return value;
|
|
3012
|
-
}
|
|
3013
|
-
return undefined;
|
|
3014
|
-
}
|
|
3015
|
-
function normalizePrefix(value) {
|
|
3016
|
-
return `/${value.replace(/^\/+|\/+$/g, "")}`;
|
|
3017
|
-
}
|
|
3018
|
-
function retryAfterSeconds(headers) {
|
|
3019
|
-
const value = headers.get("Retry-After");
|
|
3020
|
-
if (!value)
|
|
3021
|
-
return undefined;
|
|
3022
|
-
const parsed = Number(value);
|
|
3023
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
3024
|
-
}
|
|
3025
2646
|
function retryDelay(error, attempt) {
|
|
3026
2647
|
if (error instanceof HttpError && error.retryAfterSeconds)
|
|
3027
2648
|
return Math.min(error.retryAfterSeconds, 30);
|
|
@@ -3047,49 +2668,6 @@ function mimeType(path) {
|
|
|
3047
2668
|
}
|
|
3048
2669
|
return "application/octet-stream";
|
|
3049
2670
|
}
|
|
3050
|
-
function isUuid(value) {
|
|
3051
|
-
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
3052
|
-
}
|
|
3053
|
-
function isObject(value) {
|
|
3054
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3055
|
-
}
|
|
3056
|
-
function stringField(value, field) {
|
|
3057
|
-
const candidate = value[field];
|
|
3058
|
-
return typeof candidate === "string" && candidate ? candidate : undefined;
|
|
3059
|
-
}
|
|
3060
|
-
function write(output, text) {
|
|
3061
|
-
output.write(text);
|
|
3062
|
-
}
|
|
3063
|
-
function loadDotenv(env) {
|
|
3064
|
-
const cwd = dirname(fileURLToPath(import.meta.url));
|
|
3065
|
-
const candidates = [resolve(process.cwd(), ".env"), resolve(cwd, "../.env")];
|
|
3066
|
-
for (const candidate of candidates) {
|
|
3067
|
-
const content = readEnvFile(candidate);
|
|
3068
|
-
if (!content)
|
|
3069
|
-
continue;
|
|
3070
|
-
for (const line of content.split(/\r?\n/)) {
|
|
3071
|
-
const trimmed = line.trim();
|
|
3072
|
-
if (!trimmed || trimmed.startsWith("#") || !trimmed.includes("="))
|
|
3073
|
-
continue;
|
|
3074
|
-
const [rawKey, ...rawValue] = trimmed.replace(/^export\s+/, "").split("=");
|
|
3075
|
-
const key = rawKey?.trim();
|
|
3076
|
-
if (!key || env[key])
|
|
3077
|
-
continue;
|
|
3078
|
-
env[key] = rawValue
|
|
3079
|
-
.join("=")
|
|
3080
|
-
.trim()
|
|
3081
|
-
.replace(/^["']|["']$/g, "");
|
|
3082
|
-
}
|
|
3083
|
-
}
|
|
3084
|
-
}
|
|
3085
|
-
function readEnvFile(path) {
|
|
3086
|
-
try {
|
|
3087
|
-
return readFileSync(path, "utf8");
|
|
3088
|
-
}
|
|
3089
|
-
catch {
|
|
3090
|
-
return undefined;
|
|
3091
|
-
}
|
|
3092
|
-
}
|
|
3093
2671
|
function topHelp() {
|
|
3094
2672
|
return `Tiangong AI CLI
|
|
3095
2673
|
|
|
@@ -3099,8 +2677,12 @@ Usage:
|
|
|
3099
2677
|
tiangong-ai kb ingest jobs
|
|
3100
2678
|
tiangong-ai kb ingest status <document-id>
|
|
3101
2679
|
tiangong-ai kb collections list [--capability upload]
|
|
2680
|
+
tiangong-ai research search --input <request.json>|--query <query> [--sources default|all|sci|report|patent]
|
|
2681
|
+
tiangong-ai education search --input <request.json>|--query <query> [--sources default|all|course|edu|textbook]
|
|
3102
2682
|
|
|
3103
2683
|
Run "tiangong-ai kb --help" for KB options.
|
|
2684
|
+
Run "tiangong-ai research --help" for research options.
|
|
2685
|
+
Run "tiangong-ai education --help" for education options.
|
|
3104
2686
|
`;
|
|
3105
2687
|
}
|
|
3106
2688
|
function kbHelp() {
|