exomind 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -3119,7 +3119,7 @@ var {
3119
3119
  // package.json
3120
3120
  var package_default = {
3121
3121
  name: "exomind",
3122
- version: "0.6.0",
3122
+ version: "0.7.1",
3123
3123
  description: "ExoMind \u8DE8\u5E73\u53F0\u547D\u4EE4\u884C\u5BA2\u6237\u7AEF \u2014 \u901A\u8FC7 REST \u4E0E ExoMind \u77E5\u8BC6\u5E93\u4EA4\u4E92(ingest/query/search/review),\u66FF\u4EE3 Windows \u4E0D\u53EF\u7528\u7684 MCP \u5BA2\u6237\u7AEF\u3002",
3124
3124
  bin: {
3125
3125
  exomind: "dist/cli.js"
@@ -3917,31 +3917,69 @@ async function mapWithConcurrency(items, concurrency, worker) {
3917
3917
  }
3918
3918
  var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
3919
3919
  var suspended = false;
3920
- async function ingestWithRetry(client, payload, timeoutMs) {
3920
+ function friendlyMsg(e) {
3921
+ if (e instanceof ApiError) {
3922
+ if (e.status === 504) return "\u670D\u52A1\u5668\u5904\u7406\u8D85\u65F6";
3923
+ if (e.status === 502) return "\u7F51\u5173\u5F02\u5E38";
3924
+ if (e.status === 503) return "\u670D\u52A1\u6682\u4E0D\u53EF\u7528";
3925
+ if (e.status === 0) return "\u7F51\u7EDC\u9519\u8BEF";
3926
+ }
3927
+ return (e.message || String(e)).slice(0, 200);
3928
+ }
3929
+ async function ingestWithRetry(client, payload, timeoutMs, url = "/ingest") {
3921
3930
  let rateAttempts = 0;
3922
3931
  let quotaWaits = 0;
3932
+ let transientAttempts = 0;
3923
3933
  while (true) {
3924
3934
  try {
3925
- return await client.post("/ingest", payload, { timeoutMs });
3935
+ return await client.post(url, payload, { timeoutMs });
3926
3936
  } catch (e) {
3927
- if (!(e instanceof ApiError) || e.status !== 429) throw e;
3928
- const type = e.body?.type;
3929
- if (type === "daily_quota") {
3930
- if (++quotaWaits > 3) throw new ApiError(429, "\u914D\u989D\u8FDE\u7EED 3 \u4E2A\u81EA\u7136\u65E5\u672A\u6062\u590D,\u653E\u5F03");
3931
- await suspendUntilMidnight(Number(e.body?.reset ?? 0));
3932
- continue;
3937
+ if (!(e instanceof ApiError)) throw e;
3938
+ if (e.status === 429) {
3939
+ const type = e.body?.type;
3940
+ if (type === "daily_quota") {
3941
+ if (++quotaWaits > 3) throw new ApiError(429, "\u914D\u989D\u8FDE\u7EED 3 \u4E2A\u81EA\u7136\u65E5\u672A\u6062\u590D,\u653E\u5F03");
3942
+ await suspendUntilMidnight(Number(e.body?.reset ?? 0));
3943
+ continue;
3944
+ }
3945
+ if (type === "rate_limit") {
3946
+ if (++rateAttempts >= 5) throw new ApiError(429, "\u5E76\u53D1\u9650\u6D41,\u91CD\u8BD5 5 \u6B21\u4ECD\u5931\u8D25");
3947
+ const retry = Number(e.headers["retry-after"] ?? e.body?.retry_after ?? 5);
3948
+ process.stderr.write(dim(` \u23F8 \u5E76\u53D1\u9650\u6D41,${retry}s \u540E\u91CD\u8BD5
3949
+ `));
3950
+ await sleep(retry * 1e3);
3951
+ continue;
3952
+ }
3953
+ throw e;
3933
3954
  }
3934
- if (type === "rate_limit") {
3935
- if (++rateAttempts >= 5) throw new ApiError(429, "\u5E76\u53D1\u9650\u6D41,\u91CD\u8BD5 5 \u6B21\u4ECD\u5931\u8D25");
3936
- const retry = Number(e.headers["retry-after"] ?? e.body?.retry_after ?? 5);
3937
- process.stderr.write(dim(` \u23F8 \u5E76\u53D1\u9650\u6D41,${retry}s \u540E\u91CD\u8BD5
3955
+ if ([502, 503, 504].includes(e.status) || e.status === 0) {
3956
+ if (++transientAttempts > 3) throw e;
3957
+ const backoff = Math.pow(2, transientAttempts - 1) * 1e3;
3958
+ process.stderr.write(dim(` \u23F8 ${friendlyMsg(e)},${backoff / 1e3}s \u540E\u91CD\u8BD5 (${transientAttempts}/3)
3938
3959
  `));
3939
- await sleep(retry * 1e3);
3960
+ await sleep(backoff);
3961
+ continue;
3962
+ }
3963
+ throw e;
3964
+ }
3965
+ }
3966
+ }
3967
+ async function retryTransient(fn, maxAttempts = 3) {
3968
+ let lastErr;
3969
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
3970
+ try {
3971
+ return await fn();
3972
+ } catch (e) {
3973
+ lastErr = e;
3974
+ const transient = e instanceof ApiError && ([502, 503, 504].includes(e.status) || e.status === 0);
3975
+ if (transient && attempt < maxAttempts) {
3976
+ await sleep(Math.pow(2, attempt - 1) * 1e3);
3940
3977
  continue;
3941
3978
  }
3942
3979
  throw e;
3943
3980
  }
3944
3981
  }
3982
+ throw lastErr;
3945
3983
  }
3946
3984
  async function suspendUntilMidnight(resetEpoch) {
3947
3985
  suspended = true;
@@ -3975,7 +4013,7 @@ async function runDirIngestest(client, opts, dir) {
3975
4013
  const total = files.length;
3976
4014
  const concurrency = Math.max(1, opts.concurrency ?? 3);
3977
4015
  process.stderr.write(
3978
- `\u76EE\u5F55 ${dir}: ${total} \u6587\u4EF6 \u2014 \u5F85\u6444\u5165 ${plan.toIngest.length},\u8DF3\u8FC7 ${plan.toSkip.length}(\u5E76\u53D1 ${concurrency})
4016
+ `\u76EE\u5F55 ${dir}: ${total} \u6587\u4EF6 \u2014 \u5F85\u6444\u5165 ${plan.toIngest.length},\u8DF3\u8FC7 ${plan.toSkip.length}(\u5F02\u6B65,\u5E76\u53D1\u63D0\u4EA4 ${concurrency})
3979
4017
  `
3980
4018
  );
3981
4019
  const onSigInt = () => {
@@ -3989,56 +4027,101 @@ async function runDirIngestest(client, opts, dir) {
3989
4027
  let added = 0;
3990
4028
  let updated = 0;
3991
4029
  let failed = 0;
3992
- let started = 0;
3993
- const queue = plan.toIngest;
4030
+ let degraded = 0;
4031
+ const pending = [];
3994
4032
  try {
3995
- await mapWithConcurrency(queue, concurrency, async (f) => {
3996
- const prev = manifest[f.path];
3997
- const seq = ++started;
3998
- process.stderr.write(`\u23F3 [${seq}/${queue.length}] ${path4.basename(f.path)}\u2026
3999
- `);
4033
+ await mapWithConcurrency(plan.toIngest, concurrency, async (f) => {
4000
4034
  try {
4001
4035
  const res = await ingestWithRetry(
4002
4036
  client,
4003
4037
  { content: f.content, title: f.title, tags: opts.tag },
4004
- opTimeout(3e5)
4038
+ 3e4,
4039
+ "/ingest/async"
4005
4040
  );
4006
- recordFile(manifest, f.path, f.content, f.title);
4007
- saveManifest(manifest);
4008
- if (prev) {
4009
- updated++;
4010
- process.stderr.write(` ${green("\u2713")} \u66F4\u65B0 \u2014 \u5B9E\u4F53 ${res.entities ?? 0}/\u6982\u5FF5 ${res.concepts ?? 0}
4011
- `);
4012
- } else {
4013
- added++;
4014
- process.stderr.write(` ${green("\u2713")} \u65B0\u589E \u2014 \u5B9E\u4F53 ${res.entities ?? 0}/\u6982\u5FF5 ${res.concepts ?? 0}
4015
- `);
4016
- }
4041
+ pending.push({ file: f, jobId: res.job_id });
4017
4042
  } catch (e) {
4018
4043
  failed++;
4019
- process.stderr.write(` ${red("\u2717")} ${e.message}
4044
+ process.stderr.write(` ${red("\u2717")} ${path4.basename(f.path)} \u63D0\u4EA4\u5931\u8D25 \u2014 ${friendlyMsg(e)}
4020
4045
  `);
4021
4046
  }
4022
4047
  });
4048
+ const totalJobs = pending.length;
4049
+ let done = 0;
4050
+ const pollDeadline = Date.now() + opTimeout(6e5);
4051
+ const inFlight = new Map(
4052
+ pending.map((p) => [p.jobId, p.file])
4053
+ );
4054
+ while (inFlight.size > 0) {
4055
+ if (Date.now() > pollDeadline) {
4056
+ for (const [, f] of inFlight) {
4057
+ failed++;
4058
+ process.stderr.write(` ${red("\u2717")} ${path4.basename(f.path)} \u8F6E\u8BE2\u8D85\u65F6
4059
+ `);
4060
+ }
4061
+ inFlight.clear();
4062
+ break;
4063
+ }
4064
+ await Promise.all(
4065
+ [...inFlight.entries()].map(async ([jobId, f]) => {
4066
+ try {
4067
+ const s = await retryTransient(() => client.get("/ingest/status", { job_id: jobId }));
4068
+ if (s.status !== "done" && s.status !== "failed") return;
4069
+ inFlight.delete(jobId);
4070
+ done++;
4071
+ if (s.status === "failed") {
4072
+ failed++;
4073
+ process.stderr.write(` ${red("\u2717")} ${path4.basename(f.path)} \u5904\u7406\u5931\u8D25 \u2014 ${s.error || "\u672A\u77E5\u9519\u8BEF"}
4074
+ `);
4075
+ return;
4076
+ }
4077
+ const prev = manifest[f.path];
4078
+ recordFile(manifest, f.path, f.content, f.title);
4079
+ saveManifest(manifest);
4080
+ if (s.extracted === false) {
4081
+ degraded++;
4082
+ process.stderr.write(` \u26A0 ${path4.basename(f.path)} \u964D\u7EA7 \u2014 \u539F\u6587\u5DF2\u5B58,\u5B9E\u4F53\u62BD\u53D6\u5F85\u8865\u8DD1
4083
+ `);
4084
+ } else if (prev) {
4085
+ updated++;
4086
+ process.stderr.write(` ${green("\u2713")} \u66F4\u65B0 \u2014 \u5B9E\u4F53 ${s.entities ?? 0}/\u6982\u5FF5 ${s.concepts ?? 0}
4087
+ `);
4088
+ } else {
4089
+ added++;
4090
+ process.stderr.write(` ${green("\u2713")} \u65B0\u589E \u2014 \u5B9E\u4F53 ${s.entities ?? 0}/\u6982\u5FF5 ${s.concepts ?? 0}
4091
+ `);
4092
+ }
4093
+ } catch {
4094
+ }
4095
+ })
4096
+ );
4097
+ if (inFlight.size > 0) {
4098
+ process.stderr.write(`${dim("\u23F3")} [\u5B8C\u6210 ${done}/${totalJobs}] \u8F6E\u8BE2\u4E2D\u2026
4099
+ `);
4100
+ await sleep(2e3);
4101
+ }
4102
+ }
4023
4103
  cleanupStale(manifest, dir, files);
4024
4104
  saveManifest(manifest);
4025
4105
  } finally {
4026
4106
  process.removeListener("SIGINT", onSigInt);
4027
4107
  }
4028
- const allUpToDate = added + updated === 0 && plan.toSkip.length > 0 && failed === 0;
4108
+ const allUpToDate = added + updated + degraded === 0 && plan.toSkip.length > 0 && failed === 0;
4029
4109
  output(
4030
- { added, updated, skipped: plan.toSkip.length, failed, total, dir, allUpToDate },
4110
+ { added, updated, degraded, skipped: plan.toSkip.length, failed, total, dir, allUpToDate },
4031
4111
  () => {
4032
4112
  console.log(
4033
4113
  green("\u2713 \u76EE\u5F55\u6444\u5165\u5B8C\u6210") + dim(
4034
- `: \u65B0\u589E ${added} / \u66F4\u65B0 ${updated} / \u8DF3\u8FC7 ${plan.toSkip.length} / \u5931\u8D25 ${failed} (\u5171 ${total})`
4114
+ `: \u65B0\u589E ${added} / \u66F4\u65B0 ${updated} / \u964D\u7EA7 ${degraded} / \u8DF3\u8FC7 ${plan.toSkip.length} / \u5931\u8D25 ${failed} (\u5171 ${total})`
4035
4115
  )
4036
4116
  );
4037
- if (allUpToDate) {
4117
+ if (degraded > 0) {
4038
4118
  console.log(
4039
- dim("\uFF08\u5168\u90E8\u5DF2\u662F\u6700\u65B0,\u65E0\u9700\u91CD\u6444;\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5F3A\u5236\u5237\u65B0,\u5426\u5219\u4E0D\u8981\u52A0 --force\uFF09")
4119
+ dim(`\uFF08${degraded} \u6761\u964D\u7EA7:\u539F\u6587\u5DF2\u5165\u5E93,\u5B9E\u4F53\u62BD\u53D6\u5F85\u8865\u2014\u2014\u53EF\u7A0D\u540E\u91CD\u8DD1\u6216 exomind ingest --backfill \u8865\u8DD1\uFF09`)
4040
4120
  );
4041
4121
  }
4122
+ if (allUpToDate) {
4123
+ console.log(dim("\uFF08\u5168\u90E8\u5DF2\u662F\u6700\u65B0,\u65E0\u9700\u91CD\u6444;\u9664\u975E\u7528\u6237\u660E\u786E\u8981\u6C42\u5F3A\u5236\u5237\u65B0,\u5426\u5219\u4E0D\u8981\u52A0 --force\uFF09"));
4124
+ }
4042
4125
  }
4043
4126
  );
4044
4127
  }