scream-code 0.8.2 → 0.8.3

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-DWWVKqBK.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";
@@ -58216,8 +58216,8 @@ var KnowledgeLookupTool = class {
58216
58216
  const llm = { generate: async (systemPrompt, userPrompt) => {
58217
58217
  return this.agent.generateText(systemPrompt, userPrompt);
58218
58218
  } };
58219
- const { multiSearch } = await import("./src-S0QPScxL.mjs");
58220
- const results = await multiSearch(store, llm, query, { topK });
58219
+ const { multiSearchWithTrace } = await import("./src-BChzuWm-.mjs");
58220
+ const { results, trace } = await multiSearchWithTrace(store, llm, query, { topK });
58221
58221
  if (results.length === 0) return {
58222
58222
  isError: false,
58223
58223
  output: `No knowledge base entries found for query "${query}".`
@@ -58228,6 +58228,13 @@ var KnowledgeLookupTool = class {
58228
58228
  const eventLine = result.eventTitle !== null ? ` Event: ${result.eventTitle}` : void 0;
58229
58229
  lines.push(`**${i + 1}. ${heading}** (from: ${result.sourceName})`, ` Score: ${result.score.toFixed(3)}`, ...eventLine !== void 0 ? [eventLine] : [], ` ${result.content}`, "");
58230
58230
  }
58231
+ lines.push("---", "Retrieval trace:");
58232
+ if (trace.fallbackReason !== null) lines.push(` ⚠ Fallback: ${trace.fallbackReason}`);
58233
+ for (const step of trace.steps) {
58234
+ const payloadStr = step.payload !== void 0 ? ` ${JSON.stringify(step.payload)}` : "";
58235
+ lines.push(` • ${step.step} (${step.durationMs}ms) — ${step.detail}${payloadStr}`);
58236
+ }
58237
+ if (trace.rerankedEventTitles.length > 0) lines.push(` Reranked events: ${trace.rerankedEventTitles.join(" | ")}`);
58231
58238
  return {
58232
58239
  isError: false,
58233
58240
  output: lines.join("\n")
@@ -78633,6 +78640,67 @@ function formatFileOperations(ops) {
78633
78640
  lines.push("</files>");
78634
78641
  return lines.join("\n");
78635
78642
  }
78643
+ /** Max recursion depth for re-summarize fallback. Each level halves the
78644
+ * input, so depth 3 means we can compress a 8x-oversized input down to a
78645
+ * single summary by chaining 2^3 = 8 partial summaries. */
78646
+ const MAX_RE_SUMMARIZE_DEPTH = 3;
78647
+ var TruncatedError = class extends Error {};
78648
+ /**
78649
+ * Recursive re-summarize fallback for context overflow. When even the
78650
+ * minimum safe split still overflows the model (typically because a single
78651
+ * message contains a giant tool result), split the input in half at a safe
78652
+ * boundary, summarize each half, then concatenate the two partial summaries
78653
+ * and re-summarize them into one. If a half still overflows, recurse.
78654
+ *
78655
+ * The split uses `canSplitAfter` from the strategy to make sure each half
78656
+ * ends at a message boundary that doesn't orphan tool results. If no safe
78657
+ * split exists in the half (e.g. one giant message), feed it as-is to the
78658
+ * model and let the outer retry loop handle the overflow.
78659
+ */
78660
+ async function summarizeWithFallback(messages, summarizeOnce, depth = 0) {
78661
+ if (messages.length <= 1) return summarizeOnce(messages);
78662
+ let split = -1;
78663
+ const mid = Math.floor(messages.length / 2);
78664
+ for (let i = mid; i > 0; i--) if (canSplitAfterContext(messages, i - 1)) {
78665
+ split = i;
78666
+ break;
78667
+ }
78668
+ if (split === -1) {
78669
+ for (let i = mid + 1; i < messages.length; i++) if (canSplitAfterContext(messages, i - 1)) {
78670
+ split = i;
78671
+ break;
78672
+ }
78673
+ }
78674
+ if (split === -1) return summarizeOnce(messages);
78675
+ const firstHalf = messages.slice(0, split);
78676
+ const secondHalf = messages.slice(split);
78677
+ const summarizeHalf = async (half) => {
78678
+ try {
78679
+ return (await summarizeOnce(half)).summary;
78680
+ } catch (error) {
78681
+ if ((error instanceof APIContextOverflowError || error instanceof TruncatedError) && depth + 1 < MAX_RE_SUMMARIZE_DEPTH) return (await summarizeWithFallback(half, summarizeOnce, depth + 1)).summary;
78682
+ throw error;
78683
+ }
78684
+ };
78685
+ return summarizeOnce([{
78686
+ role: "user",
78687
+ content: [{
78688
+ type: "text",
78689
+ text: `${await summarizeHalf(firstHalf)}\n\n---\n\n${await summarizeHalf(secondHalf)}`
78690
+ }],
78691
+ toolCalls: []
78692
+ }]);
78693
+ }
78694
+ /** Same split-safety rule as DefaultCompactionStrategy.canSplitAfter, but
78695
+ * operates on ContextMessage (which carries toolCalls on assistant msgs). */
78696
+ function canSplitAfterContext(messages, index) {
78697
+ const m = messages[index];
78698
+ if (m === void 0) return false;
78699
+ if (m.role === "user") return false;
78700
+ if (m.role === "assistant" && m.toolCalls.length > 0) return false;
78701
+ if (messages[index + 1]?.role === "tool") return false;
78702
+ return true;
78703
+ }
78636
78704
  /** Max consecutive compaction failures before auto-compaction is
78637
78705
  * disabled for the remainder of the turn. Resets each turn. */
78638
78706
  const MAX_CONSECUTIVE_FAILURES = 3;
@@ -78648,6 +78716,16 @@ var FullCompaction = class {
78648
78716
  consecutiveCompactionFailures = 0;
78649
78717
  _shouldInjectSessionSummary = false;
78650
78718
  compactionTimedOut = false;
78719
+ /** Token count below which compaction should not re-trigger. Set after a
78720
+ * successful compaction to 110% of the post-compaction token count, so
78721
+ * that a context sitting just above triggerRatio doesn't immediately
78722
+ * re-trigger on every step. Reset each turn. */
78723
+ lowWaterMark = 0;
78724
+ /** Whether a reactive (overflow-triggered) compaction has already been
78725
+ * attempted this turn. Prevents the overflow → compact → still near
78726
+ * limit → overflow → compact cycle from consuming the entire
78727
+ * maxCompactionPerTurn budget with marginal savings. */
78728
+ reactiveAttempted = false;
78651
78729
  compacting = null;
78652
78730
  _compactedHistory = [];
78653
78731
  strategy;
@@ -78726,8 +78804,11 @@ var FullCompaction = class {
78726
78804
  resetForTurn() {
78727
78805
  this.compactionCountInTurn = 0;
78728
78806
  this.consecutiveCompactionFailures = 0;
78807
+ this.lowWaterMark = 0;
78808
+ this.reactiveAttempted = false;
78729
78809
  }
78730
78810
  async handleOverflowError(signal, error) {
78811
+ if (this.reactiveAttempted) throw error;
78731
78812
  if (!this.beginAutoCompaction(false) && !this.compacting) {
78732
78813
  if (this.consecutiveCompactionFailures >= MAX_CONSECUTIVE_FAILURES) this.agent.emitEvent({
78733
78814
  type: "warning",
@@ -78736,11 +78817,12 @@ var FullCompaction = class {
78736
78817
  });
78737
78818
  throw error;
78738
78819
  }
78820
+ this.reactiveAttempted = true;
78739
78821
  }
78740
78822
  async beforeStep(signal) {
78741
78823
  this.agent.microCompaction.detect();
78742
78824
  const effectiveTokens = this.effectiveTokenCount;
78743
- const isReactiveTrigger = this.strategy.shouldCompact(effectiveTokens);
78825
+ const isReactiveTrigger = this.strategy.shouldCompact(effectiveTokens) && effectiveTokens >= this.lowWaterMark;
78744
78826
  const isProactiveTrigger = !isReactiveTrigger && this.strategy.shouldCompactProactively(effectiveTokens, this.estimatedMaxOutputTokens);
78745
78827
  if (isReactiveTrigger) this.checkAutoCompaction();
78746
78828
  else if (isProactiveTrigger) this.beginAutoCompaction();
@@ -78763,7 +78845,9 @@ var FullCompaction = class {
78763
78845
  }
78764
78846
  checkAutoCompaction(throwOnLimit = true) {
78765
78847
  if (this.compacting) return true;
78766
- if (!this.strategy.shouldCompact(this.effectiveTokenCount)) return false;
78848
+ const effectiveTokens = this.effectiveTokenCount;
78849
+ if (!this.strategy.shouldCompact(effectiveTokens)) return false;
78850
+ if (effectiveTokens < this.lowWaterMark) return false;
78767
78851
  return this.beginAutoCompaction(throwOnLimit);
78768
78852
  }
78769
78853
  beginAutoCompaction(throwOnLimit = true) {
@@ -78815,10 +78899,7 @@ var FullCompaction = class {
78815
78899
  try {
78816
78900
  await this.triggerPreCompactHook(data, tokensBefore, signal);
78817
78901
  const delays = retryBackoffDelays(5);
78818
- let usage;
78819
- let summary;
78820
- while (true) {
78821
- const messagesToCompact = originalHistory.slice(0, compactedCount);
78902
+ const summarizeOnce = async (messagesToCompact) => {
78822
78903
  const instruction = isUpdate ? COMPACTION_UPDATE_INSTRUCTION(data.instruction) : COMPACTION_INSTRUCTION(data.instruction);
78823
78904
  const messages = [...project(messagesToCompact), {
78824
78905
  role: "user",
@@ -78828,16 +78909,37 @@ var FullCompaction = class {
78828
78909
  }],
78829
78910
  toolCalls: []
78830
78911
  }];
78831
- class TruncatedError extends Error {}
78912
+ const response = await this.agent.generate(this.agent.config.provider, COMPACTION_SYSTEM_PROMPT, [], messages, void 0, { signal });
78913
+ if (response.finishReason === "truncated") throw new TruncatedError();
78914
+ return {
78915
+ summary: extractCompactionSummary(response, model),
78916
+ usage: response.usage
78917
+ };
78918
+ };
78919
+ let usage;
78920
+ let summary;
78921
+ while (true) {
78922
+ const messagesToCompact = originalHistory.slice(0, compactedCount);
78832
78923
  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);
78924
+ const result = await summarizeOnce(messagesToCompact);
78925
+ usage = result.usage;
78926
+ summary = result.summary;
78837
78927
  break;
78838
78928
  } catch (error) {
78839
- if (error instanceof APIContextOverflowError || error instanceof TruncatedError) compactedCount = this.strategy.reduceCompactOnOverflow(messagesToCompact);
78840
- else if (!isRetryableGenerateError(error)) throw error;
78929
+ if (error instanceof APIContextOverflowError || error instanceof TruncatedError) {
78930
+ const reduced = this.strategy.reduceCompactOnOverflow(messagesToCompact);
78931
+ if (reduced < compactedCount) compactedCount = reduced;
78932
+ else {
78933
+ this.agent.log.warn("compaction overflow at minimum split, falling back to re-summarize", {
78934
+ compactedCount,
78935
+ tokensBefore: estimateTokensForMessages(messagesToCompact)
78936
+ });
78937
+ const result = await summarizeWithFallback(messagesToCompact, summarizeOnce);
78938
+ summary = result.summary;
78939
+ usage = result.usage;
78940
+ break;
78941
+ }
78942
+ } else if (!isRetryableGenerateError(error)) throw error;
78841
78943
  if (retryCount + 1 >= 5) throw error;
78842
78944
  await sleepForRetry(delays[retryCount], signal);
78843
78945
  retryCount += 1;
@@ -78868,6 +78970,7 @@ var FullCompaction = class {
78868
78970
  result
78869
78971
  });
78870
78972
  this.agent.context.applyCompaction(result);
78973
+ this.lowWaterMark = Math.floor(this.effectiveTokenCount * 1.1);
78871
78974
  await this.extractAndStoreMemos(processedSummary);
78872
78975
  this.triggerPostCompactHook(data, result);
78873
78976
  this.consecutiveCompactionFailures = 0;
@@ -79083,12 +79186,33 @@ const flags = new FlagResolver();
79083
79186
  //#region ../../packages/agent-core/src/agent/compaction/micro.ts
79084
79187
  const DEFAULT_CONFIG = {
79085
79188
  keepRecentMessages: 20,
79189
+ keepRecentTokens: 4e4,
79190
+ pruneMinReclaimTokens: 2e4,
79086
79191
  minContentTokens: 100,
79087
79192
  minContextUsageRatio: .5,
79088
79193
  truncatedMarker: "[Old tool result content cleared]",
79089
79194
  uselessMarker: "[Uneventful result elided]"
79090
79195
  };
79091
79196
  /**
79197
+ * Compute the cutoff index: everything at index < cutoff is eligible for
79198
+ * truncation. The default floor is `keepRecentMessages` (the message-count
79199
+ * protection window). But if the trailing window exceeds `keepRecentTokens`,
79200
+ * the cutoff walks forward (toward the tail) until the window fits — so a
79201
+ * few giant tool results can't pin the cutoff behind them and starve the
79202
+ * prefix of reclaimable content.
79203
+ */
79204
+ function computeCutoff(messages, config) {
79205
+ const messageFloor = Math.max(0, messages.length - config.keepRecentMessages);
79206
+ let windowTokens = estimateTokensForMessages(messages.slice(messageFloor));
79207
+ let cutoff = messageFloor;
79208
+ while (cutoff < messages.length && windowTokens > config.keepRecentTokens) {
79209
+ const removed = messages[cutoff];
79210
+ windowTokens -= estimateTokensForMessages([removed]);
79211
+ cutoff += 1;
79212
+ }
79213
+ return cutoff;
79214
+ }
79215
+ /**
79092
79216
  * Walk the message list and find Read tool calls whose file paths were
79093
79217
  * superseded by a later Read of the same path. Returns a map from the
79094
79218
  * superseded tool call's ID to the file path (for the marker text).
@@ -79157,7 +79281,10 @@ var MicroCompaction = class {
79157
79281
  const maxContextTokens = this.agent.config.modelCapabilities.max_context_tokens;
79158
79282
  const contextTokens = this.agent.context.tokenCountWithPending;
79159
79283
  if ((maxContextTokens !== void 0 && maxContextTokens > 0 ? contextTokens / maxContextTokens : 0) < config.minContextUsageRatio) return;
79160
- const nextCutoff = Math.max(0, history.length - config.keepRecentMessages);
79284
+ const nextCutoff = computeCutoff(history, config);
79285
+ if (nextCutoff <= this.cutoff) return;
79286
+ const { beforeTokens, afterTokens } = this.measureEffect(history, nextCutoff);
79287
+ if (beforeTokens - afterTokens < config.pruneMinReclaimTokens) return;
79161
79288
  this.apply(nextCutoff);
79162
79289
  }
79163
79290
  /**
@@ -122126,7 +122253,7 @@ function optionalBuildString(value) {
122126
122253
  return typeof value === "string" && value.length > 0 ? value : void 0;
122127
122254
  }
122128
122255
  const SCREAM_BUILD_INFO = {
122129
- version: optionalBuildString("0.8.2"),
122256
+ version: optionalBuildString("0.8.3"),
122130
122257
  channel: optionalBuildString(""),
122131
122258
  commit: optionalBuildString(""),
122132
122259
  buildTarget: optionalBuildString("darwin-arm64")
@@ -132953,6 +133080,517 @@ async function handleLikeCommand(host) {
132953
133080
  host.showStatus("偏好已保存(下次新会话生效)", host.state.theme.colors.success);
132954
133081
  }
132955
133082
  //#endregion
133083
+ //#region src/tui/utils/open-url.ts
133084
+ function openUrl(url) {
133085
+ const command = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", [
133086
+ "/c",
133087
+ "start",
133088
+ "",
133089
+ url
133090
+ ]] : ["xdg-open", [url]];
133091
+ execFile(command[0], command[1], () => {});
133092
+ }
133093
+ //#endregion
133094
+ //#region src/tui/commands/knowledge-store.ts
133095
+ let knowledgeStoreInstance;
133096
+ async function getKnowledgeStore() {
133097
+ if (knowledgeStoreInstance === void 0) {
133098
+ knowledgeStoreInstance = new KnowledgeStore(getDataDir());
133099
+ await knowledgeStoreInstance.init();
133100
+ knowledgeStoreInstance.setEmbeddingEngine(createFastEmbedEngine());
133101
+ }
133102
+ return knowledgeStoreInstance;
133103
+ }
133104
+ //#endregion
133105
+ //#region src/tui/commands/knowledge-web.ts
133106
+ /**
133107
+ * /knowledge web — start a local HTTP server and open the browser
133108
+ * to display an interactive Cytoscape.js knowledge graph visualization.
133109
+ */
133110
+ const activeServers = /* @__PURE__ */ new Set();
133111
+ function registerServer(server) {
133112
+ activeServers.add(server);
133113
+ server.on("close", () => {
133114
+ activeServers.delete(server);
133115
+ });
133116
+ }
133117
+ function closeAllServers() {
133118
+ for (const server of activeServers) server.close();
133119
+ }
133120
+ process.on("exit", closeAllServers);
133121
+ const HTML = `<!doctype html>
133122
+ <html lang="zh-CN">
133123
+ <head>
133124
+ <meta charset="utf-8">
133125
+ <meta name="viewport" content="width=device-width,initial-scale=1">
133126
+ <title>Scream 知识图谱</title>
133127
+ <style>
133128
+ *{margin:0;padding:0;box-sizing:border-box}
133129
+ html,body{width:100%;height:100%;overflow:hidden;font-family:Inter,"PingFang SC","Microsoft YaHei",system-ui,sans-serif;background:#fff;color:#18181b}
133130
+ #container{width:100%;height:100%;position:relative;overflow:hidden}
133131
+ svg{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:1}
133132
+ .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}
133133
+ .node.entity{background:#fff;border:1.5px solid #d4d4d8;font-weight:600;color:#1a1a1a;box-shadow:0 2px 8px rgba(0,0,0,.06)}
133134
+ .node.entity.root{border:2px solid #15803d;background:#f0fdf4;box-shadow:0 4px 20px rgba(21,128,61,.15);font-size:14px}
133135
+ .node.entity.expanded{border-color:#15803d;box-shadow:0 2px 12px rgba(21,128,61,.1)}
133136
+ .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}
133137
+ .node.event.expanded{border-color:#22c55e;background:#f8fef9;box-shadow:0 2px 8px rgba(21,128,61,.08)}
133138
+ .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}
133139
+ .node.dimmed{opacity:.12;filter:grayscale(.6)}
133140
+ .node:hover{box-shadow:0 4px 16px rgba(0,0,0,.08) !important}
133141
+ .edge{stroke:#b0b0b0;stroke-width:.8;fill:none;opacity:.6}
133142
+ .edge.animated{stroke:#999;stroke-width:1;stroke-dasharray:6 4;animation:dash 1.2s linear infinite;opacity:.45}
133143
+ .edge.dimmed{stroke:#e0e0e0;stroke-width:.4;opacity:.06}
133144
+ .edge.highlighted{stroke:#15803d;stroke-width:1.8;opacity:1}
133145
+ @keyframes dash{to{stroke-dashoffset:-10}}
133146
+ @keyframes float{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}
133147
+ #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)}
133148
+ #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}
133149
+ #toolbar button:hover{background:#f0fdf4;border-color:#15803d;color:#15803d}
133150
+ #toolbar .sep{width:1px;height:16px;background:#e5e7eb}
133151
+ #toolbar .chip{color:#a1a1aa;font-size:11px;letter-spacing:.3px}
133152
+ #toolbar .chip b{color:#15803d;font-size:13px;font-weight:700;margin-left:4px}
133153
+ #hint{position:absolute;left:16px;bottom:16px;z-index:10;color:#c4c4c4;font-size:11px;letter-spacing:.2px}
133154
+ #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)}
133155
+ #detail.open{transform:translateX(0)}
133156
+ #detail h3{font-size:17px;font-weight:700;color:#18181b;margin-bottom:20px;padding-right:32px;line-height:1.4}
133157
+ #detail .field{margin-bottom:18px}
133158
+ #detail .field .label{font-size:10px;color:#15803d;text-transform:uppercase;letter-spacing:1px;font-weight:700;margin-bottom:5px}
133159
+ #detail .field .value{font-size:13px;color:#525252;line-height:1.6}
133160
+ #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}
133161
+ #detail .close:hover{color:#18181b;background:#f4f4f5}
133162
+ #detail .conn-item{font-size:13px;color:#15803d;cursor:pointer;padding:7px 10px;border-radius:8px;transition:all .15s;border:1px solid transparent}
133163
+ #detail .conn-item:hover{background:#f0fdf4;border-color:#bbf7d0}
133164
+ #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}
133165
+ #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}
133166
+ #detail .back-btn:hover{background:#f0fdf4;border-color:#15803d;color:#15803d}
133167
+ #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}
133168
+ #detail .divider{height:1px;background:#f0f0f0;margin:18px 0}
133169
+ #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}
133170
+ #loading .dot{width:6px;height:6px;background:#15803d;border-radius:50%;animation:blink 1.2s ease-in-out infinite}
133171
+ #loading .dot:nth-child(2){animation-delay:.2s}
133172
+ #loading .dot:nth-child(3){animation-delay:.4s}
133173
+ @keyframes blink{0%,80%,100%{opacity:.2}40%{opacity:1}}
133174
+ </style>
133175
+ </head>
133176
+ <body>
133177
+ <div id="container"><svg id="edges-svg"></svg></div>
133178
+ <div id="toolbar">
133179
+ <span class="chip">实体<b id="stat-ent">0</b></span>
133180
+ <span class="chip">事件<b id="stat-evt">0</b></span>
133181
+ <span class="chip">关系<b id="stat-edg">0</b></span>
133182
+ <span class="sep"></span>
133183
+ <button id="btn-reset">重置</button>
133184
+ <button id="btn-expand">展开全部</button>
133185
+ </div>
133186
+ <div id="hint">单击展开/收起 · 双击查看详情 · 滚轮缩放 · 拖拽平移</div>
133187
+ <div id="detail"><button class="close" id="btn-close">&times;</button><div id="detail-body"></div></div>
133188
+ <div id="loading"><span class="dot"></span><span class="dot"></span><span class="dot"></span>加载知识图谱...</div>
133189
+ <script>
133190
+ (function(){
133191
+ var graphData;
133192
+ var container = document.getElementById('container');
133193
+ var svg = document.getElementById('edges-svg');
133194
+
133195
+ // camera
133196
+ var cam = {x:0,y:0,zoom:1};
133197
+ var W=window.innerWidth, H=window.innerHeight;
133198
+
133199
+ function w2s(wx,wy){return{x:(wx-cam.x)*cam.zoom+W/2,y:(wy-cam.y)*cam.zoom+H/2}}
133200
+ function s2w(sx,sy){return{x:(sx-W/2)/cam.zoom+cam.x,y:(sy-H/2)/cam.zoom+cam.y}}
133201
+
133202
+ // pan & zoom
133203
+ var drag=false,moved=false,dragX,dragY;
133204
+ container.addEventListener('mousedown',function(e){if(e.target!==container&&e.target!==svg)return;drag=true;moved=false;dragX=e.clientX;dragY=e.clientY});
133205
+ 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()});
133206
+ window.addEventListener('mouseup',function(){drag=false});
133207
+ 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});
133208
+ window.addEventListener('resize',function(){W=window.innerWidth;H=window.innerHeight;render()});
133209
+
133210
+ // layout constants (SAG)
133211
+ var EW=160,EH=40,EVW=170,EVH=40;
133212
+ var ER_START=220,ER_GAP=190,ER_SLOT=200;
133213
+ var VR_START=500,VR_GAP=180,VR_SLOT=220;
133214
+ var ROOT_LIMIT=8;
133215
+ var GA=Math.PI*(3-Math.sqrt(5));
133216
+
133217
+ // state
133218
+ var eById={},evById={};
133219
+ var evByEnt={},entByEv={};
133220
+ var expEnt=new Set(),expEv=new Set();
133221
+ var selId=null;
133222
+ var pos={};
133223
+ var navStack=[];
133224
+
133225
+ // node/edge DOM elements
133226
+ var nodeEls={},edgeEls=[];
133227
+
133228
+ fetch('/api/graph').then(function(r){return r.json()}).then(function(data){
133229
+ graphData=data;
133230
+ document.getElementById('stat-ent').textContent=data.entities.length;
133231
+ document.getElementById('stat-evt').textContent=data.events.length;
133232
+ document.getElementById('stat-edg').textContent=data.edges.length;
133233
+ document.getElementById('loading').style.display='none';
133234
+
133235
+ data.entities.sort(function(a,b){return(b.eventCount||0)-(a.eventCount||0)||a.name.localeCompare(b.name)});
133236
+ data.events.sort(function(a,b){return(a.rank||0)-(b.rank||0)||a.title.localeCompare(b.title)});
133237
+
133238
+ data.entities.forEach(function(e){eById[e.id]=e});
133239
+ data.events.forEach(function(e){evById[e.id]=e});
133240
+
133241
+ data.edges.forEach(function(e){
133242
+ if(!evByEnt[e.entityId])evByEnt[e.entityId]=[];
133243
+ evByEnt[e.entityId].push(e.eventId);
133244
+ if(!entByEv[e.eventId])entByEv[e.eventId]=[];
133245
+ entByEv[e.eventId].push(e.entityId);
133246
+ });
133247
+
133248
+ // default: show all entities, no events expanded
133249
+ graphData.entities.forEach(function(e){expEnt.add(e.id)});
133250
+
133251
+ buildPositions();
133252
+ fitView();
133253
+ rebuildDOM();
133254
+ });
133255
+
133256
+ function buildPositions(){
133257
+ pos={};
133258
+ var root=graphData.entities[0];
133259
+ if(root)pos[root.id]={x:-EW/2,y:-EH/2,root:true};
133260
+ var sec=graphData.entities.slice(1);
133261
+ placeRings(sec.map(function(e){return e.id}),ER_START,ER_GAP,ER_SLOT,EW,EH,-Math.PI/2);
133262
+ placeEvents(graphData.events,-Math.PI/2+Math.PI/12);
133263
+ }
133264
+
133265
+ function placeRings(ids,sr,gap,slot,nw,nh,aoff){
133266
+ var idx=0,ring=0;
133267
+ while(idx<ids.length){
133268
+ var r=sr+ring*gap;
133269
+ var cap=Math.max(6,Math.floor(2*Math.PI*r/slot));
133270
+ for(var s=0;s<cap&&idx<ids.length;s++){
133271
+ var a=aoff+(2*Math.PI*s)/cap;
133272
+ pos[ids[idx]]={x:Math.cos(a)*r-nw/2,y:Math.sin(a)*r-nh/2};
133273
+ idx++;
133274
+ }
133275
+ ring++;
133276
+ }
133277
+ }
133278
+
133279
+ function placeEvents(events,aoff){
133280
+ var occ={};
133281
+ for(var i=0;i<events.length;i++){
133282
+ var da=i*GA;
133283
+ var sl=findSlot(da,aoff,occ);
133284
+ var r=VR_START+sl.ring*VR_GAP;
133285
+ var a=aoff+(2*Math.PI*sl.idx)/sl.cap;
133286
+ pos[events[i].id]={x:Math.cos(a)*r-EVW/2,y:Math.sin(a)*r-EVH/2};
133287
+ }
133288
+ }
133289
+
133290
+ function findSlot(da,aoff,occ){
133291
+ var ring=0;
133292
+ while(true){
133293
+ var r=VR_START+ring*VR_GAP;
133294
+ var cap=Math.max(8,Math.floor(2*Math.PI*r/VR_SLOT));
133295
+ var o=occ[ring]||{};
133296
+ var ds=((Math.round(((da-aoff)/(2*Math.PI))*cap)%cap)+cap)%cap;
133297
+ var f=nearFree(ds,cap,o);
133298
+ if(f!=null){o[f]=true;occ[ring]=o;return{ring:ring,idx:f,cap:cap}}
133299
+ ring++;
133300
+ }
133301
+ }
133302
+
133303
+ function nearFree(d,cap,o){
133304
+ for(var i=0;i<cap;i++){
133305
+ var cs=i===0?[d]:[((d-i)%cap+cap)%cap,((d+i)%cap+cap)%cap];
133306
+ for(var j=0;j<cs.length;j++)if(!o[cs[j]])return cs[j];
133307
+ }
133308
+ return null;
133309
+ }
133310
+
133311
+ function fitView(){
133312
+ var vis=getVisible();
133313
+ var ids=[];
133314
+ vis.entities.forEach(function(id){ids.push(id)});
133315
+ vis.events.forEach(function(id){ids.push(id)});
133316
+ if(!ids.length){cam.x=0;cam.y=0;cam.zoom=1;return}
133317
+ var x0=Infinity,x1=-Infinity,y0=Infinity,y1=-Infinity;
133318
+ 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});
133319
+ cam.x=(x0+x1)/2;cam.y=(y0+y1)/2;
133320
+ cam.zoom=Math.min((W-120)/(x1-x0),(H-120)/(y1-y0),1.2);
133321
+ }
133322
+
133323
+ function getVisible(){
133324
+ var ve=new Set(),vv=new Set();
133325
+ graphData.entities.slice(0,ROOT_LIMIT).forEach(function(e){ve.add(e.id)});
133326
+ expEnt.forEach(function(eid){ve.add(eid);(evByEnt[eid]||[]).forEach(function(v){vv.add(v)})});
133327
+ expEv.forEach(function(eid){vv.add(eid);(entByEv[eid]||[]).forEach(function(e){ve.add(e)})});
133328
+ return{entities:ve,events:vv};
133329
+ }
133330
+
133331
+ function rebuildDOM(){
133332
+ // clear old
133333
+ Object.keys(nodeEls).forEach(function(id){if(nodeEls[id].parentNode)nodeEls[id].parentNode.removeChild(nodeEls[id])});
133334
+ while(svg.firstChild)svg.removeChild(svg.firstChild);
133335
+ nodeEls={};edgeEls=[];
133336
+
133337
+ var vis=getVisible();
133338
+ var visAll=new Set();vis.entities.forEach(function(id){visAll.add(id)});vis.events.forEach(function(id){visAll.add(id)});
133339
+
133340
+ // connection set for highlighting
133341
+ var conn=null;
133342
+ if(selId){
133343
+ conn=new Set();conn.add(selId);
133344
+ graphData.edges.forEach(function(e){
133345
+ if(!visAll.has(e.entityId)||!visAll.has(e.eventId))return;
133346
+ if(e.entityId===selId||e.eventId===selId){conn.add(e.entityId);conn.add(e.eventId)}
133347
+ });
133348
+ }
133349
+
133350
+ // edges
133351
+ graphData.edges.forEach(function(e){
133352
+ if(!visAll.has(e.entityId)||!visAll.has(e.eventId))return;
133353
+ var line=document.createElementNS('http://www.w3.org/2000/svg','line');
133354
+ var isDir=selId&&(e.entityId===selId||e.eventId===selId);
133355
+ var isRel=conn&&conn.has(e.entityId)&&conn.has(e.eventId);
133356
+ var isExp=expEnt.has(e.entityId)||expEv.has(e.eventId);
133357
+ if(isDir)line.setAttribute('class','edge highlighted');
133358
+ else if(isRel)line.setAttribute('class','edge');
133359
+ else if(conn)line.setAttribute('class','edge dimmed');
133360
+ else line.setAttribute('class',isExp?'edge animated':'edge');
133361
+ line.setAttribute('data-src',e.entityId);
133362
+ line.setAttribute('data-tgt',e.eventId);
133363
+ svg.appendChild(line);
133364
+ edgeEls.push(line);
133365
+ });
133366
+
133367
+ // nodes
133368
+ graphData.entities.forEach(function(ent){
133369
+ if(!vis.entities.has(ent.id))return;
133370
+ var el=mkNode(ent.id,ent.name,'entity',pos[ent.id].root,expEnt.has(ent.id));
133371
+ container.appendChild(el);nodeEls[ent.id]=el;
133372
+ });
133373
+ graphData.events.forEach(function(ev){
133374
+ if(!vis.events.has(ev.id))return;
133375
+ var el=mkNode(ev.id,ev.title,'event',false,expEv.has(ev.id));
133376
+ container.appendChild(el);nodeEls[ev.id]=el;
133377
+ });
133378
+
133379
+ // apply dimming
133380
+ if(conn){
133381
+ Object.keys(nodeEls).forEach(function(id){
133382
+ if(conn.has(id))nodeEls[id].classList.remove('dimmed');
133383
+ else nodeEls[id].classList.add('dimmed');
133384
+ });
133385
+ if(selId&&nodeEls[selId])nodeEls[selId].classList.add('selected');
133386
+ }
133387
+
133388
+ render();
133389
+ }
133390
+
133391
+ function mkNode(id,label,kind,root,expanded){
133392
+ var el=document.createElement('div');
133393
+ el.className='node '+kind;
133394
+ if(root)el.classList.add('root');
133395
+ if(expanded)el.classList.add('expanded');
133396
+ el.textContent=label;
133397
+ el.setAttribute('data-id',id);
133398
+ el.setAttribute('data-kind',kind);
133399
+ var dur=5+Math.random()*4;
133400
+ var del=Math.random()*dur;
133401
+ el.style.animationDuration=dur+'s';
133402
+ el.style.animationDelay='-'+del+'s';
133403
+
133404
+ var clickTimer=null;
133405
+ el.addEventListener('click',function(e){
133406
+ e.stopPropagation();
133407
+ if(clickTimer){clearTimeout(clickTimer);clickTimer=null;return}
133408
+ clickTimer=setTimeout(function(){
133409
+ clickTimer=null;
133410
+ toggleNode(id,kind);
133411
+ selId=id;
133412
+ rebuildDOM();
133413
+ },200);
133414
+ });
133415
+ el.addEventListener('dblclick',function(e){
133416
+ e.stopPropagation();
133417
+ if(clickTimer){clearTimeout(clickTimer);clickTimer=null}
133418
+ navStack=[];
133419
+ showDetail(id,kind);
133420
+ });
133421
+ return el;
133422
+ }
133423
+
133424
+ function toggleNode(id,kind){
133425
+ if(kind==='entity'){
133426
+ if(expEnt.has(id)){
133427
+ expEnt.delete(id);
133428
+ var related=new Set(evByEnt[id]||[]);
133429
+ expEv.forEach(function(eid){if(related.has(eid))expEv.delete(eid)});
133430
+ }else expEnt.add(id);
133431
+ }else{
133432
+ if(expEv.has(id))expEv.delete(id);
133433
+ else expEv.add(id);
133434
+ }
133435
+ }
133436
+
133437
+ function render(){
133438
+ // position nodes
133439
+ Object.keys(nodeEls).forEach(function(id){
133440
+ var p=pos[id];if(!p)return;
133441
+ var sp=w2s(p.x,p.y);
133442
+ var w=eById[id]?EW:EVW,h=eById[id]?EH:EVH;
133443
+ var el=nodeEls[id];
133444
+ el.style.left=sp.x+'px';
133445
+ el.style.top=sp.y+'px';
133446
+ el.style.width=(w*cam.zoom)+'px';
133447
+ el.style.height=(h*cam.zoom)+'px';
133448
+ el.style.fontSize=Math.max(8,12*cam.zoom)+'px';
133449
+ });
133450
+
133451
+ // position edges
133452
+ edgeEls.forEach(function(line){
133453
+ var srcId=line.getAttribute('data-src');
133454
+ var tgtId=line.getAttribute('data-tgt');
133455
+ var sp=pos[srcId],tp=pos[tgtId];
133456
+ if(!sp||!tp)return;
133457
+ var s=w2s(sp.x+EW/2,sp.y+EH/2);
133458
+ var t=w2s(tp.x+EVW/2,tp.y+EVH/2);
133459
+ line.setAttribute('x1',s.x);line.setAttribute('y1',s.y);
133460
+ line.setAttribute('x2',t.x);line.setAttribute('y2',t.y);
133461
+ });
133462
+ }
133463
+
133464
+ // click background to deselect
133465
+ container.addEventListener('click',function(e){
133466
+ if(e.target===container||e.target===svg){selId=null;rebuildDOM()}
133467
+ });
133468
+
133469
+ // detail panel
133470
+ function showDetail(id,kind,pushNav){
133471
+ if(pushNav!==false)navStack.push({id:id,kind:kind});
133472
+ var panel=document.getElementById('detail');
133473
+ var body=document.getElementById('detail-body');
133474
+ var html='';
133475
+ if(navStack.length>1)html+='<button class="back-btn" id="btn-back">← 返回</button>';
133476
+ if(kind==='entity'){
133477
+ var e=eById[id];
133478
+ html+='<div class="type-tag">'+esc(e.type)+'</div>';
133479
+ html+='<h3>'+esc(e.name)+'</h3>';
133480
+ html+='<div class="field"><div class="label">关联事件</div><div class="value">'+(e.eventCount||0)+' 个</div></div>';
133481
+ var ce=evByEnt[id]||[];
133482
+ 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>'}
133483
+ }else{
133484
+ var ev=evById[id];
133485
+ html+='<h3>'+esc(ev.title)+'</h3>';
133486
+ var ce=entByEv[id]||[];
133487
+ 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>'}
133488
+ }
133489
+ body.innerHTML=html;panel.classList.add('open');
133490
+ var backBtn=document.getElementById('btn-back');
133491
+ if(backBtn)backBtn.addEventListener('click',function(){
133492
+ navStack.pop();
133493
+ var prev=navStack[navStack.length-1];
133494
+ if(prev){selId=prev.id;rebuildDOM();showDetail(prev.id,prev.kind,false)}
133495
+ else{panel.classList.remove('open')}
133496
+ });
133497
+ body.querySelectorAll('.conn-item').forEach(function(el){
133498
+ el.addEventListener('click',function(){
133499
+ var nid=el.getAttribute('data-id'),nk=el.getAttribute('data-kind');
133500
+ if(nk==='entity'&&!expEnt.has(nid))expEnt.add(nid);
133501
+ if(nk==='event'&&!expEv.has(nid))expEv.add(nid);
133502
+ selId=nid;rebuildDOM();showDetail(nid,nk);
133503
+ });
133504
+ });
133505
+ }
133506
+
133507
+ function esc(s){if(!s)return'';return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
133508
+
133509
+ 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()};
133510
+ 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()};
133511
+ document.getElementById('btn-close').onclick=function(){document.getElementById('detail').classList.remove('open');navStack=[]};
133512
+ })();
133513
+ <\/script>
133514
+ </body>
133515
+ </html>`;
133516
+ async function serveGraphJSON(store, res) {
133517
+ try {
133518
+ const [entities, events, edges, sources] = await Promise.all([
133519
+ store.listEntities(),
133520
+ store.listEvents(),
133521
+ store.listEventEntities(),
133522
+ store.listSources()
133523
+ ]);
133524
+ const eventEntityMap = /* @__PURE__ */ new Map();
133525
+ for (const edge of edges) {
133526
+ const list = eventEntityMap.get(edge.eventId) ?? [];
133527
+ list.push(edge.entityId);
133528
+ eventEntityMap.set(edge.eventId, list);
133529
+ }
133530
+ const data = {
133531
+ entities: entities.map(({ id, sourceId, type, name, normalizedName, eventCount }) => ({
133532
+ id,
133533
+ sourceId,
133534
+ type,
133535
+ name,
133536
+ normalizedName,
133537
+ eventCount
133538
+ })),
133539
+ events: events.map(({ id, sourceId, documentId, title, rank }) => ({
133540
+ id,
133541
+ sourceId,
133542
+ documentId,
133543
+ title,
133544
+ rank,
133545
+ entityIds: eventEntityMap.get(id) ?? []
133546
+ })),
133547
+ edges,
133548
+ sources: sources.map(({ id, name }) => ({
133549
+ id,
133550
+ name
133551
+ }))
133552
+ };
133553
+ res.writeHead(200, {
133554
+ "Content-Type": "application/json",
133555
+ "Cache-Control": "no-store"
133556
+ });
133557
+ res.end(JSON.stringify(data));
133558
+ } catch (error) {
133559
+ res.writeHead(500, { "Content-Type": "application/json" });
133560
+ res.end(JSON.stringify({ error: String(error) }));
133561
+ }
133562
+ }
133563
+ function serveHTML(res) {
133564
+ res.writeHead(200, {
133565
+ "Content-Type": "text/html; charset=utf-8",
133566
+ "Cache-Control": "no-store"
133567
+ });
133568
+ res.end(HTML);
133569
+ }
133570
+ async function handleWeb() {
133571
+ const store = await getKnowledgeStore();
133572
+ const s = await store.stats();
133573
+ if (s.entities === 0 && s.events === 0) throw new Error("知识库为空,请先用 /knowledge 摄入文档");
133574
+ const server = createServer((req, res) => {
133575
+ if (req.url === "/api/graph") {
133576
+ serveGraphJSON(store, res);
133577
+ return;
133578
+ }
133579
+ serveHTML(res);
133580
+ });
133581
+ registerServer(server);
133582
+ await new Promise((resolve, reject) => {
133583
+ server.once("error", reject);
133584
+ server.listen(0, "127.0.0.1", () => {
133585
+ server.off("error", reject);
133586
+ resolve();
133587
+ });
133588
+ });
133589
+ const url = `http://127.0.0.1:${server.address().port}`;
133590
+ openUrl(url);
133591
+ console.log(`\n 知识图谱: ${url}\n 按 Ctrl+C 停止服务器\n`);
133592
+ }
133593
+ //#endregion
132956
133594
  //#region src/tui/components/dialogs/knowledge-result-viewer.ts
132957
133595
  /**
132958
133596
  * KnowledgeResultViewer — full-screen scrollable text viewer for /knowledge
@@ -133307,15 +133945,6 @@ function fitExactly$3(line, width) {
133307
133945
  }
133308
133946
  //#endregion
133309
133947
  //#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
133948
  function promptTextInput(host, title, opts) {
133320
133949
  const { promise, resolve } = Promise.withResolvers();
133321
133950
  const dialog = new TextInputDialogComponent((result) => {
@@ -133620,6 +134249,11 @@ async function handleKnowledgeCommand(host, _args) {
133620
134249
  value: "stats",
133621
134250
  label: "📊 统计信息",
133622
134251
  description: "查看知识库整体统计"
134252
+ },
134253
+ {
134254
+ value: "web",
134255
+ label: "🌐 知识图谱",
134256
+ description: "在浏览器中查看交互式知识图谱"
133623
134257
  }
133624
134258
  ];
133625
134259
  const showMenu = () => {
@@ -133637,6 +134271,7 @@ async function handleKnowledgeCommand(host, _args) {
133637
134271
  else if (value === "search") await handleSearch(host);
133638
134272
  else if (value === "delete") await handleDelete(host);
133639
134273
  else if (value === "stats") await handleStats(host);
134274
+ else if (value === "web") await handleWeb();
133640
134275
  } catch (error) {
133641
134276
  const msg = error instanceof Error ? error.message : String(error);
133642
134277
  host.showError(`操作失败: ${msg}`);
@@ -134978,17 +135613,6 @@ function mcpServerStatusKey(server) {
134978
135613
  ]);
134979
135614
  }
134980
135615
  //#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
135616
  //#region src/tui/utils/mcp-oauth.ts
134993
135617
  const MAX_AUTH_URL_SET_SIZE = 200;
134994
135618
  var McpOAuthAuthorizationUrlOpener = class {
@@ -146508,6 +147132,14 @@ var ClaudeStreamJsonWriter = class {
146508
147132
  if (input > 0) this.tokenUsage.input += input;
146509
147133
  if (output > 0) this.tokenUsage.output += output;
146510
147134
  }
147135
+ /** Returns the accumulated token usage for the current turn. Used by
147136
+ * the result event emitter so the final `result` event carries the
147137
+ * turn's total token usage. Without this, usage was accumulated but
147138
+ * never emitted — consumers (cc-connect, mobile clients) saw empty
147139
+ * usage fields. */
147140
+ getTokenUsage() {
147141
+ return { ...this.tokenUsage };
147142
+ }
146511
147143
  nextMsgId() {
146512
147144
  this.msgCounter += 1;
146513
147145
  return `msg_${this.msgCounter.toString(36)}`;
@@ -146754,22 +147386,26 @@ async function runStreamJson(opts) {
146754
147386
  let activeAgentId;
146755
147387
  let settled = false;
146756
147388
  let unsubscribe;
146757
- const finish = (error) => {
147389
+ const finish = (error, exitCode) => {
146758
147390
  if (settled) return;
146759
147391
  settled = true;
146760
147392
  unsubscribe?.();
146761
147393
  if (error) {
146762
- writer.emitResult("error", error.message);
147394
+ writer.emitResult("error", error.message, writer.getTokenUsage());
147395
+ if (exitCode !== void 0) process.exitCode = exitCode;
146763
147396
  reject(error);
146764
147397
  } else {
146765
- writer.emitResult("success", "");
147398
+ writer.emitResult("success", "", writer.getTokenUsage());
146766
147399
  resolve();
146767
147400
  }
146768
147401
  };
146769
147402
  unsubscribe = session.onEvent((event) => {
146770
147403
  if (event.type === "error") {
146771
147404
  if (event.agentId !== "main") return;
146772
- finish(/* @__PURE__ */ new Error(`${event.code}: ${event.message}`));
147405
+ const errCode = event.code;
147406
+ let exitCode = 1;
147407
+ if (errCode === "provider.auth_error" || errCode === "provider.rate_limit" || errCode === "provider.api_error" || errCode === "provider.connection_error") exitCode = 3;
147408
+ finish(/* @__PURE__ */ new Error(`${event.code}: ${event.message}`), exitCode);
146773
147409
  return;
146774
147410
  }
146775
147411
  if (event.type === "subagent.spawned") {
@@ -146806,9 +147442,14 @@ async function runStreamJson(opts) {
146806
147442
  writer.updateUsage(inputTotal, event.usage.output ?? 0);
146807
147443
  }
146808
147444
  } else if (type === "turn.ended") if (event.reason === "completed") finish();
147445
+ else if (event.reason === "cancelled") finish(/* @__PURE__ */ new Error("Turn cancelled"));
146809
147446
  else {
147447
+ const errCode = event.error?.code;
146810
147448
  const errMsg = event.error !== void 0 ? `${event.error.code}: ${event.error.message}` : `Turn ended: ${event.reason}`;
146811
- finish(new Error(errMsg));
147449
+ let code = 1;
147450
+ if (errCode === "loop.max_steps_exceeded") code = 4;
147451
+ else if (errCode === "provider.auth_error" || errCode === "provider.rate_limit" || errCode === "provider.api_error" || errCode === "provider.connection_error") code = 3;
147452
+ finish(new Error(errMsg), code);
146812
147453
  }
146813
147454
  });
146814
147455
  session.prompt(userText).catch((error) => {
@@ -146829,9 +147470,11 @@ async function runStreamJson(opts) {
146829
147470
  await turnPromise;
146830
147471
  }
146831
147472
  } catch (error) {
146832
- log.error("stream-json: fatal", { error });
146833
- writer.emitResult("error", error instanceof Error ? error.message : String(error));
146834
- process.exitCode = 1;
147473
+ if (process.exitCode === void 0) {
147474
+ log.error("stream-json: fatal", { error });
147475
+ writer.emitResult("error", error instanceof Error ? error.message : String(error), writer.getTokenUsage());
147476
+ process.exitCode = 1;
147477
+ } else log.error("stream-json: turn error (exitCode already set)", { error });
146835
147478
  } finally {
146836
147479
  for (const [, pending] of pendingApprovals) pending.resolve({
146837
147480
  decision: "rejected",