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
|
|
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 {
|
|
58220
|
-
const results = await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
78834
|
-
|
|
78835
|
-
|
|
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)
|
|
78840
|
-
|
|
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 =
|
|
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.
|
|
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">×</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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146833
|
-
|
|
146834
|
-
|
|
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",
|