scream-code 0.8.2 → 0.8.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.
@@ -5,7 +5,7 @@ const __filename = __cjsShimFileURLToPath(import.meta.url);
5
5
  const __dirname = __cjsShimDirname(__filename);
6
6
  import { a as __toESM, i as __require, r as __exportAll, t as __commonJSMin } from "./chunk-apG1qJts.mjs";
7
7
  import "./suppress-sqlite-warning-C2VB0doZ.mjs";
8
- import { C as parse$7, S as normalize, T as resolve$1, _ as KnowledgeStore, b as isAbsolute$1, i as isSupportedFile, n as ingestDirectory, r as ingestFile, t as multiSearch, v as basename$1, w as relative$1, x as join$1, y as dirname$2 } from "./src-aqNCeRzD.mjs";
8
+ import { C as join$1, D as resolve$1, E as relative$1, S as isAbsolute$1, T as parse$7, a as isSupportedFile, b as basename$1, i as ingestFile, r as ingestDirectory, t as multiSearch, w as normalize, x as dirname$2, y as KnowledgeStore } from "./src-CptHxtUY.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { createHash, randomBytes, randomInt, randomUUID } from "node:crypto";
11
11
  import Jn, { access, appendFile, chmod, copyFile, cp, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
@@ -56668,6 +56668,22 @@ function createFastEmbedEngine() {
56668
56668
  }
56669
56669
  const denom = Math.sqrt(normA) * Math.sqrt(normB);
56670
56670
  return denom === 0 ? 0 : dot / denom;
56671
+ },
56672
+ async ensureReady() {
56673
+ if (embedder !== null) return true;
56674
+ try {
56675
+ initPromise ??= loadEmbedder();
56676
+ embedder = await initPromise;
56677
+ if (embedder === null) {
56678
+ initPromise = null;
56679
+ return false;
56680
+ }
56681
+ loadFailed = false;
56682
+ return true;
56683
+ } catch {
56684
+ initPromise = null;
56685
+ return false;
56686
+ }
56671
56687
  }
56672
56688
  };
56673
56689
  }
