rcs-context 0.0.0-stage → 0.1.40

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.
@@ -0,0 +1,3034 @@
1
+ // Copyright (c) 2026 ScreamingMaggot. This source code is licensed under the MIT License.
2
+ // contextinjector-webui (client half) — "注入" tab in the conversation.view ring.
3
+ // Structured panel: workspace-grouped session whitelist + fold timeline.
4
+ // Data plane: fetch /api/context-inject/{status,config}; theme-aware via DSW tokens.
5
+ window.__ModuleLoader__.load({
6
+ id: "contextinjector-webui/client",
7
+ factory: (require) => {
8
+ var module = { exports: {} };
9
+ var exports = module.exports;
10
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
11
+ var React = require("react");
12
+ var useState = React.useState, useEffect = React.useEffect, useRef = React.useRef;
13
+ var e = React.createElement;
14
+
15
+ // ---- theme-aware stylesheet (injected once; DSW tokens with safe fallbacks) ----
16
+ var css = [
17
+ ".ci-root{font-size:13px;line-height:1.55;max-width:1240px;padding:14px 16px;color:var(--dsw-alias-label-primary,#e7e7ee)}",
18
+ // 双栏(v1.10):右栏承载管理(当前会话/白名单/日志编译),宽屏并排、窄屏 flex 自动回退单列
19
+ ".ci-cols{display:flex;gap:14px;align-items:flex-start;flex-wrap:wrap}",
20
+ ".ci-main{flex:1 1 540px;min-width:0;max-height:72vh;overflow-y:auto;overscroll-behavior:contain;padding-right:2px;padding-bottom:16px}",
21
+ ".ci-rail{flex:1 1 260px;min-width:230px;max-width:360px;display:flex;flex-direction:column;gap:10px;max-height:72vh;overflow-y:auto;overscroll-behavior:contain;padding-right:2px;padding-bottom:16px}",
22
+ ".ci-main::-webkit-scrollbar,.ci-rail::-webkit-scrollbar{width:8px}",
23
+ ".ci-main::-webkit-scrollbar-thumb,.ci-rail::-webkit-scrollbar-thumb{background:rgba(127,127,150,.35);border-radius:4px}",
24
+ ".ci-hd{display:flex;align-items:center;gap:10px;margin-bottom:4px}",
25
+ ".ci-badge{width:8px;height:8px;border-radius:50%;flex:none}",
26
+ ".ci-on{background:#34c759;box-shadow:0 0 8px rgba(52,199,89,.7)}",
27
+ ".ci-off{background:rgba(120,120,140,.45)}",
28
+ ".ci-title{font-size:13px;font-weight:650;letter-spacing:.01em}",
29
+ ".ci-sub{font-size:11px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.75))}",
30
+ // 「请求结构」默认收成一行摘要(节点数/总字节/前四组),明细按需展开,避免几百个节点铺满面板
31
+ ".ci-frames summary{cursor:pointer;list-style:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
32
+ ".ci-frames summary::-webkit-details-marker{display:none}",
33
+ ".ci-frames summary::before{content:'\\25B8';display:inline-block;width:10px;margin-right:3px;opacity:.7}",
34
+ ".ci-frames[open] summary::before{content:'\\25BE'}",
35
+ ".ci-frames[open] summary{white-space:normal;margin-bottom:4px}",
36
+ ".ci-frame-body{max-height:200px;overflow:auto;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.22));border-radius:7px;padding:4px 6px;margin-top:2px}",
37
+ ".ci-frame-body .g{display:flex;gap:8px;font-variant-numeric:tabular-nums}",
38
+ ".ci-frame-body .g b{margin-left:auto;font-weight:600}",
39
+ ".ci-frame-list{white-space:pre-wrap;word-break:break-all;font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace);font-size:10px;line-height:1.45;margin-top:4px;padding-top:4px;border-top:1px solid rgba(127,127,150,.15)}",
40
+ ".ci-sec{background:rgba(127,127,150,.06);border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.22));border-radius:12px;padding:10px 12px;margin:10px 0}",
41
+ ".ci-sech{display:flex;align-items:center;gap:8px;margin:0 0 8px;font-size:11px;font-weight:650;letter-spacing:.08em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.75))}",
42
+ ".ci-toggle{display:inline-flex;align-items:center;gap:8px;cursor:pointer;user-select:none}",
43
+ ".ci-switch{width:34px;height:19px;border-radius:999px;border:1px solid rgba(127,127,150,.45);position:relative;transition:background .18s ease;flex:none;background:transparent}",
44
+ ".ci-switch:before{content:'';position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:currentColor;opacity:.75;transition:transform .18s ease}",
45
+ ".ci-on .ci-switch{background:rgba(52,199,89,.22);border-color:rgba(52,199,89,.6)}",
46
+ ".ci-on .ci-switch:before{transform:translateX(15px);opacity:1}",
47
+ // LOGcompiler 区开关的独立 on 态(不加 ci-on,避免整行被涂绿,仅滑块位移+描边变绿)
48
+ ".ci-lg .ci-switch{background:transparent}",
49
+ ".ci-lgon .ci-switch{background:rgba(52,199,89,.22);border-color:rgba(52,199,89,.6)}",
50
+ ".ci-lgon .ci-switch:before{transform:translateX(15px);opacity:1}",
51
+ ".ci-grp{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.22));border-radius:10px;margin:6px 0;overflow:hidden}",
52
+ ".ci-grphd{display:flex;align-items:center;gap:8px;padding:7px 10px;cursor:pointer;font-size:12px;background:rgba(127,127,150,.05)}",
53
+ ".ci-grphd:hover{background:rgba(127,127,150,.1)}",
54
+ ".ci-caret{font-size:9px;transition:transform .15s ease;flex:none}",
55
+ ".ci-open .ci-caret{transform:rotate(90deg)}",
56
+ ".ci-chips{display:flex;flex-wrap:wrap;gap:6px;padding:8px 10px 10px}",
57
+ ".ci-chip{font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.3));border-radius:999px;padding:2px 8px;cursor:pointer;display:inline-flex;align-items:center;gap:5px;color:inherit;transition:border-color .12s ease,background .12s ease}",
58
+ ".ci-chipon{border-color:rgba(100,140,255,.65);background:rgba(100,140,255,.13)}",
59
+ ".ci-arch{opacity:.45;text-decoration:line-through}",
60
+ ".ci-metrics{display:flex;flex-wrap:wrap;gap:4px 14px;font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11px;margin-bottom:8px}",
61
+ ".ci-metric b{font-weight:650;color:var(--dsw-alias-label-primary,#e7e7ee)}",
62
+ ".ci-hr{border:none;border-top:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.16));margin:10px 0}",
63
+ ".ci-rnd{display:flex;gap:10px;margin:4px 0 10px}",
64
+ ".ci-rndtag{flex:none;font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:10px;letter-spacing:.06em;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.8));padding-top:3px;min-width:62px}",
65
+ ".ci-flow{flex:1;min-width:0}",
66
+ ".ci-msg{border-left:2px solid rgba(127,127,150,.18);padding:2px 10px 2px 12px;margin:3px 0}",
67
+ ".ci-u{border-left-color:rgba(120,160,255,.55)}",
68
+ ".ci-a{border-left-color:rgba(127,127,150,.14)}",
69
+ // 外置推理简报(EXTREASON `[R]` 行):独立色相(紫),与「你」(蓝)/「AI」(灰) 一眼可分
70
+ ".ci-r{border-left-color:rgba(178,130,255,.6)}",
71
+ ".ci-r .ci-role{color:rgba(198,160,255,.95)}",
72
+ ".ci-t{border-left:none;font-family:var(--ds-font-family-code,ui-monospace,SFMono-Regular,Consolas,monospace);font-size:11px;padding:1px 2px;margin:2px 0}",
73
+ ".ci-t-ok{color:#58c46b}",".ci-t-err{color:#ff6b6b}",
74
+ ".ci-role{font-size:10px;font-weight:700;letter-spacing:.08em;margin-right:6px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.7))}",
75
+ ".ci-text{white-space:pre-wrap;word-break:break-word;cursor:pointer}",
76
+ ".ci-clamp{display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}",
77
+ ".ci-actions{display:flex;gap:8px;margin-top:6px;flex-wrap:wrap}",
78
+ ".ci-btn{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.4));background:transparent;color:inherit;border-radius:7px;padding:3px 11px;font-size:12px;cursor:pointer}",
79
+ ".ci-btn:hover{background:rgba(127,127,150,.12)}",
80
+ // 右键菜单(会话 chip → 导出 LOG / 复制会话 ID)【取自 RCS-0.1.0 返回包】
81
+ ".ci-ctx{position:fixed;z-index:9999;min-width:172px;padding:4px;border-radius:9px;background:var(--dsw-alias-bg-elevated,#22222b);border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.45));box-shadow:0 10px 28px rgba(0,0,0,.45);font-size:12px}",
82
+ ".ci-ctx-hd{padding:3px 8px 6px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.75));font-size:11px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;border-bottom:1px solid rgba(127,127,150,.2);margin-bottom:4px}",
83
+ ".ci-ctx button{display:block;width:100%;text-align:left;border:0;background:transparent;color:inherit;font:inherit;padding:5px 8px;border-radius:6px;cursor:pointer}",
84
+ ".ci-ctx button:hover{background:rgba(127,127,150,.16)}",
85
+ ".ci-ctx button:disabled{opacity:.45;cursor:default}",
86
+ ".ci-ctx button:disabled:hover{background:transparent}",
87
+ ".ci-err{color:#ff6b6b;font-size:12px;margin:4px 0}",
88
+ // v1.8/v1.9 压缩模型(面板):provider/model 原生 select,直接从 DSH 已注册目录里选。
89
+ // 深色主题下原生 select 弹出层 option 文字默认继承浅色(未悬停不可见)→ option 显式配色兜底。
90
+ ".ci-sel{font-family:inherit;font-size:11px;line-height:18px;color:inherit;background:transparent;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.35));border-radius:7px;padding:2px 6px;min-width:170px;max-width:250px}",
91
+ ".ci-sel option{color:#16161d;background:#fff}",
92
+ // v1.7 composer dock:本会话压缩模式选择器(卡片上方一行,首轮发送前即可用)
93
+ ".cim-dock{box-sizing:border-box;max-width:var(--dsh-composer-card-max-width,760px);width:100%;margin:0 auto 6px;padding:0 12px;display:flex;align-items:center;gap:8px;font-size:11px;flex:none}",
94
+ ".cim-lb{color:var(--dsw-alias-label-secondary,rgba(180,180,195,.75));flex:none;letter-spacing:.02em}",
95
+ ".cim-chips{display:flex;gap:5px;flex-wrap:wrap;min-width:0}",
96
+ ".cim-chip{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.3));border-radius:999px;padding:1px 9px;cursor:pointer;color:inherit;background:transparent;font-size:11px;line-height:18px;white-space:nowrap;font-family:inherit;transition:border-color .12s ease,background .12s ease}",
97
+ ".cim-chip:hover{background:rgba(127,127,150,.12)}",
98
+ ".cim-on{border-color:rgba(100,140,255,.65);background:rgba(100,140,255,.13)}",
99
+ ".cim-hint{color:var(--dsw-alias-label-secondary,rgba(180,180,195,.6));font-size:10px;flex:none}",
100
+ ".ci-chart{width:100%;height:110px;display:block;background:rgba(127,127,150,.04);border-radius:8px;margin-top:4px}",
101
+ // 【在飞折叠】「上一轮输出压缩中…」脉冲点(琥珀 = 面板既有的警示色相,与绿=就绪/灰=停区分)
102
+ ".ci-pulse{width:8px;height:8px;border-radius:50%;flex:none;background:#ffb454;box-shadow:0 0 8px rgba(255,180,84,.75);animation:ci-pulse 1.1s ease-in-out infinite}",
103
+ "@keyframes ci-pulse{0%,100%{opacity:.35;transform:scale(.82)}50%{opacity:1;transform:scale(1.12)}}",
104
+ ".ci-fin{color:rgba(255,190,110,.95)}",
105
+ // 【会话页·在飞折叠】composer 操作行左侧的「压缩中」小标:行内 inline-flex(flex:none),
106
+ // 与同排的档位选择器同字号 13px/20px ⇒ 只占行内位置、不改变行高,不挤动 DSH 自己的状态行。
107
+ // 脉冲点复用上面的 .ci-pulse;色相与面板 .ci-fin 一致(琥珀 = 警示)。
108
+ // v2.4 可见性(浅/深两套主题都要看得见):描边/底色/脉冲点改用 DSH 自带的 warn 令牌族
109
+ // `--dsw-alias-state-warn-{primary,secondary,tertiary}`,**两套主题都给值**
110
+ // (dsh-client-ui-theme/lib/client.js:15369 浅色块 / :20674 深色块:
111
+ // primary=amber-500 #f59e0b、secondary=amber-400 #f7ad31、
112
+ // tertiary=琥珀底(浅 #fef5e7 / 深 #27241f)),
113
+ // 且 DSH 自己的审批卡就是这套配方(dsh-client-ui-conversation/lib/client.js:6010:
114
+ // `.bqrRRG_strip{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-primary)}`
115
+ // + `.bqrRRG_dot{background:var(--dsw-alias-state-warn-primary)}` 的 8px 点)。
116
+ // **文字色故意不用琥珀**:琥珀系文字在浅色主题的白卡片上对比度只有约 2.2:1(DSH 审批卡自己也是这个数),
117
+ // 而"压缩中"是要被读到的状态字。故文字用主题 label-primary(浅=#0f1115 / 深=#f9fafb,
118
+ // 在本胶囊底上对比度 ≈14:1~17:1),琥珀身份交给底色+描边+脉冲点三处承载。
119
+ // 旧实现写死 `color:rgba(255,190,110,.95)`(约 1.6:1)⇒「小标其实在渲染,但用户看不见」是真实候选原因之一。
120
+ ".ci-foldchip{display:inline-flex;align-items:center;gap:6px;flex:none;font-size:13px;line-height:20px;font-weight:500;white-space:nowrap;box-sizing:border-box;padding:0 8px;border-radius:999px;border:1px solid var(--dsw-alias-state-warn-secondary,var(--dsw-static-amber-400,rgba(255,180,84,.5)));background:var(--dsw-alias-state-warn-tertiary,var(--dsw-static-amber-100,rgba(255,180,84,.14)));color:var(--dsw-alias-label-primary,#0f1115)}",
121
+ // 胶囊内的脉冲点用同一族令牌(深色下更亮、浅色下更饱和),动画仍是上面那一套 @keyframes ci-pulse
122
+ ".ci-foldchip .ci-pulse{background:var(--dsw-alias-state-warn-primary,var(--dsw-static-amber-500,#f59e0b))}",
123
+ // 【v1.10.39 · 宿主折叠行隐藏】宿主按 compaction 协议渲染的「上下文已压缩 / 已压缩 N 条…」
124
+ // 每折一次占一行、越来越长。插件改为在 composer 左侧自绘一行紧凑状态(本折/累计比值 +
125
+ // 压缩中/记忆编译中脉冲);宿主行全部隐藏。宿主原生 compaction 由本插件另写
126
+ // native-compaction 标记并在同一行补回(见 FoldChip),不会静默丢失。
127
+ '[class*="compactionRow"]{display:none!important}',
128
+ // 完成后收成中性色(琥珀只留给“正在发生”的状态,不再长期占据注意)
129
+ ".ci-foldchip-done{border-color:var(--dsw-alias-border-l2,rgba(127,127,150,.3));background:transparent;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.8));font-weight:400}",
130
+ ".ci-foldchips{display:inline-flex;align-items:center;gap:10px;flex-wrap:wrap;flex:none}",
131
+ // ===== MEMORY 面板 =====
132
+ ".mem-kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:10px 0}",
133
+ "@media (max-width:900px){.mem-kpis{grid-template-columns:repeat(2,minmax(0,1fr))}}",
134
+ ".mem-kpi{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.22));border-radius:11px;padding:9px 11px;background:rgba(127,127,150,.04)}",
135
+ ".mem-kpi b{display:block;font-size:20px;line-height:1.2;font-weight:650;font-variant-numeric:tabular-nums}",
136
+ ".mem-kpi span{display:block;font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.75));margin-top:2px}",
137
+ ".mem-kpi.mem-warn b{color:var(--dsw-alias-state-warn-primary,#f59e0b)}",
138
+ ".mem-bars{display:flex;flex-direction:column;gap:7px}",
139
+ ".mem-bar{display:grid;grid-template-columns:112px minmax(40px,1fr) 44px;align-items:center;gap:8px;font-size:11px}",
140
+ ".mem-bar .k{font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace);color:var(--dsw-alias-label-secondary,rgba(180,180,195,.85));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
141
+ ".mem-track{display:block;height:7px;border-radius:99px;background:rgba(127,127,150,.14);overflow:hidden}",
142
+ ".mem-fill{display:block;height:100%;border-radius:99px;background:rgba(100,140,255,.72)}",
143
+ ".mem-bar .n{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace)}",
144
+ ".mem-ins{display:flex;flex-direction:column;gap:7px;max-height:300px;overflow:auto;overscroll-behavior:contain}",
145
+ ".mem-card{border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.2));border-radius:10px;padding:8px 10px;background:rgba(127,127,150,.035)}",
146
+ ".mem-card-h{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
147
+ ".mem-key{font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace);font-size:11px;padding:1px 7px;border-radius:6px;border:1px solid rgba(100,140,255,.4);background:rgba(100,140,255,.12)}",
148
+ ".mem-conf{font-size:10px;padding:1px 6px;border-radius:999px;border:1px solid rgba(127,127,150,.35);color:var(--dsw-alias-label-secondary,rgba(180,180,195,.85))}",
149
+ ".mem-conf.low{border-color:rgba(245,158,11,.5);background:rgba(245,158,11,.12);color:inherit}",
150
+ ".mem-summary{margin-top:4px;font-size:12px;line-height:1.5;word-break:break-word}",
151
+ ".mem-meta{margin-top:4px;font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.7));font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
152
+ ".mem-prov{font-size:10px;padding:1px 6px;border-radius:999px;border:1px solid rgba(127,127,150,.3);color:var(--dsw-alias-label-secondary,rgba(180,180,195,.85));white-space:nowrap}",
153
+ ".mem-prov.private{border-color:rgba(25,145,70,.45);background:rgba(25,145,70,.1)}",
154
+ ".mem-recent{display:flex;flex-direction:column;gap:2px;max-height:240px;overflow:auto;overscroll-behavior:contain}",
155
+ ".mem-row{display:grid;grid-template-columns:92px minmax(80px,1fr) 70px 74px;gap:8px;align-items:center;font-size:11px;padding:3px 2px;border-bottom:1px solid rgba(127,127,150,.08)}",
156
+ ".mem-row .k{font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
157
+ ".mem-st{font-size:10px;letter-spacing:.04em}",
158
+ ".mem-st.active{color:#58c46b}",
159
+ ".mem-st.resolved{color:var(--dsw-alias-label-secondary,rgba(180,180,195,.6))}",
160
+ ".mem-off{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:9px;border:1px solid var(--dsw-alias-state-warn-secondary,rgba(255,180,84,.5));background:var(--dsw-alias-state-warn-tertiary,rgba(255,180,84,.14));font-size:12px;margin:8px 0;color:var(--dsw-alias-label-primary,#0f1115)}",
161
+ ".mem-flag{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 0;border-bottom:1px solid rgba(127,127,150,.08)}",
162
+ ".mem-flag:last-child{border-bottom:none}",
163
+ ".mem-foot{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.7));font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace);flex-wrap:wrap;word-break:break-all}",
164
+ ".mem-empty{padding:14px 4px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.75));font-size:12px}",
165
+ // 记忆面板补充:状态胶囊 / 可访问开关 / 作用域表 / 五列近期记录
166
+ ".mem-hd-t{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
167
+ ".mem-state{font-size:10px;line-height:16px;padding:0 7px;border-radius:999px;border:1px solid rgba(127,127,150,.35);color:var(--dsw-alias-label-secondary,rgba(180,180,195,.8));white-space:nowrap}",
168
+ ".mem-state.on{border-color:rgba(52,199,89,.55);background:rgba(52,199,89,.12);color:#58c46b}",
169
+ ".mem-state.pending{border-color:rgba(245,158,11,.55);background:rgba(245,158,11,.12);color:var(--dsw-alias-state-warn-primary,#f59e0b)}",
170
+ ".mem-dot-pending{background:var(--dsw-alias-state-warn-primary,#f59e0b);box-shadow:0 0 8px rgba(245,158,11,.6)}",
171
+ ".mem-state.off{border-color:rgba(127,127,150,.35);background:rgba(127,127,150,.08)}",
172
+ ".mem-flag-t{min-width:0}",
173
+ ".mem-flag-state{font-size:11px;min-width:26px;text-align:right;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.8))}",
174
+ ".mem-dis{opacity:.42;cursor:not-allowed}",
175
+ // 开关容器不能再挂 .ci-on(那会把整块染绿);用独立的 mem-on 只驱动滑块
176
+ ".ci-toggle.mem-on .ci-switch{background:rgba(52,199,89,.22);border-color:rgba(52,199,89,.6)}",
177
+ ".ci-toggle.mem-on .ci-switch:before{transform:translateX(15px);opacity:1}",
178
+ ".mem-evidence{font-size:10px;padding:1px 6px;border-radius:999px;border:1px solid rgba(100,140,255,.35);color:var(--dsw-alias-label-secondary,rgba(180,180,195,.85))}",
179
+ ".mem-ws{display:flex;flex-direction:column;gap:4px}",
180
+ ".mem-ws-row{display:grid;grid-template-columns:minmax(0,1fr) 44px 76px;gap:8px;align-items:center;font-size:11px;padding:2px 0}",
181
+ ".mem-ws-row.current{background:rgba(100,140,255,.07);border-radius:6px}",
182
+ ".mem-ws-row .k{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
183
+ ".mem-ws-row .n{text-align:right;font-variant-numeric:tabular-nums;font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace)}",
184
+ ".mem-ws-row .t{text-align:right;font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.7))}",
185
+ ".mem-group-t{margin-top:9px;font-size:11px;font-weight:600;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.85))}",
186
+ ".mem-cur{font-size:9px;line-height:15px;padding:0 5px;border-radius:999px;border:1px solid rgba(52,199,89,.5);background:rgba(52,199,89,.12);color:#58c46b;margin-left:6px;white-space:nowrap}",
187
+ ".mem-origin{font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.68));margin-left:5px;white-space:nowrap}",
188
+ ".mem-range{display:flex;gap:6px;flex-wrap:wrap;margin-top:6px}",
189
+ ".mem-range-btn{font-size:11px;padding:3px 9px}",
190
+ ".mem-range-btn.on{border-color:rgba(100,140,255,.65);background:rgba(100,140,255,.16)}",
191
+ ".mem-row{grid-template-columns:86px minmax(80px,1fr) 112px 62px 76px}",
192
+ ".mem-row .mem-scope{font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.8));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
193
+ ".mem-row .mem-time{text-align:right;font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.7))}",
194
+ ".mem-row.mem-head{font-size:9px;letter-spacing:.08em;text-transform:uppercase;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.65));border-bottom:1px solid rgba(127,127,150,.18);padding-bottom:4px;margin-bottom:2px}",
195
+ "@media (max-width:720px){.mem-row{grid-template-columns:80px minmax(70px,1fr) 60px 70px}.mem-row .mem-scope,.mem-row.mem-head .mem-scope{display:none}}",
196
+ // 图谱 / 列表 / 设置 三页签
197
+ ".mem-tabs{display:flex;align-items:center;gap:4px;margin:10px 0 0;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.18))}",
198
+ ".mem-tab{font-family:inherit;font-size:12px;line-height:24px;padding:1px 11px 3px;border:1px solid transparent;border-bottom:none;border-radius:8px 8px 0 0;background:transparent;color:inherit;cursor:pointer;opacity:.72}",
199
+ ".mem-tab:hover{background:rgba(127,127,150,.1);opacity:1}",
200
+ ".mem-tab.on{opacity:1;border-color:var(--dsw-alias-border-l2,rgba(127,127,150,.22));background:rgba(127,127,150,.09);box-shadow:inset 0 -2px 0 rgba(100,140,255,.75)}",
201
+ ".mem-tabs-note{margin-left:auto;font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.7));padding-bottom:4px;white-space:nowrap}",
202
+ ".mem-strip{display:flex;flex-wrap:wrap;gap:4px 18px;padding:8px 11px;margin:10px 0 8px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.2));border-radius:10px;background:rgba(127,127,150,.035);font-size:11px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.85))}",
203
+ ".mem-strip b{color:var(--dsw-alias-label-primary,inherit);font-variant-numeric:tabular-nums;margin-left:3px}",
204
+ ".mem-limit{margin:2px 0 8px;padding:4px 9px;border:1px dashed rgba(127,127,150,.35);border-radius:7px;font-size:10px;line-height:1.5;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.82));display:flex;gap:8px;align-items:center;flex-wrap:wrap}",
205
+ ".mem-toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:0 0 8px}",
206
+ ".mem-toolbar .ci-sel{min-width:88px;max-width:150px;width:auto;font-size:11px;padding:2px 4px}",
207
+ ".mem-toolbar-label{font-size:10px;letter-spacing:.08em;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.7))}",
208
+ ".mem-search{min-width:130px;flex:1 1 130px;max-width:240px}",
209
+ ".mem-focus{display:inline-flex;align-items:center;gap:3px;font-size:10px;line-height:18px;padding:0 3px 0 8px;border-radius:999px;border:1px solid rgba(100,140,255,.45);background:rgba(100,140,255,.12);max-width:220px}",
210
+ ".mem-focus>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
211
+ ".mem-focus-x{border:none;background:transparent;color:inherit;cursor:pointer;font-family:inherit;font-size:13px;line-height:1;padding:0 3px}",
212
+ ".mem-settings{max-width:900px;margin:10px auto 0}",
213
+ // 图谱
214
+ ".mem-graph-wrap{position:relative;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.2));border-radius:12px;background:radial-gradient(circle at 50% 45%,rgba(100,140,255,.07),transparent 64%),rgba(127,127,150,.025);overflow:hidden}",
215
+ ".mem-graph{display:block;width:100%;height:clamp(220px,32vh,430px);touch-action:none;cursor:grab}",
216
+ ".mem-graph:active{cursor:grabbing}",
217
+ ".mem-graph-tools{position:absolute;right:8px;top:8px;display:flex;flex-direction:column;gap:4px}",
218
+ ".mem-graph-tools .ci-btn{padding:1px 7px;min-width:26px;line-height:17px}",
219
+ ".mem-graph-wrap .mem-legend{border-top:1px solid rgba(127,127,150,.12);background:rgba(127,127,150,.03)}",
220
+ ".mem-legend{display:flex;flex-wrap:wrap;align-items:center;gap:4px 12px;padding:5px 9px;font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.8))}",
221
+ ".mem-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:4px;vertical-align:middle;background:rgba(127,127,150,.7)}",
222
+ ".mem-dot.ws{border-radius:2px;background:rgba(100,140,255,.9)}",
223
+ ".mem-dot.sess{background:rgba(88,196,107,.85)}",
224
+ ".mem-dot.fact.constraint{background:#e2a33b}",
225
+ ".mem-dot.fact.state{background:#5b9dff}",
226
+ ".mem-dot.fact.task{background:#a78bfa}",
227
+ ".mem-dot.fact.history{background:#8b95a5}",
228
+ ".mem-line{display:inline-block;width:16px;height:0;border-top:2px solid rgba(127,127,150,.4);margin-right:4px;vertical-align:middle}",
229
+ ".mem-line.shared{border-top-color:rgba(100,140,255,.7)}",
230
+ ".mem-line.origin{border-top-style:dashed;border-top-color:rgba(167,139,250,.75)}",
231
+ ".mem-line.private{border-top-color:rgba(88,196,107,.75)}",
232
+ ".mem-line.member{border-top-style:dashed;border-top-color:rgba(127,127,150,.5)}",
233
+ ".mem-edge{stroke:rgba(127,127,150,.3);stroke-width:1;fill:none;transition:stroke .12s ease,stroke-width .12s ease}",
234
+ ".mem-edge.shared{stroke:rgba(100,140,255,.55);stroke-width:1.4}",
235
+ ".mem-edge.origin{stroke:rgba(167,139,250,.55);stroke-dasharray:4 3}",
236
+ ".mem-edge.private{stroke:rgba(88,196,107,.55)}",
237
+ ".mem-edge.member{stroke:rgba(127,127,150,.26);stroke-dasharray:2 3}",
238
+ ".mem-edge.on{stroke:rgba(100,140,255,.95);stroke-width:2;stroke-dasharray:none}",
239
+ ".mem-node circle,.mem-node rect{stroke:rgba(127,127,150,.5);stroke-width:1;fill:rgba(127,127,150,.18);transition:opacity .12s ease}",
240
+ ".mem-node.workspace rect{fill:rgba(100,140,255,.2);stroke:rgba(100,140,255,.8)}",
241
+ ".mem-node.session circle{fill:rgba(88,196,107,.18);stroke:rgba(88,196,107,.75)}",
242
+ ".mem-node.fact circle{fill:rgba(127,127,150,.28);stroke:rgba(127,127,150,.6)}",
243
+ ".mem-node.fact.constraint circle{fill:rgba(226,163,59,.3);stroke:#e2a33b}",
244
+ ".mem-node.fact.state circle{fill:rgba(91,157,255,.28);stroke:#5b9dff}",
245
+ ".mem-node.fact.task circle{fill:rgba(167,139,250,.28);stroke:#a78bfa}",
246
+ ".mem-node.fact.history circle{fill:rgba(139,149,165,.26);stroke:#8b95a5}",
247
+ ".mem-node.current circle:not(.mem-ring),.mem-node.current rect{stroke-width:2.2}",
248
+ ".mem-node .mem-ring{fill:none;stroke:rgba(100,140,255,.6);stroke-width:1.1;stroke-dasharray:3 3}",
249
+ ".mem-node.sel circle,.mem-node.sel rect{stroke-width:2.6}",
250
+ ".mem-node.dim{opacity:.22}",
251
+ ".mem-node.muted{opacity:.48}",
252
+ ".mem-node:focus{outline:none}",
253
+ ".mem-focus-ring{fill:none;stroke:#7cc4ff;stroke-width:2;stroke-dasharray:3 3;pointer-events:none}",
254
+ ".mem-graph-tools .ci-btn.on{border-color:rgba(100,140,255,.65);background:rgba(100,140,255,.16)}",
255
+ ".mem-zen .mem-strip,.mem-zen .mem-graph-wrap .mem-legend{display:none}",
256
+ ".mem-zen .mem-graph{height:clamp(300px,50vh,640px)}",
257
+ ".mem-node-label{font-size:14px;fill:currentColor;pointer-events:none;user-select:none}",
258
+ // 详情栏与列表卡片
259
+ ".mem-detail-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;font-size:11px;padding:4px 0;border-bottom:1px solid rgba(127,127,150,.08)}",
260
+ ".mem-detail-row:last-of-type{border-bottom:none}",
261
+ ".mem-detail-row span{color:var(--dsw-alias-label-secondary,rgba(180,180,195,.75));flex:none}",
262
+ ".mem-detail-row b{font-weight:500;text-align:right;word-break:break-all;min-width:0}",
263
+ ".mem-detail-actions{display:flex;gap:6px;flex-wrap:wrap;margin-top:10px}",
264
+ ".mem-detail-sub{margin-top:10px;font-size:10px;letter-spacing:.05em;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.62));text-transform:uppercase}",
265
+ ".mem-evs{display:flex;flex-direction:column;gap:2px;margin-top:4px}",
266
+ ".mem-ev{border-bottom:1px solid rgba(127,127,150,.08);padding:3px 0}",
267
+ ".mem-ev summary{display:flex;align-items:baseline;gap:6px;cursor:pointer;list-style:none;font-size:11px}",
268
+ ".mem-ev summary::-webkit-details-marker{display:none}",
269
+ ".mem-ev-k{flex:none;font-size:9px;padding:0 5px;border-radius:999px;border:1px solid rgba(127,127,150,.3);color:var(--dsw-alias-label-secondary,rgba(180,180,195,.8))}",
270
+ ".mem-ev-c{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,rgba(205,205,220,.9))}",
271
+ ".mem-ev-t{flex:none;font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.6))}",
272
+ ".mem-ev-meta{font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.7));padding:3px 0 0 2px}",
273
+ ".mem-ev-raw{white-space:pre-wrap;word-break:break-all;font-size:10px;max-height:160px;overflow:auto;background:rgba(127,127,150,.08);padding:6px;border-radius:5px;margin:5px 0 0;font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace)}",
274
+ ".mem-fact-label{font-size:14px;fill:var(--dsw-alias-label-secondary,rgba(214,218,228,.92));pointer-events:none}",
275
+ ".mem-fact{cursor:pointer}",
276
+ ".mem-fact:hover{border-color:rgba(100,140,255,.4);background:rgba(100,140,255,.055)}",
277
+ ".mem-fact.sel{border-color:rgba(100,140,255,.6);background:rgba(100,140,255,.09)}",
278
+ ".mem-fact.muted{opacity:.58}",
279
+ ".mem-type{font-size:10px;padding:1px 6px;border-radius:999px;border:1px solid rgba(127,127,150,.32);color:var(--dsw-alias-label-secondary,rgba(180,180,195,.85))}",
280
+ ".mem-type.constraint{border-color:rgba(226,163,59,.5);background:rgba(226,163,59,.1)}",
281
+ ".mem-type.state{border-color:rgba(91,157,255,.45)}",
282
+ ".mem-type.task{border-color:rgba(167,139,250,.45)}",
283
+ // 检索轨迹(Phase 4):列表 + 详情;查询文本已由服务端脱敏,仍按不可信数据显示。
284
+ ".mem-trace-list{display:flex;flex-direction:column;gap:3px;max-height:360px;overflow:auto;overscroll-behavior:contain}",
285
+ ".mem-trace{display:grid;grid-template-columns:64px 56px minmax(0,1fr) 74px 72px 62px;gap:8px;align-items:center;font-size:11px;padding:5px 7px;border:1px solid transparent;border-radius:7px;background:rgba(127,127,150,.035);cursor:pointer}",
286
+ ".mem-trace:hover{background:rgba(100,140,255,.07)}",
287
+ ".mem-trace.on{border-color:rgba(100,140,255,.55);background:rgba(100,140,255,.1)}",
288
+ ".mem-trace .t{font-size:10px;color:var(--dsw-alias-label-secondary,rgba(180,180,195,.72));font-variant-numeric:tabular-nums;white-space:nowrap}",
289
+ ".mem-trace .q{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-secondary,rgba(205,205,220,.92))}",
290
+ ".mem-trace .n{text-align:right;font-variant-numeric:tabular-nums;font-size:10px}",
291
+ ".mem-trigger{font-size:9px;line-height:16px;padding:0 5px;border-radius:999px;border:1px solid rgba(127,127,150,.35);text-align:center;white-space:nowrap}",
292
+ ".mem-trigger.tool{border-color:rgba(100,140,255,.5);background:rgba(100,140,255,.1)}",
293
+ ".mem-trigger.ls{border-color:rgba(88,196,107,.45)}",
294
+ ".mem-trigger.recall{border-color:rgba(226,163,59,.5)}",
295
+ ".mem-trigger.inject{border-color:rgba(167,139,250,.45)}",
296
+ ".mem-trigger.handover{border-color:rgba(127,127,150,.3)}",
297
+ ".mem-trace-detail{margin-top:10px;border-top:1px solid rgba(127,127,150,.15);padding-top:8px}",
298
+ ".mem-stage-row{display:grid;grid-template-columns:80px 44px 60px minmax(0,1fr);gap:8px;font-size:10px;padding:2px 0;border-bottom:1px solid rgba(127,127,150,.06);align-items:baseline}",
299
+ ".mem-stage-row .why{color:var(--dsw-alias-label-secondary,rgba(180,180,195,.7));overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
300
+ ".mem-uri{font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace);font-size:10px;word-break:break-all}",
301
+ ".mem-code{white-space:pre-wrap;word-break:break-word;font-size:10px;line-height:1.5;max-height:180px;overflow:auto;background:rgba(127,127,150,.07);padding:6px 8px;border-radius:6px;margin:5px 0 0;font-family:var(--ds-font-family-code,ui-monospace,Consolas,monospace)}",
302
+ ".mem-diag{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:8px 0}",
303
+ "@media (max-width:900px){.mem-diag{grid-template-columns:repeat(2,minmax(0,1fr))}}",
304
+ // 向量/指标诊断只读块:状态色与图谱一致(amber=降级,green=可用,灰=关闭)。
305
+ ".mem-vec{display:flex;flex-wrap:wrap;gap:4px 16px;font-size:11px;align-items:center}",
306
+ ".mem-vec b{font-variant-numeric:tabular-nums;margin-left:3px}",
307
+ "@media (max-width:720px){.mem-tabs-note{display:none}.mem-graph{height:30vh}.mem-toolbar .ci-sel{flex:1 1 96px;max-width:none}.mem-search{flex:1 1 100%;max-width:none}.mem-trace{grid-template-columns:54px minmax(0,1fr) 54px}.mem-trace .t.mode,.mem-trace .n.cand,.mem-trace .n.ms{display:none}}",
308
+ "@media (max-width:900px){.mem-node-label{font-size:17px}.mem-fact-label{font-size:15px}.mem-toolbar-label{display:none}}",
309
+ "@media (prefers-color-scheme:light){.mem-edge{stroke:rgba(60,75,110,.5)}.mem-edge.shared{stroke:rgba(40,90,215,.8);stroke-width:1.6}.mem-edge.origin{stroke:rgba(110,70,205,.8)}.mem-edge.private{stroke:rgba(25,145,70,.85)}.mem-edge.member{stroke:rgba(90,100,120,.45)}.mem-node circle,.mem-node rect{fill:rgba(90,105,130,.16);stroke:rgba(60,75,105,.75)}.mem-node.workspace rect{fill:rgba(50,95,215,.14);stroke:rgba(40,90,215,.95)}.mem-node.session circle{fill:rgba(25,145,70,.12);stroke:rgba(25,145,70,.9)}.mem-node.fact.constraint circle{fill:rgba(200,140,30,.25);stroke:#b7791f}.mem-node.fact.state circle{fill:rgba(40,110,220,.2);stroke:#2b6cd4}.mem-node.fact.task circle{fill:rgba(120,85,200,.22);stroke:#6d4bc0}.mem-node.fact.history circle{fill:rgba(100,110,125,.18);stroke:#5d6674}.mem-node .mem-ring{stroke:rgba(40,90,215,.7)}.mem-node-label{fill:#1b1f27}.mem-fact-label{fill:#3a4150}.mem-focus-ring{stroke:#1b64c8}}",
310
+ "@media (prefers-reduced-motion:reduce){.ci-switch,.ci-caret,.cim-chip{transition:none}.ci-pulse{animation:none;opacity:.9}}"
311
+ ].join("");
312
+ var CSS_TAG = "contextinjector-webui/client.css";
313
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(CSS_TAG) + "]") === null) {
314
+ var tag = document.createElement("style");
315
+ tag.dataset.plugin = "contextinjector-webui";
316
+ tag.dataset.pluginCss = CSS_TAG;
317
+ tag.textContent = css;
318
+ document.head.appendChild(tag);
319
+ }
320
+
321
+ function shortId(id) { return String(id || "").replace(/^session-/, "").slice(0, 8); }
322
+ function fmtB(n) {
323
+ var v = Number(n) || 0;
324
+ if (v < 1024) return v + "B";
325
+ if (v < 1048576) return (v / 1024).toFixed(1) + "KB";
326
+ return (v / 1048576).toFixed(2) + "MB";
327
+ }
328
+ // 折叠后请求结构:给面板一行摘要(节点数/总字节/最大的几组),明细按需展开。
329
+ // 为什么按 role/src 聚合:一次折叠常有数百个同质节点(assistant/model、tool/tool),
330
+ // 逐节点平铺会把面板撑成一面字墙(实测 303 节点)。分组后仍保留逐节点明细供核验。
331
+ function frameSummary(nodes) {
332
+ var list = Array.isArray(nodes) ? nodes : [];
333
+ var groups = {}, order = [], total = 0;
334
+ list.forEach(function (x) {
335
+ var b = Number(x && x.bytes) || 0;
336
+ var role = String((x && x.role) || "?");
337
+ var src = x && x.src ? String(x.src) : "";
338
+ var k = src ? role + "/" + src : role;
339
+ total += b;
340
+ if (!groups[k]) { groups[k] = { k: k, n: 0, b: 0 }; order.push(k); }
341
+ groups[k].n += 1; groups[k].b += b;
342
+ });
343
+ var gs = order.map(function (k) { return groups[k]; }).sort(function (a, b) { return b.b - a.b || (a.k < b.k ? -1 : 1); });
344
+ return {
345
+ n: list.length, total: total, groups: gs, top: gs.slice(0, 4),
346
+ text: list.map(function (x) {
347
+ return String((x && x.role) || "?") + (x && x.src ? "/" + x.src : "") + ":" + (Number(x && x.bytes) || 0) + "B";
348
+ }).join("\n")
349
+ };
350
+ }
351
+ function parseTranscript(text) {
352
+ var rounds = [], cur = null;
353
+ (text || "").split("\n").forEach(function (line) {
354
+ var rm = /^\[ROUND\]\s*(\d+)/.exec(line);
355
+ if (rm) { cur = { n: Number(rm[1]), blocks: [] }; rounds.push(cur); return; }
356
+ if (!cur) return;
357
+ var m;
358
+ if ((m = /^\[U\]\s?(.*)/.exec(line))) { cur.blocks.push({ k: "u", t: m[1] }); return; }
359
+ if ((m = /^\[A\]\s?(.*)/.exec(line))) { cur.blocks.push({ k: "a", t: m[1] }); return; }
360
+ // EXTREASON 外置推理简报:转录里的 `[R] …` 行自成一块(kind "r" = 推理),
361
+ // 不再被下面「未识别行按 AI 显示」的兜底吞成 AI 块(用户报:`[R]` 段还顶着 AI 图标)。
362
+ if ((m = /^\[R\]\s?(.*)/.exec(line))) { cur.blocks.push({ k: "r", t: m[1] }); return; }
363
+ if ((m = /^\[T\]\s?(.*)/.exec(line))) { cur.blocks.push({ k: "t", t: m[1] }); return; }
364
+ if (line.trim()) { cur.blocks.push({ k: "a", t: line.trim() }); }
365
+ });
366
+ return rounds;
367
+ }
368
+
369
+ class ErrorBoundary extends React.Component {
370
+ constructor(props) { super(props); this.state = { error: null }; }
371
+ static getDerivedStateFromError(error) { return { error: error }; }
372
+ render() {
373
+ if (this.state.error) {
374
+ return React.createElement("div", { style: { color: "#ff6b6b", padding: 12, fontFamily: "ui-monospace,SFMono-Regular,Consolas,monospace", fontSize: 12, whiteSpace: "pre-wrap" } },
375
+ "[\u6CE8\u5165\u9762\u677F\u6E32\u67D3\u9519\u8BEF] " + String((this.state.error && this.state.error.message) || this.state.error) + "\n" + String((this.state.error && this.state.error.stack) || ""));
376
+ }
377
+ return this.props.children;
378
+ }
379
+ }
380
+ // ===== 模块级共享 /status 采样器(会话页「压缩中」小标 + 「注入」面板共用;全局唯一 500ms 定时器)=====
381
+ // 为什么收口:两个消费者都要看同一份 /status(尤其 foldInflight)。各起一个 500ms setInterval =
382
+ // 同一 URL 双份请求 + 两处可能泄漏的定时器。这里只留一个采样面:
383
+ // key = sessionId("" = 全局)→ 每个 key 一份快照 + 一张订阅表;订阅数归零 → clearInterval。
384
+ // 生命周期:首个消费者挂载才启动;最后一个卸载即停,并丢掉该 key 的快照(不留"在飞"残留假象)。
385
+ // 失败:保留上一次快照,把错误写进快照 err(面板照旧 setErr;小标只读 data.foldInflight,静默)。
386
+ var CI_STATUS_MS = 500;
387
+ var ciStatus = (function () {
388
+ var subs = {}; // key -> [fn]
389
+ var snaps = {}; // key -> { data, err, at }
390
+ var timer = null;
391
+ function notify(key, snap) {
392
+ if (!subs[key]) return; // 已无订阅者:卸载后的迟到响应直接丢弃(不写状态、不回调)
393
+ snaps[key] = snap;
394
+ subs[key].slice().forEach(function (fn) { try { fn(snap); } catch (x) {} });
395
+ }
396
+ function read(key) {
397
+ var q = key ? "?sessionId=" + encodeURIComponent(key) : "";
398
+ return fetch("/api/context-inject/status" + q, { headers: { "Content-Type": "application/json" } })
399
+ .then(function (r) { return r.json(); })
400
+ .then(function (s) { notify(key, { data: s, err: null, at: Date.now() }); return s; })
401
+ .catch(function (x) { notify(key, { data: (snaps[key] && snaps[key].data) || null, err: x, at: Date.now() }); return null; });
402
+ }
403
+ function tick() { Object.keys(subs).forEach(read); }
404
+ function stopIfIdle() {
405
+ if (Object.keys(subs).length === 0 && timer !== null) { clearInterval(timer); timer = null; }
406
+ }
407
+ return {
408
+ snapshot: function (key) { return snaps[key] || null; },
409
+ subscribe: function (key, fn) {
410
+ if (!subs[key]) subs[key] = [];
411
+ subs[key].push(fn);
412
+ if (timer === null) timer = setInterval(tick, CI_STATUS_MS); // 首个消费者:启动采样
413
+ if (snaps[key]) fn(snaps[key]); // 命中缓存先回放(切 tab 回来不闪空白),下一拍再刷
414
+ else read(key); // 否则立即采一次(等价于旧实现的"挂载即 status()")
415
+ return function () {
416
+ var list = subs[key];
417
+ if (!list) return; // 幂等:重复调用/已停
418
+ var i = list.indexOf(fn);
419
+ if (i >= 0) list.splice(i, 1);
420
+ if (list.length !== 0) return;
421
+ delete subs[key];
422
+ delete snaps[key]; // 订阅数归零即丢快照
423
+ stopIfIdle();
424
+ };
425
+ },
426
+ refresh: function (key) { return read(key); },
427
+ };
428
+ })();
429
+ // 触发点中文标签(v1.10.37 起生产触发点只有 turn-end;旧记录里的 turn-stopping 仍照常显示)
430
+ function trigLabel(t) {
431
+ return t === "turn-end" ? "本轮结束触发"
432
+ : t === "turn-stopping" ? "本轮结束触发(旧触发点)"
433
+ : t === "pre-step" ? "下一轮开始前触发(旧触发点)"
434
+ : (t || "触发点未知");
435
+ }
436
+ function InjectView(props) {
437
+ var _d = useState(null), data = _d[0], setData = _d[1];
438
+ var _e = useState(null), err = _e[0], setErr = _e[1];
439
+ var _on = useState(false), on = _on[0], setOn = _on[1];
440
+ var _sel = useState({}), sel = _sel[0], setSel = _sel[1];
441
+ var _ws = useState({}), wss = _ws[0], setWss = _ws[1];
442
+ // v1.9:顶部「AI 输出压缩档」chips 已删(每会话在 composer 独立选档)→ cd 全局档状态移除
443
+ var _mdlErr = useState(null), mdlErr = _mdlErr[0], setMdlErr = _mdlErr[1];
444
+ // v1.8 压缩模型:目录(providers/models)+ 当前覆盖值
445
+ var _mdl = useState(null), mdl = _mdl[0], setMdl = _mdl[1];
446
+ var _selP = useState(""), selP = _selP[0], setSelP = _selP[1];
447
+ var _selM = useState(""), selM = _selM[0], setSelM = _selM[1];
448
+ var _picked = useState(false), picked = _picked[0], setPicked = _picked[1];
449
+ var _ow = useState({}), openWs = _ow[0], setOpenWs = _ow[1];
450
+ // LOGcompiler「会话记录」区分组展开态(独立于白名单 openWs,避免互相影响)
451
+ var _lgow = useState({}), lgOpenWs = _lgow[0], setLgOpenWs = _lgow[1];
452
+ function toggleLgGroup(w) { setLgOpenWs(function (prev) { var n = Object.assign({}, prev); if (n[w]) delete n[w]; else n[w] = true; return n; }); }
453
+ // 归档会话不进入白名单候选(与左侧栏一致)
454
+ var _all = useState(false), allRounds = _all[0], setAllRounds = _all[1];
455
+ var _ex = useState({}), expanded = _ex[0], setExpanded = _ex[1];
456
+ var _busy = useState(false), busy = _busy[0], setBusy = _busy[1];
457
+ // 右键菜单(会话 chip → 导出 LOG / 复制会话 ID)【取自 RCS-0.1.0 返回包】
458
+ // 菜单只在"有菜单"时挂全局监听:点空白/Escape/滚动都关掉(滚动用捕获阶段,容器内滚动也生效)。
459
+ var _ctx = useState(null), ctxMenu = _ctx[0], setCtxMenu = _ctx[1];
460
+ useEffect(function () {
461
+ if (!ctxMenu) return;
462
+ var close = function () { setCtxMenu(null); };
463
+ var onKey = function (ev) { if (ev && ev.key === "Escape") close(); };
464
+ document.addEventListener("click", close);
465
+ document.addEventListener("keydown", onKey);
466
+ window.addEventListener("scroll", close, true);
467
+ return function () {
468
+ document.removeEventListener("click", close);
469
+ document.removeEventListener("keydown", onKey);
470
+ window.removeEventListener("scroll", close, true);
471
+ };
472
+ }, [ctxMenu]);
473
+ var cur = (props && props.sessionId) || null;
474
+ // #7 maxtokens UI 输入:存"编辑中的值";null=未编辑,展示当前 control.condenseMaxTokens
475
+ var _mtInp = useState(""), mtInp = _mtInp[0], setMtInp = _mtInp[1];
476
+ var curMT = (data && data.control && typeof data.control.condenseMaxTokens === 'number') ? data.control.condenseMaxTokens : null; // null=跟随默认(700)
477
+ function saveMaxTokens(v) { // v: number|null
478
+ setBusy(true);
479
+ fetch("/api/context-inject/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ condenseMaxTokens: v }) })
480
+ .then(function (r) { return r.json(); }).then(function (resp) { if (!resp || resp.ok !== true) throw new Error(JSON.stringify(resp)); setMtInp(""); return status(); })
481
+ .catch(function (x) { setErr(String((x && x.message) || x)); }).finally(function () { setBusy(false); });
482
+ }
483
+ // 【取自 RCS-0.1.0 返回包】condenseMinBytes UI 输入:与 maxtokens 同盒同排;存"编辑中的值",空=清除(回内置 240)
484
+ var _mbInp = useState(""), mbInp = _mbInp[0], setMbInp = _mbInp[1];
485
+ var curMB = (data && data.control && typeof data.control.condenseMinBytes === 'number') ? data.control.condenseMinBytes : null; // null=跟随默认(240)
486
+ function saveMinBytes(v) { // v: number|null(null=清除该字段,回落内置 240)
487
+ setBusy(true);
488
+ fetch("/api/context-inject/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ condenseMinBytes: v }) })
489
+ .then(function (r) { return r.json(); }).then(function (resp) { if (!resp || resp.ok !== true) throw new Error(JSON.stringify(resp)); setMbInp(""); return status(); })
490
+ .catch(function (x) { setErr(String((x && x.message) || x)); }).finally(function () { setBusy(false); });
491
+ }
492
+ // #10 自定义压缩提示词:编辑中的 single/keys;null=用内置默认
493
+ var _ps = useState({ single: "", keys: "" }), prompts = _ps[0], setPrompts = _ps[1];
494
+ var _showD = useState(false), showDefPrompts = _showD[0], setShowDefPrompts = _showD[1]; // 是否展开查看默认提示词
495
+ var defaultPrompts = (data && data.runtime && data.runtime.defaultPrompts) || null; // 内置默认(single/keys)
496
+ var curPrompts = (data && data.control && data.control.prompts && typeof data.control.prompts === 'object') ? data.control.prompts : null; // null=内置默认
497
+ function savePrompts(patch) {
498
+ setBusy(true);
499
+ fetch("/api/context-inject/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ prompts: patch || null }) })
500
+ .then(function (r) { return r.json(); }).then(function (resp) { if (!resp || resp.ok !== true) throw new Error(JSON.stringify(resp)); setPrompts({ single: "", keys: "" }); return status(); })
501
+ .catch(function (x) { setErr(String((x && x.message) || x)); }).finally(function () { setBusy(false); });
502
+ }
503
+
504
+ // 一次采样落到本视图状态(原 status().then 的主体,逐字保留;现由共享采样器的订阅回调驱动)
505
+ function applyStatus(s) {
506
+ setData(s);
507
+ setOn(!!(s.control && s.control.enabled));
508
+ var m = {}, g = {};
509
+ (s.control && s.control.sessions || []).forEach(function (x) { m[x] = true; });
510
+ setSel(m);
511
+ (s.sessions || []).forEach(function (s2) { if (!g[s2.workspace]) g[s2.workspace] = []; g[s2.workspace].push(s2); });
512
+ Object.keys(g).forEach(function (w) { g[w].sort(function (a, b) { return (a.archived === b.archived ? 0 : a.archived ? 1 : -1) || a.id.localeCompare(b.id); }); });
513
+ setWss(g);
514
+ return s;
515
+ }
516
+ // 手动/写后刷新:交给共享采样器(同一 URL、同一 500ms 采样面)——本视图不再自建定时器;
517
+ // 失败同样经快照 err 回到下面的订阅回调(错误处理口径不变)。
518
+ function status() { return ciStatus.refresh(cur || ""); }
519
+ function save(patch) {
520
+ setBusy(true);
521
+ return fetch("/api/context-inject/config", {
522
+ method: "POST", headers: { "Content-Type": "application/json" },
523
+ body: JSON.stringify(patch || { enabled: on, sessions: Object.keys(sel).filter(function (k) { return sel[k]; }) }),
524
+ }).then(function (r) { return r.json(); }).then(function (resp) {
525
+ if (!resp || resp.ok !== true) throw new Error(JSON.stringify(resp));
526
+ return status();
527
+ }).catch(function (x) { setErr(String((x && x.message) || x)); }).finally(function () { setBusy(false); });
528
+ }
529
+ useEffect(function () {
530
+ loadModels(); // v1.8:压缩模型目录(一次性;轮询只刷 status)
531
+ // 轮询周期 2s → 0.5s:后台折叠常在 2s 内就落定,「在飞 → 已完成」这整段状态会被 2s 采样整段跳过,
532
+ // 用户看不到「上一轮输出压缩中…」。0.5s 下 2s 的折叠约能采到 4 次。
533
+ // v2.3:采样面收口到模块级 ciStatus(与会话页「压缩中」小标同一轮采样、同一 500ms 周期);
534
+ // 本视图只订阅快照,不再自建 setInterval ⇒ 两个消费者合计仍只有一份定时器,卸载由 refCount 停掉。
535
+ return ciStatus.subscribe(cur || "", function (snap) {
536
+ if (snap && snap.err) { setErr(String((snap.err && snap.err.message) || snap.err)); return; }
537
+ if (snap && snap.data) applyStatus(snap.data);
538
+ });
539
+ }, [cur]);
540
+ // v1.9:本会话档位经 /session 端点读写(与 composer 同一通道)——
541
+ // 勾选 = 启用无损折叠(写 sessionModes=off 并入白名单);取消 = none(移出白名单+清档)。
542
+ // 保证白名单与 sessionModes 永远一致:无"白名单内却无档"的悬空态(插件侧无档=不折叠)。
543
+ function setSessionMode(id, mode) {
544
+ if (!id) return Promise.resolve();
545
+ setBusy(true);
546
+ return fetch("/api/context-inject/session", {
547
+ method: "POST", headers: { "Content-Type": "application/json" },
548
+ body: JSON.stringify({ sessionId: id, mode: mode }),
549
+ }).then(function (r) { return r.json(); }).then(function (resp) {
550
+ if (!resp || resp.ok !== true) throw new Error(JSON.stringify(resp));
551
+ return status();
552
+ }).catch(function (x) { setErr(String((x && x.message) || x)); }).finally(function () { setBusy(false); });
553
+ }
554
+ function toggleSession(id) {
555
+ setSessionMode(id, sel[id] ? "none" : "off");
556
+ }
557
+ // v1.8/v1.9:压缩模型目录。不带 provider = 列 provider;带 provider = 列该 provider 的模型
558
+ // (listModels 可能是网络调用,故按需拉取,不在轮询里刷新)。错误上浮到面板(mdlErr)。
559
+ function loadModels(provider) {
560
+ // 模型列表走 POST(body 传 provider),避免依赖 query 解析;列 provider 用 GET
561
+ var req = provider
562
+ ? fetch("/api/context-inject/models", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ provider: provider }) })
563
+ : fetch("/api/context-inject/models", { headers: { "Content-Type": "application/json" } });
564
+ return req
565
+ .then(function (r) { return r.json(); })
566
+ .then(function (j) {
567
+ if (!j) return;
568
+ if (j.ok === false) { setMdlErr((j && j.error) || "模型目录请求失败"); return; }
569
+ setMdlErr(null);
570
+ if (provider) setMdl(function (prev) { return Object.assign({}, prev, { models: j.models || [] }); });
571
+ else {
572
+ setMdl(j);
573
+ // 仅当用户尚未手动选择时,才用 current 初始化;否则初始请求晚到会清掉用户的选择(竞态)
574
+ if (!picked && j.current && j.current.provider) { setSelP(j.current.provider); setSelM(j.current.model || ""); loadModels(j.current.provider); }
575
+ else if (!picked) { setSelP(""); setSelM(""); }
576
+ }
577
+ })
578
+ .catch(function (x) { setMdlErr("模型目录不可用:" + String((x && x.message) || x)); });
579
+ }
580
+ // v1.9:select 收口 —— provider 首项「跟随会话模型」= 清除覆盖(provider/model 一并传 null);
581
+ // 选 provider 即拉该 provider 模型列表;选 model 即保存成对覆盖(半配置由 host 忽略)。
582
+ function onProviderChange(ev) {
583
+ var p = ev && ev.target ? ev.target.value : "";
584
+ setPicked(true);
585
+ if (!p) {
586
+ setSelP(""); setSelM("");
587
+ setMdl(function (m) { return m ? Object.assign({}, m, { models: [] }) : m; });
588
+ save({ condenseProvider: null, condenseModel: null });
589
+ return;
590
+ }
591
+ setSelP(p); setSelM("");
592
+ loadModels(p);
593
+ }
594
+ function onModelChange(ev) {
595
+ var m = ev && ev.target ? ev.target.value : "";
596
+ setSelM(m);
597
+ if (m) save({ condenseProvider: selP, condenseModel: m });
598
+ }
599
+ function toggleGroup(w) {
600
+ setOpenWs(function (prev) { var n = Object.assign({}, prev); if (n[w]) delete n[w]; else n[w] = true; return n; });
601
+ }
602
+ function toggleExpand(id) {
603
+ var n = Object.assign({}, expanded);
604
+ if (n[id]) delete n[id]; else n[id] = true;
605
+ setExpanded(n);
606
+ }
607
+ var lf = (data && data.lastFold) || null;
608
+ // 面板长期显示"上次折叠":不给时间与版本,一条一天前的失败会被读成正在持续报错
609
+ // (真机 11-02:260e1607 的记录停在 11-01 22:43 / v1.10.58,该会话此后再没折叠过)。
610
+ var lfWhen = '';
611
+ var lfVer = '';
612
+ var lfStale = false;
613
+ if (lf && lf.t) {
614
+ var _d = new Date(lf.t), _p = function (n) { return (n < 10 ? '0' : '') + n; };
615
+ lfWhen = (_d.getMonth() + 1) + '-' + _p(_d.getDate()) + ' ' + _p(_d.getHours()) + ':' + _p(_d.getMinutes());
616
+ lfStale = (Date.now() - lf.t) > 6 * 3600 * 1000;
617
+ var _m = lf.plugin ? String(lf.plugin).match(/^v[0-9.]+/) : null;
618
+ lfVer = _m ? _m[0] : '';
619
+ }
620
+ var lfLabel = '上次折叠' + (lfWhen ? '(' + lfWhen + (lfVer ? ' · ' + lfVer : '') + ')' : '');
621
+ // 【在飞折叠】「上一轮输出压缩中…」:数据来自 /status.foldInflight(host 读插件写的在飞标记)。
622
+ // 为什么需要:turn-end 触发的是**后台**折叠(只排队、不阻塞本轮),浓缩模型要跑几秒~十几秒;
623
+ // 这段时间 lastFold 仍是上一轮的结果,面板看起来"什么都没发生"(用户实测困惑点)。
624
+ // 秒数不需要额外定时器:本视图每 0.5s 轮询 /status,重渲染即自动走秒。
625
+ var fin = (data && data.foldInflight) || null;
626
+ var finOn = !!fin;
627
+ // 「已完成」小标签:只在"在飞 → 消失"那一次转换后短暂出现,靠 0.5s 轮询重渲染自然消退(不起定时器)。
628
+ // 保留期 30s(原 8s):落定本身很容易一闪而过,8s 常在被看到之前就消失;30s 保证用户至少看见一次。
629
+ var FIN_DONE_MS = 30000;
630
+ var _finDone = useState(0), finDoneAt = _finDone[0], setFinDoneAt = _finDone[1];
631
+ var prevFinOn = useRef(false);
632
+ useEffect(function () {
633
+ if (prevFinOn.current && !finOn) setFinDoneAt(Date.now()); // 落定(成功/失败都算)
634
+ prevFinOn.current = finOn;
635
+ }, [finOn]);
636
+ // 触发点中文标签已提到模块级(trigLabel):会话页「压缩中」小标与本面板共用同一份口径
637
+ function inflightLine(f) {
638
+ var t0 = Number(f && f.startedAt);
639
+ var sec = Number.isFinite(t0) ? Math.max(0, Math.round((Date.now() - t0) / 1000)) : null;
640
+ return e("div", { className: "ci-sech ci-fin", style: { margin: "0 0 6px" }, title: "后台折叠进行中:本轮输出结束后已排队,浓缩模型返回即落 last-fold(下一轮请求前会 join)" },
641
+ e("span", { className: "ci-pulse" }),
642
+ "上一轮输出压缩中…",
643
+ e("span", { className: "ci-sub" }, trigLabel(f && f.trigger) + (sec != null ? " · 已 " + sec + "s" : "")));
644
+ }
645
+ var rounds = parseTranscript(lf && (lf.full || lf.preview || ""));
646
+ var shown = allRounds ? rounds : rounds.slice(-3);
647
+ var lfOn = !!(data && data.control && data.control.enabled);
648
+ var wsNames = Object.keys(wss).sort();
649
+
650
+ function chip(s, curChip) {
651
+ var ttl = s.title || "";
652
+ var label = ttl || shortId(s.id);
653
+ // 右键出菜单(导出 LOG / 复制会话 ID);stopPropagation 防止顺带触发 chip 的勾选
654
+ // 【取自 RCS-0.1.0 返回包】
655
+ return e("span", { key: s.id, className: "ci-chip" + (sel[s.id] ? " ci-chipon" : ""), role: "checkbox", "aria-checked": !!sel[s.id], tabIndex: 0, onClick: function () { toggleSession(s.id); }, onKeyDown: function (ev) { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); toggleSession(s.id); } }, title: s.id + (s.workspace ? " @ " + s.workspace : "") + "(右键可导出 LOG)",
656
+ onContextMenu: function (ev) {
657
+ ev.preventDefault(); ev.stopPropagation();
658
+ setCtxMenu({ x: ev.clientX, y: ev.clientY, sid: s.id, title: ttl || shortId(s.id), hasLog: lgHasLog(s.id) });
659
+ } },
660
+ (sel[s.id] ? "✓ " : "") + label);
661
+ }
662
+ function msgBlock(b, i, roundId) {
663
+ var id = roundId + ":" + i;
664
+ var open = !!expanded[id];
665
+ if (b.k === "t") {
666
+ // 【修复·本地缺陷#2】【取自 RCS-0.1.0 返回包】状态判定改为"整行找状态 token":
667
+ // 旧实现 /OK$/ 只认行尾 OK → 结构化 [T](…|read|文件|OK|P1…,status 不在行尾)恒被误判为 err。
668
+ // 现按 | 切分取首个状态 token;兼容 OK|FAIL|ERR|PARTIAL|CANCEL(仅 OK 绿,其余红)。
669
+ var st = String(b.t).split("|").map(function (s) { return s.trim(); })
670
+ .filter(function (s) { return /^(OK|FAIL|ERR|PARTIAL|CANCEL)$/.test(s); })[0] || "";
671
+ var ok = st === "OK";
672
+ // 超长 [T] 行同样走点击折叠(与消息块同规则),避免长工具结果把面板撑爆。
673
+ return e("div", { key: id, className: "ci-t" + (ok ? " ci-t-ok" : " ci-t-err") },
674
+ e("div", { className: "ci-text" + (open ? "" : " ci-clamp"), role: "button", tabIndex: 0, "aria-expanded": !!open, onClick: function () { toggleExpand(id); }, onKeyDown: function (ev) { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); toggleExpand(id); } }, title: open ? "收起" : "展开" },
675
+ "▸ " + b.t));
676
+ }
677
+ // 角色标记(等宽、同尺寸,时间线不因新增「推理」行而重排):
678
+ // u=你 / r=推理(EXTREASON 外置推理简报)/ 其余=AI。图标放 role 左边与 AI 行同槽位。
679
+ var isU = b.k === "u", isR = b.k === "r";
680
+ var ico = isR ? "\u25C8 " : "";
681
+ var role = isU ? "你" : (isR ? "推理" : "AI");
682
+ return e("div", { key: id, className: "ci-msg " + (isU ? "ci-u" : isR ? "ci-r" : "ci-a") },
683
+ e("div", { className: "ci-text" + (open ? "" : " ci-clamp"), role: "button", tabIndex: 0, "aria-expanded": !!open, onClick: function () { toggleExpand(id); }, onKeyDown: function (ev) { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); toggleExpand(id); } }, title: open ? "收起" : "展开" },
684
+ e("span", { className: "ci-role" }, ico + role), b.t));
685
+ }
686
+ function timeline(rounds2) {
687
+ if (!rounds2.length) return e("div", { className: "ci-sub" }, "尚无折叠记录(对话跨轮后自动生成)");
688
+ return rounds2.map(function (r) {
689
+ return e("div", { key: r.n, className: "ci-rnd" },
690
+ e("div", { className: "ci-rndtag" }, "ROUND " + r.n),
691
+ e("div", { className: "ci-flow" }, r.blocks.map(function (b, i) { return msgBlock(b, i, r.n); })));
692
+ });
693
+ }
694
+ // ===== LOGcompiler「日志编译」区(并入本面板;数据来自 /status.logcompiler,写入 /api/logcompiler/config)=====
695
+ var _lgDir = useState(""), lgDir = _lgDir[0], setLgDir = _lgDir[1];
696
+ var _lgB = useState(false), lgBusy = _lgB[0], setLgBusy = _lgB[1];
697
+ useEffect(function () {
698
+ var v = (data && data.logcompiler && data.logcompiler.control && data.logcompiler.control.logsDir) || "";
699
+ setLgDir(v);
700
+ }, [data && data.logcompiler && data.logcompiler.control && data.logcompiler.control.logsDir]);
701
+ var lg = (data && data.logcompiler) || null;
702
+ // ===== 本会话压缩档(v1.11:从 composer dock 移入注入面板「当前会话」卡片)=====
703
+ var MODE_OPTS = [["none", "关"], ["off", "工具折叠"], ["single", "单压缩"], ["double", "双压缩"]];
704
+ function curOwn(id) {
705
+ var sm = data && data.control && data.control.sessionModes;
706
+ var n = String(id || "").replace(/^session-/i, "");
707
+ if (sm && typeof sm === "object") { for (var k in sm) if (String(k).replace(/^session-/i, "") === n) return sm[k]; }
708
+ return null;
709
+ }
710
+ function pickMode(id, mode) {
711
+ if (!id) return;
712
+ setBusy(true);
713
+ fetch("/api/context-inject/session", {
714
+ method: "POST", headers: { "Content-Type": "application/json" },
715
+ body: JSON.stringify({ sessionId: id, mode: mode }),
716
+ }).then(function (r) { return r.json(); }).then(function (resp) {
717
+ if (!resp || resp.ok !== true) throw new Error(JSON.stringify(resp));
718
+ return status();
719
+ }).catch(function (x) { setErr(String((x && x.message) || x)); }).finally(function () { setBusy(false); });
720
+ }
721
+ var lgCtrl = (lg && lg.control) || null;
722
+ var lgEnabled = !lgCtrl || lgCtrl.enabled !== false;
723
+ var lgSm = (lgCtrl && lgCtrl.sessions) || null;
724
+ function lgRec(sid) {
725
+ if (!lgEnabled) return false;
726
+ if (!lgSm) return true;
727
+ var n = String(sid || "").replace(/^session-/i, "");
728
+ for (var k in lgSm) if (String(k).replace(/^session-/i, "") === n) return lgSm[k] !== false;
729
+ return true;
730
+ }
731
+ function lgPost(patch) {
732
+ setLgBusy(true);
733
+ return fetch("/api/logcompiler/config", {
734
+ method: "POST", headers: { "Content-Type": "application/json" },
735
+ body: JSON.stringify(patch || {}),
736
+ }).then(function (r) { return r.json(); }).then(function (resp) {
737
+ if (!resp || resp.ok !== true) throw new Error(JSON.stringify(resp));
738
+ return status();
739
+ }).catch(function (x) { setErr(String((x && x.message) || x)); }).finally(function () { setLgBusy(false); });
740
+ }
741
+ // —— 会话 → 转录文件(右键菜单 /「导出 LOG」按钮共用;文件名规则与插件 normKey、host lgKey 一致)——
742
+ // 【取自 RCS-0.1.0 返回包】
743
+ function lgKeyOf(s) { return String(s || "").replace(/^session-?/i, "").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80); }
744
+ // 是否有转录文件(决定菜单项/按钮是否置灰)
745
+ // 单文件模式(LOGCOMPILER_OUT):全部轮次写进同一个文件、不按 sid 命名 ⇒ host 快照自
746
+ // 2026-09-10 起透出 `single`/`outFile`(见 index.mjs logcompilerSnapshot),据此直接放行,
747
+ // 避免"所有会话的导出按钮恒灰、功能不可达"。
748
+ // 其余情况按归一 sid 匹配 files[].sid(host lgKey 与插件 normKey 同规则)。
749
+ function lgHasLog(id) {
750
+ if (lg && lg.single === true) return true;
751
+ var k = lgKeyOf(id), fs2 = (lg && lg.files) || [];
752
+ for (var i = 0; i < fs2.length; i++) if (fs2[i].sid === k) return true;
753
+ return false;
754
+ }
755
+ // 下载:走临时 <a href="/api/logcompiler/export?sid=…" download>(同源 GET,浏览器直接落盘)
756
+ function lgExport(id) {
757
+ try {
758
+ var a = document.createElement("a");
759
+ a.href = "/api/logcompiler/export?sid=" + encodeURIComponent(id);
760
+ a.download = "";
761
+ document.body.appendChild(a);
762
+ a.click();
763
+ if (a.parentNode) a.parentNode.removeChild(a);
764
+ } catch (x) { setErr(String((x && x.message) || x)); }
765
+ }
766
+ function lgCopy(t) {
767
+ try {
768
+ if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(t); return; }
769
+ } catch (x) { /* 剪贴板不可用时忽略 */ }
770
+ }
771
+ function logSec() {
772
+ var files = (lg && lg.files) || [];
773
+ var dirIn = { fontFamily: "inherit", fontSize: 11, lineHeight: "18px", color: "inherit", background: "rgba(127,127,150,.06)", border: "1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.35))", borderRadius: 7, padding: "2px 6px", minWidth: 110, flex: "1 1 120px", maxWidth: 180 };
774
+ return e("div", { className: "ci-sec" },
775
+ e("div", { className: "ci-sech" }, "日志编译 · LOGcompiler",
776
+ e("span", { className: "ci-badge " + ((lg && lg.mounted) ? "ci-on" : "ci-off"), style: { marginLeft: 6 }, title: (lg && lg.mounted) ? "插件已装载" : "插件未装载(仅配置保存)" })),
777
+ (!(lg && lg.mounted) ? e("div", { className: "ci-sub", style: { color: "rgba(255,170,90,.9)" } }, "LOGcompiler 插件未装载:设置先保存,装载后生效") : null),
778
+ e("div", { className: "ci-toggle ci-lg" + (lgEnabled ? " ci-lgon" : ""), style: { marginTop: 6 }, onClick: function () { lgPost({ enabled: !lgEnabled }); } },
779
+ e("span", { className: "ci-switch" }), e("span", { style: { fontSize: 12 } }, lgEnabled ? "记录中" : "已暂停")),
780
+ e("div", { className: "ci-sub", style: { margin: "8px 0 4px" } }, "输出目录:" + ((lg && lg.logsDir) || "(内部默认 <state>/transcripts)")),
781
+ e("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" } },
782
+ e("input", { style: dirIn, placeholder: "留空=内部默认", value: lgDir, onChange: function (ev) { setLgDir((ev && ev.target && ev.target.value) || ""); } }),
783
+ e("button", { className: "ci-btn", disabled: lgBusy, onClick: function () { lgPost({ logsDir: lgDir.trim() || null }); } }, "保存目录"),
784
+ e("button", { className: "ci-btn", disabled: lgBusy, onClick: function () { setLgDir(""); lgPost({ logsDir: null }); } }, "恢复默认")),
785
+ e("div", { className: "ci-grp", style: { marginTop: 6 } },
786
+ e("div", { className: "ci-grphd", style: { cursor: "default" } },
787
+ e("span", { style: { flex: 1 } }, "会话记录 · 按工作区"),
788
+ e("span", { className: "ci-sub" }, lgEnabled ? "默认全开" : "已停")),
789
+ wsNames.length ? wsNames.map(function (w) {
790
+ var list = (wss[w] || []).filter(function (s) { return !s.archived; });
791
+ if (!list.length) return null;
792
+ var open = !!lgOpenWs[w] || (wsNames.length === 1); // 单工作区默认展开
793
+ var recN = 0; list.forEach(function (s) { if (lgRec(s.id)) recN++; });
794
+ return e("div", { key: w, className: "ci-grp" + (open ? " ci-open" : "") },
795
+ e("div", { className: "ci-grphd", role: "button", tabIndex: 0, "aria-expanded": !!open, onClick: function () { toggleLgGroup(w); }, onKeyDown: function (ev) { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); toggleLgGroup(w); } } },
796
+ e("span", { className: "ci-caret" }, "▶"),
797
+ e("span", { style: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, w || "(未分组)"),
798
+ e("span", { className: "ci-sub" }, recN + "/" + list.length)),
799
+ (open ? e("div", { className: "ci-chips" }, list.map(function (s) {
800
+ var rec = lgRec(s.id);
801
+ return e("span", { key: s.id, className: "ci-chip" + (rec ? " ci-chipon" : ""), role: "checkbox", "aria-checked": !!rec, tabIndex: 0, title: s.id + (s.workspace ? " @ " + s.workspace : "") + (rec ? " · 记录中" : " · 已关") + "(右键可导出 LOG)", onClick: function () { lgPost({ sessionId: s.id, on: !rec }); }, onKeyDown: function (ev) { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); lgPost({ sessionId: s.id, on: !rec }); } },
802
+ onContextMenu: function (ev) {
803
+ ev.preventDefault(); ev.stopPropagation();
804
+ setCtxMenu({ x: ev.clientX, y: ev.clientY, sid: s.id, title: s.title || shortId(s.id), hasLog: lgHasLog(s.id) });
805
+ } },
806
+ (rec ? "● " : "○ ") + (s.title || shortId(s.id)));
807
+ })) : null));
808
+ }) : e("div", { className: "ci-sub" }, "暂无会话")),
809
+ e("div", { className: "ci-sub", style: { marginTop: 6 } }, files.length ? "已生成 " + files.length + " 份转录文件" : "(尚无转录文件)"),
810
+ // 右键菜单:会话 chip 上右键弹出(fixed 定位;贴边时向内收,视口外不溢出)
811
+ // 【取自 RCS-0.1.0 返回包】菜单自身 stopPropagation,点它不会顺带关掉/触发底下的 chip。
812
+ ctxMenu ? e("div", {
813
+ className: "ci-ctx",
814
+ style: {
815
+ left: Math.max(6, Math.min(ctxMenu.x, (window.innerWidth || 1200) - 200)),
816
+ top: Math.max(6, Math.min(ctxMenu.y, (window.innerHeight || 800) - 110)),
817
+ },
818
+ onClick: function (ev) { ev.stopPropagation(); },
819
+ onContextMenu: function (ev) { ev.preventDefault(); ev.stopPropagation(); },
820
+ },
821
+ e("div", { className: "ci-ctx-hd", title: ctxMenu.sid }, ctxMenu.title),
822
+ e("button", { disabled: !ctxMenu.hasLog, title: ctxMenu.hasLog ? "" : "该会话还没有转录文件", onClick: function () { lgExport(ctxMenu.sid); setCtxMenu(null); } }, "导出 LOG"),
823
+ (!ctxMenu.hasLog ? e("div", { className: "ci-ctx-hd", style: { borderBottom: 0, marginBottom: 0, paddingTop: 0 } }, "该会话还没有转录文件") : null),
824
+ e("button", { onClick: function () { lgCopy(ctxMenu.sid); setCtxMenu(null); } }, "复制会话 ID")) : null);
825
+ }
826
+
827
+ // A档实验开关:工具结果结构化折叠([T]+[V],toolfold;默认 OFF)
828
+ var tfOn = !!(data && data.control && data.control.toolfoldStructured === true);
829
+ function setToolfold(on) {
830
+ fetch("/api/context-inject/config", {
831
+ method: "POST", headers: { "Content-Type": "application/json" },
832
+ body: JSON.stringify({ toolfoldStructured: !!on }),
833
+ }).then(function (r) { return r.json(); }).then(function (resp) {
834
+ if (!resp || resp.ok !== true) throw new Error(JSON.stringify(resp));
835
+ return status();
836
+ }).catch(function (x) { setErr(String((x && x.message) || x)); });
837
+ }
838
+
839
+ return e(ErrorBoundary, null,
840
+ e("div", { className: "ci-root" },
841
+ e("div", { className: "ci-hd " + (lfOn ? "ci-on" : ""), style: { marginBottom: 8 } },
842
+ e("div", { className: "ci-badge " + (lfOn ? "ci-on" : "ci-off") }),
843
+ e("div", { style: { flex: 1, minWidth: 0 } },
844
+ e("div", { className: "ci-title" }, "CONTEXTinjector"),
845
+ e("div", { className: "ci-sub" }, lfOn ? "注入中 · 白名单会话在跨轮后折叠为转录" : "默认关闭 · 勾选会话后才生效")),
846
+ e("div", { className: "ci-toggle", onClick: function () { save({ enabled: !on, sessions: Object.keys(sel).filter(function (k) { return sel[k]; }) }); } },
847
+ e("span", { className: "ci-switch" }), e("span", { style: { fontSize: 12 } }, on ? "开" : "关"))),
848
+ (err ? e("div", { className: "ci-err", role: "alert", "aria-live": "assertive" }, "⚠ " + err) : null),
849
+ e("div", { className: "ci-cols" },
850
+ e("div", { className: "ci-main" },
851
+ e("div", { className: "ci-sec" },
852
+ e("div", { className: "ci-sech" }, "\u538B\u7F29\u5668\u6A21\u578B"),
853
+ e("div", { className: "ci-sub", style: { marginBottom: 6 } }, "single/double \u6863\u538B\u7F29 AI \u6B63\u6587\u65F6\u7528\u7684\u6A21\u578B\uFF1B\u9ED8\u8BA4\u8DDF\u968F\u4F1A\u8BDD\u5F53\u524D\u6A21\u578B\uFF08\u65E0\u9700\u914D\u7F6E\uFF09"),
854
+ (mdlErr ? e("div", { className: "ci-err" }, "\u26A0 " + mdlErr) : null),
855
+ (mdl && mdl.llmAvailable === false ? e("div", { className: "ci-err" }, "DSH \u672A\u63D0\u4F9B llm \u670D\u52A1\uFF1Asingle/double \u6863\u5C06\u81EA\u52A8\u56DE\u9000\u65E0\u635F") : null),
856
+ (lf && lf.condenseErr && !(lf.condenseErrResolved === true || (lf.condenseErrResolved === undefined && lf.aFolded > 0 && !((lf.fallbackReasons && lf.fallbackReasons.llmErr) || 0)))
857
+ ? e("div", { className: "ci-err" }, lfLabel + "压缩调用错误:" + (lf.condenseErrKind ? "[" + lf.condenseErrKind + "] " : "") + lf.condenseErr + (lfStale ? " · 此后未再折叠,以上为历史记录" : ""))
858
+ : null),
859
+ e("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" } },
860
+ e("select", { className: "ci-sel", value: selP, onChange: onProviderChange, title: "\u538B\u7F29\u5668\u7528 provider\uFF1B\u300C\u8DDF\u968F\u4F1A\u8BDD\u6A21\u578B\u300D = \u7528\u8BE5\u4F1A\u8BDD\u5F53\u524D\u6A21\u578B\uFF08\u6E05\u9664\u8986\u76D6\uFF09" },
861
+ e("option", { value: "" }, "\u8DDF\u968F\u4F1A\u8BDD\u6A21\u578B"),
862
+ ((mdl && mdl.providers) || []).map(function (p) { return e("option", { key: p.id, value: p.id }, (p.name || p.id)); })),
863
+ e("select", { className: "ci-sel", value: selM, disabled: !selP, onChange: onModelChange, title: "\u9009\u5B9A\u5373\u4FDD\u5B58\u4E3A\u8BE5\u4F1A\u8BDD\u538B\u7F29\u5668\u6A21\u578B\uFF08\u4E0E provider \u6210\u5BF9\uFF09" },
864
+ e("option", { value: "" }, selP ? "\u9009\u62E9\u6A21\u578B\u2026" : "\u2014"),
865
+ ((mdl && mdl.models) || []).map(function (m) { return e("option", { key: m.id, value: m.id }, (m.name || m.id)); }))),
866
+ e("div", { className: "ci-sub", style: { marginTop: 6 } },
867
+ (lf && lf.condenseRoute
868
+ ? (lfLabel + " \u5B9E\u9645\u4F7F\u7528\uFF1A" + lf.condenseRoute.provider + " / " + lf.condenseRoute.model + " (" + lf.condenseRoute.via + ")" + (lf.aCount && lf.aFolded < lf.aCount ? " · 本折 " + lf.aFolded + "/" + lf.aCount + " 段成文,其余保留原文" : "") + (lf.condenseErr ? " · 调用曾出错,已恢复" : "") + (selP && selM && (selP !== lf.condenseRoute.provider || selM !== lf.condenseRoute.model) ? "\u3000\u00B7\u3000\u5F53\u524D\u8BBE\u7F6E\uFF1A" + selP + " / " + selM + "\uFF08\u4E0B\u4E00\u6B21\u6298\u53E0\u751F\u6548\uFF09" : ""))
869
+ : "\u5F53\u524D\uFF1A" + ((mdl && mdl.current && mdl.current.provider) ? mdl.current.provider + " / " + mdl.current.model : "\u8DDF\u968F\u4F1A\u8BDD\u6A21\u578B")))),
870
+ e("div", { className: "ci-sec" },
871
+ e("div", { className: "ci-sech" }, "\u538B\u7F29\u53C2\u6570\uFF08maxtokens / \u6700\u5C0F\u957F\u5EA6\uFF09"),
872
+ e("div", { className: "ci-sub", style: { marginBottom: 6 } }, "single/double \u6863\u6BCF\u6B21\u538B\u7F29\u7684 token \u4E0A\u9650\uFF1B\u7559\u7A7A = \u5185\u7F6E\u9ED8\u8BA4(700)\uFF0C\u8DDF\u968F\u6A21\u578B\u7A97\u53E3\u5F85\u63A5"),
873
+ e("div", { style: { display: "flex", gap: 6, alignItems: "center", flexWrap: "wrap" } },
874
+ e("input", { className: "ci-sel", style: { width: 120 }, type: "number", min: 64, step: 64, placeholder: curMT != null ? String(curMT) : "\u9ED8\u8BA4700", value: mtInp, onChange: function (ev) { setMtInp((ev && ev.target && ev.target.value) || ""); } }),
875
+ e("button", { className: "ci-btn", disabled: busy, onClick: function () { var v = Number(mtInp); saveMaxTokens(Number.isFinite(v) && v >= 64 ? Math.floor(v) : null); } }, "\u4FDD\u5B58"),
876
+ e("button", { className: "ci-btn", disabled: busy, onClick: function () { setMtInp(""); saveMaxTokens(null); } }, "\u6062\u590D\u9ED8\u8BA4"),
877
+ e("span", { className: "ci-sub" }, curMT != null ? "\u5F53\u524D " + curMT : "\u9ED8\u8BA4 700"),
878
+ e("span", { className: "ci-sub", style: { marginLeft: 10 } }, "\u538B\u7F29\u6700\u5C0F\u957F\u5EA6"),
879
+ e("input", { className: "ci-sel", style: { width: 110 }, type: "number", min: 0, step: 1, placeholder: curMB != null ? String(curMB) : "\u9ED8\u8BA4240", title: "0 = \u4E0D\u8BBE\u95E8\u69DB\uFF08\u6BCF\u6761 [A] \u90FD\u538B\u7F29\uFF0C\u538B\u7F29\u8C03\u7528\u6B21\u6570\u660E\u663E\u589E\u52A0\uFF09", value: mbInp, onChange: function (ev) { setMbInp((ev && ev.target && ev.target.value) || ""); } }),
880
+ e("button", { className: "ci-btn", disabled: busy, onClick: function () { var t = String(mbInp).trim(); var v = Number(t); saveMinBytes(t !== "" && Number.isFinite(v) && v >= 0 ? Math.floor(v) : null); } }, "\u4FDD\u5B58"),
881
+ e("button", { className: "ci-btn", disabled: busy, onClick: function () { setMbInp(""); saveMinBytes(null); } }, "\u6062\u590D\u9ED8\u8BA4"),
882
+ e("span", { className: "ci-sub" }, curMB != null ? "\u5F53\u524D " + curMB : "\u9ED8\u8BA4 240")),
883
+ e("div", { className: "ci-sub", style: { marginTop: 6 } }, "\u538B\u7F29\u6700\u5C0F\u957F\u5EA6\uFF1A[A] \u6B63\u6587\u77ED\u4E8E\u6B64\u957F\u5EA6\u4E0D\u538B\u7F29\uFF1B0 = \u4E0D\u8BBE\u95E8\u69DB\uFF08\u6BCF\u6761 [A] \u90FD\u538B\u7F29\uFF0C\u538B\u7F29\u8C03\u7528\u6B21\u6570\u660E\u663E\u589E\u52A0\uFF09\uFF1B\u7559\u7A7A/\u6E05\u9664 = \u5185\u7F6E\u9ED8\u8BA4 240")),
884
+ e("div", { className: "ci-sec" },
885
+ e("div", { className: "ci-sech" }, "\u538B\u7F29\u63D0\u793A\u8BCD"),
886
+ e("div", { className: "ci-sub", style: { marginBottom: 6 } }, "single\uff1a\u5355\u6B21\u538B\u7F29\u6A21\u5F0F\uff1bkeys\uff1a\u989D\u5916\u8FDB\u884C\u5173\u952E\u503C\u63D0\u53D6\u6A21\u5F0F\uff1b\u7559\u7A7A = \u5185\u7F6E\u9ED8\u8BA4"),
887
+ e("div", { className: "ci-sub", style: { margin: "6px 0 2px" } }, "single \u63D0\u793A\u8BCD\uff08\u5355\u6B21\u538B\u7F29\uff09\uff1A"),
888
+ e("textarea", { rows: 3, style: { width: "100%", boxSizing: "border-box", fontFamily: "inherit", fontSize: 11, color: "inherit", background: "rgba(127,127,150,.06)", border: "1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.3))", borderRadius: 7, padding: "4px 6px" }, placeholder: curPrompts && curPrompts.single ? "\u5DF2\u81EA\u5B9A\u4E49" : "\u5185\u7F6E\u9ED8\u8BA4\uFF08\u7A7A=\u6E05\u9664\u56DE\u5F52\u9ED8\u8BA4\uFF09", value: prompts.single, onChange: function (ev) { setPrompts({ single: (ev && ev.target && ev.target.value) || "", keys: prompts.keys }); } }),
889
+ e("div", { className: "ci-sub", style: { margin: "6px 0 2px" } }, "keys \u63D0\u793A\u8BCD\uff08\u5173\u952E\u503C\u63D0\u53D6\uff09\uff1A"),
890
+ e("textarea", { rows: 3, style: { width: "100%", boxSizing: "border-box", fontFamily: "inherit", fontSize: 11, color: "inherit", background: "rgba(127,127,150,.06)", border: "1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.3))", borderRadius: 7, padding: "4px 6px" }, placeholder: curPrompts && curPrompts.keys ? "\u5DF2\u81EA\u5B9A\u4E49" : "\u5185\u7F6E\u9ED8\u8BA4\uFF08\u7A7A=\u6E05\u9664\u56DE\u5F52\u9ED8\u8BA4\uFF09", value: prompts.keys, onChange: function (ev) { setPrompts({ single: prompts.single, keys: (ev && ev.target && ev.target.value) || "" }); } }),
891
+ e("div", { style: { display: "flex", gap: 6, marginTop: 6 } },
892
+ e("button", { className: "ci-btn", disabled: busy, onClick: function () { savePrompts({ single: prompts.single.trim() || null, keys: prompts.keys.trim() || null }); } }, "\u5E94\u7528"),
893
+ e("button", { className: "ci-btn", disabled: busy, onClick: function () { setPrompts({ single: "", keys: "" }); savePrompts(null); } }, "\u56DE\u5F52\u9ED8\u8BA4"))),
894
+ e("div", { style: { marginTop: 8 } },
895
+ e("button", { className: "ci-btn", onClick: function () { setShowDefPrompts(!showDefPrompts); } }, showDefPrompts ? "\u6536\u8D77\u9ED8\u8BA4\u63D0\u793A\u8BCD" : "\u67E5\u770B\u9ED8\u8BA4\u63D0\u793A\u8BCD")),
896
+ (showDefPrompts ? [
897
+ e("div", { key: "ds", className: "ci-sub", style: { margin: "6px 0 2px" } }, "\u9ED8\u8BA4 single\uFF1A"),
898
+ e("div", { key: "dsv", style: { maxHeight: 90, overflowY: "auto", whiteSpace: "pre-wrap", wordBreak: "break-word", fontFamily: "inherit", fontSize: 11, color: "var(--dsw-alias-label-secondary,rgba(180,180,195,.8))", background: "rgba(127,127,150,.05)", border: "1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.2))", borderRadius: 7, padding: "4px 6px" } }, (defaultPrompts && defaultPrompts.single) || "(\u65E0)"),
899
+ e("div", { key: "dk", className: "ci-sub", style: { margin: "6px 0 2px" } }, "\u9ED8\u8BA4 keys\uFF1A"),
900
+ e("div", { key: "dkv", style: { maxHeight: 90, overflowY: "auto", whiteSpace: "pre-wrap", wordBreak: "break-word", fontFamily: "inherit", fontSize: 11, color: "var(--dsw-alias-label-secondary,rgba(180,180,195,.8))", background: "rgba(127,127,150,.05)", border: "1px solid var(--dsw-alias-border-l2,rgba(127,127,150,.2))", borderRadius: 7, padding: "4px 6px" } }, (defaultPrompts && defaultPrompts.keys) || "(\u65E0)")
901
+ ] : null),
902
+ e("div", { className: "ci-sec" },
903
+ e("div", { className: "ci-sech" }, "\u538B\u7F29\u5668IO\u6BD4"),
904
+ (data && data.ivr && data.ivr.rounds && data.ivr.rounds.length ? [
905
+ e("div", { key: "iv", className: "ci-metrics" },
906
+ e("span", null, "\u7D2F\u8BA1\u8F93\u5165 ", e("b", null, (data.ivr.cumIn ?? 0) + "B")),
907
+ e("span", null, "\u7D2F\u8BA1\u8F93\u51FA ", e("b", null, (data.ivr.cumOut ?? 0) + "B")),
908
+ e("span", null, "\u6BD4\u503C ", e("b", null, data.ivr.cumRatio ?? "?")),
909
+ e("span", null, "\u5171 ", e("b", null, data.ivr.rounds.length), " \u8F6E")),
910
+ e(IvrChart, { key: "ch", rounds: data.ivr.rounds }),
911
+ e("div", { key: "ivr", style: { maxHeight: 90, overflowY: "auto", fontSize: 11, fontFamily: "var(--ds-font-family-code,ui-monospace,Consolas,monospace)" } },
912
+ data.ivr.rounds.map(function (r, i) { return e("div", { key: i }, "\u7B2C" + (i + 1) + "\u8F6E\uFF1A\u8F93\u5165 " + r.inBytes + "B \u2192 \u8F93\u51FA " + r.outBytes + "B\uFF0C\u6BD4\u503C " + r.ratio); }))
913
+ ] : e("div", { className: "ci-sub" }, "\u8FD9\u4E2A\u4F1A\u8BDD\u8FD8\u6CA1\u6298\u53E0\u8FC7")),
914
+ e("div", { className: "ci-sub", style: { marginTop: 6 } }, "\u6BD4\u503C = \u8F93\u5165 \u00F7 \u8F93\u51FA\u3002\u503C\u8D8A\u5927\u5219\u538B\u7F29\u6389\u7684\u91CF\u8D8A\u5927\uFF0C\u4FDD\u5B88\u53CD\u6620\u8F93\u5165\u7ECF\u6D4E\u6027\u3002\u84DD\u7EBF\uFF1D\u6BCF\u8F6E\uFF0C\u7EFF\u7EBF\uFF1D\u7D2F\u8BA1\uFF1B\u865A\u7EBF\uFF1D\u6BD4\u503C 1 \u53C2\u8003\u7EBF\u3002")),
915
+ e("div", { className: "ci-sec" },
916
+ e("div", { className: "ci-sech" }, "最近一次折叠",
917
+ // 上次折叠的触发点标签:用户靠它判断"本轮结束即折"(turn-end)到底有没有跑过
918
+ (lf && lf.trigger ? e("span", { className: "ci-sub", title: "这次折叠由哪个触发点产生:turn-end = 本轮 AI 输出结束后台折;pre-step = 旧版的下一轮请求前折(已移除)" }, "触发:" + trigLabel(lf.trigger)) : null),
919
+ e("button", { className: "ci-btn", style: { marginLeft: "auto" }, onClick: status }, "刷新")),
920
+ // 在飞 → 醒目显示「上一轮输出压缩中…」;刚结束 → 极简「已完成」;平时什么都不显示
921
+ (finOn ? inflightLine(fin) : (finDoneAt && (Date.now() - finDoneAt) < FIN_DONE_MS ? e("div", { className: "ci-sub", style: { marginBottom: 6, color: "rgba(52,199,89,.9)" } }, "已完成") : null)),
922
+ (lf ? [
923
+ e("div", { key: "m", className: "ci-metrics" },
924
+ e("span", null, "round ", e("b", null, lf.round ?? "?"), " · step ", e("b", null, lf.step ?? "?")),
925
+ e("span", null, "本折 ", e("b", null, (lf.incrementalRawBytes ?? lf.shadowedBytes ?? "?") + " → " + (lf.appendedBytes ?? lf.transcriptBytes ?? "?") + " B"), " (", e("b", null, Math.round((lf.foldRatio ?? lf.shrink ?? 1) * 100) + "%"), ")"),
926
+ e("span", null, "累计 ", e("b", null, lf.cumRatio == null ? "?" : Math.round(lf.cumRatio * 100) + "%")),
927
+ e("span", null, "gate ", e("b", null, lf.gate || "?")),
928
+ e("span", null, "档 ", e("b", null, lf.mode || "off")),
929
+ e("span", null, "hash ", e("b", null, String(lf.transcriptHash || "").slice(0, 8))),
930
+ e("span", null, "monotonic ", e("b", null, lf.monotonic === true ? "✓" : lf.monotonic || "?"))),
931
+ (lf.afterNodes && lf.afterNodes.length ? (function () {
932
+ var fs = frameSummary(lf.afterNodes);
933
+ var summary = "\u8BF7\u6C42\u7ED3\u6784\uFF1A" + fs.n + " \u8282\u70B9 / " + fmtB(fs.total) + " \u00B7 " + fs.top.map(function (g) { return g.k + "\u00D7" + g.n + " " + fmtB(g.b); }).join(" \u00B7 ") + (fs.groups.length > fs.top.length ? " \u2026" : "");
934
+ return e("details", { key: "n", className: "ci-sub ci-frames" },
935
+ e("summary", { title: summary }, summary),
936
+ e("div", { className: "ci-frame-body" },
937
+ fs.groups.map(function (g) { return e("div", { key: "g:" + g.k, className: "g" }, e("span", null, g.k), e("b", null, g.n + " \u00D7 " + fmtB(g.b))); }),
938
+ e("div", { className: "ci-frame-list" }, fs.text)));
939
+ })() : e("div", { key: "n", className: "ci-sub" }, "\u8BF7\u6C42\u7ED3\u6784\uFF1A\u2014")),
940
+ e("hr", { key: "r", className: "ci-hr" }),
941
+ timeline(shown),
942
+ (rounds.length > 3 ? e("div", { key: "ra", className: "ci-actions" }, e("button", { className: "ci-btn", onClick: function () { setAllRounds(!allRounds); } }, allRounds ? "仅显示最近 3 轮" : "显示全部 " + rounds.length + " 轮")) : null)
943
+ // 在飞且还没折过:上面的「压缩中…」已是唯一可说的内容,不再并排一句"尚无折叠发生"(自相矛盾)
944
+ ] : (finOn ? null : e("div", { className: "ci-sub" }, "尚无折叠发生(把当前会话加入白名单,跨轮对话后出现)"))))),
945
+ e("div", { className: "ci-rail" },
946
+ (cur ? e("div", { className: "ci-sec" },
947
+ e("div", { className: "ci-sech" }, "当前会话",
948
+ e("span", { className: "ci-badge " + (sel[cur] ? "ci-on" : "ci-off"), style: { marginLeft: 6 }, title: sel[cur] ? "已入白名单(将折叠)" : "未入白名单(不折叠)" }),
949
+ // 导出本会话 LOG 的显式入口(右键菜单不好找;无转录时置灰并说明)【取自 RCS-0.1.0 返回包】
950
+ e("button", { className: "ci-btn", style: { marginLeft: "auto", padding: "2px 9px" }, disabled: !lgHasLog(cur),
951
+ title: lgHasLog(cur) ? "下载本会话的 LOGcompiler 转录文件" : "本会话还没有转录文件(尚未记录到任何一轮)",
952
+ onClick: function () { lgExport(cur); } }, "导出 LOG")),
953
+ e("div", { className: "ci-sub", style: { margin: "0 0 8px", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, cur),
954
+ e("div", { className: "ci-sech", style: { marginBottom: 6 } }, "本会话压缩档"),
955
+ e("div", { className: "cim-chips", style: { marginBottom: 4 } }, MODE_OPTS.map(function (o) {
956
+ var active = (curOwn(cur) || "none") === o[0];
957
+ return e("button", { key: o[0], className: "cim-chip" + (active ? " cim-on" : ""), disabled: busy,
958
+ title: o[0] === "none" ? "本会话不折叠" : o[0] === "off" ? "对每轮对话做工具转录折叠(保留原始信息)" : o[0] === "single" ? "对每轮AI输出进行语义压缩" : "单次压缩基础上进行关键值保真提取",
959
+ onClick: function () { pickMode(cur, o[0]); } }, o[1]);
960
+ })),
961
+ e("hr", { className: "ci-hr" }),
962
+ e("div", { className: "ci-toggle ci-lg" + (tfOn ? " ci-lgon" : ""), style: { marginTop: 2 }, title: "实验(A档,默认关):工具结果折叠为结构化 [T](P0–P3)+ 值保真链 [V]", onClick: function () { setToolfold(!tfOn); } },
963
+ e("span", { className: "ci-switch" }), e("span", { style: { fontSize: 12 } }, "工具结果结构化折叠(实验)")),
964
+ e("div", { className: "ci-sub", style: { marginTop: 2 } }, tfOn ? "开启:read/edit/grep/bash/ask_user 等折叠为结构化行 + [V] 值保真链" : "默认关(保现行为)。开启后对工具结果生成 P 级/指纹/err 结构化行。"),
965
+ (sel[cur] && !curOwn(cur) ? e("div", { className: "ci-sub", style: { marginTop: 4 } }, "白名单内但未选档→不折叠;选任一档即启用") : null)) : null),
966
+ e("div", { className: "ci-sec" },
967
+ e("div", { className: "ci-sech" }, "会话白名单 · 按工作区(已归档不列出)"),
968
+ (wsNames.length ? wsNames.map(function (w) {
969
+ var list = (wss[w] || []).filter(function (s) { return !s.archived; });
970
+ if (!list.length) return null;
971
+ var open = !!openWs[w];
972
+ var picked = list.filter(function (s) { return sel[s.id]; }).length;
973
+ return e("div", { key: w, className: "ci-grp" + (open ? " ci-open" : "") },
974
+ e("div", { className: "ci-grphd", role: "button", tabIndex: 0, "aria-expanded": !!open, onClick: function () { toggleGroup(w); }, onKeyDown: function (ev) { if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); toggleGroup(w); } } },
975
+ e("span", { className: "ci-caret" }, "▶"),
976
+ e("span", { style: { flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, w || "(未分组)"),
977
+ e("span", { className: "ci-sub" }, picked + "/" + list.length)),
978
+ (open ? e("div", { className: "ci-chips" }, list.map(function (s) { return chip(s); })) : null));
979
+ }) : e("div", { className: "ci-sub" }, "暂无会话"))),
980
+ logSec())
981
+ )
982
+ )
983
+ );
984
+ }
985
+ // ===== composer dock:首条发送前的「本会话压缩模式」选择器 =====
986
+ // 注册于 conversation.input.dock —— 该槽位在空白会话(首轮发送前)同样渲染,故首轮之前即可选档。
987
+ // 规则(修:切回已选会话不再复现;点"关"也消失):
988
+ // - 会话在 sessionModes 里已选档(off/single/double)→ 隐藏(有注入 tab 可改)。
989
+ // - 会话已被本机"决策过"(本地 dismissed 记忆,含选"关")→ 隐藏,跨会话切入不重现。
990
+ // - 否则(新会话未决策)→ 显示;点任一档(含"关")即写档并把 sid 记为已决策。
991
+ // 本地记忆用 localStorage keyed by 归一 sid;换 sid(新会话)自然重新出现。
992
+ var MODE_OPTS = [["none", "关"], ["off", "工具折叠"], ["single", "单压缩"], ["double", "双压缩"]];
993
+ function DockMemo() { return "contextinjector.dock.dismissed"; }
994
+ function dockMemoRead() { try { return JSON.parse(localStorage.getItem(DockMemo()) || "{}") || {} } catch (e) { return {} } }
995
+ function dockMemoAdd(sid) { try { var m = dockMemoRead(); m[normSidStr(sid)] = 1; localStorage.setItem(DockMemo(), JSON.stringify(m)) } catch (e) {} }
996
+ function normSidStr(s) { return String(s || "").replace(/^session-/i, ""); }
997
+ function ModeDock(props) {
998
+ var sid = (props && props.sessionId) || null;
999
+ var _c = useState(null), ctrl = _c[0], setCtrl = _c[1];
1000
+ var _b = useState(false), busy = _b[0], setBusy = _b[1];
1001
+ var _hid = useState(false), hidden = _hid[0], setHidden = _hid[1];
1002
+ function pull() {
1003
+ return fetch("/api/context-inject/status", { headers: { "Content-Type": "application/json" } })
1004
+ .then(function (r) { return r.json(); })
1005
+ .then(function (s) { setCtrl(s && s.control ? s.control : null); })
1006
+ .catch(function () {});
1007
+ }
1008
+ useEffect(function () {
1009
+ setHidden(false); // 会话切换后重置本态(是否已选仍由 ctrl 决定)
1010
+ pull();
1011
+ var iv = setInterval(pull, 6000);
1012
+ return function () { clearInterval(iv); };
1013
+ }, [sid]);
1014
+ function pick(mode) {
1015
+ if (!sid) return;
1016
+ setBusy(true);
1017
+ fetch("/api/context-inject/session", {
1018
+ method: "POST", headers: { "Content-Type": "application/json" },
1019
+ body: JSON.stringify({ sessionId: sid, mode: mode }),
1020
+ }).then(function (r) { return r.json(); }).then(function (resp) {
1021
+ if (!resp || resp.ok !== true) throw new Error(JSON.stringify(resp));
1022
+ dockMemoAdd(sid); // 无论选何档(含"关")都记为已决策 → 隐藏
1023
+ setHidden(true);
1024
+ return pull();
1025
+ }).catch(function () {}).finally(function () { setBusy(false); });
1026
+ }
1027
+ if (!sid) return null;
1028
+ var n = normSidStr(sid);
1029
+ var sm = ctrl && ctrl.sessionModes;
1030
+ var own = null;
1031
+ if (sm && typeof sm === "object") { Object.keys(sm).forEach(function (k) { if (own === null && normSidStr(k) === n) own = sm[k]; }); }
1032
+ // 已选档 / 已本地决策 / 本实例已点过 → 隐藏
1033
+ if (own !== null || hidden || dockMemoRead()[n]) return null;
1034
+ var inList = ((ctrl && ctrl.sessions) || []).some(function (x) { return normSidStr(x) === n; });
1035
+ return e("div", { className: "cim-dock" },
1036
+ e("span", { className: "cim-lb", title: "本会话的上下文压缩模式;随时可改,在跨轮折叠时生效" }, "压缩"),
1037
+ e("div", { className: "cim-chips" }, MODE_OPTS.map(function (o) {
1038
+ return e("button", {
1039
+ key: o[0], className: "cim-chip", disabled: busy,
1040
+ title: o[0] === "none" ? "本会话不折叠" : o[0] === "off" ? "工具转录折叠" : o[0] === "single" ? "语义压缩" : "语义压缩 + 值保真(保真最高)",
1041
+ onClick: function () { pick(o[0]); },
1042
+ }, o[1]);
1043
+ })),
1044
+ (inList && !own ? e("span", { className: "cim-hint", title: "白名单内但未选档 → 不折叠;选择任一档即启用" }, "未选档·不折叠") : null));
1045
+ }
1046
+ // ===== 【会话页·在飞折叠】「压缩中」小标的参数形态兼容层(v2.4)=====
1047
+ // 背景(用户反复报「会话页从来没有『压缩中』小标」):小标只由 /status.foldInflight 驱动,
1048
+ // 所以"渲染不出来"只可能是 ①插槽根本没渲染到我们 ②拿不到会话 id ③颜色看不见 ④窗口太短没逮到。
1049
+ // ②的成因是**插槽参数形态**——本文件原先只吃 `props.sessionId` 一种形态:
1050
+ // · owner props:`conversation.input.left` 传的是 **zone 对象** `{session, input}`
1051
+ // (dsh-client-ui-conversation/lib/client.js:7191-7194 构造、:7241 `renderSlot("conversation.input.left", zone)`;
1052
+ // 契约文档 dsh-client-ui-conversation/lib/types/client/contract/slots.d.ts:270-274 `owner: InputZone`,
1053
+ // 同文件 :395-397 `interface InputZone { session: ConversationSnapshot; input: InputState }`);
1054
+ // · standard kit:渲染器给每个 entry 铺 `sessionId`(dsh-client-ui-renderer/lib/client.js:535-567,
1055
+ // 其中 :563 `standard["sessionId"] = info.sessionId`);
1056
+ // · entry 自己的 inject:以 `(sessionId, actions)` 调用(同文件 :334-341 `runInject`),
1057
+ // 本文件的 inject 正是据此写(另见 DSH 自带 entry 的写法
1058
+ // dsh-client-ui-conversation/lib/client.js:10006 `inject: (sessionId) => ({...})`)。
1059
+ // ⇒ 结论:正常路径下 `props.sessionId` **就是**裸会话 id;但三者任一被 DSH 改口径都会**静默**打死小标
1060
+ // (渲染函数返回 null,控制台一句都不说)。这里把 5 种可能形态全部吃下 + 挂载一行、**每次状态跃迁
1061
+ // 各一行**诊断日志(`[ci-foldchip]`;跃迁行带 `Δ=`),再也不可能出现"看不到又查不出"的局面。
1062
+ // 形态清单:裸字符串 / {sessionId} / {session:{id}} / {session:"sid"} / {id}
1063
+ function ciSidFrom(v) {
1064
+ if (typeof v === "string") return v.trim() || null;
1065
+ if (typeof v === "number" && Number.isFinite(v)) return String(v);
1066
+ if (!v || typeof v !== "object") return null;
1067
+ if (typeof v.sessionId === "string" && v.sessionId.trim()) return v.sessionId.trim();
1068
+ var s = v.session;
1069
+ if (typeof s === "string" && s.trim()) return s.trim();
1070
+ if (s && typeof s === "object") {
1071
+ if (typeof s.sessionId === "string" && s.sessionId.trim()) return s.sessionId.trim();
1072
+ if (typeof s.id === "string" && s.id.trim()) return s.id.trim();
1073
+ }
1074
+ if (typeof v.id === "string" && v.id.trim()) return v.id.trim();
1075
+ return null;
1076
+ }
1077
+ // props(对象或裸字符串)→ { sid, via };via 只用于诊断日志,取最具体的来源名
1078
+ function ciChipSid(props) {
1079
+ if (typeof props === "string") { var b = props.trim(); return { sid: b || null, via: b ? "bare-string" : "none" }; }
1080
+ var p = props || {};
1081
+ if (typeof p.sessionId === "string" && p.sessionId.trim()) return { sid: p.sessionId.trim(), via: "props.sessionId" };
1082
+ var s = p.session;
1083
+ if (typeof s === "string" && s.trim()) return { sid: s.trim(), via: "props.session" };
1084
+ if (s && typeof s === "object") {
1085
+ if (typeof s.sessionId === "string" && s.sessionId.trim()) return { sid: s.sessionId.trim(), via: "props.session.sessionId" };
1086
+ if (typeof s.id === "string" && s.id.trim()) return { sid: s.id.trim(), via: "props.session.id" };
1087
+ }
1088
+ if (typeof p.id === "string" && p.id.trim()) return { sid: p.id.trim(), via: "props.id" };
1089
+ return { sid: null, via: "none" };
1090
+ }
1091
+ // 形态摘要(日志与 title 共用):只报"在不在 + 什么类型 + 多长",绝不打值本身(会话 id 不进 UI 文案)
1092
+ function ciShapeBrief(props) {
1093
+ if (typeof props === "string") return "arg=string[" + props.length + "]";
1094
+ var p = props || {};
1095
+ return ["sessionId", "session", "id", "input"].map(function (k) {
1096
+ if (!(k in p)) return k + "=absent";
1097
+ var v = p[k];
1098
+ return k + "=" + (v === null ? "null" : typeof v === "string" ? "string[" + v.length + "]" : Array.isArray(v) ? "array" : typeof v);
1099
+ }).join(" ");
1100
+ }
1101
+ function ciPropsKeys(props) {
1102
+ try { return typeof props === "string" ? "(bare-string)" : Object.keys(props || {}).join(","); }
1103
+ catch (x) { return "(unreadable)"; }
1104
+ }
1105
+ // 【诊断】挂载打一行 + **每次状态跃迁各打一行**(旧实现只在挂载打一行,压缩期在控制台没有任何证据行)。
1106
+ // 为什么改(用户实测 2026-09-10):旧口径下控制台永远只有 `foldInflight=no`(挂载那一刻本来就没在折),
1107
+ // 真实压缩的瞬间没有日志 ⇒ 无法一次性判定是"标记根本没写"还是"写了但采样窗口没对上"。
1108
+ // 现口径:**状态指纹**不变就不打(0.5s 连续采样 ⇒ 同一状态绝不刷屏),指纹变化时打一行,跃迁行带 `Δ=`。
1109
+ // 指纹 = 形状 + keys + sid + via + foldInflight(含 trigger) + globalInflight 计数 + force + 采样错误;
1110
+ // ⚠ `age`(秒数)**不进指纹** —— 它每秒都在变,进了就会按秒刷屏(违背"不许按拍刷屏")。
1111
+ // 排障口径(devtools 控制台筛 `[ci-foldchip]`):
1112
+ // · 一行都没有 ⇒ 插槽没渲染到我们(落点/注册问题,与 sessionId 无关);
1113
+ // · sid=(none) ⇒ 形态没解析出来(把这行原样贴回来即可);
1114
+ // · 压缩期间出现 `Δ= … foldInflight=YES trigger=pre-step age=Ns` ⇒ 标记在写、采样也逮到了(渲染另查);
1115
+ // · 压缩期间一条 Δ 都没有 ⇒ 要么标记没写,要么该次折叠短于一拍(CI_STATUS_MS=500ms);两者用
1116
+ // `<DSH_HOME>/state-compiler/.ctxinjector/fold-inflight/<sid>.json`(=写没写)与 last-fold.json 的
1117
+ // trigger 字段(=折没折)可一次判定。
1118
+ // 纯函数(故可回归测试,见 fold-boundary.test.mjs):返回 { key, line } = 需要打这一行;返回 null = 与上一次同状态、不打。
1119
+ function ciFoldChipLogLine(prevKey, props, r, data, errText, forced) {
1120
+ try {
1121
+ var keys = ciPropsKeys(props);
1122
+ var shape = ciShapeBrief(props);
1123
+ var f = (data && data.foldInflight) || null;
1124
+ var all = (data && data.foldInflightAll) || null;
1125
+ var age = (f && Number.isFinite(Number(f.startedAt))) ? Math.max(0, Math.round((Date.now() - Number(f.startedAt)) / 1000)) : null;
1126
+ var fiKey = errText ? ("sample-error:" + errText) : f ? ("YES trigger=" + String(f.trigger)) : "no"; // 指纹用(不含 age)
1127
+ var fiLine = fiKey + (age != null ? " age=" + age + "s" : ""); // 日志用(含 age,与旧格式逐字一致)
1128
+ var g = all ? String(all.length) : "n/a";
1129
+ var key = shape + " | keys=[" + keys + "] | sid=" + (r.sid || "(none)") + " via=" + r.via
1130
+ + " | foldInflight=" + fiKey + " | globalInflight=" + g + " | force=" + (forced ? "1" : "0");
1131
+ if (key === prevKey) return null; // 同状态连续采样:一行都不新增(防按拍刷屏)
1132
+ return {
1133
+ key: key,
1134
+ // 单行;字段与旧版一致(sid/via/foldInflight/trigger/age/globalInflight/force),跃迁行只多一个 `Δ=`
1135
+ line: "[ci-foldchip] " + (prevKey == null ? "mount" : "Δ=") + " " + shape + " | keys=[" + keys + "] | sid=" + (r.sid || "(none)") + " via=" + r.via
1136
+ + " | foldInflight=" + fiLine + " | globalInflight=" + g + " | force=" + (forced ? "1" : "0"),
1137
+ };
1138
+ } catch (x) { return null; /* 诊断日志绝不允许影响渲染 */ }
1139
+ }
1140
+ // 【调试强制位】localStorage.setItem('ciFoldChipForce','1') + 刷新页面 ⇒ 不管有没有在飞折叠都渲染小标
1141
+ // (样式与真身完全一致,只是 title 标注"强制显示(调试)"、DOM 上带 `data-ci-foldchip="force"`;
1142
+ // 若强制位开着而 sid 也没解析出来,title 里会一并写明,便于一次判定"落点/颜色/形态"。)
1143
+ // 关闭:localStorage.removeItem('ciFoldChipForce') + 刷新页面(无需重启服务)。
1144
+ // 为什么需要:真身只在后台折叠在飞的那几秒出现,用户很难逮住 ⇒ 用它把"看不见"这件事
1145
+ // 确定性地拆成"落点对不对"与"窗口够不够长"两个独立问题。
1146
+ var CI_FORCE_KEY = "ciFoldChipForce";
1147
+ function ciFoldForceOn() {
1148
+ try { return typeof localStorage !== "undefined" && localStorage.getItem(CI_FORCE_KEY) === "1"; }
1149
+ catch (x) { return false; } // 隐私模式/存储被禁 → 当没开(绝不影响渲染)
1150
+ }
1151
+ // 【会话页·在飞折叠】「压缩中」小标:会话页上直接可见(不再只藏在「注入」面板里)。
1152
+ // 数据源:与面板同一份共享 /status 快照(foldInflight per-sid)⇒ 无在飞时**不渲染任何节点**(return null)。
1153
+ // 落点 conversation.input.left(composer 操作行左侧工具簇 = 输入卡片左下角):该处是 flex 行内位置,
1154
+ // 与同排档位选择器同字号 13px/20px,不改变行高 ⇒ 不挤动/不盖住 DSH 自己的「Deep diving...」状态行。
1155
+ // 秒数不另起定时器:共享采样器每 0.5s 重渲染即自动走秒(与面板 inflightLine 同一口径)。
1156
+ function FoldChip(props) {
1157
+ var r = ciChipSid(props);
1158
+ var sid = r.sid;
1159
+ var forced = ciFoldForceOn();
1160
+ var _s = useState(null), st = _s[0], setSt = _s[1];
1161
+ var logKeyRef = useRef(null); // 上一次已打日志的**状态指纹**(null = 还没打过 ⇒ 下一次是挂载行)
1162
+ // 【诊断】挂载一行 + 状态跃迁各一行;同状态连续采样不重复打。纯观测:只 console.log,不写文件、
1163
+ // 不起定时器(沿用模块级共享 500ms 采样器的回调),也不碰任何 state ⇒ 绝不影响小标渲染条件/样式。
1164
+ function logFoldChip(data, errText) {
1165
+ var lg = ciFoldChipLogLine(logKeyRef.current, props, r, data, errText, forced);
1166
+ if (!lg) return; // 同状态:不打(防按拍刷屏)
1167
+ logKeyRef.current = lg.key;
1168
+ try { console.log(lg.line); } catch (x) { /* 打日志失败也绝不影响渲染 */ }
1169
+ }
1170
+ // 有 sid → 订阅该 sid;**没有 sid** → 订阅全局键 ""(见下面的兜底推断,绝不再静默 null)。
1171
+ var key = sid || "";
1172
+ useEffect(function () {
1173
+ setSt(null); // 键变化(换会话)先清空,别把上一个会话的"在飞"带过来
1174
+ return ciStatus.subscribe(key, function (snap) {
1175
+ // 采样失败 → 当作"无在飞"(宁可不报,也不误报一个永远转的小标)
1176
+ if (snap && snap.err) {
1177
+ setSt(null);
1178
+ logFoldChip(null, String((snap.err && snap.err.message) || snap.err));
1179
+ return;
1180
+ }
1181
+ var data = (snap && snap.data) || null;
1182
+ setSt(data);
1183
+ logFoldChip(data, null);
1184
+ });
1185
+ }, [key]);
1186
+ var f = (st && st.foldInflight) || null;
1187
+ // 【无 sid 兜底】本插槽按理一定拿得到会话 id(见上面形态层)。真拿不到时**不静默放弃**:
1188
+ // 用 host 在"不带 ?sessionId=" 时额外上报的 `foldInflightAll`(逐会话在飞标记列表)。
1189
+ // 限制(明确写下):全局快照不知道当前看的是哪个会话 ⇒ 只有**全机恰好 1 个会话在飞**时才敢推断;
1190
+ // 多会话同时折叠时一律不显示(避免把别人的折叠显示成本会话的);显示时 title 会写明"会话未知"。
1191
+ var all = (st && st.foldInflightAll) || null;
1192
+ var infer = (!sid && all && all.length === 1) ? all[0] : null;
1193
+ // v1.10.39:宿主折叠行已由 CSS 隐藏;这里自绘一行紧凑状态。
1194
+ // · 折叠完成 ⇒ 「本折 98% · 累计 62%」(百分比 <100% 才是省)
1195
+ // · 折叠在飞 ⇒ 琥珀脉冲 +「压缩中」(原样保留)
1196
+ // · 记忆编译在飞 ⇒ 琥珀脉冲 +「记忆编译中」;成功 30s 内 ⇒「记忆 +N」
1197
+ // · 宿主原生 compaction 在飞 ⇒「原生压缩中」(补回被隐藏的宿主信号)
1198
+ var lf = (st && st.lastFold) || null;
1199
+ var mi = (st && st.memoryInflight) || null;
1200
+ var ml = (st && st.memoryLast) || null;
1201
+ var ni = (st && st.nativeCompaction) || null;
1202
+ var co = (st && st.consolidate) || null;
1203
+ var coi = (co && co.inflight) || null;
1204
+ var col = (co && co.last) || null;
1205
+ var pctOf = function (v) {
1206
+ var n = Number(v);
1207
+ return Number.isFinite(n) ? Math.round(n * 100) : null;
1208
+ };
1209
+ var foldPct = pctOf(lf && (lf.foldRatio != null ? lf.foldRatio : lf.shrink));
1210
+ var cumPct = pctOf(lf && lf.cumRatio);
1211
+ // 只在拿到本会话自己的折叠结果时显示(status 无 per-sid 文件时会回退全局 last-fold ⇒ 别拿别人会话的比值冒充)
1212
+ var hasFoldDone = foldPct != null && cumPct != null && (!sid || (st && st.lastFoldPerSession === true));
1213
+ var memRecent = !!(ml && ml.ok === true && Number.isFinite(Number(ml.t)) && (Date.now() - Number(ml.t)) < 30000);
1214
+ var coRecent = !!(col && col.ok === true && Number.isFinite(Number(col.t)) && (Date.now() - Number(col.t)) < 30000);
1215
+ if (!forced && !f && !infer && !ni && !mi && !hasFoldDone && !memRecent && !coi && !coRecent) return null; // 常态:一个节点都不渲染
1216
+ var secOf = function (x) {
1217
+ var t0 = Number(x && x.startedAt);
1218
+ return Number.isFinite(t0) ? Math.max(0, Math.round((Date.now() - t0) / 1000)) : null;
1219
+ };
1220
+ var chips = [];
1221
+ if (f || infer || forced) {
1222
+ var sec = secOf(f);
1223
+ var title;
1224
+ if (f) {
1225
+ title = "上一轮输出正在后台压缩(" + trigLabel(f && f.trigger) + (sec != null ? " · 已 " + sec + "s" : "") + "):浓缩模型返回后写入 last-fold,下一轮请求前并入上下文";
1226
+ } else if (infer) {
1227
+ title = "有会话正在后台压缩,但会话未知:全机恰好只有 1 个会话在飞(" + shortId(infer.sessionId) + " · " + trigLabel(infer.trigger) + ")。本插槽没拿到当前会话 id,故按全局唯一在飞推断,可能与当前会话无关;多会话同时在飞时本小标不显示";
1228
+ } else {
1229
+ title = "强制显示(调试):实际只在折叠/记忆编译有状态时出现。当前采样=" + (st ? "无在飞" : "尚未返回") + ",sid=" + (sid || "(未解析到)") + ",形态 " + ciShapeBrief(props)
1230
+ + "。关闭:localStorage.removeItem('ciFoldChipForce') 后刷新页面";
1231
+ }
1232
+ chips.push(e("span", { key: "f", className: "ci-foldchip", title: title,
1233
+ "data-ci-foldchip": forced ? "force" : (f ? "inflight" : "inferred") },
1234
+ e("span", { className: "ci-pulse" }),
1235
+ "压缩中" + (f && sec != null ? " " + sec + "s" : "")));
1236
+ } else if (hasFoldDone) {
1237
+ chips.push(e("span", { key: "f", className: "ci-foldchip ci-foldchip-done", "data-ci-foldchip": "done",
1238
+ title: "本折 " + (lf.foldRatio != null ? Number(lf.foldRatio).toFixed(2) : "—")
1239
+ + " · 累计 " + (lf.cumRatio != null ? Number(lf.cumRatio).toFixed(2) : "—")
1240
+ + "(<1 表示省);本折 = (本次新增转录 − 首折原样保留的 head) ÷ 本次新原料,累计 = 当前转录 ÷ 累计折叠原料" },
1241
+ "本折 " + foldPct + "% · 累计 " + cumPct + "%"));
1242
+ }
1243
+ if (coi) {
1244
+ var cosec = secOf(coi);
1245
+ var stageText = ({ sweep: "核对文件", gather: "收集事件", llm: "模型考证", repair: "修复输出", apply: "写入结果" }[coi.stage] || "整理中");
1246
+ chips.push(e("span", { key: "c", className: "ci-foldchip", "data-ci-foldchip": "consolidate",
1247
+ title: "记忆整理(" + (coi.trigger === "manual" ? "手动" : "空闲自动") + "):" + stageText + (cosec != null ? " · 已 " + cosec + "s" : "") },
1248
+ e("span", { className: "ci-pulse" }), "记忆整理中" + (cosec != null ? " " + cosec + "s" : "")));
1249
+ } else if (coRecent) {
1250
+ var coN = (Number(col.invalidated) || 0) + (Number(col.superseded) || 0) + (Number(col.merged) || 0) + (Number(col.added) || 0);
1251
+ chips.push(e("span", { key: "c", className: "ci-foldchip ci-foldchip-done", "data-ci-foldchip": "consolidate-done",
1252
+ title: "上次整理:" + (col.invalidated || 0) + " 条失效 · " + (col.superseded || 0) + " 条取代 · "
1253
+ + (col.merged || 0) + " 条合并 · " + (col.added || 0) + " 条新增"
1254
+ + (col.ms != null ? " · " + (col.ms / 1000).toFixed(1) + "s" : "") },
1255
+ "记忆已整理" + (coN ? " · " + coN : "")));
1256
+ }
1257
+ if (ni) {
1258
+ var nsec = secOf(ni);
1259
+ chips.push(e("span", { key: "n", className: "ci-foldchip", "data-ci-foldchip": "native",
1260
+ title: "DSH 原生上下文压缩正在运行" + (nsec != null ? "(已 " + nsec + "s)" : "") + ";插件折叠已让路,避免作废它的总结" },
1261
+ e("span", { className: "ci-pulse" }), "原生压缩中"));
1262
+ }
1263
+ if (mi) {
1264
+ var msec = secOf(mi);
1265
+ chips.push(e("span", { key: "m", className: "ci-foldchip", "data-ci-foldchip": "memory",
1266
+ title: "轻 LLM 正在把本轮新事件编译为记忆" + (msec != null ? "(已 " + msec + "s)" : "") + ";完成后写入 memory.sqlite" },
1267
+ e("span", { className: "ci-pulse" }), "记忆编译中"));
1268
+ } else if (memRecent) {
1269
+ chips.push(e("span", { key: "m", className: "ci-foldchip ci-foldchip-done", "data-ci-foldchip": "memory-done",
1270
+ title: "最近一次记忆编译:+" + (ml.insights || 0) + " 条洞察" + (ml.through != null ? ",至 seq " + ml.through : "") + (ml.model ? " · " + ml.model : "") },
1271
+ "记忆 +" + (ml.insights || 0)));
1272
+ }
1273
+ return e("span", { className: "ci-foldchips", role: "status", "aria-live": "polite" }, chips);
1274
+ }
1275
+ // #5 压缩器IO比折线:横轴逐轮,蓝=每轮 I/O 比值,绿=累计 I/O 比值。★=里程碑轮
1276
+ // ⚠ 修(本地缺陷:标签被拉扁/挤扁)【取自 RCS-0.1.0 返回包】:
1277
+ // 旧实现固定 viewBox 600×130 + preserveAspectRatio="none",而 CSS 强制 width:100%;height:110px
1278
+ // ⇒ x/y 各自缩放,容器不是 600 宽时文字被横向拉伸。现改为**实测容器像素**当 viewBox
1279
+ // (1 用户单位 = 1 px,且不再用 preserveAspectRatio),文字等比、笔画不变形。
1280
+ function ivrChart(rounds, W, H, svgRef) {
1281
+ var pad = 6, top = 10, bot = 18;
1282
+ W = Math.max(240, Math.round(W || 600));
1283
+ H = Math.max(80, Math.round(H || 110));
1284
+ if (!rounds || !rounds.length) return null;
1285
+ var n = rounds.length;
1286
+ var xs = function (i) { return pad + (n === 1 ? W / 2 : (i / (n - 1)) * (W - 2 * pad)); };
1287
+ // 纵轴用"每轮比值",自适应量程(比值可能 <1 也可能 >1)
1288
+ var vals = rounds.map(function (r) { return r.ratio; });
1289
+ var vmax = Math.max.apply(null, vals.concat([1.2]));
1290
+ var vmin = Math.min.apply(null, vals.concat([0.8]));
1291
+ var span = Math.max(0.2, vmax - vmin);
1292
+ var yR = function (v) { return top + (1 - (v - vmin) / span) * (H - top - bot); };
1293
+ var grid = [vmin, (vmin + vmax) / 2, vmax].map(function (g, i) {
1294
+ var yy = yR(g).toFixed(1);
1295
+ return e("g", { key: "g" + i },
1296
+ e("line", { x1: pad, y1: yy, x2: W - 24, y2: yy, stroke: "rgba(127,127,150,.14)", "stroke-width": 1 }),
1297
+ e("text", { x: W - 20, y: Number(yy) + 3, fontSize: 9, fill: "rgba(180,180,195,.6)", textAnchor: "start" }, g.toFixed(2)));
1298
+ });
1299
+ // 参考线:比值 1(输入=输出 ⇒ 没有冗余可压)。量程恒含 [0.8, 1.2],所以它一定落在图内。
1300
+ var refY = yR(1).toFixed(1);
1301
+ // 窄面板只写 "1",避免长标签压住左侧数据点
1302
+ var refLabel = W >= 360 ? "\u6BD4\u503C 1\uFF08\u65E0\u5197\u4F59\u53EF\u538B\uFF09" : "1";
1303
+ var ref1 = e("g", { key: "ref1" },
1304
+ e("line", { x1: pad, y1: refY, x2: W - 24, y2: refY, stroke: "rgba(200,200,215,.45)", "stroke-width": 1, "stroke-dasharray": "4 3" }),
1305
+ e("text", { x: pad + 2, y: Number(refY) - 4, fontSize: 9, fill: "rgba(200,200,215,.8)", textAnchor: "start" }, refLabel));
1306
+ var lineR = rounds.map(function (r, i) { return (i ? "L" : "M") + xs(i).toFixed(1) + " " + yR(r.ratio).toFixed(1); }).join(" ");
1307
+ var lineC = rounds.map(function (r, i) { return (i ? "L" : "M") + xs(i).toFixed(1) + " " + yR(r.cumRatio).toFixed(1); }).join(" ");
1308
+ var dots = rounds.map(function (r, i) { return e("circle", { key: i, cx: xs(i), cy: yR(r.ratio), r: r.stageAnchor ? 3 : 1.8, fill: r.stageAnchor ? "#ffd479" : "#6ea8ff" }); });
1309
+ var xl0 = e("text", { key: "x0", x: pad, y: H - 3, fontSize: 9, fill: "rgba(180,180,195,.6)", textAnchor: "start" }, "r" + rounds[0].n);
1310
+ var xl1 = e("text", { key: "x1", x: W - 24, y: H - 3, fontSize: 9, fill: "rgba(180,180,195,.6)", textAnchor: "end" }, "r" + rounds[n - 1].n);
1311
+ // viewBox = 实测像素(无 preserveAspectRatio ⇒ 默认 meet,因比例一致即 1:1)
1312
+ return e("svg", { className: "ci-chart", ref: svgRef, viewBox: "0 0 " + W + " " + H },
1313
+ grid, ref1,
1314
+ e("path", { d: lineC, fill: "none", stroke: "rgba(120,200,120,.6)", "stroke-width": 1.5 }),
1315
+ e("path", { d: lineR, fill: "none", stroke: "#6ea8ff", "stroke-width": 1.5 }),
1316
+ dots, xl0, xl1);
1317
+ }
1318
+ // 图表宿主:用 ResizeObserver 实测容器像素尺寸喂给 viewBox(避免非等比缩放把字拉扁)
1319
+ // 【取自 RCS-0.1.0 返回包】ResizeObserver 不可用时回落 600×110(与 CSS 高度一致);
1320
+ // 尺寸未变时不 setState(幂等守卫),免得每次 observe 回调都白重渲染一遍。
1321
+ function IvrChart(props) {
1322
+ var _wh = useState({ w: 600, h: 110 }), wh = _wh[0], setWh = _wh[1];
1323
+ var ref = useRef(null);
1324
+ useEffect(function () {
1325
+ var el = ref.current;
1326
+ if (!el || typeof ResizeObserver === "undefined") return;
1327
+ var measure = function () {
1328
+ var w = Math.round(el.clientWidth || 600), h = Math.round(el.clientHeight || 110);
1329
+ setWh(function (p) { return (p.w === w && p.h === h) ? p : { w: w, h: h }; });
1330
+ };
1331
+ measure();
1332
+ var ro = new ResizeObserver(measure);
1333
+ ro.observe(el);
1334
+ return function () { ro.disconnect(); };
1335
+ }, []);
1336
+ return ivrChart(props.rounds, wh.w, wh.h, ref);
1337
+ }
1338
+ // ===== MEMORY「外部记忆」面板 =====
1339
+ // 数据面:GET /api/memory/status(只读 SQLite 快照)+ POST /api/memory/config(三个开关热写 control.json)。
1340
+ // 视觉沿用本插件的 DSW token 体系;KPI 用显式 4 列网格(窄屏 2 列),不依赖自动排布。
1341
+ function memAgo(ts) {
1342
+ var n = Number(ts);
1343
+ if (!isFinite(n) || n <= 0) return "—";
1344
+ var d = Date.now() - n;
1345
+ if (d < 60000) return Math.max(1, Math.round(d / 1000)) + " 秒前";
1346
+ if (d < 3600000) return Math.round(d / 60000) + " 分钟前";
1347
+ if (d < 86400000) return Math.round(d / 3600000) + " 小时前";
1348
+ return new Date(n).toLocaleDateString();
1349
+ }
1350
+ function memScope(s) {
1351
+ var t = String(s || "");
1352
+ if (t.indexOf("ws:") === 0) {
1353
+ var p = t.slice(3).split("/").filter(Boolean);
1354
+ return "工作区 " + (p.length ? p[p.length - 1] : t.slice(3));
1355
+ }
1356
+ if (t.indexOf("sess:") === 0) return "会话 " + t.slice(5, 13);
1357
+ return t || "—";
1358
+ }
1359
+ function memBytes(n) {
1360
+ var v = Number(n) || 0;
1361
+ if (v < 1024) return v + " B";
1362
+ if (v < 1048576) return (v / 1024).toFixed(1) + " KB";
1363
+ return (v / 1048576).toFixed(1) + " MB";
1364
+ }
1365
+ function memCursor(n) {
1366
+ var v = Number(n);
1367
+ return Number.isFinite(v) && v >= 0 ? String(v) : "—";
1368
+ }
1369
+ // ---- 记忆图谱:来源结构(工作区 → 会话 → 洞察) ----
1370
+ // 纯函数、确定性布局;命令/文件不画点,只作为节点计数。便于单测与重绘。
1371
+ function memHash(s) {
1372
+ var h = 2166136261;
1373
+ var t = String(s || "");
1374
+ for (var i = 0; i < t.length; i++) { h ^= t.charCodeAt(i); h = Math.imul(h, 16777619); }
1375
+ return h >>> 0;
1376
+ }
1377
+ function memClip(s, n) {
1378
+ var t = String(s || "");
1379
+ return t.length > n ? (t.slice(0, n) + "…") : t;
1380
+ }
1381
+ function memNodeLabel(n) {
1382
+ if (!n) return "";
1383
+ if (n.kind === "workspace") return memClip(n.label || "工作区", 16);
1384
+ if (n.kind === "session") return n.title ? ("「" + memClip(n.title, 13) + "」") : ("会话 " + String(n.sid || "").slice(0, 8));
1385
+ return n.key || "洞察";
1386
+ }
1387
+ function memNodeRadius(n) {
1388
+ if (n.kind === "workspace") return 17 + Math.min(10, Math.log((Number(n.n) || 1) + 1) * 2);
1389
+ if (n.kind === "session") return 9 + Math.min(8, Math.log((Number(n.n) || 1) + 1) * 1.6);
1390
+ return 4 + Math.min(4, (Number(n.confidence) || 0.5) * 4);
1391
+ }
1392
+ // 布局签名的单条缓存:hover/平移/缩放都可能触发重绘,节点集合没变时不应重跑 24 轮 O(n²) 斥力。
1393
+ var memLayoutCache = { sig: "", at: null };
1394
+ function memoryGraphLayout(nodes, edges, W, H) {
1395
+ W = Number(W) || 900; H = Number(H) || 560;
1396
+ nodes = (nodes || []).map(function (n) { return n.id ? n : Object.assign({}, n, { id: n.scope }); });
1397
+ var sig = W + "x" + H + "|" + nodes.map(function (n) {
1398
+ return n.id + ":" + n.kind + ":" + (Number(n.n) || 0) + ":" + (Number(n.confidence) || 0) + ":" + (n.originSid || "") + ":" + (n.workspaceScope || "");
1399
+ }).join("|");
1400
+ if (memLayoutCache.sig === sig && memLayoutCache.at) {
1401
+ var atCached = memLayoutCache.at;
1402
+ return {
1403
+ nodes: nodes.map(function (n) { var p = atCached.get(n.id); return Object.assign({}, n, { x: p.x, y: p.y }); }),
1404
+ edges: edges || [],
1405
+ };
1406
+ }
1407
+ var ws = nodes.filter(function (n) { return n.kind === "workspace"; });
1408
+ var sess = nodes.filter(function (n) { return n.kind === "session"; });
1409
+ var facts = nodes.filter(function (n) { return n.kind === "fact"; });
1410
+ var cx = W / 2, cy = H / 2;
1411
+ var pos = new Map();
1412
+ var wr = ws.length > 1 ? Math.min(W, H) * 0.30 : 0;
1413
+ ws.forEach(function (n, i) {
1414
+ var a = ws.length > 1 ? (Math.PI * 2 * i / ws.length) - Math.PI : 0;
1415
+ pos.set(n.id, { x: cx + Math.cos(a) * wr, y: cy + Math.sin(a) * wr });
1416
+ });
1417
+ var sessByWs = new Map();
1418
+ sess.forEach(function (n) {
1419
+ var k = n.workspaceScope || "";
1420
+ if (!sessByWs.has(k)) sessByWs.set(k, []);
1421
+ sessByWs.get(k).push(n);
1422
+ });
1423
+ sessByWs.forEach(function (list, k) {
1424
+ var base = pos.get(k) || { x: cx, y: cy };
1425
+ var rr = list.length > 1 ? Math.min(210, 86 + list.length * 26) : 128;
1426
+ list.forEach(function (n, i) {
1427
+ var a = (Math.PI * 2 * i / Math.max(1, list.length)) + (memHash(n.id) % 100) / 500;
1428
+ pos.set(n.id, { x: base.x + Math.cos(a) * rr, y: base.y + Math.sin(a) * rr });
1429
+ });
1430
+ });
1431
+ var factsByScope = new Map();
1432
+ facts.forEach(function (n) {
1433
+ var k = n.scope || "";
1434
+ if (!factsByScope.has(k)) factsByScope.set(k, []);
1435
+ factsByScope.get(k).push(n);
1436
+ });
1437
+ factsByScope.forEach(function (list, scope) {
1438
+ var base = pos.get(scope) || { x: cx, y: cy };
1439
+ var rr = list.length > 1 ? Math.min(210, 72 + list.length * 15) : 74;
1440
+ list.forEach(function (n, i) {
1441
+ var a = (Math.PI * 2 * i / Math.max(1, list.length)) + (memHash(n.id) % 100) / 700;
1442
+ var p = { x: base.x + Math.cos(a) * rr, y: base.y + Math.sin(a) * rr };
1443
+ if (String(scope).indexOf("ws:") === 0 && n.originSid) {
1444
+ var op = pos.get("sess:" + n.originSid);
1445
+ if (op) p = { x: (p.x * 2 + op.x) / 3, y: (p.y * 2 + op.y) / 3 };
1446
+ }
1447
+ pos.set(n.id, p);
1448
+ });
1449
+ });
1450
+ nodes.forEach(function (n) {
1451
+ if (pos.has(n.id)) return;
1452
+ var a = (memHash(n.id) % 360) * Math.PI / 180;
1453
+ pos.set(n.id, { x: cx + Math.cos(a) * 80, y: cy + Math.sin(a) * 80 });
1454
+ });
1455
+ var arr = nodes.map(function (n) { var p = pos.get(n.id); return { id: n.id, kind: n.kind, x: p.x, y: p.y }; });
1456
+ var minOf = function (a, b) {
1457
+ if (a.kind === "workspace" || b.kind === "workspace") return 70;
1458
+ if (a.kind === "session" || b.kind === "session") return 34;
1459
+ return 14;
1460
+ };
1461
+ for (var it = 0; it < 24; it++) {
1462
+ for (var i = 0; i < arr.length; i++) {
1463
+ for (var j = i + 1; j < arr.length; j++) {
1464
+ var a = arr[i], b = arr[j];
1465
+ var dx = b.x - a.x, dy = b.y - a.y;
1466
+ var d = Math.sqrt(dx * dx + dy * dy);
1467
+ if (d < 0.5) {
1468
+ // 完全重合时方向未定义;用 id 哈希给一个确定性分离方向(纯随机会让每次重绘跳动)。
1469
+ var ang = (memHash(a.id + '|' + b.id) % 360) * Math.PI / 180;
1470
+ dx = Math.cos(ang) * 0.5; dy = Math.sin(ang) * 0.5; d = 0.5;
1471
+ }
1472
+ var min = minOf(a, b);
1473
+ if (d < min) {
1474
+ var push = (min - d) / d * 0.5;
1475
+ a.x -= dx * push; a.y -= dy * push;
1476
+ b.x += dx * push; b.y += dy * push;
1477
+ }
1478
+ }
1479
+ }
1480
+ for (i = 0; i < arr.length; i++) {
1481
+ arr[i].x = Math.max(24, Math.min(W - 24, arr[i].x));
1482
+ arr[i].y = Math.max(24, Math.min(H - 24, arr[i].y));
1483
+ }
1484
+ }
1485
+ // 初始环半径只保证结构可读;再按包围盒整体缩放,让稀疏图也能撑满画布(留出标签余量)。
1486
+ var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
1487
+ arr.forEach(function (p) {
1488
+ if (p.x < minX) minX = p.x;
1489
+ if (p.y < minY) minY = p.y;
1490
+ if (p.x > maxX) maxX = p.x;
1491
+ if (p.y > maxY) maxY = p.y;
1492
+ });
1493
+ var bw = Math.max(1, maxX - minX), bh = Math.max(1, maxY - minY);
1494
+ // 画布是横向的:x/y 分别缩放,稀疏图也能用满宽度;节点半径与字号不变。
1495
+ var sx = Math.min(1.9, (W - 200) / bw);
1496
+ var sy = Math.min(1.6, (H - 160) / bh);
1497
+ if (Number.isFinite(sx) && Number.isFinite(sy) && (sx > 1 || sy > 1)) {
1498
+ sx = Math.max(1, sx); sy = Math.max(1, sy);
1499
+ arr.forEach(function (p) { p.x = cx + (p.x - cx) * sx; p.y = cy + (p.y - cy) * sy; });
1500
+ }
1501
+ var at = new Map(arr.map(function (n) { return [n.id, n]; }));
1502
+ memLayoutCache = { sig: sig, at: at };
1503
+ return {
1504
+ nodes: nodes.map(function (n) { var p = at.get(n.id); return Object.assign({}, n, { x: p.x, y: p.y }); }),
1505
+ edges: edges || [],
1506
+ };
1507
+ }
1508
+ // 范围/类型/状态/搜索/聚焦过滤;返回可绘制的子图。
1509
+ function visibleMemoryGraph(graph, opts) {
1510
+ graph = graph || { nodes: [], edges: [] };
1511
+ opts = opts || {};
1512
+ var range = opts.range || "all";
1513
+ var type = opts.type || "all";
1514
+ var status = opts.status || "active";
1515
+ var q = String(opts.query || "").trim().toLowerCase();
1516
+ var focus = opts.focus || null;
1517
+ var curWsScope = opts.curWsScope || null;
1518
+ var curSessScope = opts.curSessScope || null;
1519
+ var allNodes = (graph.nodes || []).map(function (n) { return n.id ? n : Object.assign({}, n, { id: n.scope }); });
1520
+ // 列表分页:更早的事实只进列表,不把图谱撑到上千节点。
1521
+ var extraFacts = Array.isArray(opts.extraFacts) ? opts.extraFacts : [];
1522
+ var sessMap = new Map();
1523
+ allNodes.forEach(function (n) { if (n.kind === "session") sessMap.set(n.scope, n); });
1524
+ function inRange(scope) {
1525
+ if (range === "all") return true;
1526
+ if (range === "workspace") return scope === curWsScope;
1527
+ if (range === "session") return scope === curSessScope;
1528
+ return scope === curWsScope || scope === curSessScope;
1529
+ }
1530
+ function factOk(n) {
1531
+ if (!n || n.kind !== "fact") return false;
1532
+ if (status !== "all" && n.status !== status) return false;
1533
+ if (type !== "all" && n.factType !== type) return false;
1534
+ if (q && (String(n.summary) + " " + String(n.key)).toLowerCase().indexOf(q) < 0) return false;
1535
+ if (!inRange(n.scope)) return false;
1536
+ if (focus) {
1537
+ if (focus.indexOf("ws:") === 0) {
1538
+ if (n.scope === focus) return true;
1539
+ if (String(n.scope).indexOf("sess:") === 0) {
1540
+ var s = sessMap.get(n.scope);
1541
+ return !!(s && s.workspaceScope === focus);
1542
+ }
1543
+ return false;
1544
+ }
1545
+ if (focus.indexOf("sess:") === 0) {
1546
+ if (n.scope === focus) return true;
1547
+ if (String(n.scope).indexOf("ws:") === 0 && ("sess:" + n.originSid) === focus) return true;
1548
+ return false;
1549
+ }
1550
+ }
1551
+ return true;
1552
+ }
1553
+ var graphFacts = allNodes.filter(factOk);
1554
+ var facts = graphFacts.slice();
1555
+ var factIds = new Set(facts.map(function (f) { return f.id; }));
1556
+ extraFacts.forEach(function (f) {
1557
+ if (!factOk(f) || factIds.has(f.id)) return;
1558
+ factIds.add(f.id);
1559
+ facts.push(f);
1560
+ });
1561
+ var keep = new Set();
1562
+ graphFacts.forEach(function (n) {
1563
+ keep.add(n.id); keep.add(n.scope);
1564
+ if (n.originSid) keep.add("sess:" + n.originSid);
1565
+ });
1566
+ if (range !== "all") {
1567
+ if (curWsScope) keep.add(curWsScope);
1568
+ if (curSessScope) keep.add(curSessScope);
1569
+ }
1570
+ Array.from(keep).forEach(function (id) {
1571
+ var s = sessMap.get(id);
1572
+ if (s && s.workspaceScope) keep.add(s.workspaceScope);
1573
+ });
1574
+ var nodes = allNodes.filter(function (n) {
1575
+ if (n.kind === "fact") return keep.has(n.id);
1576
+ if (range === "all" && !focus) return true;
1577
+ return keep.has(n.scope);
1578
+ });
1579
+ var ids = new Set(nodes.map(function (n) { return n.id; }));
1580
+ var edges = (graph.edges || []).filter(function (x) { return ids.has(x.source) && ids.has(x.target); });
1581
+ return { nodes: nodes, edges: edges, facts: facts, graphFacts: graphFacts };
1582
+ }
1583
+ var MEM_ENV = { memoryEnabled: "DSH_MEMORY", memoryLlmEnabled: "DSH_MEMORY_LLM", memoryInjectCatalog: "DSH_MEMORY_INJECT", memoryHandoff: "DSH_MEMORY_HANDOFF", memoryConsolidateAuto: "DSH_MEMORY_CONSOLIDATE", memoryJanitorEnabled: "DSH_MEMORY_JANITOR" };
1584
+ var MEM_LOCK = { memoryEnabled: "enabled", memoryLlmEnabled: "llmEnabled", memoryInjectCatalog: "injectCatalog", memoryHandoff: "handoff", memoryConsolidateAuto: "consolidateAuto", memoryJanitorEnabled: "janitorEnabled" };
1585
+ // 注入回执:把“模型这轮到底看到了什么”从 console 搬到面板。全文是不可信数据,不执行。
1586
+ function InjectionReceipt(inj) {
1587
+ if (!inj) return null;
1588
+ if (inj.legacy) return e("div", { className: "ci-sub", style: { marginTop: 6 } }, "注入回执:旧版只记录了“已注入”,无原文");
1589
+ return e("div", { className: "ci-sub", style: { marginTop: 6 } },
1590
+ "注入回执 " + inj.chars + " 字 · sha " + (inj.sha1 || "-") + (inj.handover ? " · 含交接卡" : ""),
1591
+ inj.text ? e("details", null,
1592
+ e("summary", { style: { cursor: "pointer" } }, "查看注入原文(不可信背景)"),
1593
+ e("pre", { style: { whiteSpace: "pre-wrap", maxHeight: 180, overflow: "auto", fontSize: 11, margin: "4px 0 0" } }, inj.text)) : null);
1594
+ }
1595
+ function MemoryView(props) {
1596
+ var _d = useState(null), data = _d[0], setData = _d[1];
1597
+ var _e = useState(null), err = _e[0], setErr = _e[1];
1598
+ var _b = useState(false), busy = _b[0], setBusy = _b[1];
1599
+ var sid = (props && props.sessionId) || "";
1600
+ // 记忆范围:current = 当前工作区共享 + 本会话私有;缺省打开具体会话时看当前上下文。
1601
+ var _rg = useState(sid ? "current" : "all"), range = _rg[0], setRange = _rg[1];
1602
+ var _tab = useState("graph"), tab = _tab[0], setTab = _tab[1];
1603
+ var _ty = useState("all"), type = _ty[0], setType = _ty[1];
1604
+ var _stt = useState("active"), status = _stt[0], setStatus = _stt[1];
1605
+ var _q = useState(""), query = _q[0], setQuery = _q[1];
1606
+ var _sel = useState([]), selected = _sel[0], setSelected = _sel[1];
1607
+ var _fo = useState(null), focus = _fo[0], setFocus = _fo[1];
1608
+ var _vp = useState({ x: 0, y: 0, k: 1 }), viewport = _vp[0], setViewport = _vp[1];
1609
+ var _hv = useState(null), hover = _hv[0], setHover = _hv[1];
1610
+ // 编译模型覆盖(control.memoryLlmProvider/Model):空 = 跟随会话当前模型。
1611
+ var _mp = useState(""), memP = _mp[0], setMemP = _mp[1];
1612
+ var _mm = useState(""), memM = _mm[0], setMemM = _mm[1];
1613
+ var _mps = useState([]), memProviders = _mps[0], setMemProviders = _mps[1];
1614
+ var _mms = useState([]), memModels = _mms[0], setMemModels = _mms[1];
1615
+ var _mde = useState(null), memMdlErr = _mde[0], setMemMdlErr = _mde[1];
1616
+ // 证据钻取:选中事实后懒加载证据链;切换选择时清空,避免旧证据短暂挂在新事实上。
1617
+ var _evd = useState(null), evData = _evd[0], setEvData = _evd[1];
1618
+ var _evx = useState(null), evErr = _evx[0], setEvErr = _evx[1];
1619
+ // 画布比例跟随容器:固定 900x560 在 600x220 的槽里会按高度缩到 0.39,标签实际只有 5–6px。
1620
+ var _gb = useState({ w: 900, h: 560 }), graphBox = _gb[0], setGraphBox = _gb[1];
1621
+ // roving tabindex:整个图谱只占一个 Tab 位,方向键在节点间移动。
1622
+ var _kb = useState(null), kbId = _kb[0], setKbId = _kb[1];
1623
+ var _zn = useState(false), zen = _zn[0], setZen = _zn[1];
1624
+ // 时间窗(天):0=全部;只影响事实/最近记录,不改全库计数。
1625
+ var _sd = useState(0), sinceDays = _sd[0], setSinceDays = _sd[1];
1626
+ // 列表分页:更早的事实只在列表追加;查询签名变化时清空,避免跨范围串页。
1627
+ var _old = useState({ sig: "", facts: [], cursor: null, hasMore: false, pages: 0, loading: false, err: null }), older = _old[0], setOlder = _old[1];
1628
+ // 图谱独立端点:graph tab 激活时按需拉取,避免 5s 轮询 /api/memory/status 携带 64.9% 的图谱负载。
1629
+ var _gd = useState(null), graphData = _gd[0], setGraphData = _gd[1];
1630
+ // 框选:在图谱空白处拖拽画矩形,松开时选中矩形内所有节点。
1631
+ var _bs = useState(null), boxSelect = _bs[0], setBoxSelect = _bs[1];
1632
+ var boxRef = useRef(null);
1633
+ // 检索轨迹(Phase 4):按当前会话加载,详情懒加载;5s 轮询只在轨迹页可见时进行。
1634
+ // 注意:新增 useState 一律追加在既有状态之后,渲染冒烟测试按钩子顺序播种。
1635
+ var _tr = useState({ sig: "", rows: [], cursor: null, hasMore: false, loading: false, err: null }), traces = _tr[0], setTraces = _tr[1];
1636
+ var _tt = useState(""), traceTrigger = _tt[0], setTraceTrigger = _tt[1];
1637
+ var _trd = useState(null), traceDetail = _trd[0], setTraceDetail = _trd[1];
1638
+ var _trx = useState(null), traceDetailErr = _trx[0], setTraceDetailErr = _trx[1];
1639
+ // 指标(/api/memory/metrics)与巡检操作提示:只在设置页按需拉取,不参与 5s 轮询。
1640
+ var _mtr = useState(null), metrics = _mtr[0], setMetrics = _mtr[1];
1641
+ var _jmsg = useState(null), janitorMsg = _jmsg[0], setJanitorMsg = _jmsg[1];
1642
+ var traceSig = sid + "|" + traceTrigger;
1643
+ var tracesRef = useRef(traces);
1644
+ tracesRef.current = traces;
1645
+ var listSig = [sid, range, focus || "", sinceDays].join("|");
1646
+ var wrapRef = useRef(null);
1647
+ var memPicked = useRef(false); // 用户手动改过后,轮询不再覆盖选择
1648
+ var dragRef = useRef(null);
1649
+ var svgRef = useRef(null);
1650
+ var viewRef = useRef(viewport);
1651
+ viewRef.current = viewport;
1652
+ function loadMemModels(p) {
1653
+ if (!p) { setMemModels([]); return Promise.resolve(); }
1654
+ return fetch("/api/context-inject/models", {
1655
+ method: "POST", headers: { "Content-Type": "application/json" },
1656
+ body: JSON.stringify({ provider: p }),
1657
+ }).then(function (r) { return r.json(); }).then(function (j) {
1658
+ if (j && j.ok === false) { setMemMdlErr((j && j.error) || "模型目录请求失败"); return; }
1659
+ setMemMdlErr(null);
1660
+ setMemModels((j && j.models) || []);
1661
+ }).catch(function (x) { setMemMdlErr("模型目录不可用:" + String((x && x.message) || x)); });
1662
+ }
1663
+ useEffect(function () {
1664
+ fetch("/api/context-inject/models", { headers: { "Content-Type": "application/json" } })
1665
+ .then(function (r) { return r.json(); })
1666
+ .then(function (j) { if (j && j.ok !== false) setMemProviders(j.providers || []); })
1667
+ .catch(function (x) { setMemMdlErr("模型目录不可用:" + String((x && x.message) || x)); });
1668
+ }, []);
1669
+ function saveMemRoute(p, m) {
1670
+ if (busy) return Promise.resolve();
1671
+ setBusy(true);
1672
+ return fetch("/api/memory/config", {
1673
+ method: "POST", headers: { "Content-Type": "application/json" },
1674
+ body: JSON.stringify({ memoryLlmProvider: p || null, memoryLlmModel: m || null }),
1675
+ }).then(function (r) { return r.json(); }).then(function (s) {
1676
+ if (!s || !s.ok) setErr(new Error((s && s.error) || "config failed"));
1677
+ else setErr(null);
1678
+ return pull();
1679
+ }).catch(function (x) { setErr(x); }).finally(function () { setBusy(false); });
1680
+ }
1681
+ function onMemProviderChange(ev) {
1682
+ var p = (ev && ev.target && ev.target.value) || "";
1683
+ memPicked.current = true;
1684
+ setMemP(p);
1685
+ setMemM("");
1686
+ if (!p) saveMemRoute("", "");
1687
+ }
1688
+ function onMemModelChange(ev) {
1689
+ var m = (ev && ev.target && ev.target.value) || "";
1690
+ memPicked.current = true;
1691
+ setMemM(m);
1692
+ if (m) saveMemRoute(memP, m);
1693
+ }
1694
+ function memoryQuery(useRange, opts) {
1695
+ var o = opts || {};
1696
+ var q = [];
1697
+ if (sid) q.push("sessionId=" + encodeURIComponent(sid));
1698
+ q.push("range=" + encodeURIComponent(useRange || range));
1699
+ // 聚焦某个工作区/会话时向服务端下钻并放宽上限;未聚焦仍保持 200 条默认值。
1700
+ var focusReq = focus && /^(ws|sess):/.test(focus) ? focus : "";
1701
+ q.push("factLimit=" + (o.factLimit || (focusReq ? 500 : 200)));
1702
+ if (focusReq) q.push("focusScope=" + encodeURIComponent(focusReq));
1703
+ var days = Number(sinceDays) || 0;
1704
+ if (days > 0) {
1705
+ var cut = new Date();
1706
+ cut.setHours(0, 0, 0, 0);
1707
+ q.push("sinceTs=" + (cut.getTime() - (days - 1) * 86400000));
1708
+ }
1709
+ if (o.cursor) q.push("cursor=" + encodeURIComponent(o.cursor));
1710
+ return q.join("&");
1711
+ }
1712
+ function pull(nextRange) {
1713
+ var useRange = nextRange || range;
1714
+ return fetch("/api/memory/status?" + memoryQuery(useRange), { headers: { "Content-Type": "application/json" } })
1715
+ .then(function (r) { return r.json(); })
1716
+ .then(function (s) {
1717
+ setData(s); setErr(null);
1718
+ // 服务端可能因会话没有工作区/记录而回退到 all;跟随它,否则当前范围会渲染成空图。
1719
+ if (s && s.rangeEffective && s.rangeEffective !== useRange) setRange(s.rangeEffective);
1720
+ return s;
1721
+ })
1722
+ .catch(function (x) { setErr(x); return null; });
1723
+ }
1724
+ function loadOlder() {
1725
+ var gs = (data && data.graph && data.graph.stats) || {};
1726
+ if (!gs.focusScope || older.loading) return;
1727
+ var baseFacts = older.sig === listSig ? older.facts : [];
1728
+ var cursor = older.sig === listSig && older.cursor ? older.cursor : gs.nextCursor;
1729
+ if (!cursor) return;
1730
+ setOlder({ sig: listSig, facts: baseFacts, cursor: cursor, hasMore: !!gs.hasMore, pages: older.sig === listSig ? older.pages : 0, loading: true, err: null });
1731
+ fetch("/api/memory/status?" + memoryQuery(range, { cursor: cursor, factLimit: 500 }), { headers: { "Content-Type": "application/json" } })
1732
+ .then(function (r) { return r.json(); })
1733
+ .then(function (j) {
1734
+ if (!j || j.ok === false) throw new Error((j && (j.error || j.message)) || "加载失败");
1735
+ var st = (j.graph && j.graph.stats) || {};
1736
+ var add = ((j.graph && j.graph.nodes) || []).filter(function (n) { return n.kind === "fact"; });
1737
+ var seen = new Set(baseFacts.map(function (f) { return f.id; }));
1738
+ var merged = baseFacts.slice();
1739
+ add.forEach(function (f) { if (!seen.has(f.id)) { seen.add(f.id); merged.push(f); } });
1740
+ setOlder({ sig: listSig, facts: merged, cursor: st.nextCursor || null, hasMore: !!st.hasMore, pages: (older.sig === listSig ? older.pages : 0) + 1, loading: false, err: null });
1741
+ })
1742
+ .catch(function (x) {
1743
+ setOlder({ sig: listSig, facts: baseFacts, cursor: cursor, hasMore: !!gs.hasMore, pages: older.sig === listSig ? older.pages : 0, loading: false, err: x });
1744
+ });
1745
+ }
1746
+ // ---- 检索轨迹:列表/详情/导出/清空(后端已脱敏 + 单会话隔离) ----
1747
+ function loadTraces(more) {
1748
+ if (!sid) return;
1749
+ var sig = traceSig;
1750
+ var prev = tracesRef.current;
1751
+ var base = prev.sig === sig ? prev : { sig: sig, rows: [], cursor: null, hasMore: false, loading: false, err: null };
1752
+ if (base.loading || (more && !base.cursor)) return;
1753
+ setTraces({ sig: sig, rows: base.rows, cursor: base.cursor, hasMore: base.hasMore, loading: true, err: null });
1754
+ var q = ["sessionId=" + encodeURIComponent(sid), "limit=50"];
1755
+ if (traceTrigger) q.push("trigger=" + encodeURIComponent(traceTrigger));
1756
+ if (more && base.cursor) q.push("cursor=" + encodeURIComponent(base.cursor));
1757
+ fetch("/api/memory/traces?" + q.join("&"), { headers: { "Content-Type": "application/json" } })
1758
+ .then(function (r) { return r.json(); })
1759
+ .then(function (j) {
1760
+ if (!j || j.ok === false) throw new Error((j && (j.error || j.message)) || "轨迹加载失败");
1761
+ var seen = new Set(base.rows.map(function (x) { return x.id; }));
1762
+ var add = (j.traces || []).filter(function (x) { return !seen.has(x.id); });
1763
+ setTraces({ sig: sig, rows: base.rows.concat(add), cursor: j.nextCursor || null, hasMore: !!j.hasMore, loading: false, err: null });
1764
+ })
1765
+ .catch(function (x) {
1766
+ setTraces({ sig: sig, rows: base.rows, cursor: base.cursor, hasMore: base.hasMore, loading: false, err: x });
1767
+ });
1768
+ }
1769
+ function openTrace(id) {
1770
+ setTraceDetail(null); setTraceDetailErr(null);
1771
+ fetch("/api/memory/traces/" + encodeURIComponent(id), { headers: { "Content-Type": "application/json" } })
1772
+ .then(function (r) {
1773
+ // 宿主未匹配到路由时返回纯文本 "not found";直接 r.json() 会把
1774
+ // 面板错误变成 `Unexpected token 'o'`。先读文本,给出可读原因。
1775
+ return r.text().then(function (t) {
1776
+ var j = null;
1777
+ try { j = JSON.parse(t); } catch (e) { /* 非 JSON */ }
1778
+ if (!j) {
1779
+ var why = r.status === 404 ? "接口未匹配(404)" : ("HTTP " + r.status);
1780
+ throw new Error(why + (t ? ":" + String(t).slice(0, 120) : ""));
1781
+ }
1782
+ return j;
1783
+ });
1784
+ })
1785
+ .then(function (j) {
1786
+ if (!j || j.ok === false) throw new Error((j && j.error) || "轨迹加载失败");
1787
+ if (!j.trace) throw new Error("轨迹不存在或已清理");
1788
+ setTraceDetail(j.trace);
1789
+ })
1790
+ .catch(function (x) { setTraceDetailErr(x); });
1791
+ }
1792
+ function clearTraceHistory() {
1793
+ if (!sid || traces.loading) return;
1794
+ fetch("/api/memory/traces/clear", {
1795
+ method: "POST", headers: { "Content-Type": "application/json" },
1796
+ body: JSON.stringify({ sessionId: sid }),
1797
+ }).then(function (r) { return r.json(); }).then(function (j) {
1798
+ if (!j || !j.ok) throw new Error((j && j.error) || "清空失败");
1799
+ setTraceDetail(null);
1800
+ // 不要紧接着 loadTraces:tracesRef 还是旧行,合并会把刚清掉的轨迹又加回来。
1801
+ // 下一次 5s 轮询会从空状态重新拉取。
1802
+ setTraces({ sig: traceSig, rows: [], cursor: null, hasMore: false, loading: false, err: null });
1803
+ }).catch(function (x) { setTraceDetailErr(x); });
1804
+ }
1805
+ function exportTraceHistory() {
1806
+ if (!sid || typeof document === "undefined") return;
1807
+ var a = document.createElement("a");
1808
+ a.href = "/api/memory/traces/export?sessionId=" + encodeURIComponent(sid) + "&format=jsonl";
1809
+ a.download = "";
1810
+ document.body.appendChild(a); a.click(); a.remove();
1811
+ }
1812
+ function pullGraph() {
1813
+ if (tab !== "graph") return;
1814
+ var focusReq = focus && /^(ws|sess):/.test(focus) ? focus : "";
1815
+ var q = [];
1816
+ if (sid) q.push("sessionId=" + encodeURIComponent(sid));
1817
+ q.push("range=" + encodeURIComponent(range));
1818
+ q.push("factLimit=" + (focusReq ? 500 : 200));
1819
+ if (focusReq) q.push("focusScope=" + encodeURIComponent(focusReq));
1820
+ var days = Number(sinceDays) || 0;
1821
+ if (days > 0) {
1822
+ var cut = new Date();
1823
+ cut.setHours(0, 0, 0, 0);
1824
+ q.push("sinceTs=" + (cut.getTime() - (days - 1) * 86400000));
1825
+ }
1826
+ fetch("/api/memory/graph?" + q.join("&"), { headers: { "Content-Type": "application/json" } })
1827
+ .then(function (r) { return r.json(); })
1828
+ .then(function (j) {
1829
+ if (j && j.ok !== false && j.graph) setGraphData(j.graph);
1830
+ })
1831
+ .catch(function () { /* graph fetch failure is non-fatal */ });
1832
+ }
1833
+ useEffect(function () {
1834
+ setOlder({ sig: listSig, facts: [], cursor: null, hasMore: false, pages: 0, loading: false, err: null });
1835
+ }, [listSig]);
1836
+ // 会话或触发筛选变化:清空轨迹列表与详情,避免旧会话轨迹短暂串台。
1837
+ useEffect(function () {
1838
+ setTraceDetail(null); setTraceDetailErr(null);
1839
+ setTraces({ sig: traceSig, rows: [], cursor: null, hasMore: false, loading: false, err: null });
1840
+ }, [traceSig]);
1841
+ // 轨迹页可见时才轮询;切走立即停(与主状态轮询同口径)。
1842
+ useEffect(function () {
1843
+ if (tab !== "traces" || !sid) return;
1844
+ loadTraces(false);
1845
+ function tick() { if (typeof document !== "undefined" && document.hidden) return; loadTraces(false); }
1846
+ var iv = setInterval(tick, 5000);
1847
+ return function () { clearInterval(iv); };
1848
+ }, [tab, traceSig]);
1849
+ // 指标只在设置页拉一次;失败不影响其它区(metrics.json 可能尚未生成)。
1850
+ useEffect(function () {
1851
+ if (tab !== "settings") return;
1852
+ fetch("/api/memory/metrics", { headers: { "Content-Type": "application/json" } })
1853
+ .then(function (r) { return r.json(); })
1854
+ .then(function (j) { setMetrics(j && j.ok ? j.metrics : null); })
1855
+ .catch(function () { setMetrics(null); });
1856
+ }, [tab]);
1857
+ useEffect(function () {
1858
+ // 页面不可见时停轮询;切回来立即补一帧(5s 固定间隔在后台纯属浪费)
1859
+ function tick() { if (typeof document !== "undefined" && document.hidden) return; pull(); }
1860
+ function onVis() { if (!document.hidden) pull(); }
1861
+ var iv = setInterval(tick, 5000);
1862
+ pull();
1863
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVis);
1864
+ return function () {
1865
+ clearInterval(iv);
1866
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVis);
1867
+ };
1868
+ }, [sid, range, focus, sinceDays]);
1869
+ useEffect(function () { setRange(sid ? "current" : "all"); setFocus(null); setSelected([]); }, [sid]);
1870
+ // 图谱 tab 激活时按需拉取独立端点;参数变化时重新拉取。
1871
+ useEffect(function () {
1872
+ if (tab === "graph") pullGraph();
1873
+ }, [tab, sid, range, focus, sinceDays]);
1874
+ // 图谱滚轮缩放:passive:false,避免 React 合成 wheel 不能 preventDefault。
1875
+ // 用 ref 取当前视口:平移后 k 不变,闭包里的旧 x/y 会让缩放把画面拽回旧位置。
1876
+ // 依赖 graphReady:首帧数据未到时不渲染 svg,只挂 [tab] 会让监听永远缺失。
1877
+ var hasGraph = !!graphData;
1878
+ useEffect(function () {
1879
+ var el = svgRef.current;
1880
+ if (!el || !el.addEventListener) return;
1881
+ function onWheel(ev) {
1882
+ ev.preventDefault();
1883
+ var vp = viewRef.current;
1884
+ var nk = Math.max(0.55, Math.min(2.4, vp.k * (ev.deltaY < 0 ? 1.12 : 0.9)));
1885
+ if (nk === vp.k) return;
1886
+ var nx = vp.x, ny = vp.y;
1887
+ try {
1888
+ var ctm = el.getScreenCTM && el.getScreenCTM();
1889
+ if (ctm && el.createSVGPoint) {
1890
+ var pt = el.createSVGPoint();
1891
+ pt.x = ev.clientX; pt.y = ev.clientY;
1892
+ var p = pt.matrixTransform(ctm.inverse());
1893
+ nx = p.x - (p.x - vp.x) * (nk / vp.k);
1894
+ ny = p.y - (p.y - vp.y) * (nk / vp.k);
1895
+ }
1896
+ } catch { /* keep center zoom */ }
1897
+ setViewport({ x: nx, y: ny, k: nk });
1898
+ }
1899
+ el.addEventListener("wheel", onWheel, { passive: false });
1900
+ return function () { el.removeEventListener("wheel", onWheel); };
1901
+ }, [tab, hasGraph]);
1902
+ // 按实际容器比例调整 viewBox(宽度固定 900,高度按容器高宽比换算)。
1903
+ // 不这么做时,宽扁容器会按高度缩放,14px 标签只剩约 5.5px。
1904
+ useEffect(function () {
1905
+ var el = wrapRef.current;
1906
+ if (!el || !el.getBoundingClientRect) return;
1907
+ function fit() {
1908
+ var r = el.getBoundingClientRect();
1909
+ if (!(r.width > 0) || !(r.height > 0)) return;
1910
+ var H = Math.max(280, Math.min(620, Math.round(900 * r.height / r.width)));
1911
+ setGraphBox(function (prev) { return (prev.w === 900 && Math.abs(prev.h - H) < 8) ? prev : { w: 900, h: H }; });
1912
+ }
1913
+ fit();
1914
+ var ro = null;
1915
+ if (typeof ResizeObserver !== "undefined") { ro = new ResizeObserver(fit); ro.observe(el); }
1916
+ return function () { if (ro) ro.disconnect(); };
1917
+ }, [tab, hasGraph]);
1918
+ // 证据链懒加载:只在选中事实时请求,不把证据塞进 5s 轮询的状态响应。
1919
+ // 列表分页载入的旧事实不在首屏图谱里,也要能回查证据。
1920
+ useEffect(function () {
1921
+ if (!selected.length) { setEvData(null); setEvErr(null); return; }
1922
+ if (selected.length > 1) { setEvData(null); setEvErr(null); return; }
1923
+ var pool = ((graphData && graphData.nodes) || []).concat(older.facts || []);
1924
+ var node = pool.filter(function (x) { return x.id === selected[0]; })[0];
1925
+ if (!node || node.kind !== "fact") { setEvData(null); setEvErr(null); return; }
1926
+ var alive = true;
1927
+ setEvData(null); setEvErr(null);
1928
+ fetch("/api/memory/evidence?scope=" + encodeURIComponent(node.scope) + "&key=" + encodeURIComponent(node.key), { headers: { "Content-Type": "application/json" } })
1929
+ .then(function (r) { return r.json(); })
1930
+ .then(function (j) {
1931
+ if (!alive) return;
1932
+ if (j && j.ok) setEvData(j);
1933
+ else setEvErr(new Error((j && j.error) || "evidence-failed"));
1934
+ })
1935
+ .catch(function (x) { if (alive) setEvErr(x); });
1936
+ return function () { alive = false; };
1937
+ }, [selected, older.facts]);
1938
+ // 首次拿到快照 / 服务端路由变化时同步选择器;用户手动改过后不再覆盖。
1939
+ var memRouteP = data && data.llmRoute ? (data.llmRoute.provider || "") : "";
1940
+ var memRouteM = data && data.llmRoute ? (data.llmRoute.model || "") : "";
1941
+ useEffect(function () {
1942
+ if (memPicked.current) return;
1943
+ setMemP(memRouteP);
1944
+ setMemM(memRouteM);
1945
+ }, [memRouteP, memRouteM]);
1946
+ useEffect(function () { loadMemModels(memP); }, [memP]);
1947
+ function applyConfig(patch) {
1948
+ if (busy) return;
1949
+ setBusy(true);
1950
+ fetch("/api/memory/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(patch || {}) })
1951
+ .then(function (r) { return r.json(); })
1952
+ .then(function (s) {
1953
+ if (!s || !s.ok) setErr(new Error((s && s.error) || "config failed"));
1954
+ else setErr(null);
1955
+ })
1956
+ .catch(function (x) { setErr(x); })
1957
+ .then(function () { setBusy(false); pull(); });
1958
+ }
1959
+ function applyFlag(key, value) {
1960
+ var body = {}; body[key] = value;
1961
+ applyConfig(body);
1962
+ }
1963
+ if (!data) {
1964
+ return e("div", { className: "ci-root" },
1965
+ e("div", { className: "ci-hd" },
1966
+ e("span", { className: "ci-badge ci-off" }),
1967
+ e("div", null,
1968
+ e("div", { className: "ci-title" }, "外部记忆"),
1969
+ e("div", { className: "ci-sub" }, err ? ("读取失败:" + String(err.message || err)) : "读取中…")),
1970
+ e("button", { className: "ci-btn", style: { marginLeft: "auto" }, onClick: function () { pull(range); } }, "重试")));
1971
+ }
1972
+ var f = data.flags || {};
1973
+ var t = data.totals || {};
1974
+ var db = data.db || {};
1975
+ var co = data.consolidate || {};
1976
+ var coi = co.inflight || null;
1977
+ var col = co.last || null;
1978
+ var coManual = co.manualRoute || null;
1979
+ var coManText = (coManual && coManual.provider)
1980
+ ? ("手动模型:" + coManual.provider + " / " + (coManual.model || "—"))
1981
+ : "手动模型:跟随编译/会话模型";
1982
+ var conext = co.next || null;
1983
+ var coIdleMin = Math.max(1, Math.round((Number(co.idleMs) || 900000) / 60000));
1984
+ var coReady = !!(f.enabled && f.llmEnabled);
1985
+ var coStage = coi ? ({ sweep: "核对文件", gather: "收集事件", llm: "模型考证", repair: "修复输出", apply: "写入结果" }[coi.stage] || "整理中") : "";
1986
+ var coSec = coi && Number(coi.startedAt) ? Math.max(0, Math.round((Date.now() - Number(coi.startedAt)) / 1000)) : null;
1987
+ var coNextAt = conext && Number(conext.nextAt) ? Number(conext.nextAt) : null;
1988
+ var coReasonText = {
1989
+ "no-route": "没有可用的模型路由(请在「编译模型」里指定)",
1990
+ "disabled": "记忆层或轻 LLM 编译未开启",
1991
+ "no-workspace": "找不到该会话的工作区",
1992
+ "llm-failed": "模型调用失败",
1993
+ "parse-failed": "模型输出未通过校验",
1994
+ "busy": "已有整理在运行",
1995
+ "error": "整理过程异常",
1996
+ };
1997
+ var coCountdown = !sid ? "打开具体会话后可手动整理"
1998
+ : (co.auto === false ? "自动整理已关闭(仍可手动)"
1999
+ : (!coNextAt ? "空闲时自动整理" : (coNextAt <= Date.now() ? "即将整理" : "约 " + Math.max(1, Math.round((coNextAt - Date.now()) / 60000)) + " 分钟后")));
2000
+ var coParts = [];
2001
+ if (col && col.ok) {
2002
+ if (col.invalidated) coParts.push(col.invalidated + " 条失效");
2003
+ if (col.superseded) coParts.push(col.superseded + " 条取代");
2004
+ if (col.merged) coParts.push(col.merged + " 条合并");
2005
+ if (col.added) coParts.push(col.added + " 条新增");
2006
+ if (col.skipped) coParts.push(col.skipped + " 条未通过校验");
2007
+ if (col.protected) coParts.push(col.protected + " 条约束受保护");
2008
+ if (!coParts.length) coParts.push("没有需要变更的事实");
2009
+ }
2010
+ var coLastText = col ? (col.ok
2011
+ ? ("上次整理:" + coParts.join(" · ") + (col.ms != null ? " · " + (col.ms / 1000).toFixed(1) + "s" : "") + (col.model ? " · " + col.model : ""))
2012
+ : ("上次整理失败:" + (coReasonText[col.reason] || col.reason || "未知"))) : null;
2013
+ // ---- 文件巡检管家(Phase A):开关、间隔、立即巡检与上次结果 ----
2014
+ var jn = data.janitor || {};
2015
+ var jlast = jn.last || null;
2016
+ var jinf = jn.inflight || null;
2017
+ var jReady = !!(f.enabled && f.llmEnabled && jn.enabled);
2018
+ var jIntervalMin = Math.max(1, Math.round((Number(jn.intervalMs) || 1800000) / 60000));
2019
+ // 环境变量/control.json 可能给非预设值(如 15 分钟);保留当前值,避免下拉框显示空白。
2020
+ var jIntervalOpts = [10, 30, 60, 360, 1440];
2021
+ if (jIntervalOpts.indexOf(jIntervalMin) < 0) {
2022
+ jIntervalOpts.push(jIntervalMin);
2023
+ jIntervalOpts.sort(function (a, b) { return a - b; });
2024
+ }
2025
+ var jStage = jinf ? ({ collect: "收集候选", llm: "模型检查", apply: "写入结果" }[jinf.stage] || "巡检中") : "";
2026
+ var jSec = jinf && (jinf.startedAt || jinf.at) ? Math.max(0, Math.round((Date.now() - Number(jinf.startedAt || jinf.at)) / 1000)) : null;
2027
+ var jReasonText = {
2028
+ "no-candidates": "没有超过 24 小时未验证的文件事实",
2029
+ "disabled": "记忆层、轻 LLM 编译或文件巡检未开启",
2030
+ "no-route": "没有可用的模型路由",
2031
+ "no-workspace": "找不到该会话的工作区",
2032
+ "busy": "已有巡检在运行",
2033
+ "no-operations": "模型没有给出可执行操作",
2034
+ "llm-failed": "模型调用失败",
2035
+ "timeout": "模型调用超时",
2036
+ "parse-failed": "模型输出未通过校验",
2037
+ };
2038
+ var jOps = (jlast && jlast.ops) || null;
2039
+ // 自动巡检记在工作区最近会话名下:跨会话回退时标明来源,避免误以为是本会话刚跑过。
2040
+ var jFrom = jlast && jlast.fromSid ? ("(工作区会话 " + String(jlast.fromSid).slice(0, 8) + ")") : "";
2041
+ var jLastText = jlast ? (jlast.skipped
2042
+ ? ("上次巡检" + jFrom + ":" + (jReasonText[jlast.reason] || jlast.reason || "跳过"))
2043
+ : (jlast.ok
2044
+ ? ("上次巡检" + jFrom + ":刷新 " + (jOps ? (jOps.refresh || 0) : 0) + " · 失效 " + (jOps ? (jOps.forget || 0) : 0)
2045
+ + " · 拒绝 " + (jOps ? (jOps.rejected || 0) : 0) + (jlast.facts ? (" · 候选 " + jlast.facts) : "")
2046
+ + (jlast.ms != null ? " · " + (jlast.ms / 1000).toFixed(1) + "s" : "")
2047
+ + (jlast.model && jlast.model.model ? " · " + jlast.model.model : ""))
2048
+ : ("上次巡检失败" + jFrom + ":" + (jReasonText[jlast.reason] || jlast.reason || "未知")))) : null;
2049
+ function janitorNow() {
2050
+ if (!sid || busy || jinf) return;
2051
+ setJanitorMsg(null);
2052
+ setBusy(true);
2053
+ fetch("/api/memory/janitor", {
2054
+ method: "POST", headers: { "Content-Type": "application/json" },
2055
+ body: JSON.stringify({ sessionId: sid }),
2056
+ }).then(function (r) { return r.json(); }).then(function (j) {
2057
+ if (!j || !j.ok) throw new Error((j && j.error) || "触发失败");
2058
+ setJanitorMsg("已排队:2 秒内开始,完成后自动刷新");
2059
+ setTimeout(function () { pull(); }, 2500);
2060
+ setTimeout(function () { pull(); }, 9000);
2061
+ }).catch(function (x) { setJanitorMsg("触发失败:" + String((x && x.message) || x)); })
2062
+ .then(function () { setBusy(false); });
2063
+ }
2064
+ // ---- 只读诊断:向量层当前状态 + metrics.json 成功率 ----
2065
+ var mv = data.vector || null;
2066
+ var mtr = metrics || null;
2067
+ function rateOf(o) {
2068
+ return o && Number(o.total) > 0 ? Math.round(100 * Number(o.success || 0) / Number(o.total)) + "%" : "—";
2069
+ }
2070
+ function hitOf(o) {
2071
+ return o && Number(o.total) > 0 ? Math.round(100 * Number(o.hit || 0) / Number(o.total)) + "%" : "—";
2072
+ }
2073
+ function consolidateNow() {
2074
+ if (busy || coi || !sid) return;
2075
+ setBusy(true);
2076
+ fetch("/api/memory/consolidate", {
2077
+ method: "POST", headers: { "Content-Type": "application/json" },
2078
+ body: JSON.stringify({ sessionId: sid }),
2079
+ }).then(function (r) { return r.json(); }).then(function (j) {
2080
+ if (!j || !j.ok) setErr(new Error((j && j.error) || "consolidate failed"));
2081
+ else setErr(null);
2082
+ }).catch(function (x) { setErr(x); }).then(function () {
2083
+ setBusy(false);
2084
+ setTimeout(function () { pull(); }, 800);
2085
+ setTimeout(function () { pull(); }, 3500);
2086
+ });
2087
+ }
2088
+ var lock = data.envLocked || {};
2089
+ var lockN = ["enabled", "llmEnabled", "injectCatalog"].filter(function (k) { return lock[k] === true; }).length;
2090
+ var armed = data.armed === true;
2091
+ // 三种真实状态:运行中(本进程已武装)/ 待重启(配置已开但启动时未加载)/ 未启用(已热停写或配置关闭)
2092
+ var st = !f.enabled ? "off" : (armed ? "on" : "pending");
2093
+ var stText = st === "on" ? "运行中" : st === "pending" ? "待重启" : "未启用";
2094
+ var banner = !armed && !f.enabled
2095
+ ? "记忆层未启用:不写入、不注册工具、不注入目录。启用后需重启 dsh web。"
2096
+ : (!armed && f.enabled
2097
+ ? "配置已启用,但当前进程启动时未加载记忆插件:重启 dsh web 后生效。"
2098
+ : (!f.enabled ? "已停止写入:不新增记录、不编译、不注入;memory_* 仍可读取既有记忆。重新开启即时恢复。" : null));
2099
+ var maxKind = 1;
2100
+ (data.kinds || []).forEach(function (k) { if (k.n > maxKind) maxKind = k.n; });
2101
+ function kpi(label, value, warn, title) {
2102
+ return e("div", { className: "mem-kpi" + (warn ? " mem-warn" : ""), title: title || null },
2103
+ e("b", null, value), e("span", null, label));
2104
+ }
2105
+ function flagRow(key, label, hint, on) {
2106
+ var locked = lock[MEM_LOCK[key]] === true;
2107
+ var dis = busy || locked;
2108
+ return e("div", { key: key, className: "mem-flag" },
2109
+ e("div", { className: "mem-flag-t" },
2110
+ e("div", { className: "ci-title", style: { fontSize: 12 } }, label),
2111
+ e("div", { className: "ci-sub" }, hint + (locked ? " · 由 " + MEM_ENV[key] + " 锁定" : ""))),
2112
+ e("div", {
2113
+ className: "ci-toggle" + (on ? " mem-on" : "") + (dis ? " mem-dis" : ""),
2114
+ role: "switch",
2115
+ "aria-checked": on === true,
2116
+ "aria-label": label,
2117
+ "aria-disabled": dis,
2118
+ tabIndex: dis ? -1 : 0,
2119
+ title: locked ? ("环境变量 " + MEM_ENV[key] + " 覆盖此开关,面板写入无效")
2120
+ : (busy ? "正在保存…" : "点击切换(运行时生效)"),
2121
+ onClick: function () { if (!dis) applyFlag(key, !on); },
2122
+ onKeyDown: function (ev) {
2123
+ if (dis) return;
2124
+ if (ev.key === "Enter" || ev.key === " " || ev.key === "Spacebar") {
2125
+ ev.preventDefault();
2126
+ applyFlag(key, !on);
2127
+ }
2128
+ },
2129
+ },
2130
+ e("span", { className: "ci-switch" }),
2131
+ e("span", { className: "mem-flag-state" }, locked ? "锁定" : (on ? "开" : "关"))));
2132
+ }
2133
+ // ---- 当前上下文与图谱视图 ----
2134
+ var cur = data.current || null;
2135
+ var curWs = (cur && cur.workspace) || null;
2136
+ var curSess = (cur && cur.session) || null;
2137
+ var curWsScope = curWs ? curWs.scope : null;
2138
+ var curSessScope = curSess ? curSess.scope : null;
2139
+ var graph = graphData || { nodes: [], edges: [], stats: {} };
2140
+ var olderFacts = older.sig === listSig ? older.facts : [];
2141
+ var visible = visibleMemoryGraph(graph, {
2142
+ range: range, type: type, status: status, query: query, focus: focus,
2143
+ curWsScope: curWsScope, curSessScope: curSessScope,
2144
+ extraFacts: tab === "list" ? olderFacts : [],
2145
+ });
2146
+ var factsVisible = visible.facts || [];
2147
+ var graphFactsVisible = visible.graphFacts || factsVisible;
2148
+ var selectedNode = null, focusNode = null;
2149
+ (graph.nodes || []).forEach(function (n) {
2150
+ if (selected.length === 1 && n.id === selected[0]) selectedNode = n;
2151
+ if (focus && n.scope === focus) focusNode = n;
2152
+ });
2153
+ if (!selectedNode && selected.length === 1) {
2154
+ selectedNode = olderFacts.filter(function (n) { return n.id === selected[0]; })[0] || null;
2155
+ }
2156
+ var factTypeText = { state: "状态", constraint: "约束", task: "任务", history: "历史" };
2157
+ function rangeSelect() {
2158
+ var opts = sid
2159
+ ? [["current", "当前上下文"], ["workspace", "本工作区"], ["session", "本会话私有"], ["all", "全部"]]
2160
+ : [["all", "全部"]];
2161
+ return e("select", {
2162
+ className: "ci-sel", value: sid ? range : "all",
2163
+ "aria-label": "范围选择",
2164
+ title: "默认跟随当前选中的会话页;展开可看其他工作区",
2165
+ onChange: function (ev) {
2166
+ setFocus(null); setSelected([]);
2167
+ setRange((ev && ev.target && ev.target.value) || (sid ? "current" : "all"));
2168
+ },
2169
+ }, opts.map(function (o) { return e("option", { key: o[0], value: o[0] }, o[1]); }));
2170
+ }
2171
+ function typeSelect() {
2172
+ var opts = [["all", "全部类型"], ["constraint", "约束"], ["state", "状态"], ["task", "任务"], ["history", "历史"]];
2173
+ return e("select", { className: "ci-sel", value: type, "aria-label": "类型过滤", title: "按事实类型过滤",
2174
+ onChange: function (ev) { setType((ev && ev.target && ev.target.value) || "all"); } },
2175
+ opts.map(function (o) { return e("option", { key: o[0], value: o[0] }, o[1]); }));
2176
+ }
2177
+ function statusSelect() {
2178
+ var opts = [["active", "有效"], ["all", "全部状态"], ["stale", "已失效"], ["superseded", "被取代"]];
2179
+ return e("select", { className: "ci-sel", value: status, "aria-label": "状态过滤", title: "按生命周期状态过滤",
2180
+ onChange: function (ev) { setStatus((ev && ev.target && ev.target.value) || "active"); } },
2181
+ opts.map(function (o) { return e("option", { key: o[0], value: o[0] }, o[1]); }));
2182
+ }
2183
+ function timeSelect() {
2184
+ var opts = [["0", "全部时间"], ["7", "最近 7 天"], ["30", "最近 30 天"], ["90", "最近 90 天"]];
2185
+ return e("select", { className: "ci-sel", value: String(sinceDays || 0), "aria-label": "时间范围", title: "按事实更新时间过滤(只影响事实与最近记录,不改全库计数)",
2186
+ onChange: function (ev) { setSelected([]); setSinceDays(Number((ev && ev.target && ev.target.value) || 0) || 0); } },
2187
+ opts.map(function (o) { return e("option", { key: o[0], value: o[0] }, o[1]); }));
2188
+ }
2189
+ function focusChip() {
2190
+ if (!focus) return null;
2191
+ return e("span", { className: "mem-focus" },
2192
+ e("span", null, "聚焦 " + (focusNode ? memNodeLabel(focusNode) : memScope(focus))),
2193
+ e("button", { className: "mem-focus-x", "aria-label": "清除聚焦", title: "清除聚焦", onClick: function () { setFocus(null); } }, "×"));
2194
+ }
2195
+ function expandButton() {
2196
+ if (range !== "all") {
2197
+ return e("button", { className: "ci-btn", title: "显示其他工作区与会话",
2198
+ onClick: function () { setRange("all"); setFocus(null); } }, "展开全部");
2199
+ }
2200
+ if (sid) {
2201
+ return e("button", { className: "ci-btn", title: "回到当前会话上下文",
2202
+ onClick: function () { setRange("current"); setFocus(null); setSelected([]); } }, "回到当前");
2203
+ }
2204
+ return null;
2205
+ }
2206
+ function selectNode(n, ev) {
2207
+ if (!n) return;
2208
+ var ctrlKey = ev && (ev.ctrlKey || ev.metaKey);
2209
+ var shiftKey = ev && ev.shiftKey;
2210
+ if (ctrlKey) {
2211
+ // Toggle in multi-select
2212
+ var idx = selected.indexOf(n.id);
2213
+ if (idx >= 0) {
2214
+ setSelected(selected.filter(function (id) { return id !== n.id; }));
2215
+ } else {
2216
+ setSelected(selected.concat([n.id]));
2217
+ }
2218
+ } else if (shiftKey && selected.length > 0) {
2219
+ // Range select: add all nodes between last selected and current
2220
+ // For now, just add to selection (proper range requires layout order)
2221
+ if (selected.indexOf(n.id) < 0) {
2222
+ setSelected(selected.concat([n.id]));
2223
+ }
2224
+ } else {
2225
+ // Single select
2226
+ setSelected([n.id]);
2227
+ }
2228
+ }
2229
+ function openNodeInList(n) {
2230
+ if (!n) return;
2231
+ setTab("list");
2232
+ setSelected(n.id);
2233
+ var inCur = n.scope === curWsScope || n.scope === curSessScope;
2234
+ if (n.kind === "fact") {
2235
+ // 可见节点已满足当前过滤;只有从详情栏打开一个被过滤掉的事实时才放宽筛选。
2236
+ var seen = (visible.facts || []).some(function (f) { return f.id === n.id; });
2237
+ if (!seen) { setType("all"); setStatus("all"); setQuery(""); }
2238
+ if (!inCur) setRange("all");
2239
+ setFocus(null);
2240
+ return;
2241
+ }
2242
+ if (n.kind === "session" || n.kind === "workspace") {
2243
+ if (!inCur) setRange("all");
2244
+ setFocus(n.scope);
2245
+ }
2246
+ }
2247
+ function tabButton(id, label) {
2248
+ return e("button", { key: id, className: "mem-tab" + (tab === id ? " on" : ""), role: "tab", "aria-selected": tab === id, onClick: function () { setTab(id); } }, label);
2249
+ }
2250
+ function overviewStrip() {
2251
+ return e("div", { className: "mem-strip" },
2252
+ e("span", null, "全库活跃记录 ", e("b", null, t.active || 0)),
2253
+ e("span", null, "当前上下文记录 ", e("b", null, cur ? cur.records : 0), " 条"),
2254
+ e("span", null, "当前有效洞察 ", e("b", null, cur ? cur.insights : 0)),
2255
+ e("span", null, "本会话私有记录 ", e("b", null, curSess ? curSess.records : 0)),
2256
+ e("span", null, "待处理错误 ", e("b", null, t.errors || 0)));
2257
+ }
2258
+ function memoryToolbar() {
2259
+ return e("div", { className: "mem-toolbar" },
2260
+ e("span", { className: "mem-toolbar-label" }, "范围"), rangeSelect(),
2261
+ e("span", { className: "mem-toolbar-label" }, "时间"), timeSelect(),
2262
+ e("span", { className: "mem-toolbar-label" }, "类型"), typeSelect(),
2263
+ e("span", { className: "mem-toolbar-label" }, "状态"), statusSelect(),
2264
+ e("input", { className: "ci-sel mem-search", type: "search", placeholder: "搜索摘要/键…", value: query,
2265
+ onChange: function (ev) { setQuery((ev && ev.target && ev.target.value) || ""); } }),
2266
+ focusChip(),
2267
+ e("span", { style: { flex: 1 } }),
2268
+ e("span", { className: "ci-sub" }, (tab === "list" ? factsVisible : graphFactsVisible).length + " 条洞察"),
2269
+ expandButton());
2270
+ }
2271
+ function factCard(f) {
2272
+ var cls = "mem-card mem-fact" + (f.status !== "active" ? " muted" : "") + (selected.indexOf(f.id) >= 0 ? " sel" : "");
2273
+ return e("div", { key: f.id, className: cls,
2274
+ onClick: function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); selectNode(f, ev); },
2275
+ onDoubleClick: function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); openNodeInList(f); } },
2276
+ e("div", { className: "mem-card-h" },
2277
+ e("span", { className: "mem-key" }, f.key),
2278
+ e("span", { className: "mem-type " + (f.factType || "") }, factTypeText[f.factType] || f.factType || "?"),
2279
+ e("span", { className: "mem-prov" + (String(f.scope || "").indexOf("sess:") === 0 ? " private" : "") },
2280
+ String(f.scope || "").indexOf("sess:") === 0 ? "本会话私有" : "工作区共享"),
2281
+ e("span", { className: "mem-conf" + ((f.confidence || 0) < 0.6 ? " low" : "") }, "置信 " + Math.round((f.confidence || 0) * 100) + "%"),
2282
+ f.status !== "active" ? e("span", { className: "mem-st " + f.status }, f.status) : null),
2283
+ e("div", { className: "mem-summary" }, f.summary || "(无摘要)"),
2284
+ e("div", { className: "mem-meta", title: f.scope },
2285
+ (String(f.scope || "").indexOf("ws:") === 0 && f.originTitle ? ("来自「" + f.originTitle + "」 · ") : "")
2286
+ + memAgo(f.updated_ts)));
2287
+ }
2288
+ function limitNotice(withPaging) {
2289
+ var gs = (withPaging ? (data && data.graph && data.graph.stats) : (graphData && graphData.stats)) || {};
2290
+ var same = older.sig === listSig;
2291
+ var extra = withPaging && same ? (older.facts || []).length : 0;
2292
+ var err = same ? older.err : null;
2293
+ if (!gs.truncated && extra === 0 && !err) return null;
2294
+ var shown = (Number(gs.facts) || 0) + extra;
2295
+ var total = Math.max(Number(gs.factTotal) || 0, shown);
2296
+ var win = Number(gs.sinceTs) > 0 ? "(当前时间窗内)" : "";
2297
+ // 分页状态以“已翻过页”的游标为准;否则沿用首页游标,避免翻到底后按钮重新出现。
2298
+ var paged = same && older.pages > 0;
2299
+ var cursor = paged ? older.cursor : gs.nextCursor;
2300
+ var more = paged ? older.hasMore : !!gs.hasMore;
2301
+ var showBtn = !!(withPaging && gs.focusScope && (older.loading || (more && cursor)));
2302
+ var msg = !gs.focusScope ? "点工作区或会话钻取可看全。"
2303
+ : (more ? (withPaging ? "可继续加载更早的事实。" : "切到列表可加载更早的事实。") : "已到最早一条。");
2304
+ return e("div", { className: "mem-limit", role: "status" },
2305
+ e("span", null, "已载入 " + shown + " / 共 " + total + " 条事实" + win
2306
+ + (paged ? "(含更早 " + extra + " 条)" : "(单次上限 " + (gs.factLimit || 200) + ")")
2307
+ + ";" + msg),
2308
+ showBtn ? e("button", {
2309
+ className: "ci-btn", disabled: !!older.loading,
2310
+ onClick: loadOlder,
2311
+ }, older.loading ? "加载中…" : "加载更早") : null,
2312
+ err ? e("span", { className: "ci-err" }, "加载失败:" + String((err && err.message) || err)) : null);
2313
+ }
2314
+ function graphBody() {
2315
+ if (err) {
2316
+ return e("div", { className: "ci-sec" },
2317
+ e("div", { className: "ci-err", role: "alert" }, "图谱加载失败:" + String(err.message || err)),
2318
+ e("button", { className: "ci-btn", onClick: function () { pull(); } }, "重试"));
2319
+ }
2320
+ if (!visible.nodes.length) {
2321
+ return e("div", {},
2322
+ limitNotice(),
2323
+ e("div", { className: "ci-sec" },
2324
+ e("div", { className: "mem-empty" }, "当前范围没有可画的节点。"),
2325
+ range !== "all" ? e("button", { className: "ci-btn", onClick: function () { setRange("all"); setFocus(null); } }, "展开全部") : null));
2326
+ }
2327
+ var lay = memoryGraphLayout(visible.nodes, visible.edges, graphBox.w, graphBox.h);
2328
+ var at = new Map(lay.nodes.map(function (n) { return [n.id, n]; }));
2329
+ // 节点标签的计数按当前可见事实算:状态/类型/搜索过滤后不应继续显示全量数字。
2330
+ var visFacts = new Map();
2331
+ (visible.graphFacts || factsVisible || []).forEach(function (f) { visFacts.set(f.scope, (visFacts.get(f.scope) || 0) + 1); });
2332
+ var activeId = selected.length === 1 ? selected[0] : hover;
2333
+ var activeSet = selected.length > 0 ? new Set(selected) : null;
2334
+ // 【v1.10.54 · 路径传播高亮】从选中节点沿边做 BFS,找出所有可达节点用于路径高亮。
2335
+ // 这样选中一个事实时,其来源会话与所属工作区都会被高亮,形成完整的来源链路。
2336
+ var pathSet = null;
2337
+ if (activeSet) {
2338
+ pathSet = new Set(activeSet);
2339
+ var queue = Array.from(activeSet);
2340
+ while (queue.length > 0) {
2341
+ var cur = queue.shift();
2342
+ visible.edges.forEach(function (ed) {
2343
+ var neighbor = ed.source === cur ? ed.target : (ed.target === cur ? ed.source : null);
2344
+ if (neighbor && !pathSet.has(neighbor)) {
2345
+ pathSet.add(neighbor);
2346
+ queue.push(neighbor);
2347
+ }
2348
+ });
2349
+ }
2350
+ }
2351
+ // roving focus:只看当前获得焦点的节点,按方向找“主轴最近、副轴最小”的邻居。
2352
+ function nodeAria(n) {
2353
+ if (n.kind === "workspace") return "工作区 " + (n.label || "") + "," + (n.n || 0) + " 条记录";
2354
+ if (n.kind === "session") return "会话 " + (n.title || n.sid || "") + "," + (n.n || 0) + " 条记录";
2355
+ return (n.summary || n.key) + "," + (factTypeText[n.factType] || n.factType || "事实")
2356
+ + "," + (n.status || "") + ",置信 " + Math.round((Number(n.confidence) || 0) * 100) + "%";
2357
+ }
2358
+ function moveFocus(dx, dy) {
2359
+ if (typeof document === "undefined") return;
2360
+ var cur = document.activeElement && document.activeElement.getAttribute
2361
+ ? document.activeElement.getAttribute("data-node") : null;
2362
+ var from = cur ? at.get(cur) : null;
2363
+ var best = null, bestScore = Infinity;
2364
+ lay.nodes.forEach(function (n) {
2365
+ if (!from) return;
2366
+ var ddx = n.x - from.x, ddy = n.y - from.y;
2367
+ var primary = ddx * dx + ddy * dy;
2368
+ if (primary <= 2) return;
2369
+ var score = primary + Math.abs(ddx * dy - ddy * dx) * 2.2;
2370
+ if (score < bestScore) { bestScore = score; best = n; }
2371
+ });
2372
+ if (!from) best = lay.nodes[0];
2373
+ if (!best) return;
2374
+ setKbId(best.id);
2375
+ var gs = svgRef.current && svgRef.current.querySelectorAll ? svgRef.current.querySelectorAll("g[data-node]") : [];
2376
+ for (var i = 0; i < gs.length; i++) {
2377
+ if (gs[i].getAttribute("data-node") === best.id) { try { gs[i].focus(); } catch (e) { /* ignore */ } break; }
2378
+ }
2379
+ }
2380
+ // 语义缩放 + 贪心避让:小图直接标事实摘要;大图放大到 1.25× 才标。
2381
+ // 工作区/会话标签永远优先,重叠时先隐藏事实标签,再按重要性保留。
2382
+ var showFactLabels = viewport.k >= 1.25 || graphFactsVisible.length <= 10;
2383
+ // 小图只标最可信的前 6 条事实,避免把画布变成文字墙;放大后全部参与避让。
2384
+ var factRank = new Map();
2385
+ graphFactsVisible.slice().sort(function (a, b) {
2386
+ return ((b.status === "active") - (a.status === "active")) || ((Number(b.confidence) || 0) - (Number(a.confidence) || 0));
2387
+ }).forEach(function (f, i) { factRank.set(f.id, i); });
2388
+ var labelBoxes = [];
2389
+ var labelText = new Map();
2390
+ function labelBox(n, text, size) {
2391
+ var w = 0;
2392
+ for (var i = 0; i < text.length; i++) w += (text.charCodeAt(i) > 255 ? 1 : 0.62) * size;
2393
+ var r = memNodeRadius(n);
2394
+ var baseY = n.y + r + (n.kind === "fact" ? 16 : 15);
2395
+ if (baseY + size * 0.3 > graphBox.h - 6) baseY = Math.max(size + 4, n.y - r - 8);
2396
+ var dx = 0;
2397
+ var left = n.x - w / 2;
2398
+ var right = n.x + w / 2;
2399
+ if (left < 6) dx = 6 - left;
2400
+ else if (right > graphBox.w - 6) dx = (graphBox.w - 6) - right;
2401
+ return {
2402
+ x1: left + dx - 2, x2: right + dx + 2,
2403
+ y1: baseY - size * 0.85 - 2, y2: baseY + size * 0.3 + 2,
2404
+ dx: dx, ty: baseY - n.y,
2405
+ };
2406
+ }
2407
+ function boxHit(a, b) {
2408
+ // 留 2 单位容差:字形上下留白本就不重叠,1px 级的框相交不该整条隐藏。
2409
+ var ix = Math.min(a.x2, b.x2) - Math.max(a.x1, b.x1);
2410
+ var iy = Math.min(a.y2, b.y2) - Math.max(a.y1, b.y1);
2411
+ return ix > 2 && iy > 2;
2412
+ }
2413
+ var labelCandidates = [];
2414
+ lay.nodes.forEach(function (n) {
2415
+ if (n.kind !== "fact") {
2416
+ var cnt = visFacts.get(n.scope) || 0;
2417
+ labelCandidates.push({ n: n, text: memNodeLabel(n) + (cnt ? " · " + cnt : ""), size: 14, pr: n.current ? 0.5 : 1 });
2418
+ } else if (selected.indexOf(n.id) >= 0 || hover === n.id) {
2419
+ labelCandidates.push({ n: n, text: memClip(n.summary || n.key, 16), size: 14, pr: 0 });
2420
+ } else if (showFactLabels) {
2421
+ var rank = factRank.has(n.id) ? factRank.get(n.id) : 99;
2422
+ if (viewport.k < 1.25 && rank >= 6) return;
2423
+ labelCandidates.push({ n: n, text: memClip(n.summary || n.key, 10), size: 14, pr: 2 + rank * 0.01 + (1 - (Number(n.confidence) || 0)) + (n.status !== "active" ? 1 : 0) });
2424
+ }
2425
+ });
2426
+ labelCandidates.sort(function (a, b) { return a.pr - b.pr || a.n.y - b.n.y; });
2427
+ labelCandidates.forEach(function (c) {
2428
+ var box = labelBox(c.n, c.text, c.size);
2429
+ if (labelBoxes.some(function (b) { return boxHit(box, b); })) return;
2430
+ labelBoxes.push(box);
2431
+ labelText.set(c.n.id, { text: c.text, size: c.size, fact: c.n.kind === "fact", dx: box.dx, ty: box.ty });
2432
+ });
2433
+ var edgeEls = visible.edges.map(function (ed, i) {
2434
+ var a = at.get(ed.source), b = at.get(ed.target);
2435
+ if (!a || !b) return null;
2436
+ var on = pathSet && (pathSet.has(ed.source) && pathSet.has(ed.target));
2437
+ return e("line", { key: "e" + i, className: "mem-edge " + ed.kind + (on ? " on" : ""), x1: a.x, y1: a.y, x2: b.x, y2: b.y });
2438
+ });
2439
+ var nodeEls = lay.nodes.map(function (n) {
2440
+ var r = memNodeRadius(n);
2441
+ var linked = !pathSet || pathSet.has(n.id);
2442
+ var cls = "mem-node " + n.kind
2443
+ + (n.kind === "fact" ? " " + (n.factType || "unknown") : "")
2444
+ + (n.status && n.status !== "active" ? " muted" : "")
2445
+ + (n.current ? " current" : "")
2446
+ + (selected.indexOf(n.id) >= 0 ? " sel" : "")
2447
+ + (linked ? "" : " dim");
2448
+ return e("g", { key: n.id, className: cls, "data-node": n.id, transform: "translate(" + n.x + "," + n.y + ")",
2449
+ tabIndex: (kbId ? n.id === kbId : lay.nodes[0] && lay.nodes[0].id === n.id) ? 0 : -1,
2450
+ role: "button",
2451
+ "aria-label": nodeAria(n),
2452
+ "aria-pressed": selected.indexOf(n.id) >= 0 ? "true" : "false",
2453
+ "aria-current": n.current ? "true" : undefined,
2454
+ style: { cursor: "pointer" },
2455
+ onFocus: function () { setKbId(n.id); setHover(n.id); },
2456
+ onBlur: function () { setHover(null); },
2457
+ onKeyDown: function (ev) {
2458
+ if (ev.key === "Enter" || ev.key === " " || ev.key === "Spacebar") {
2459
+ ev.preventDefault();
2460
+ selectNode(n);
2461
+ } else if (ev.key === "Escape") {
2462
+ ev.preventDefault();
2463
+ setSelected([]);
2464
+ }
2465
+ },
2466
+ onClick: function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); selectNode(n, ev); },
2467
+ onDoubleClick: function (ev) { if (ev && ev.stopPropagation) ev.stopPropagation(); openNodeInList(n); },
2468
+ onMouseEnter: function () { setHover(n.id); }, onMouseLeave: function () { setHover(null); } },
2469
+ n.kind === "workspace"
2470
+ ? e("rect", { x: -r, y: -r, width: r * 2, height: r * 2, rx: 5 })
2471
+ : e("circle", { r: r }),
2472
+ n.current ? e("circle", { r: r + 4, className: "mem-ring" }) : null,
2473
+ kbId === n.id ? e("circle", { r: r + 7, className: "mem-focus-ring" }) : null,
2474
+ (function () {
2475
+ var lab = labelText.get(n.id);
2476
+ if (!lab) return null;
2477
+ return e("text", { x: lab.dx, y: lab.ty, textAnchor: "middle",
2478
+ className: n.kind === "fact" ? "mem-fact-label" : "mem-node-label" }, lab.text);
2479
+ })(),
2480
+ e("title", null, n.kind === "fact" ? (n.summary || n.key) : (memNodeLabel(n) + (n.path ? ("\n" + n.path) : ""))));
2481
+ });
2482
+ function zoom(delta) {
2483
+ var k = Math.max(0.55, Math.min(2.4, viewport.k * delta));
2484
+ setViewport(Object.assign({}, viewport, { k: k }));
2485
+ }
2486
+ return e("div", {},
2487
+ limitNotice(),
2488
+ e("div", { className: "mem-graph-wrap", ref: wrapRef },
2489
+ e("svg", { ref: svgRef, className: "mem-graph", viewBox: "0 0 " + graphBox.w + " " + graphBox.h,
2490
+ role: "group",
2491
+ "aria-label": "记忆来源图谱:方向键遍历节点,Enter 打开详情,Esc 关闭详情",
2492
+ onKeyDown: function (ev) {
2493
+ var dir = { ArrowRight: [1, 0], ArrowLeft: [-1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] }[ev.key];
2494
+ if (dir) { ev.preventDefault(); moveFocus(dir[0], dir[1]); }
2495
+ else if (ev.key === "Escape") { ev.preventDefault(); setSelected([]); }
2496
+ },
2497
+ onPointerDown: function (ev) {
2498
+ var t = ev.target;
2499
+ if (t && t.closest && t.closest("[data-node]")) return;
2500
+ var r = ev.currentTarget.getBoundingClientRect ? ev.currentTarget.getBoundingClientRect() : null;
2501
+ var s = r && r.width && r.height ? Math.min(r.width / graphBox.w, r.height / graphBox.h) : 1;
2502
+ // Ctrl/Shift + 拖拽 = 框选;否则 = 平移。
2503
+ if (ev.ctrlKey || ev.shiftKey || ev.button === 0) {
2504
+ var startX = (ev.clientX - (r ? r.left : 0)) / s;
2505
+ var startY = (ev.clientY - (r ? r.top : 0)) / s;
2506
+ boxRef.current = { x1: startX, y1: startY, x2: startX, y2: startY, s: s, r: r, ctrl: ev.ctrlKey, shift: ev.shiftKey };
2507
+ setBoxSelect({ x1: startX, y1: startY, x2: startX, y2: startY });
2508
+ } else {
2509
+ dragRef.current = { x: ev.clientX, y: ev.clientY, vx: viewport.x, vy: viewport.y, s: s > 0 ? s : 1 };
2510
+ }
2511
+ if (ev.currentTarget && ev.currentTarget.setPointerCapture) { try { ev.currentTarget.setPointerCapture(ev.pointerId); } catch (_) { /* ignore */ } }
2512
+ },
2513
+ onPointerMove: function (ev) {
2514
+ var b = boxRef.current;
2515
+ if (b) {
2516
+ var x2 = (ev.clientX - (b.r ? b.r.left : 0)) / b.s;
2517
+ var y2 = (ev.clientY - (b.r ? b.r.top : 0)) / b.s;
2518
+ setBoxSelect({ x1: b.x1, y1: b.y1, x2: x2, y2: y2 });
2519
+ return;
2520
+ }
2521
+ var d = dragRef.current;
2522
+ if (!d) return;
2523
+ setViewport(Object.assign({}, viewport, { x: d.vx + (ev.clientX - d.x) / d.s, y: d.vy + (ev.clientY - d.y) / d.s }));
2524
+ },
2525
+ onPointerUp: function (ev) {
2526
+ var b = boxRef.current;
2527
+ if (b) {
2528
+ // 框选结束:找出矩形内的节点并加入选择。
2529
+ var x1 = Math.min(b.x1, b.x2), x2 = Math.max(b.x1, b.x2);
2530
+ var y1 = Math.min(b.y1, b.y2), y2 = Math.max(b.y1, b.y2);
2531
+ // 太小的拖拽(<5px)视为点击空白,清空选择。
2532
+ if (Math.abs(x2 - x1) < 5 && Math.abs(y2 - y1) < 5) {
2533
+ if (!b.ctrl && !b.shift) setSelected([]);
2534
+ } else {
2535
+ var hit = lay.nodes.filter(function (n) {
2536
+ return n.x >= x1 && n.x <= x2 && n.y >= y1 && n.y <= y2;
2537
+ }).map(function (n) { return n.id; });
2538
+ if (hit.length > 0) {
2539
+ if (b.ctrl) {
2540
+ // Ctrl:追加/移除(已选的取消,未选的加入)
2541
+ var newSel = selected.slice();
2542
+ hit.forEach(function (id) {
2543
+ var idx = newSel.indexOf(id);
2544
+ if (idx >= 0) newSel.splice(idx, 1);
2545
+ else newSel.push(id);
2546
+ });
2547
+ setSelected(newSel);
2548
+ } else {
2549
+ setSelected(hit);
2550
+ }
2551
+ } else if (!b.ctrl && !b.shift) {
2552
+ setSelected([]);
2553
+ }
2554
+ }
2555
+ boxRef.current = null;
2556
+ setBoxSelect(null);
2557
+ return;
2558
+ }
2559
+ dragRef.current = null;
2560
+ },
2561
+ onPointerLeave: function () { dragRef.current = null; boxRef.current = null; setBoxSelect(null); } },
2562
+ e("g", { transform: "translate(" + viewport.x + "," + viewport.y + ") scale(" + viewport.k + ")" }, edgeEls, nodeEls,
2563
+ boxSelect ? e("rect", {
2564
+ x: Math.min(boxSelect.x1, boxSelect.x2),
2565
+ y: Math.min(boxSelect.y1, boxSelect.y2),
2566
+ width: Math.abs(boxSelect.x2 - boxSelect.x1),
2567
+ height: Math.abs(boxSelect.y2 - boxSelect.y1),
2568
+ className: "mem-box-select",
2569
+ fill: "rgba(100, 150, 255, 0.15)",
2570
+ stroke: "rgba(100, 150, 255, 0.6)",
2571
+ strokeWidth: 1,
2572
+ strokeDasharray: "4,2"
2573
+ }) : null)),
2574
+ e("div", { className: "mem-graph-tools" },
2575
+ e("button", { className: "ci-btn", "aria-label": "放大", onClick: function () { zoom(1.18); }, title: "放大" }, "+"),
2576
+ e("button", { className: "ci-btn", "aria-label": "缩小", onClick: function () { zoom(0.85); }, title: "缩小" }, "−"),
2577
+ e("button", { className: "ci-btn", "aria-label": "适配视图", onClick: function () { setViewport({ x: 0, y: 0, k: 1 }); }, title: "适配" }, "适配"),
2578
+ e("button", { className: "ci-btn" + (zen ? " on" : ""), "aria-label": zen ? "退出聚焦模式" : "进入聚焦模式", onClick: function () { setZen(!zen); }, title: "隐藏概览与图例,放大图谱" }, zen ? "退出聚焦" : "聚焦")),
2579
+ e("div", { className: "mem-legend" },
2580
+ e("span", null, e("i", { className: "mem-dot ws" }), "工作区"),
2581
+ e("span", null, e("i", { className: "mem-dot sess" }), "会话"),
2582
+ e("span", null, e("i", { className: "mem-dot fact constraint" }), "约束"),
2583
+ e("span", null, e("i", { className: "mem-dot fact state" }), "状态"),
2584
+ e("span", null, e("i", { className: "mem-dot fact task" }), "任务"),
2585
+ e("span", null, e("i", { className: "mem-dot fact history" }), "历史"),
2586
+ e("span", null, e("i", { className: "mem-line shared" }), "共享"),
2587
+ e("span", null, e("i", { className: "mem-line origin" }), "来源"),
2588
+ e("span", null, e("i", { className: "mem-line private" }), "私有"),
2589
+ e("span", null, e("i", { className: "mem-line member" }), "成员"),
2590
+ e("span", { className: "ci-sub" }, "点/方向键=详情 · 双击=聚焦 · 滚轮缩放标签"))));
2591
+ }
2592
+ function listBody() {
2593
+ if (err) {
2594
+ return e("div", { className: "ci-sec" },
2595
+ e("div", { className: "ci-err", role: "alert" }, "列表加载失败:" + String(err.message || err)),
2596
+ e("button", { className: "ci-btn", onClick: function () { pull(); } }, "重试"));
2597
+ }
2598
+ var groups = new Map();
2599
+ factsVisible.forEach(function (f) {
2600
+ if (!groups.has(f.scope)) groups.set(f.scope, []);
2601
+ groups.get(f.scope).push(f);
2602
+ });
2603
+ var order = Array.from(groups.keys()).sort(function (a, b) {
2604
+ var sa = groups.get(a)[0], sb = groups.get(b)[0];
2605
+ var ca = (a === curSessScope ? 2 : 0) + (a === curWsScope ? 1 : 0);
2606
+ var cb = (b === curSessScope ? 2 : 0) + (b === curWsScope ? 1 : 0);
2607
+ return (cb - ca) || ((sb.updated_ts || 0) - (sa.updated_ts || 0));
2608
+ });
2609
+ var sections = order.map(function (scope) {
2610
+ var list = groups.get(scope);
2611
+ return e("div", { key: scope, className: "ci-sec" },
2612
+ e("div", { className: "ci-sech" }, list[0].scopeLabel || memScope(scope), e("span", { className: "ci-sub" }, list.length + " 条")),
2613
+ list.map(factCard));
2614
+ });
2615
+ var recs = (data.recent || []).filter(function (r) { return r.kind !== "insight"; });
2616
+ return e("div", {},
2617
+ limitNotice(true),
2618
+ factsVisible.length ? sections : e("div", { className: "ci-sec" },
2619
+ e("div", { className: "mem-empty" }, "当前范围没有洞察。"),
2620
+ range !== "all" ? e("button", { className: "ci-btn", onClick: function () { setRange("all"); setFocus(null); } }, "展开全部") : null),
2621
+ e("div", { className: "ci-sec" },
2622
+ e("div", { className: "ci-sech" }, "最近记录", e("span", { className: "ci-sub" }, recs.length + " 条")),
2623
+ recs.length ? e("div", { className: "mem-recent" },
2624
+ e("div", { className: "mem-row mem-head" },
2625
+ e("span", null, "类型"), e("span", null, "键"), e("span", { className: "mem-scope" }, "作用域"),
2626
+ e("span", null, "状态"), e("span", { style: { textAlign: "right" } }, "更新")),
2627
+ recs.map(function (r, i) {
2628
+ return e("div", { key: i, className: "mem-row" },
2629
+ e("span", null, r.kind),
2630
+ e("span", { className: "k", title: r.key }, r.key),
2631
+ e("span", { className: "mem-scope", title: r.scope }, r.scopeLabel || memScope(r.scope)),
2632
+ e("span", { className: "mem-st " + r.status }, r.status),
2633
+ e("span", { className: "mem-time" }, memAgo(r.updated_ts)));
2634
+ })) : e("div", { className: "mem-empty" }, "—")));
2635
+ }
2636
+ // ---- 轨迹页:当前会话的检索轨迹(Phase 4) ----
2637
+ var TRACE_TRIGGER_TEXT = { tool: "工具", ls: "目录", recall: "召回", inject: "注入", handover: "交接" };
2638
+ function traceRow(t) {
2639
+ var on = traceDetail && traceDetail.id === t.id;
2640
+ return e("div", {
2641
+ key: t.id, className: "mem-trace" + (on ? " on" : ""), role: "button", tabIndex: 0,
2642
+ "aria-pressed": !!on, title: t.preview || t.uri || "",
2643
+ onClick: function () { openTrace(t.id); },
2644
+ onKeyDown: function (ev) {
2645
+ if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); openTrace(t.id); }
2646
+ },
2647
+ },
2648
+ e("span", { className: "t" }, memAgo(t.ts)),
2649
+ e("span", { className: "mem-trigger " + (t.trigger || "") }, TRACE_TRIGGER_TEXT[t.trigger] || t.trigger || "?"),
2650
+ e("span", { className: "q" }, t.preview || (t.uri ? t.uri : "(无查询文本)")),
2651
+ e("span", { className: "t mode" }, t.mode || "—"),
2652
+ e("span", { className: "n cand" }, (t.selTotal || 0) + "/" + (t.candTotal || 0)),
2653
+ e("span", { className: "n ms" }, t.ms != null ? (Number(t.ms).toFixed(1) + "ms") : "—"));
2654
+ }
2655
+ function traceDetailBody() {
2656
+ if (traceDetailErr) return e("div", { className: "ci-err", style: { marginTop: 8 } }, "轨迹详情加载失败:" + String((traceDetailErr && traceDetailErr.message) || traceDetailErr));
2657
+ if (!traceDetail) return e("div", { className: "ci-sub", style: { marginTop: 8 } }, "点一条轨迹查看阶段、候选与选中。");
2658
+ var tr = traceDetail;
2659
+ return e("div", { className: "mem-trace-detail" },
2660
+ e("div", { className: "ci-sech" }, "轨迹 #" + tr.id, e("span", { className: "ci-sub" }, tr.ref || "")),
2661
+ detailRow("时间", new Date(tr.ts).toLocaleString()),
2662
+ detailRow("触发", TRACE_TRIGGER_TEXT[tr.trigger] || tr.trigger || "—"),
2663
+ detailRow("模式", tr.mode || "—"),
2664
+ detailRow("耗时", tr.ms != null ? (Number(tr.ms).toFixed(2) + " ms") : "—"),
2665
+ detailRow("候选 → 选中", (tr.candTotal || 0) + " → " + (tr.selTotal || 0)),
2666
+ detailRow("工作区", tr.workspace || "—"),
2667
+ tr.truncated ? e("div", { className: "ci-sub", style: { marginTop: 4 } }, "详情已裁剪(标记 " + tr.truncated + "):候选/选中/阶段超出单条上限。") : null,
2668
+ tr.uri ? e("div", { className: "ci-sub", style: { marginTop: 6, wordBreak: "break-all" } }, "URI ", e("span", { className: "mem-uri" }, tr.uri)) : null,
2669
+ tr.query && tr.query.text
2670
+ ? e("div", { className: "ci-sub", style: { marginTop: 6 } },
2671
+ "查询(已脱敏 · " + (tr.query.len || 0) + " 字 · hash " + (tr.query.hash || "—") + ")",
2672
+ e("pre", { className: "mem-code" }, tr.query.text))
2673
+ : e("div", { className: "ci-sub", style: { marginTop: 6 } }, "查询文本未记录(DSH_MEMORY_TRACE_TEXT=0)"),
2674
+ (tr.widened && tr.widened.length) ? e("div", { className: "ci-sub", style: { marginTop: 6 } }, "回退:" + tr.widened.join(" → ")) : null,
2675
+ (tr.steps && tr.steps.length)
2676
+ ? e("div", null,
2677
+ e("div", { className: "mem-detail-sub" }, "阶段 · " + tr.steps.length),
2678
+ tr.steps.map(function (s, i) {
2679
+ return e("div", { key: i, className: "mem-stage-row" },
2680
+ e("span", null, s.stage || "—"),
2681
+ e("span", null, s.n != null ? s.n : "—"),
2682
+ e("span", null, s.ms != null ? (s.ms + " ms") : "—"),
2683
+ e("span", { className: "why", title: s.why || s.uri || "" }, s.why || s.uri || ""));
2684
+ }))
2685
+ : null,
2686
+ (tr.selected && tr.selected.length)
2687
+ ? e("div", null,
2688
+ e("div", { className: "mem-detail-sub" }, "选中 · 显示 " + tr.selected.length + " / 共 " + (tr.selTotal || 0)),
2689
+ e("pre", { className: "mem-code" }, tr.selected.map(function (c) {
2690
+ return "#" + (c.rank != null ? c.rank : "?") + " " + (c.kind || "") + (c.why ? (" [" + c.why + "]") : "") + " " + (c.uri || "");
2691
+ }).join("\n")))
2692
+ : null,
2693
+ (tr.candidates && tr.candidates.length)
2694
+ ? e("div", null,
2695
+ e("div", { className: "mem-detail-sub" }, "候选 · 显示 " + tr.candidates.length + " / 共 " + (tr.candTotal || 0)),
2696
+ e("pre", { className: "mem-code" }, tr.candidates.map(function (c) {
2697
+ return "#" + (c.rank != null ? c.rank : "?") + " " + (c.kind || "") + " " + (c.src || "") + (c.score != null ? (" " + c.score) : "") + (c.why ? (" [" + c.why + "]") : "") + " " + (c.uri || "");
2698
+ }).join("\n")))
2699
+ : null,
2700
+ tr.error ? e("div", { className: "ci-err", style: { marginTop: 6 } }, "错误:" + tr.error) : null);
2701
+ }
2702
+ function tracesBody() {
2703
+ if (!sid) {
2704
+ return e("div", { className: "ci-sec" },
2705
+ e("div", { className: "mem-empty" }, "打开具体会话后查看检索轨迹。"));
2706
+ }
2707
+ return e("div", {},
2708
+ e("div", { className: "ci-sec" },
2709
+ e("div", { className: "ci-sech" }, "检索轨迹", e("span", { className: "ci-sub" }, traces.rows.length + " 条" + (traces.hasMore ? "(还有更早)" : ""))),
2710
+ e("div", { className: "ci-sub", style: { marginBottom: 6 } },
2711
+ "记录检索入口、阶段、候选与选中(查询已脱敏);只在当前会话内查看,导出 JSONL 会二次脱敏。"),
2712
+ e("div", { className: "mem-toolbar" },
2713
+ e("span", { className: "mem-toolbar-label" }, "触发"),
2714
+ e("select", {
2715
+ className: "ci-sel", value: traceTrigger,
2716
+ onChange: function (ev) { setTraceTrigger((ev && ev.target && ev.target.value) || ""); },
2717
+ },
2718
+ e("option", { value: "" }, "全部"),
2719
+ ["tool", "ls", "recall", "inject", "handover"].map(function (k) {
2720
+ return e("option", { key: k, value: k }, TRACE_TRIGGER_TEXT[k]);
2721
+ })),
2722
+ e("button", { className: "ci-btn", disabled: traces.loading, onClick: function () { loadTraces(false); } },
2723
+ traces.loading ? "加载中…" : "刷新"),
2724
+ e("button", { className: "ci-btn", disabled: !traces.rows.length, onClick: exportTraceHistory, title: "下载当前会话 JSONL" }, "导出"),
2725
+ e("button", { className: "ci-btn", disabled: !traces.rows.length || traces.loading, onClick: clearTraceHistory, title: "只清空本会话轨迹" }, "清空"),
2726
+ e("span", { style: { flex: 1 } }),
2727
+ e("span", { className: "ci-sub" }, traces.rows.length + " 条")),
2728
+ traces.err ? e("div", { className: "ci-err" }, "轨迹加载失败:" + String((traces.err && traces.err.message) || traces.err)) : null,
2729
+ !traces.rows.length && !traces.loading
2730
+ ? e("div", { className: "mem-empty" }, "还没有轨迹。发出一次检索/注入后点「刷新」。")
2731
+ : e("div", { className: "mem-trace-list" }, traces.rows.map(traceRow)),
2732
+ traces.hasMore ? e("button", {
2733
+ className: "ci-btn", style: { marginTop: 6 }, disabled: traces.loading,
2734
+ onClick: function () { loadTraces(true); },
2735
+ }, traces.loading ? "加载中…" : "加载更早") : null),
2736
+ traceDetailBody());
2737
+ }
2738
+ function detailRow(k, v) {
2739
+ return e("div", { className: "mem-detail-row" }, e("span", null, k), e("b", null, v));
2740
+ }
2741
+ function evidenceBody() {
2742
+ if (evErr) return e("div", { className: "ci-sub" }, "证据加载失败:" + String((evErr && evErr.message) || evErr));
2743
+ if (!evData) return e("div", { className: "ci-sub" }, "证据加载中…");
2744
+ var list = evData.evidence || [];
2745
+ if (!list.length) return e("div", { className: "ci-sub" }, "没有可回查的事件证据。");
2746
+ return e("div", { className: "mem-evs" }, list.map(function (x, i) {
2747
+ return e("details", { key: x.id || i, className: "mem-ev" },
2748
+ e("summary", null,
2749
+ e("span", { className: "mem-ev-k" }, x.kind || "ref"),
2750
+ e("span", { className: "mem-ev-c", title: x.content || "" }, x.content || "—"),
2751
+ e("span", { className: "mem-ev-t" }, memAgo(x.ts))),
2752
+ e("div", { className: "mem-ev-meta" },
2753
+ (x.sessionTitle ? ("来源会话:" + x.sessionTitle) : (x.sid ? ("会话 " + String(x.sid).slice(0, 8)) : "无会话来源"))
2754
+ + (x.seq != null ? (" · seq " + x.seq) : "")),
2755
+ x.raw ? e("pre", { className: "mem-ev-raw" }, x.raw) : null);
2756
+ }));
2757
+ }
2758
+ function railBody() {
2759
+ // 多选:显示摘要而非单个节点详情。
2760
+ if (selected.length > 1) {
2761
+ var close = e("button", { className: "ci-btn", style: { marginLeft: "auto", padding: "2px 9px" }, "aria-label": "清除选择", onClick: function () { setSelected([]); } }, "清除");
2762
+ var allNodes = (graphData && graphData.nodes) || [];
2763
+ var selNodes = allNodes.filter(function (n) { return selected.indexOf(n.id) >= 0; });
2764
+ var byKind = { workspace: 0, session: 0, fact: 0 };
2765
+ selNodes.forEach(function (n) { byKind[n.kind] = (byKind[n.kind] || 0) + 1; });
2766
+ return e("div", { className: "ci-sec" },
2767
+ e("div", { className: "ci-sech" }, "多选摘要", close),
2768
+ e("div", { className: "ci-sub" }, "已选 " + selected.length + " 个节点"),
2769
+ e("div", { className: "mem-detail-row" }, e("span", null, "工作区"), e("b", null, byKind.workspace || 0)),
2770
+ e("div", { className: "mem-detail-row" }, e("span", null, "会话"), e("b", null, byKind.session || 0)),
2771
+ e("div", { className: "mem-detail-row" }, e("span", null, "洞察"), e("b", null, byKind.fact || 0)),
2772
+ e("div", { className: "ci-sub", style: { marginTop: 8 } }, "Ctrl+点击=追加/取消 · 拖拽空白=框选"));
2773
+ }
2774
+ if (selectedNode) {
2775
+ var n = selectedNode;
2776
+ var close = e("button", { className: "ci-btn", style: { marginLeft: "auto", padding: "2px 9px" }, onClick: function () { setSelected([]); } }, "关闭");
2777
+ if (n.kind === "fact") {
2778
+ return e("div", { className: "ci-sec" },
2779
+ e("div", { className: "ci-sech" }, "洞察详情", close),
2780
+ e("div", { className: "mem-key" }, n.key),
2781
+ e("div", { className: "mem-summary", style: { marginBottom: 8 } }, n.summary || "(无摘要)"),
2782
+ detailRow("事实类型", factTypeText[n.factType] || n.factType || "—"),
2783
+ detailRow("生命周期", n.status || "—"),
2784
+ detailRow("置信", Math.round((n.confidence || 0) * 100) + "%"),
2785
+ detailRow("范围", n.scopeLabel || memScope(n.scope)),
2786
+ detailRow("来源会话", n.originTitle || "—"),
2787
+ detailRow("更新", memAgo(n.updated_ts)),
2788
+ detailRow("模型", n.model || "—"),
2789
+ e("div", { className: "mem-detail-sub" }, "证据链 · " + (n.evidence || 0) + " 条"),
2790
+ evidenceBody(),
2791
+ e("div", { className: "mem-detail-actions" },
2792
+ e("button", { className: "ci-btn", onClick: function () { openNodeInList(n); } }, "在列表中查看"),
2793
+ n.originSid ? e("button", { className: "ci-btn", onClick: function () { setFocus("sess:" + n.originSid); setTab("list"); } }, "只看来源会话") : null));
2794
+ }
2795
+ if (n.kind === "workspace") {
2796
+ var sessN = (graph.nodes || []).filter(function (x) { return x.kind === "session" && x.workspaceScope === n.scope; }).length;
2797
+ return e("div", { className: "ci-sec" },
2798
+ e("div", { className: "ci-sech" }, "工作区详情", close),
2799
+ e("div", { className: "ci-title" }, n.label || "工作区", n.current ? e("span", { className: "mem-cur" }, "当前") : null),
2800
+ e("div", { className: "ci-sub", style: { wordBreak: "break-all" } }, n.path || n.scope),
2801
+ detailRow("记录", n.n || 0),
2802
+ detailRow("洞察", (n.facts || 0) + (Number(n.factTotal) > (n.facts || 0) ? (" / " + n.factTotal) : "")),
2803
+ detailRow("会话", sessN),
2804
+ e("div", { className: "mem-detail-actions" },
2805
+ e("button", { className: "ci-btn", onClick: function () { setFocus(n.scope); setTab("list"); } }, "只看此工作区"),
2806
+ n.current ? null : e("button", { className: "ci-btn", onClick: function () { setRange("all"); }, title: "展开全部后才能聚焦其他工作区" }, "展开全部")));
2807
+ }
2808
+ return e("div", { className: "ci-sec" },
2809
+ e("div", { className: "ci-sech" }, "会话详情", close),
2810
+ e("div", { className: "ci-title" }, n.title || ("会话 " + String(n.sid || "").slice(0, 8)), n.current ? e("span", { className: "mem-cur" }, "当前") : null),
2811
+ e("div", { className: "ci-sub" }, n.workspace ? ("工作区 " + n.workspace) : "未定位工作区"),
2812
+ detailRow("记录", n.n || 0),
2813
+ detailRow("私有洞察", (n.facts || 0) + (Number(n.factTotal) > (n.facts || 0) ? (" / " + n.factTotal) : "")),
2814
+ detailRow("来源洞察", n.originFacts || 0),
2815
+ e("div", { className: "mem-detail-actions" },
2816
+ e("button", { className: "ci-btn", onClick: function () { setFocus(n.scope); setTab("list"); } }, "只看此会话"),
2817
+ e("button", { className: "ci-btn", onClick: function () { setSelected([]); } }, "返回概览")));
2818
+ }
2819
+ return e("div", { className: "ci-sec" },
2820
+ e("div", { className: "ci-sech" }, "当前上下文"),
2821
+ curWs ? e("div", { className: "mem-detail-row" }, e("span", null, "工作区"), e("b", null, curWs.title)) : null,
2822
+ curSess ? e("div", { className: "mem-detail-row" }, e("span", null, "会话"), e("b", null, curSess.title || curSess.scope)) : null,
2823
+ e("div", { className: "mem-detail-row" }, e("span", null, "上下文记录"), e("b", null, cur ? cur.records : 0)),
2824
+ e("div", { className: "mem-detail-row" }, e("span", null, "上下文有效洞察"), e("b", null, cur ? cur.insights : 0)),
2825
+ e("div", { className: "mem-detail-row" }, e("span", null, "本会话私有记录"), e("b", null, curSess ? curSess.records : 0)),
2826
+ e("div", { className: "ci-sub", style: { marginTop: 8 } }, "默认跟随当前选中的会话页显示;点「展开全部」查看其他工作区。"),
2827
+ e("div", { className: "mem-legend", style: { marginTop: 10 } },
2828
+ e("span", null, e("i", { className: "mem-dot ws" }), "工作区"),
2829
+ e("span", null, e("i", { className: "mem-dot sess" }), "会话"),
2830
+ e("span", null, e("i", { className: "mem-dot fact constraint" }), "约束"),
2831
+ e("span", null, e("i", { className: "mem-dot fact state" }), "状态")),
2832
+ data.session ? InjectionReceipt(data.session.injection) : null);
2833
+ }
2834
+ function settingsBody() {
2835
+ return e("div", {},
2836
+ e("div", { className: "ci-sec" },
2837
+ e("div", { className: "ci-sech" }, "全库概览"),
2838
+ e("div", { className: "mem-kpis" },
2839
+ kpi("全部活跃", t.active || 0, false, "另有已解决 " + (t.resolved || 0) + " 条"),
2840
+ kpi("全部有效洞察", t.insights || 0, false, "轻 LLM 提炼且仍 active 的持久事实"),
2841
+ kpi("工作区", t.workspaces || 0, false, "按 cwd 共享的 ws:* 作用域数"),
2842
+ kpi("待处理错误", t.errors || 0, (t.errors || 0) > 0, "status=active 的 error 记录"))),
2843
+ e("div", { className: "ci-sec" },
2844
+ e("div", { className: "ci-sech" }, "开关"),
2845
+ flagRow("memoryEnabled", "记忆层", "订阅事件流,写入 SQLite,注册 memory_* 工具", f.enabled),
2846
+ flagRow("memoryLlmEnabled", "轻 LLM 编译", "每轮结束把新事件编译为 insight(模型见下方「编译模型」)", f.llmEnabled),
2847
+ flagRow("memoryInjectCatalog", "目录注入", "会话首次 pre-step 注入一次记忆目录(每步重读开关)", f.injectCatalog),
2848
+ flagRow("memoryHandoff", "会话交接", "每轮结束写交接卡;同工作区新会话首次注入最近一张", f.handoff),
2849
+ flagRow("memoryConsolidateAuto", "自动整理", "空闲 " + coIdleMin + " 分钟后自动整理;无新事件不跑(手动不受影响)", f.consolidateAuto),
2850
+ e("div", { className: "ci-sub", style: { marginTop: 6 } }, "已武装进程内,未被环境变量锁定的开关即时生效;启动时未加载记忆层则需重启。环境变量(DSH_MEMORY / DSH_MEMORY_LLM / DSH_MEMORY_INJECT / DSH_MEMORY_HANDOFF / DSH_MEMORY_CONSOLIDATE)优先级高于此面板。")),
2851
+ e("div", { className: "ci-sec" },
2852
+ e("div", { className: "ci-sech" }, "整理"),
2853
+ e("div", { className: "ci-sub", style: { marginBottom: 6 } }, "核对目录重置、失效旧状态与合并重复事实。约束(端口/规则/偏好)受保护,不会因整理被删。"),
2854
+ e("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
2855
+ e("button", {
2856
+ className: "ci-btn", disabled: busy || !!coi || !sid || !coReady,
2857
+ title: !coReady ? "需先开启「记忆层」与「轻 LLM 编译」"
2858
+ : (!sid ? "打开具体会话后可手动整理" : (coi ? ("整理中:" + coStage) : "立即执行一次整理")),
2859
+ onClick: consolidateNow,
2860
+ }, coi ? ("整理中 " + (coSec != null ? coSec + "s" : "") + " · " + coStage) : "立即整理"),
2861
+ e("span", { className: "ci-sub" }, coCountdown)),
2862
+ coReady ? null : e("div", { className: "ci-sub", style: { marginTop: 6 } }, "需先开启「记忆层」与「轻 LLM 编译」"),
2863
+ e("div", { className: "ci-sub", style: { marginTop: 6 } }, coManText),
2864
+ coLastText ? e("div", { className: "ci-sub", style: { marginTop: 6 } }, coLastText) : null),
2865
+ e("div", { className: "ci-sec" },
2866
+ e("div", { className: "ci-sech" }, "编译模型"),
2867
+ e("div", { className: "ci-sub", style: { marginBottom: 6 } }, "轻 LLM 编译使用的模型;留空 = 跟随会话当前模型。可选本地轻模型(如 qwen3:4b)。"),
2868
+ (memMdlErr ? e("div", { className: "ci-err" }, "⚠ " + memMdlErr) : null),
2869
+ e("div", { style: { display: "flex", gap: 6, flexWrap: "wrap", alignItems: "center" } },
2870
+ e("select", { className: "ci-sel", value: memP, disabled: busy, onChange: onMemProviderChange,
2871
+ title: "记忆编译用 provider;「跟随会话模型」= 清除覆盖" },
2872
+ e("option", { value: "" }, "跟随会话模型"),
2873
+ memProviders.map(function (p) { return e("option", { key: p.id, value: p.id }, (p.name || p.id)); })),
2874
+ e("select", { className: "ci-sel", value: memM, disabled: busy || !memP, onChange: onMemModelChange,
2875
+ title: "选定即保存 provider/model 成对覆盖;清除 provider 即恢复跟随会话" },
2876
+ e("option", { value: "" }, memP ? "选择模型…" : "—"),
2877
+ memModels.map(function (m) { return e("option", { key: m.id, value: m.id }, (m.name || m.id)); }))),
2878
+ e("div", { className: "ci-sub", style: { marginTop: 6 } },
2879
+ (data.llmRoute && data.llmRoute.provider)
2880
+ ? "当前覆盖:" + data.llmRoute.provider + " / " + (data.llmRoute.model || "—")
2881
+ : "当前:跟随会话模型(每轮重新读取 control.json)")),
2882
+ e("div", { className: "ci-sec" },
2883
+ e("div", { className: "ci-sech" }, "文件巡检"),
2884
+ e("div", { className: "ci-sub", style: { marginBottom: 6 } },
2885
+ "每 " + jIntervalMin + " 分钟选出超过 24 小时未验证的文件事实(每次最多 10 条;事实段超过 400 tokens 时只检最旧的 5 条),由轻模型检查后刷新或标记失效;"
2886
+ + "宿主在写入前重新 stat 复核。与每会话首步的确定性巡检互补。"),
2887
+ flagRow("memoryJanitorEnabled", "文件巡检管家", "LLM 驱动的定期巡检;默认关", jn.enabled),
2888
+ e("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap", marginTop: 8 } },
2889
+ e("span", { className: "ci-sub" }, "间隔"),
2890
+ e("select", {
2891
+ className: "ci-sel", value: String(jIntervalMin), disabled: busy || lock.janitorEnabled,
2892
+ title: "巡检间隔(分钟);也可用 DSH_MEMORY_JANITOR_INTERVAL_MS 覆盖",
2893
+ onChange: function (ev) {
2894
+ var n = Number(ev && ev.target && ev.target.value);
2895
+ if (Number.isFinite(n) && n > 0) applyConfig({ memoryJanitorIntervalMs: n * 60000 });
2896
+ },
2897
+ }, jIntervalOpts.map(function (n, i) {
2898
+ return e("option", { key: i, value: String(n) }, n < 60 ? (n + " 分钟") : (n / 60 + " 小时"));
2899
+ })),
2900
+ e("button", {
2901
+ className: "ci-btn",
2902
+ disabled: busy || !sid || !jReady || !!jinf,
2903
+ title: !sid ? "打开具体会话后可立即巡检"
2904
+ : (!jReady ? "需先开启记忆层、轻 LLM 编译与文件巡检"
2905
+ : (jinf ? ("巡检进行中:" + jStage) : "立即执行一次巡检")),
2906
+ onClick: janitorNow,
2907
+ }, jinf ? ("巡检中 " + (jSec != null ? jSec + "s" : "") + " · " + jStage) : "立即巡检"),
2908
+ e("span", { className: "ci-sub" },
2909
+ jn.route ? ("模型:" + jn.route.provider + " / " + (jn.route.model || "—")) : "模型:跟随自动整理/编译路由")),
2910
+ jReady ? null : e("div", { className: "ci-sub", style: { marginTop: 6 } }, "需先开启「记忆层」「轻 LLM 编译」与「文件巡检」。"),
2911
+ jLastText ? e("div", { className: "ci-sub", style: { marginTop: 6 } }, jLastText) : null,
2912
+ janitorMsg ? e("div", { className: "ci-sub", style: { marginTop: 6 } }, janitorMsg) : null),
2913
+ e("div", { className: "ci-sec" },
2914
+ e("div", { className: "ci-sech" }, "检索与向量"),
2915
+ e("div", { className: "ci-sub", style: { marginBottom: 6 } },
2916
+ "向量语义臂默认关闭;开启需本机 embedding 模型(DSH_MEMORY_VEC=on)。词法检索始终可用,向量只作为排序信号。"),
2917
+ e("div", { className: "mem-vec" },
2918
+ e("span", null, "状态 ", e("b", null, mv ? ({ off: "关闭", degraded: "降级", ok: "可用" }[mv.state] || mv.state) : "—")),
2919
+ e("span", null, "模式 ", e("b", null, mv ? (mv.mode || "off") : "—")),
2920
+ e("span", null, "模型 ", e("b", null, mv && mv.model ? mv.model : "—")),
2921
+ e("span", null, "维度 ", e("b", null, mv && mv.dim ? mv.dim : "—")),
2922
+ e("span", null, "已嵌 ", e("b", null, mv ? mv.rows : 0)),
2923
+ e("span", null, "待处理 ", e("b", null, mv ? mv.pending : 0))),
2924
+ mv && mv.lastError ? e("div", { className: "ci-err", style: { marginTop: 6 } }, "向量层最近错误:" + mv.lastError) : null,
2925
+ e("div", { className: "mem-diag" },
2926
+ kpi("编译成功率", mtr && mtr.compile ? rateOf(mtr.compile) : "—", false, "metrics.json:轻 LLM 编译成功 / 总数"),
2927
+ kpi("检索成功率", mtr && mtr.retrieval ? rateOf(mtr.retrieval) : "—", false, "metrics.json:查询条件化召回有命中 / 总数"),
2928
+ kpi("注入命中率", mtr && mtr.injection ? hitOf(mtr.injection) : "—", false, "metrics.json:自动注入实际产生内容的次数 / 总数")),
2929
+ mtr && mtr.vector ? e("div", { className: "ci-sub" },
2930
+ "向量臂命中 " + (mtr.vector.armHit || 0) + " · 未命中 " + (mtr.vector.armMiss || 0)
2931
+ + " · 嵌入失败 " + (mtr.vector.embedFail || 0) + " · 精确跳过 " + (mtr.vector.skippedExact || 0)) : null,
2932
+ e("div", { className: "ci-sub", style: { marginTop: 6 } },
2933
+ mtr ? ("指标更新 " + memAgo(mtr.lastUpdated)) : "指标文件尚未生成(首次编译/检索后出现)")),
2934
+ e("div", { className: "ci-sec" },
2935
+ e("div", { className: "ci-sech" }, "记录构成"),
2936
+ (data.kinds && data.kinds.length) ? e("div", { className: "mem-bars" }, data.kinds.map(function (k) {
2937
+ return e("div", { key: k.kind, className: "mem-bar" },
2938
+ e("span", { className: "k", title: k.kind }, k.kind),
2939
+ e("span", { className: "mem-track" }, e("span", { className: "mem-fill", style: { width: Math.max(2, Math.round(100 * k.n / maxKind)) + "%" } })),
2940
+ e("span", { className: "n" }, k.n));
2941
+ })) : e("div", { className: "mem-empty" }, "还没有记录。")),
2942
+ e("div", { className: "ci-sec" },
2943
+ e("div", { className: "ci-sech" }, "数据库"),
2944
+ e("div", { className: "ci-sub", style: { wordBreak: "break-all" } }, "DB " + (db.exists ? memBytes(db.bytes) : "未创建") + " · " + (db.path || "")),
2945
+ e("div", { className: "ci-sub" }, "已编译会话 " + ((data.compile && data.compile.cursors) || 0)
2946
+ + (data.compile && data.compile.last ? (" · 最近编译 " + String(data.compile.last.sid).slice(0, 8) + "… @ " + data.compile.last.through) : "")),
2947
+ e("div", { className: "ci-sub" }, "只读快照 · 5s 轮询"),
2948
+ data.error ? e("div", { className: "ci-err" }, "读取异常:" + data.error) : null,
2949
+ (db.exists && data.sqlite === false) ? e("div", { className: "ci-err" }, "Node 缺少 node:sqlite,面板无法读取") : null));
2950
+ }
2951
+ return e("div", { className: "ci-root" + (zen && tab === "graph" ? " mem-zen" : "") },
2952
+ e("div", { className: "ci-hd" },
2953
+ e("span", { className: "ci-badge " + (st === "on" ? "ci-on" : st === "pending" ? "mem-dot-pending" : "ci-off") }),
2954
+ e("div", null,
2955
+ e("div", { className: "mem-hd-t" },
2956
+ e("div", { className: "ci-title" }, "外部记忆"),
2957
+ e("span", { className: "mem-state " + st }, stText),
2958
+ (lockN ? e("span", { className: "mem-evidence", title: "环境变量优先级高于面板:被锁定的开关不可点击" }, "env 锁定 " + lockN + " 项") : null)),
2959
+ e("div", { className: "ci-sub" }, "SQLite · 工作区共享事实 · memory_catalog / search / get / ls(+ 受控写入)")),
2960
+ e("button", { className: "ci-btn", style: { marginLeft: "auto" }, onClick: function () { pull(); } }, "刷新")),
2961
+ banner ? e("div", { className: "mem-off" }, banner) : null,
2962
+ err ? e("div", { className: "ci-err" }, "读取失败:" + String(err.message || err)) : null,
2963
+ e("div", { className: "mem-tabs", role: "tablist" },
2964
+ tabButton("graph", "图谱"), tabButton("list", "列表"), tabButton("traces", "轨迹"), tabButton("settings", "设置"),
2965
+ e("span", { className: "mem-tabs-note" }, tab === "settings" ? "写入、巡检与诊断"
2966
+ : (tab === "traces" ? "当前会话的检索轨迹(本机)" : "默认跟随当前选中的会话页"))),
2967
+ tab === "settings"
2968
+ ? e("div", { className: "mem-settings" }, settingsBody())
2969
+ : tab === "traces"
2970
+ ? e("div", { className: "mem-settings" }, tracesBody())
2971
+ : e("div", { className: "ci-cols" },
2972
+ e("div", { className: "ci-main" },
2973
+ overviewStrip(),
2974
+ memoryToolbar(),
2975
+ tab === "graph" ? graphBody() : listBody()),
2976
+ e("div", { className: "ci-rail" }, railBody())));
2977
+ }
2978
+
2979
+ var inject = ["slots"];
2980
+ function apply(ctx) {
2981
+ var slots = ctx.get("slots");
2982
+ if (slots === undefined) return;
2983
+ ctx.slots.inject("conversation.view", function () {
2984
+ return ctx.slots.register(
2985
+ { name: "conversation.view", id: "contextinjector", order: 20, label: function () { return "\u6CE8\u5165"; },
2986
+ inject: function (sessionId) { return { sessionId: sessionId || null }; } },
2987
+ function (props) { return React.createElement(InjectView, props || {}); });
2988
+ });
2989
+ ctx.slots.inject("conversation.view", function () {
2990
+ return ctx.slots.register(
2991
+ { name: "conversation.view", id: "memory", order: 21, label: function () { return "记忆"; },
2992
+ inject: function (sessionId) { return { sessionId: sessionId || null }; } },
2993
+ function (props) { return React.createElement(MemoryView, props || {}); });
2994
+ });
2995
+ ctx.slots.inject("conversation.input.dock", function () {
2996
+ return ctx.slots.register(
2997
+ { name: "conversation.input.dock", id: "contextinjector-mode", order: 80,
2998
+ inject: function (sessionId) { return { sessionId: sessionId || null }; } },
2999
+ function (props) { return React.createElement(ModeDock, props || {}); });
3000
+ });
3001
+ // v2.3【会话页·在飞折叠】「压缩中」小标:让"上一轮输出正在压缩"在会话页可见(不再只在注入面板里)。
3002
+ // 落点选择(按与 DSH「Deep diving...」状态行的贴近度排序):
3003
+ // DSH 那个状态行(tokens 在 dsh-client-ui-conversation/lib/client.js:5610 的 TurnStatus,由 ChatView
3004
+ // 直接内联渲染在聊天列尾部)**没有插槽** ⇒ 没有"同一行"可挂;会话页可用的最近插槽只有:
3005
+ // ① conversation.input.left(本实现选用):composer 操作行左侧工具簇,位于状态行正下方、同为左对齐;
3006
+ // 且是行内 flex 位置 ⇒ 新增 inline 小标不改行高,不会把 DSH 的状态行顶上去。
3007
+ // ② conversation.input.dock(ModeDock 所在):会多出整整一行,把上方聊天内容顶动(弃)。
3008
+ // ③ conversation.composer.dock / session.header.utilities:分别在输入卡片下方、会话页顶部右角,更远(备选)。
3009
+ // 注:list 插槽的 options.id 是必填(SlotCore 对 kind:"list" 强校验),故带 id。
3010
+ // 【v2.4 参数形态结论(源码实证,勿再猜)】`conversation.input.left` 的 **owner props 是 zone 对象**
3011
+ // `{session, input}`(dsh-client-ui-conversation/lib/client.js:7191-7194 构造 + :7241 传入;
3012
+ // 契约 slots.d.ts:270-274 `owner: InputZone`、:395-397 InputZone 定义);
3013
+ // 而 **entry 自己的 inject 回调吃的是裸会话 id**(dsh-client-ui-renderer/lib/client.js:334-341:
3014
+ // `if (info !== void 0) args.push(info.sessionId)` 后 `inject(...args)`;
3015
+ // DSH 自带 entry 同款写法见 dsh-client-ui-conversation/lib/client.js:10006);
3016
+ // 另外渲染器还会在 standard kit 里铺一份 `sessionId`(dsh-client-ui-renderer/lib/client.js:563)。
3017
+ // ⇒ 组件侧 `props.sessionId` 本来是**有值**的;但为了任何一处口径变化都不再"静默打死小标",
3018
+ // ① 这个 inject 只在**拿到非空字符串**时才回填(若某版本改传 zone 对象,绝不回填,
3019
+ // 否则用对象覆盖掉 kit 里正确的 sessionId,小标会永远渲染不出来);
3020
+ // ② 组件侧另有 5 形态解析(ciChipSid)+ 挂载一行 / **每次状态跃迁各一行** `[ci-foldchip]` 诊断日志
3021
+ // (跃迁行带 `Δ=`;同一状态连续采样不重复打,见 ciFoldChipLogLine)。
3022
+ ctx.slots.inject("conversation.input.left", function () {
3023
+ return ctx.slots.register(
3024
+ { name: "conversation.input.left", id: "contextinjector-fold", order: 90,
3025
+ inject: function (arg) { return (typeof arg === "string" && arg.trim()) ? { sessionId: arg.trim() } : {}; } },
3026
+ function (props) { return React.createElement(FoldChip, props || {}); });
3027
+ });
3028
+ }
3029
+ exports.apply = apply;
3030
+ exports.inject = inject;
3031
+ exports.__test = { frameSummary: frameSummary, fmtB: fmtB };
3032
+ return module.exports;
3033
+ }
3034
+ });