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