@@ -58216,8 +58232,8 @@ var KnowledgeLookupTool = class {
58216
58232
  const llm = { generate: async (systemPrompt, userPrompt) => {
58217
58233
  return this.agent.generateText(systemPrompt, userPrompt);
58218
58234
  } };
58219
- const { multiSearch } = await import("./src-S0QPScxL.mjs");
58220
- const results = await multiSearch(store, llm, query, { topK });
58235
+ const { multiSearchWithTrace } = await import("./src-DhT3eU2C.mjs");
58236
+ const { results, trace } = await multiSearchWithTrace(store, llm, query, { topK });
58221
58237
  if (results.length === 0) return {
58222
58238
  isError: false,
58223
58239
  output: `No knowledge base entries found for query "${query}".`
@@ -58228,6 +58244,13 @@ var KnowledgeLookupTool = class {
58228
58244
  const eventLine = result.eventTitle !== null ? ` Event: ${result.eventTitle}` : void 0;
58229
58245
  lines.push(`**${i + 1}. ${heading}** (from: ${result.sourceName})`, ` Score: ${result.score.toFixed(3)}`, ...eventLine !== void 0 ? [eventLine] : [], ` ${result.content}`, "");
58230
58246
  }
58247
+ lines.push("---", "Retrieval trace:");
58248
+ if (trace.fallbackReason !== null) lines.push(` ⚠ Fallback: ${trace.fallbackReason}`);
58249
+ for (const step of trace.steps) {
58250
+ const payloadStr = step.payload !== void 0 ? ` ${JSON.stringify(step.payload)}` : "";
58251
+ lines.push(` • ${step.step} (${step.durationMs}ms) — ${step.detail}${payloadStr}`);
58252
+ }
58253
+ if (trace.rerankedEventTitles.length > 0) lines.push(` Reranked events: ${trace.rerankedEventTitles.join(" | ")}`);
58231
58254
  return {
58232
58255
  isError: false,
58233
58256
  output: lines.join("\n")
@@ -78633,6 +78656,67 @@ function formatFileOperations(ops) {
78633
78656
  lines.push("</files>");
78634
78657
  return lines.join("\n");
78635
78658
  }
78659
+ /** Max recursion depth for re-summarize fallback. Each level halves the
78660
+ * input, so depth 3 means we can compress a 8x-oversized input down to a
78661
+ * single summary by chaining 2^3 = 8 partial summaries. */
78662
+ const MAX_RE_SUMMARIZE_DEPTH = 3;
78663
+ var TruncatedError = class extends Error {};
78664
+ /**
78665
+ * Recursive re-summarize fallback for context overflow. When even the
78666
+ * minimum safe split still overflows the model (typically because a single
78667
+ * message contains a giant tool result), split the input in half at a safe
78668
+ * boundary, summarize each half, then concatenate the two partial summaries
78669
+ * and re-summarize them into one. If a half still overflows, recurse.
78670
+ *
78671
+ * The split uses `canSplitAfter` from the strategy to make sure each half
78672
+ * ends at a message boundary that doesn't orphan tool results. If no safe
78673
+ * split exists in the half (e.g. one giant message), feed it as-is to the
78674
+ * model and let the outer retry loop handle the overflow.
78675
+ */
78676
+ async function summarizeWithFallback(messages, summarizeOnce, depth = 0) {
78677
+ if (messages.length <= 1) return summarizeOnce(messages);
78678
+ let split = -1;
78679
+ const mid = Math.floor(messages.length / 2);
78680
+ for (let i = mid; i > 0; i--) if (canSplitAfterContext(messages, i - 1)) {
78681
+ split = i;
78682
+ break;
78683
+ }
78684
+ if (split === -1) {
78685
+ for (let i = mid + 1; i < messages.length; i++) if (canSplitAfterContext(messages, i - 1)) {
78686
+ split = i;
78687
+ break;
78688
+ }
78689
+ }
78690
+ if (split === -1) return summarizeOnce(messages);
78691
+ const firstHalf = messages.slice(0, split);
78692
+ const secondHalf = messages.slice(split);
78693
+ const summarizeHalf = async (half) => {
78694
+ try {
78695
+ return (await summarizeOnce(half)).summary;
78696
+ } catch (error) {
78697
+ if ((error instanceof APIContextOverflowError || error instanceof TruncatedError) && depth + 1 < MAX_RE_SUMMARIZE_DEPTH) return (await summarizeWithFallback(half, summarizeOnce, depth + 1)).summary;
78698
+ throw error;
78699
+ }
78700
+ };
78701
+ return summarizeOnce([{
78702
+ role: "user",
78703
+ content: [{
78704
+ type: "text",
78705
+ text: `${await summarizeHalf(firstHalf)}\n\n---\n\n${await summarizeHalf(secondHalf)}`
78706
+ }],
78707
+ toolCalls: []
78708
+ }]);
78709
+ }
78710
+ /** Same split-safety rule as DefaultCompactionStrategy.canSplitAfter, but
78711
+ * operates on ContextMessage (which carries toolCalls on assistant msgs). */
78712
+ function canSplitAfterContext(messages, index) {
78713
+ const m = messages[index];
78714
+ if (m === void 0) return false;
78715
+ if (m.role === "user") return false;
78716
+ if (m.role === "assistant" && m.toolCalls.length > 0) return false;
78717
+ if (messages[index + 1]?.role === "tool") return false;
78718
+ return true;
78719
+ }
78636
78720
  /** Max consecutive compaction failures before auto-compaction is
78637
78721
  * disabled for the remainder of the turn. Resets each turn. */
78638
78722
  const MAX_CONSECUTIVE_FAILURES = 3;
@@ -78648,6 +78732,16 @@ var FullCompaction = class {
78648
78732
  consecutiveCompactionFailures = 0;
78649
78733
  _shouldInjectSessionSummary = false;
78650
78734
  compactionTimedOut = false;
78735
+ /** Token count below which compaction should not re-trigger. Set after a
78736
+ * successful compaction to 110% of the post-compaction token count, so
78737
+ * that a context sitting just above triggerRatio doesn't immediately
78738
+ * re-trigger on every step. Reset each turn. */
78739
+ lowWaterMark = 0;
78740
+ /** Whether a reactive (overflow-triggered) compaction has already been
78741
+ * attempted this turn. Prevents the overflow → compact → still near
78742
+ * limit → overflow → compact cycle from consuming the entire
78743
+ * maxCompactionPerTurn budget with marginal savings. */
78744
+ reactiveAttempted = false;
78651
78745
  compacting = null;
78652
78746
  _compactedHistory = [];
78653
78747
  strategy;
@@ -78726,8 +78820,11 @@ var FullCompaction = class {
78726
78820
  resetForTurn() {
78727
78821
  this.compactionCountInTurn = 0;
78728
78822
  this.consecutiveCompactionFailures = 0;
78823
+ this.lowWaterMark = 0;
78824
+ this.reactiveAttempted = false;
78729
78825
  }
78730
78826
  async handleOverflowError(signal, error) {
78827
+ if (this.reactiveAttempted) throw error;
78731
78828
  if (!this.beginAutoCompaction(false) && !this.compacting) {
78732
78829
  if (this.consecutiveCompactionFailures >= MAX_CONSECUTIVE_FAILURES) this.agent.emitEvent({
78733
78830
  type: "warning",
@@ -78736,11 +78833,12 @@ var FullCompaction = class {
78736
78833
  });
78737
78834
  throw error;
78738
78835
  }
78836
+ this.reactiveAttempted = true;
78739
78837
  }
78740
78838
  async beforeStep(signal) {
78741
78839
  this.agent.microCompaction.detect();
78742
78840
  const effectiveTokens = this.effectiveTokenCount;
78743
- const isReactiveTrigger = this.strategy.shouldCompact(effectiveTokens);
78841
+ const isReactiveTrigger = this.strategy.shouldCompact(effectiveTokens) && effectiveTokens >= this.lowWaterMark;
78744
78842
  const isProactiveTrigger = !isReactiveTrigger && this.strategy.shouldCompactProactively(effectiveTokens, this.estimatedMaxOutputTokens);
78745
78843
  if (isReactiveTrigger) this.checkAutoCompaction();
78746
78844
  else if (isProactiveTrigger) this.beginAutoCompaction();
@@ -78763,7 +78861,9 @@ var FullCompaction = class {
78763
78861
  }
78764
78862
  checkAutoCompaction(throwOnLimit = true) {
78765
78863
  if (this.compacting) return true;
78766
- if (!this.strategy.shouldCompact(this.effectiveTokenCount)) return false;
78864
+ const effectiveTokens = this.effectiveTokenCount;
78865
+ if (!this.strategy.shouldCompact(effectiveTokens)) return false;
78866
+ if (effectiveTokens < this.lowWaterMark) return false;
78767
78867
  return this.beginAutoCompaction(throwOnLimit);
78768
78868
  }
78769
78869
  beginAutoCompaction(throwOnLimit = true) {
@@ -78815,10 +78915,7 @@ var FullCompaction = class {
78815
78915
  try {
78816
78916
  await this.triggerPreCompactHook(data, tokensBefore, signal);
78817
78917
  const delays = retryBackoffDelays(5);
78818
- let usage;
78819
- let summary;
78820
- while (true) {
78821
- const messagesToCompact = originalHistory.slice(0, compactedCount);
78918
+ const summarizeOnce = async (messagesToCompact) => {
78822
78919
  const instruction = isUpdate ? COMPACTION_UPDATE_INSTRUCTION(data.instruction) : COMPACTION_INSTRUCTION(data.instruction);
78823
78920
  const messages = [...project(messagesToCompact), {
78824
78921
  role: "user",
@@ -78828,16 +78925,37 @@ var FullCompaction = class {
78828
78925
  }],
78829
78926
  toolCalls: []
78830
78927
  }];
78831
- class TruncatedError extends Error {}
78928
+ const response = await this.agent.generate(this.agent.config.provider, COMPACTION_SYSTEM_PROMPT, [], messages, void 0, { signal });
78929
+ if (response.finishReason === "truncated") throw new TruncatedError();
78930
+ return {
78931
+ summary: extractCompactionSummary(response, model),
78932
+ usage: response.usage
78933
+ };
78934
+ };
78935
+ let usage;
78936
+ let summary;
78937
+ while (true) {
78938
+ const messagesToCompact = originalHistory.slice(0, compactedCount);
78832
78939
  try {
78833
- const response = await this.agent.generate(this.agent.config.provider, COMPACTION_SYSTEM_PROMPT, [], messages, void 0, { signal });
78834
- if (response.finishReason === "truncated") throw new TruncatedError();
78835
- usage = response.usage;
78836
- summary = extractCompactionSummary(response, model);
78940
+ const result = await summarizeOnce(messagesToCompact);
78941
+ usage = result.usage;
78942
+ summary = result.summary;
78837
78943
  break;
78838
78944
  } catch (error) {
78839
- if (error instanceof APIContextOverflowError || error instanceof TruncatedError) compactedCount = this.strategy.reduceCompactOnOverflow(messagesToCompact);
78840
- else if (!isRetryableGenerateError(error)) throw error;
78945
+ if (error instanceof APIContextOverflowError || error instanceof TruncatedError) {
78946
+ const reduced = this.strategy.reduceCompactOnOverflow(messagesToCompact);
78947
+ if (reduced < compactedCount) compactedCount = reduced;
78948
+ else {
78949
+ this.agent.log.warn("compaction overflow at minimum split, falling back to re-summarize", {
78950
+ compactedCount,
78951
+ tokensBefore: estimateTokensForMessages(messagesToCompact)
78952
+ });
78953
+ const result = await summarizeWithFallback(messagesToCompact, summarizeOnce);
78954
+ summary = result.summary;
78955
+ usage = result.usage;
78956
+ break;
78957
+ }
78958
+ } else if (!isRetryableGenerateError(error)) throw error;
78841
78959
  if (retryCount + 1 >= 5) throw error;
78842
78960
  await sleepForRetry(delays[retryCount], signal);
78843
78961
  retryCount += 1;
@@ -78868,6 +78986,7 @@ var FullCompaction = class {
78868
78986
  result
78869
78987
  });
78870
78988
  this.agent.context.applyCompaction(result);
78989
+ this.lowWaterMark = Math.floor(this.effectiveTokenCount * 1.1);
78871
78990
  await this.extractAndStoreMemos(processedSummary);
78872
78991
  this.triggerPostCompactHook(data, result);
78873
78992
  this.consecutiveCompactionFailures = 0;
@@ -79083,12 +79202,33 @@ const flags = new FlagResolver();
79083
79202
  //#region ../../packages/agent-core/src/agent/compaction/micro.ts
79084
79203
  const DEFAULT_CONFIG = {
79085
79204
  keepRecentMessages: 20,
79205
+ keepRecentTokens: 4e4,
79206
+ pruneMinReclaimTokens: 2e4,
79086
79207
  minContentTokens: 100,
79087
79208
  minContextUsageRatio: .5,
79088
79209
  truncatedMarker: "[Old tool result content cleared]",
79089
79210
  uselessMarker: "[Uneventful result elided]"
79090
79211
  };
79091
79212
  /**
79213
+ * Compute the cutoff index: everything at index < cutoff is eligible for
79214
+ * truncation. The default floor is `keepRecentMessages` (the message-count
79215
+ * protection window). But if the trailing window exceeds `keepRecentTokens`,
79216
+ * the cutoff walks forward (toward the tail) until the window fits — so a
79217
+ * few giant tool results can't pin the cutoff behind them and starve the
79218
+ * prefix of reclaimable content.
79219
+ */
79220
+ function computeCutoff(messages, config) {
79221
+ const messageFloor = Math.max(0, messages.length - config.keepRecentMessages);
79222
+ let windowTokens = estimateTokensForMessages(messages.slice(messageFloor));
79223
+ let cutoff = messageFloor;
79224
+ while (cutoff < messages.length && windowTokens > config.keepRecentTokens) {
79225
+ const removed = messages[cutoff];
79226
+ windowTokens -= estimateTokensForMessages([removed]);
79227
+ cutoff += 1;
79228
+ }
79229
+ return cutoff;
79230
+ }
79231
+ /**
79092
79232
  * Walk the message list and find Read tool calls whose file paths were
79093
79233
  * superseded by a later Read of the same path. Returns a map from the
79094
79234
  * superseded tool call's ID to the file path (for the marker text).
@@ -79157,7 +79297,10 @@ var MicroCompaction = class {
79157
79297
  const maxContextTokens = this.agent.config.modelCapabilities.max_context_tokens;
79158
79298
  const contextTokens = this.agent.context.tokenCountWithPending;
79159
79299
  if ((maxContextTokens !== void 0 && maxContextTokens > 0 ? contextTokens / maxContextTokens : 0) < config.minContextUsageRatio) return;
79160
- const nextCutoff = Math.max(0, history.length - config.keepRecentMessages);
79300
+ const nextCutoff = computeCutoff(history, config);
79301
+ if (nextCutoff <= this.cutoff) return;
79302
+ const { beforeTokens, afterTokens } = this.measureEffect(history, nextCutoff);
79303
+ if (beforeTokens - afterTokens < config.pruneMinReclaimTokens) return;
79161
79304
  this.apply(nextCutoff);
79162
79305
  }
79163
79306
  /**
@@ -122126,7 +122269,7 @@ function optionalBuildString(value) {
122126
122269
  return typeof value === "string" && value.length > 0 ? value : void 0;
122127
122270
  }
122128
122271
  const SCREAM_BUILD_INFO = {
122129
- version: optionalBuildString("0.8.2"),
122272
+ version: optionalBuildString("0.8.4"),
122130
122273
  channel: optionalBuildString(""),
122131
122274
  commit: optionalBuildString(""),
122132
122275
  buildTarget: optionalBuildString("darwin-arm64")
@@ -132953,6 +133096,517 @@ async function handleLikeCommand(host) {
132953
133096
  host.showStatus("偏好已保存(下次新会话生效)", host.state.theme.colors.success);
132954
133097
  }
132955
133098
  //#endregion
133099
+ //#region src/tui/utils/open-url.ts
133100
+ function openUrl(url) {
133101
+ const command = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
133102
+ "/c",
133103
+ "start",
133104
+ "",
133105
+ url
133106
+ ]] : ["xdg-open", [url]];
133107
+ execFile(command[0], command[1], () => {});
133108
+ }
133109
+ //#endregion
133110
+ //#region src/tui/commands/knowledge-store.ts
133111
+ let knowledgeStoreInstance;
133112
+ async function getKnowledgeStore() {
133113
+ if (knowledgeStoreInstance === void 0) {
133114
+ knowledgeStoreInstance = new KnowledgeStore(getDataDir());
133115
+ await knowledgeStoreInstance.init();
133116
+ knowledgeStoreInstance.setEmbeddingEngine(createFastEmbedEngine());
133117
+ }
133118
+ return knowledgeStoreInstance;
133119
+ }
133120
+ //#endregion
133121
+ //#region src/tui/commands/knowledge-web.ts
133122
+ /**
133123
+ * /knowledge web — start a local HTTP server and open the browser
133124
+ * to display an interactive Cytoscape.js knowledge graph visualization.
133125
+ */
133126
+ const activeServers = /* @__PURE__ */ new Set();
133127
+ function registerServer(server) {
133128
+ activeServers.add(server);
133129
+ server.on("close", () => {
133130
+ activeServers.delete(server);
133131
+ });
133132
+ }
133133
+ function closeAllServers() {
133134
+ for (const server of activeServers) server.close();
133135
+ }
133136
+ process.on("exit", closeAllServers);
133137
+ const HTML = `<!doctype html>
133138
+ <html lang="zh-CN">
133139
+ <head>
133140
+ <meta charset="utf-8">
133141
+ <meta name="viewport" content="width=device-width,initial-scale=1">
133142
+ <title>Scream 知识图谱</title>
133143
+ <style>
133144
+ *{margin:0;padding:0;box-sizing:border-box}
133145
+ html,body{width:100%;height:100%;overflow:hidden;font-family:Inter,"PingFang SC","Microsoft YaHei",system-ui,sans-serif;background:#fff;color:#18181b}
133146
+ #container{width:100%;height:100%;position:relative;overflow:hidden}
133147
+ svg{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1}
133148
+ .node{position:absolute;border-radius:10px;display:flex;align-items:center;justify-content:center;text-align:center;cursor:pointer;z-index:2;user-select:none;transition:opacity .25s,box-shadow .25s,border-color .25s;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;padding:0 14px;font-size:12px;line-height:1.3;animation:float 6s ease-in-out infinite}
133149
+ .node.entity{background:#fff;border:1.5px solid #d4d4d8;font-weight:600;color:#1a1a1a;box-shadow:0 2px 8px rgba(0,0,0,.06)}
133150
+ .node.entity.root{border:2px solid #15803d;background:#f0fdf4;box-shadow:0 4px 20px rgba(21,128,61,.15);font-size:14px}
133151
+ .node.entity.expanded{border-color:#15803d;box-shadow:0 2px 12px rgba(21,128,61,.1)}
133152
+ .node.event{background:#fafafa;border:1px solid #e5e7eb;font-weight:500;color:#525252;box-shadow:0 1px 4px rgba(0,0,0,.04);border-radius:8px}
133153
+ .node.event.expanded{border-color:#22c55e;background:#f8fef9;box-shadow:0 2px 8px rgba(21,128,61,.08)}
133154
+ .node.selected{border-color:#15803d !important;background:#f0fdf4 !important;box-shadow:0 0 0 3px rgba(21,128,61,.12),0 4px 16px rgba(21,128,61,.14) !important}
133155
+ .node.dimmed{opacity:.12;filter:grayscale(.6)}
133156
+ .node:hover{box-shadow:0 4px 16px rgba(0,0,0,.08) !important}
133157
+ .edge{stroke:#b0b0b0;stroke-width:.8;fill:none;opacity:.6}
133158
+ .edge.animated{stroke:#999;stroke-width:1;stroke-dasharray:6 4;animation:dash 1.2s linear infinite;opacity:.45}
133159
+ .edge.dimmed{stroke:#e0e0e0;stroke-width:.4;opacity:.06}
133160
+ .edge.highlighted{stroke:#15803d;stroke-width:1.8;opacity:1}
133161
+ @keyframes dash{to{stroke-dashoffset:-10}}
133162
+ @keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
133163
+ #toolbar{position:absolute;left:16px;top:16px;z-index:10;display:flex;align-items:center;gap:10px;background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:8px 18px;box-shadow:0 2px 8px rgba(0,0,0,.04)}
133164
+ #toolbar button{background:#fff;color:#525252;border:1px solid #d4d4d8;padding:5px 16px;border-radius:8px;cursor:pointer;font-size:12px;font-weight:500;transition:all .15s}
133165
+ #toolbar button:hover{background:#f0fdf4;border-color:#15803d;color:#15803d}
133166
+ #toolbar .sep{width:1px;height:16px;background:#e5e7eb}
133167
+ #toolbar .chip{color:#a1a1aa;font-size:11px;letter-spacing:.3px}
133168
+ #toolbar .chip b{color:#15803d;font-size:13px;font-weight:700;margin-left:4px}
133169
+ #hint{position:absolute;left:16px;bottom:16px;z-index:10;color:#c4c4c4;font-size:11px;letter-spacing:.2px}
133170
+ #detail{position:fixed;right:0;top:0;width:380px;height:100vh;background:#fff;border-left:1px solid #e5e7eb;transform:translateX(100%);transition:transform .3s cubic-bezier(.4,0,.2,1);z-index:20;overflow-y:auto;padding:32px 28px;box-shadow:-4px 0 24px rgba(0,0,0,.04)}
133171
+ #detail.open{transform:translateX(0)}
133172
+ #detail h3{font-size:17px;font-weight:700;color:#18181b;margin-bottom:20px;padding-right:32px;line-height:1.4}
133173
+ #detail .field{margin-bottom:18px}
133174
+ #detail .field .label{font-size:10px;color:#15803d;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:5px}
133175
+ #detail .field .value{font-size:13px;color:#525252;line-height:1.6}
133176
+ #detail .close{position:absolute;top:20px;right:20px;background:none;border:none;color:#c4c4c4;cursor:pointer;font-size:18px;width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:8px;transition:all .15s}
133177
+ #detail .close:hover{color:#18181b;background:#f4f4f5}
133178
+ #detail .conn-item{font-size:13px;color:#15803d;cursor:pointer;padding:7px 10px;border-radius:8px;transition:all .15s;border:1px solid transparent}
133179
+ #detail .conn-item:hover{background:#f0fdf4;border-color:#bbf7d0}
133180
+ #detail .badge{font-size:10px;color:#15803d;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:5px;padding:1px 7px;margin-left:8px;font-weight:500}
133181
+ #detail .back-btn{display:inline-flex;align-items:center;gap:5px;background:none;border:1px solid #d4d4d8;color:#525252;padding:6px 14px;border-radius:8px;cursor:pointer;font-size:12px;font-weight:500;margin-bottom:16px;transition:all .15s}
133182
+ #detail .back-btn:hover{background:#f0fdf4;border-color:#15803d;color:#15803d}
133183
+ #detail .type-tag{display:inline-block;font-size:10px;color:#15803d;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:5px;padding:2px 10px;font-weight:600;margin-bottom:16px;letter-spacing:.3px}
133184
+ #detail .divider{height:1px;background:#f0f0f0;margin:18px 0}
133185
+ #loading{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:30;color:#15803d;font-size:14px;font-weight:500;display:flex;align-items:center;gap:8px}
133186
+ #loading .dot{width:6px;height:6px;background:#15803d;border-radius:50%;animation:blink 1.2s ease-in-out infinite}
133187
+ #loading .dot:nth-child(2){animation-delay:.2s}
133188
+ #loading .dot:nth-child(3){animation-delay:.4s}
133189
+ @keyframes blink{0%,80%,100%{opacity:.2}40%{opacity:1}}
133190
+ </style>
133191
+ </head>
133192
+ <body>
133193
+ <div id="container"><svg id="edges-svg"></svg></div>
133194
+ <div id="toolbar">
133195
+ <span class="chip">实体<b id="stat-ent">0</b></span>
133196
+ <span class="chip">事件<b id="stat-evt">0</b></span>
133197
+ <span class="chip">关系<b id="stat-edg">0</b></span>
133198
+ <span class="sep"></span>
133199
+ <button id="btn-reset">重置</button>
133200
+ <button id="btn-expand">展开全部</button>
133201
+ </div>
133202
+ <div id="hint">单击展开/收起 · 双击查看详情 · 滚轮缩放 · 拖拽平移</div>
133203
+ <div id="detail"><button class="close" id="btn-close">&times;</button><div id="detail-body"></div></div>
133204
+ <div id="loading"><span class="dot"></span><span class="dot"></span><span class="dot"></span>加载知识图谱...</div>
133205
+ <script>
133206
+ (function(){
133207
+ var graphData;
133208
+ var container = document.getElementById('container');
133209
+ var svg = document.getElementById('edges-svg');
133210
+
133211
+ // camera
133212
+ var cam = {x:0,y:0,zoom:1};
133213
+ var W=window.innerWidth, H=window.innerHeight;
133214
+
133215
+ function w2s(wx,wy){return{x:(wx-cam.x)*cam.zoom+W/2,y:(wy-cam.y)*cam.zoom+H/2}}
133216
+ function s2w(sx,sy){return{x:(sx-W/2)/cam.zoom+cam.x,y:(sy-H/2)/cam.zoom+cam.y}}
133217
+
133218
+ // pan & zoom
133219
+ var drag=false,moved=false,dragX,dragY;
133220
+ container.addEventListener('mousedown',function(e){if(e.target!==container&&e.target!==svg)return;drag=true;moved=false;dragX=e.clientX;dragY=e.clientY});
133221
+ window.addEventListener('mousemove',function(e){if(!drag)return;var dx=e.clientX-dragX,dy=e.clientY-dragY;if(Math.abs(dx)>2||Math.abs(dy)>2)moved=true;cam.x-=dx/cam.zoom;cam.y-=dy/cam.zoom;dragX=e.clientX;dragY=e.clientY;render()});
133222
+ window.addEventListener('mouseup',function(){drag=false});
133223
+ container.addEventListener('wheel',function(e){e.preventDefault();var pt=s2w(e.clientX,e.clientY);var f=e.deltaY<0?1.12:1/1.12;cam.zoom=Math.max(.15,Math.min(4,cam.zoom*f));cam.x=pt.x-(e.clientX-W/2)/cam.zoom;cam.y=pt.y-(e.clientY-H/2)/cam.zoom;render()},{passive:false});
133224
+ window.addEventListener('resize',function(){W=window.innerWidth;H=window.innerHeight;render()});
133225
+
133226
+ // layout constants (SAG)
133227
+ var EW=160,EH=40,EVW=170,EVH=40;
133228
+ var ER_START=220,ER_GAP=190,ER_SLOT=200;
133229
+ var VR_START=500,VR_GAP=180,VR_SLOT=220;
133230
+ var ROOT_LIMIT=8;
133231
+ var GA=Math.PI*(3-Math.sqrt(5));
133232
+
133233
+ // state
133234
+ var eById={},evById={};
133235
+ var evByEnt={},entByEv={};
133236
+ var expEnt=new Set(),expEv=new Set();
133237
+ var selId=null;
133238
+ var pos={};
133239
+ var navStack=[];
133240
+
133241
+ // node/edge DOM elements
133242
+ var nodeEls={},edgeEls=[];
133243
+
133244
+ fetch('/api/graph').then(function(r){return r.json()}).then(function(data){
133245
+ graphData=data;
133246
+ document.getElementById('stat-ent').textContent=data.entities.length;
133247
+ document.getElementById('stat-evt').textContent=data.events.length;
133248
+ document.getElementById('stat-edg').textContent=data.edges.length;
133249
+ document.getElementById('loading').style.display='none';
133250
+
133251
+ data.entities.sort(function(a,b){return(b.eventCount||0)-(a.eventCount||0)||a.name.localeCompare(b.name)});
133252
+ data.events.sort(function(a,b){return(a.rank||0)-(b.rank||0)||a.title.localeCompare(b.title)});
133253
+
133254
+ data.entities.forEach(function(e){eById[e.id]=e});
133255
+ data.events.forEach(function(e){evById[e.id]=e});
133256
+
133257
+ data.edges.forEach(function(e){
133258
+ if(!evByEnt[e.entityId])evByEnt[e.entityId]=[];
133259
+ evByEnt[e.entityId].push(e.eventId);
133260
+ if(!entByEv[e.eventId])entByEv[e.eventId]=[];
133261
+ entByEv[e.eventId].push(e.entityId);
133262
+ });
133263
+
133264
+ // default: show all entities, no events expanded
133265
+ graphData.entities.forEach(function(e){expEnt.add(e.id)});
133266
+
133267
+ buildPositions();
133268
+ fitView();
133269
+ rebuildDOM();
133270
+ });
133271
+
133272
+ function buildPositions(){
133273
+ pos={};
133274
+ var root=graphData.entities[0];
133275
+ if(root)pos[root.id]={x:-EW/2,y:-EH/2,root:true};
133276
+ var sec=graphData.entities.slice(1);
133277
+ placeRings(sec.map(function(e){return e.id}),ER_START,ER_GAP,ER_SLOT,EW,EH,-Math.PI/2);
133278
+ placeEvents(graphData.events,-Math.PI/2+Math.PI/12);
133279
+ }
133280
+
133281
+ function placeRings(ids,sr,gap,slot,nw,nh,aoff){
133282
+ var idx=0,ring=0;
133283
+ while(idx<ids.length){
133284
+ var r=sr+ring*gap;
133285
+ var cap=Math.max(6,Math.floor(2*Math.PI*r/slot));
133286
+ for(var s=0;s<cap&&idx<ids.length;s++){
133287
+ var a=aoff+(2*Math.PI*s)/cap;
133288
+ pos[ids[idx]]={x:Math.cos(a)*r-nw/2,y:Math.sin(a)*r-nh/2};
133289
+ idx++;
133290
+ }
133291
+ ring++;
133292
+ }
133293
+ }
133294
+
133295
+ function placeEvents(events,aoff){
133296
+ var occ={};
133297
+ for(var i=0;i<events.length;i++){
133298
+ var da=i*GA;
133299
+ var sl=findSlot(da,aoff,occ);
133300
+ var r=VR_START+sl.ring*VR_GAP;
133301
+ var a=aoff+(2*Math.PI*sl.idx)/sl.cap;
133302
+ pos[events[i].id]={x:Math.cos(a)*r-EVW/2,y:Math.sin(a)*r-EVH/2};
133303
+ }
133304
+ }
133305
+
133306
+ function findSlot(da,aoff,occ){
133307
+ var ring=0;
133308
+ while(true){
133309
+ var r=VR_START+ring*VR_GAP;
133310
+ var cap=Math.max(8,Math.floor(2*Math.PI*r/VR_SLOT));
133311
+ var o=occ[ring]||{};
133312
+ var ds=((Math.round(((da-aoff)/(2*Math.PI))*cap)%cap)+cap)%cap;
133313
+ var f=nearFree(ds,cap,o);
133314
+ if(f!=null){o[f]=true;occ[ring]=o;return{ring:ring,idx:f,cap:cap}}
133315
+ ring++;
133316
+ }
133317
+ }
133318
+
133319
+ function nearFree(d,cap,o){
133320
+ for(var i=0;i<cap;i++){
133321
+ var cs=i===0?[d]:[((d-i)%cap+cap)%cap,((d+i)%cap+cap)%cap];
133322
+ for(var j=0;j<cs.length;j++)if(!o[cs[j]])return cs[j];
133323
+ }
133324
+ return null;
133325
+ }
133326
+
133327
+ function fitView(){
133328
+ var vis=getVisible();
133329
+ var ids=[];
133330
+ vis.entities.forEach(function(id){ids.push(id)});
133331
+ vis.events.forEach(function(id){ids.push(id)});
133332
+ if(!ids.length){cam.x=0;cam.y=0;cam.zoom=1;return}
133333
+ var x0=Infinity,x1=-Infinity,y0=Infinity,y1=-Infinity;
133334
+ ids.forEach(function(id){var p=pos[id];if(!p)return;var w=eById[id]?EW:EVW,h=eById[id]?EH:EVH;if(p.x<x0)x0=p.x;if(p.x+w>x1)x1=p.x+w;if(p.y<y0)y0=p.y;if(p.y+h>y1)y1=p.y+h});
133335
+ cam.x=(x0+x1)/2;cam.y=(y0+y1)/2;
133336
+ cam.zoom=Math.min((W-120)/(x1-x0),(H-120)/(y1-y0),1.2);
133337
+ }
133338
+
133339
+ function getVisible(){
133340
+ var ve=new Set(),vv=new Set();
133341
+ graphData.entities.slice(0,ROOT_LIMIT).forEach(function(e){ve.add(e.id)});
133342
+ expEnt.forEach(function(eid){ve.add(eid);(evByEnt[eid]||[]).forEach(function(v){vv.add(v)})});
133343
+ expEv.forEach(function(eid){vv.add(eid);(entByEv[eid]||[]).forEach(function(e){ve.add(e)})});
133344
+ return{entities:ve,events:vv};
133345
+ }
133346
+
133347
+ function rebuildDOM(){
133348
+ // clear old
133349
+ Object.keys(nodeEls).forEach(function(id){if(nodeEls[id].parentNode)nodeEls[id].parentNode.removeChild(nodeEls[id])});
133350
+ while(svg.firstChild)svg.removeChild(svg.firstChild);
133351
+ nodeEls={};edgeEls=[];
133352
+
133353
+ var vis=getVisible();
133354
+ var visAll=new Set();vis.entities.forEach(function(id){visAll.add(id)});vis.events.forEach(function(id){visAll.add(id)});
133355
+
133356
+ // connection set for highlighting
133357
+ var conn=null;
133358
+ if(selId){
133359
+ conn=new Set();conn.add(selId);
133360
+ graphData.edges.forEach(function(e){
133361
+ if(!visAll.has(e.entityId)||!visAll.has(e.eventId))return;
133362
+ if(e.entityId===selId||e.eventId===selId){conn.add(e.entityId);conn.add(e.eventId)}
133363
+ });
133364
+ }
133365
+
133366
+ // edges
133367
+ graphData.edges.forEach(function(e){
133368
+ if(!visAll.has(e.entityId)||!visAll.has(e.eventId))return;
133369
+ var line=document.createElementNS('http://www.w3.org/2000/svg','line');
133370
+ var isDir=selId&&(e.entityId===selId||e.eventId===selId);
133371
+ var isRel=conn&&conn.has(e.entityId)&&conn.has(e.eventId);
133372
+ var isExp=expEnt.has(e.entityId)||expEv.has(e.eventId);
133373
+ if(isDir)line.setAttribute('class','edge highlighted');
133374
+ else if(isRel)line.setAttribute('class','edge');
133375
+ else if(conn)line.setAttribute('class','edge dimmed');
133376
+ else line.setAttribute('class',isExp?'edge animated':'edge');
133377
+ line.setAttribute('data-src',e.entityId);
133378
+ line.setAttribute('data-tgt',e.eventId);
133379
+ svg.appendChild(line);
133380
+ edgeEls.push(line);
133381
+ });
133382
+
133383
+ // nodes
133384
+ graphData.entities.forEach(function(ent){
133385
+ if(!vis.entities.has(ent.id))return;
133386
+ var el=mkNode(ent.id,ent.name,'entity',pos[ent.id].root,expEnt.has(ent.id));
133387
+ container.appendChild(el);nodeEls[ent.id]=el;
133388
+ });
133389
+ graphData.events.forEach(function(ev){
133390
+ if(!vis.events.has(ev.id))return;
133391
+ var el=mkNode(ev.id,ev.title,'event',false,expEv.has(ev.id));
133392
+ container.appendChild(el);nodeEls[ev.id]=el;
133393
+ });
133394
+
133395
+ // apply dimming
133396
+ if(conn){
133397
+ Object.keys(nodeEls).forEach(function(id){
133398
+ if(conn.has(id))nodeEls[id].classList.remove('dimmed');
133399
+ else nodeEls[id].classList.add('dimmed');
133400
+ });
133401
+ if(selId&&nodeEls[selId])nodeEls[selId].classList.add('selected');
133402
+ }
133403
+
133404
+ render();
133405
+ }
133406
+
133407
+ function mkNode(id,label,kind,root,expanded){
133408
+ var el=document.createElement('div');
133409
+ el.className='node '+kind;
133410
+ if(root)el.classList.add('root');
133411
+ if(expanded)el.classList.add('expanded');
133412
+ el.textContent=label;
133413
+ el.setAttribute('data-id',id);
133414
+ el.setAttribute('data-kind',kind);
133415
+ var dur=5+Math.random()*4;
133416
+ var del=Math.random()*dur;
133417
+ el.style.animationDuration=dur+'s';
133418
+ el.style.animationDelay='-'+del+'s';
133419
+
133420
+ var clickTimer=null;
133421
+ el.addEventListener('click',function(e){
133422
+ e.stopPropagation();
133423
+ if(clickTimer){clearTimeout(clickTimer);clickTimer=null;return}
133424
+ clickTimer=setTimeout(function(){
133425
+ clickTimer=null;
133426
+ toggleNode(id,kind);
133427
+ selId=id;
133428
+ rebuildDOM();
133429
+ },200);
133430
+ });
133431
+ el.addEventListener('dblclick',function(e){
133432
+ e.stopPropagation();
133433
+ if(clickTimer){clearTimeout(clickTimer);clickTimer=null}
133434
+ navStack=[];
133435
+ showDetail(id,kind);
133436
+ });
133437
+ return el;
133438
+ }
133439
+
133440
+ function toggleNode(id,kind){
133441
+ if(kind==='entity'){
133442
+ if(expEnt.has(id)){
133443
+ expEnt.delete(id);
133444
+ var related=new Set(evByEnt[id]||[]);
133445
+ expEv.forEach(function(eid){if(related.has(eid))expEv.delete(eid)});
133446
+ }else expEnt.add(id);
133447
+ }else{
133448
+ if(expEv.has(id))expEv.delete(id);
133449
+ else expEv.add(id);
133450
+ }
133451
+ }
133452
+
133453
+ function render(){
133454
+ // position nodes
133455
+ Object.keys(nodeEls).forEach(function(id){
133456
+ var p=pos[id];if(!p)return;
133457
+ var sp=w2s(p.x,p.y);
133458
+ var w=eById[id]?EW:EVW,h=eById[id]?EH:EVH;
133459
+ var el=nodeEls[id];
133460
+ el.style.left=sp.x+'px';
133461
+ el.style.top=sp.y+'px';
133462
+ el.style.width=(w*cam.zoom)+'px';
133463
+ el.style.height=(h*cam.zoom)+'px';
133464
+ el.style.fontSize=Math.max(8,12*cam.zoom)+'px';
133465
+ });
133466
+
133467
+ // position edges
133468
+ edgeEls.forEach(function(line){
133469
+ var srcId=line.getAttribute('data-src');
133470
+ var tgtId=line.getAttribute('data-tgt');
133471
+ var sp=pos[srcId],tp=pos[tgtId];
133472
+ if(!sp||!tp)return;
133473
+ var s=w2s(sp.x+EW/2,sp.y+EH/2);
133474
+ var t=w2s(tp.x+EVW/2,tp.y+EVH/2);
133475
+ line.setAttribute('x1',s.x);line.setAttribute('y1',s.y);
133476
+ line.setAttribute('x2',t.x);line.setAttribute('y2',t.y);
133477
+ });
133478
+ }
133479
+
133480
+ // click background to deselect
133481
+ container.addEventListener('click',function(e){
133482
+ if(e.target===container||e.target===svg){selId=null;rebuildDOM()}
133483
+ });
133484
+
133485
+ // detail panel
133486
+ function showDetail(id,kind,pushNav){
133487
+ if(pushNav!==false)navStack.push({id:id,kind:kind});
133488
+ var panel=document.getElementById('detail');
133489
+ var body=document.getElementById('detail-body');
133490
+ var html='';
133491
+ if(navStack.length>1)html+='<button class="back-btn" id="btn-back">← 返回</button>';
133492
+ if(kind==='entity'){
133493
+ var e=eById[id];
133494
+ html+='<div class="type-tag">'+esc(e.type)+'</div>';
133495
+ html+='<h3>'+esc(e.name)+'</h3>';
133496
+ html+='<div class="field"><div class="label">关联事件</div><div class="value">'+(e.eventCount||0)+' 个</div></div>';
133497
+ var ce=evByEnt[id]||[];
133498
+ if(ce.length){html+='<div class="divider"></div><div class="field"><div class="label">事件列表</div>';ce.forEach(function(eid){var ev=evById[eid];if(ev)html+='<div class="conn-item" data-id="'+eid+'" data-kind="event">'+esc(ev.title)+'</div>'});html+='</div>'}
133499
+ }else{
133500
+ var ev=evById[id];
133501
+ html+='<h3>'+esc(ev.title)+'</h3>';
133502
+ var ce=entByEv[id]||[];
133503
+ if(ce.length){html+='<div class="divider"></div><div class="field"><div class="label">关联实体</div>';ce.forEach(function(eid){var e=eById[eid];if(e)html+='<div class="conn-item" data-id="'+eid+'" data-kind="entity">'+esc(e.name)+'<span class="badge">'+esc(e.type)+'</span></div>'});html+='</div>'}
133504
+ }
133505
+ body.innerHTML=html;panel.classList.add('open');
133506
+ var backBtn=document.getElementById('btn-back');
133507
+ if(backBtn)backBtn.addEventListener('click',function(){
133508
+ navStack.pop();
133509
+ var prev=navStack[navStack.length-1];
133510
+ if(prev){selId=prev.id;rebuildDOM();showDetail(prev.id,prev.kind,false)}
133511
+ else{panel.classList.remove('open')}
133512
+ });
133513
+ body.querySelectorAll('.conn-item').forEach(function(el){
133514
+ el.addEventListener('click',function(){
133515
+ var nid=el.getAttribute('data-id'),nk=el.getAttribute('data-kind');
133516
+ if(nk==='entity'&&!expEnt.has(nid))expEnt.add(nid);
133517
+ if(nk==='event'&&!expEv.has(nid))expEv.add(nid);
133518
+ selId=nid;rebuildDOM();showDetail(nid,nk);
133519
+ });
133520
+ });
133521
+ }
133522
+
133523
+ function esc(s){if(!s)return'';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
133524
+
133525
+ document.getElementById('btn-reset').onclick=function(){expEnt.clear();expEv.clear();selId=null;graphData.entities.forEach(function(e){expEnt.add(e.id)});document.getElementById('detail').classList.remove('open');navStack=[];fitView();rebuildDOM()};
133526
+ document.getElementById('btn-expand').onclick=function(){graphData.entities.forEach(function(e){expEnt.add(e.id)});graphData.events.forEach(function(e){expEv.add(e.id)});selId=null;document.getElementById('detail').classList.remove('open');fitView();rebuildDOM()};
133527
+ document.getElementById('btn-close').onclick=function(){document.getElementById('detail').classList.remove('open');navStack=[]};
133528
+ })();
133529
+ <\/script>
133530
+ </body>
133531
+ </html>`;
133532
+ async function serveGraphJSON(store, res) {
133533
+ try {
133534
+ const [entities, events, edges, sources] = await Promise.all([
133535
+ store.listEntities(),
133536
+ store.listEvents(),
133537
+ store.listEventEntities(),
133538
+ store.listSources()
133539
+ ]);
133540
+ const eventEntityMap = /* @__PURE__ */ new Map();
133541
+ for (const edge of edges) {
133542
+ const list = eventEntityMap.get(edge.eventId) ?? [];
133543
+ list.push(edge.entityId);
133544
+ eventEntityMap.set(edge.eventId, list);
133545
+ }
133546
+ const data = {
133547
+ entities: entities.map(({ id, sourceId, type, name, normalizedName, eventCount }) => ({
133548
+ id,
133549
+ sourceId,
133550
+ type,
133551
+ name,
133552
+ normalizedName,
133553
+ eventCount
133554
+ })),
133555
+ events: events.map(({ id, sourceId, documentId, title, rank }) => ({
133556
+ id,
133557
+ sourceId,
133558
+ documentId,
133559
+ title,
133560
+ rank,
133561
+ entityIds: eventEntityMap.get(id) ?? []
133562
+ })),
133563
+ edges,
133564
+ sources: sources.map(({ id, name }) => ({
133565
+ id,
133566
+ name
133567
+ }))
133568
+ };
133569
+ res.writeHead(200, {
133570
+ "Content-Type": "application/json",
133571
+ "Cache-Control": "no-store"
133572
+ });
133573
+ res.end(JSON.stringify(data));
133574
+ } catch (error) {
133575
+ res.writeHead(500, { "Content-Type": "application/json" });
133576
+ res.end(JSON.stringify({ error: String(error) }));
133577
+ }
133578
+ }
133579
+ function serveHTML(res) {
133580
+ res.writeHead(200, {
133581
+ "Content-Type": "text/html; charset=utf-8",
133582
+ "Cache-Control": "no-store"
133583
+ });
133584
+ res.end(HTML);
133585
+ }
133586
+ async function handleWeb() {
133587
+ const store = await getKnowledgeStore();
133588
+ const s = await store.stats();
133589
+ if (s.entities === 0 && s.events === 0) throw new Error("知识库为空,请先用 /knowledge 摄入文档");
133590
+ const server = createServer((req, res) => {
133591
+ if (req.url === "/api/graph") {
133592
+ serveGraphJSON(store, res);
133593
+ return;
133594
+ }
133595
+ serveHTML(res);
133596
+ });
133597
+ registerServer(server);
133598
+ await new Promise((resolve, reject) => {
133599
+ server.once("error", reject);
133600
+ server.listen(0, "127.0.0.1", () => {
133601
+ server.off("error", reject);
133602
+ resolve();
133603
+ });
133604
+ });
133605
+ const url = `http://127.0.0.1:${server.address().port}`;
133606
+ openUrl(url);
133607
+ console.log(`\n 知识图谱: ${url}\n 按 Ctrl+C 停止服务器\n`);
133608
+ }
133609
+ //#endregion
132956
133610
  //#region src/tui/components/dialogs/knowledge-result-viewer.ts
132957
133611
  /**
132958
133612
  * KnowledgeResultViewer — full-screen scrollable text viewer for /knowledge
@@ -133307,15 +133961,6 @@ function fitExactly$3(line, width) {
133307
133961
  }
133308
133962
  //#endregion
133309
133963
  //#region src/tui/commands/knowledge.ts
133310
- let knowledgeStoreInstance;
133311
- async function getKnowledgeStore() {
133312
- if (knowledgeStoreInstance === void 0) {
133313
- knowledgeStoreInstance = new KnowledgeStore(getDataDir());
133314
- await knowledgeStoreInstance.init();
133315
- knowledgeStoreInstance.setEmbeddingEngine(createFastEmbedEngine());
133316
- }
133317
- return knowledgeStoreInstance;
133318
- }
133319
133964
  function promptTextInput(host, title, opts) {
133320
133965
  const { promise, resolve } = Promise.withResolvers();
133321
133966
  const dialog = new TextInputDialogComponent((result) => {
@@ -133385,6 +134030,19 @@ async function handleIngest(host) {
133385
134030
  }
133386
134031
  const store = await getKnowledgeStore();
133387
134032
  const llm = makeLlmCaller(host);
134033
+ const checkSpinner = host.showProgressSpinner("检查向量模型...");
134034
+ if (!(await store.ensureEmbeddingReady()).ok) {
134035
+ checkSpinner.stop({
134036
+ ok: false,
134037
+ label: "向量模型下载失败"
134038
+ });
134039
+ host.showNotice("向量模型下载失败", "知识库需要中文向量模型 bge-small-zh-v1.5(约 95MB)才能进行语义检索。\n\n可能原因:\n• 网络不通,无法下载模型文件\n• onnxruntime native binding 加载失败\n\n请检查网络后重试。");
134040
+ return;
134041
+ }
134042
+ checkSpinner.stop({
134043
+ ok: true,
134044
+ label: "向量模型就绪"
134045
+ });
133388
134046
  const spinner = host.showProgressSpinner("开始摄入...");
133389
134047
  try {
133390
134048
  if (stats.isDirectory()) {
@@ -133620,6 +134278,11 @@ async function handleKnowledgeCommand(host, _args) {
133620
134278
  value: "stats",
133621
134279
  label: "📊 统计信息",
133622
134280
  description: "查看知识库整体统计"
134281
+ },
134282
+ {
134283
+ value: "web",
134284
+ label: "🌐 知识图谱",
134285
+ description: "在浏览器中查看交互式知识图谱"
133623
134286
  }
133624
134287
  ];
133625
134288
  const showMenu = () => {
@@ -133637,6 +134300,7 @@ async function handleKnowledgeCommand(host, _args) {
133637
134300
  else if (value === "search") await handleSearch(host);
133638
134301
  else if (value === "delete") await handleDelete(host);
133639
134302
  else if (value === "stats") await handleStats(host);
134303
+ else if (value === "web") await handleWeb();
133640
134304
  } catch (error) {
133641
134305
  const msg = error instanceof Error ? error.message : String(error);
133642
134306
  host.showError(`操作失败: ${msg}`);
@@ -134978,17 +135642,6 @@ function mcpServerStatusKey(server) {
134978
135642
  ]);
134979
135643
  }
134980
135644
  //#endregion
134981
- //#region src/tui/utils/open-url.ts
134982
- function openUrl(url) {
134983
- const command = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
134984
- "/c",
134985
- "start",
134986
- "",
134987
- url
134988
- ]] : ["xdg-open", [url]];
134989
- execFile(command[0], command[1], () => {});
134990
- }
134991
- //#endregion
134992
135645
  //#region src/tui/utils/mcp-oauth.ts
134993
135646
  const MAX_AUTH_URL_SET_SIZE = 200;
134994
135647
  var McpOAuthAuthorizationUrlOpener = class {
@@ -146508,6 +147161,14 @@ var ClaudeStreamJsonWriter = class {
146508
147161
  if (input > 0) this.tokenUsage.input += input;
146509
147162
  if (output > 0) this.tokenUsage.output += output;
146510
147163
  }
147164
+ /** Returns the accumulated token usage for the current turn. Used by
147165
+ * the result event emitter so the final `result` event carries the
147166
+ * turn's total token usage. Without this, usage was accumulated but
147167
+ * never emitted — consumers (cc-connect, mobile clients) saw empty
147168
+ * usage fields. */
147169
+ getTokenUsage() {
147170
+ return { ...this.tokenUsage };
147171
+ }
146511
147172
  nextMsgId() {
146512
147173
  this.msgCounter += 1;
146513
147174
  return `msg_${this.msgCounter.toString(36)}`;
@@ -146754,22 +147415,26 @@ async function runStreamJson(opts) {
146754
147415
  let activeAgentId;
146755
147416
  let settled = false;
146756
147417
  let unsubscribe;
146757
- const finish = (error) => {
147418
+ const finish = (error, exitCode) => {
146758
147419
  if (settled) return;
146759
147420
  settled = true;
146760
147421
  unsubscribe?.();
146761
147422
  if (error) {
146762
- writer.emitResult("error", error.message);
147423
+ writer.emitResult("error", error.message, writer.getTokenUsage());
147424
+ if (exitCode !== void 0) process.exitCode = exitCode;
146763
147425
  reject(error);
146764
147426
  } else {
146765
- writer.emitResult("success", "");
147427
+ writer.emitResult("success", "", writer.getTokenUsage());
146766
147428
  resolve();
146767
147429
  }
146768
147430
  };
146769
147431
  unsubscribe = session.onEvent((event) => {
146770
147432
  if (event.type === "error") {
146771
147433
  if (event.agentId !== "main") return;
146772
- finish(/* @__PURE__ */ new Error(`${event.code}: ${event.message}`));
147434
+ const errCode = event.code;
147435
+ let exitCode = 1;
147436
+ if (errCode === "provider.auth_error" || errCode === "provider.rate_limit" || errCode === "provider.api_error" || errCode === "provider.connection_error") exitCode = 3;
147437
+ finish(/* @__PURE__ */ new Error(`${event.code}: ${event.message}`), exitCode);
146773
147438
  return;
146774
147439
  }
146775
147440
  if (event.type === "subagent.spawned") {
@@ -146806,9 +147471,14 @@ async function runStreamJson(opts) {
146806
147471
  writer.updateUsage(inputTotal, event.usage.output ?? 0);
146807
147472
  }
146808
147473
  } else if (type === "turn.ended") if (event.reason === "completed") finish();
147474
+ else if (event.reason === "cancelled") finish(/* @__PURE__ */ new Error("Turn cancelled"));
146809
147475
  else {
147476
+ const errCode = event.error?.code;
146810
147477
  const errMsg = event.error !== void 0 ? `${event.error.code}: ${event.error.message}` : `Turn ended: ${event.reason}`;
146811
- finish(new Error(errMsg));
147478
+ let code = 1;
147479
+ if (errCode === "loop.max_steps_exceeded") code = 4;
147480
+ else if (errCode === "provider.auth_error" || errCode === "provider.rate_limit" || errCode === "provider.api_error" || errCode === "provider.connection_error") code = 3;
147481
+ finish(new Error(errMsg), code);
146812
147482
  }
146813
147483
  });
146814
147484
  session.prompt(userText).catch((error) => {
@@ -146829,9 +147499,11 @@ async function runStreamJson(opts) {
146829
147499
  await turnPromise;
146830
147500
  }
146831
147501
  } catch (error) {
146832
- log.error("stream-json: fatal", { error });
146833
- writer.emitResult("error", error instanceof Error ? error.message : String(error));
146834
- process.exitCode = 1;
147502
+ if (process.exitCode === void 0) {
147503
+ log.error("stream-json: fatal", { error });
147504
+ writer.emitResult("error", error instanceof Error ? error.message : String(error), writer.getTokenUsage());
147505
+ process.exitCode = 1;
147506
+ } else log.error("stream-json: turn error (exitCode already set)", { error });
146835
147507
  } finally {
146836
147508
  for (const [, pending] of pendingApprovals) pending.resolve({
146837
147509
  decision: "rejected",