@jiugeqqqqq/dsh-house2 0.2.1 → 0.3.0

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.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  > 每次发版都必须在这里写一条:改了什么、为什么改、别人要不要跟着动。
4
4
  > 版本号跟 `package.json` 走,`npm i` 的人看这里就知道升了什么。
5
5
 
6
+ ## 0.3.0 — 2026-10-30
7
+
8
+ - 新视图「识海」:房子页顶部切「房子 | 识海」。3D 星图——原意识在中心、记忆按时间沉底(越老越暗)、24 小时内的「心光」发光、点一条记忆亮起它牵出的念头链(心流),右侧看详情。
9
+ - 数据走 host 的 `sea` 接口(读 `sea-graph.json`,由 `scripts/sea-graph.mjs` 生成);路径可用 `seaGraphPath` 配,默认 `<DSH_HOME>/sea-graph.json`。
10
+
11
+ ## 0.2.2 — 2026-10-30
12
+
13
+ - 压缩改成**按 token 触发**(不再挂在「任务完成」上):house2 自己统计累计 token(口径同 dsh-session-rotate,监听 `assistant/message` 的 `usage`),到阈值 80% 就在注入里提醒「该准备交接/换会话」。阈值 `thresholdTokens`,默认 1 亿。
14
+ - 不自己调压缩(DSH 没给插件公开的手动压缩接口),只按 token 提醒;工段只负责记任务。
15
+
6
16
  ## 0.2.1 — 2026-10-30
7
17
 
8
18
  - `house_search` 加 `stage` 参数:先按工段名找到它挂的积木文件(`stage_done` 时写的「积木:xxx」),只在那一个文件里搜,比全目录翻更快更准。不传就还是全目录搜。
package/lib/client.js CHANGED
@@ -80,6 +80,16 @@ window.__ModuleLoader__.load({
80
80
  },
81
81
  err: { fontSize: '13px', color: 'var(--dsw-alias-state-error-primary,#e5534b)' },
82
82
  note: { fontSize: '12px', opacity: 0.65 },
83
+ // 2026-10-30:识海视图的切换 tab
84
+ tab: {
85
+ padding: '5px 14px', borderRadius: '999px', cursor: 'pointer', fontSize: '13px',
86
+ border: '1px solid var(--dsw-alias-border-l2,#3a3a3a)', background: 'transparent', color: 'inherit', opacity: 0.7,
87
+ },
88
+ tabOn: {
89
+ padding: '5px 14px', borderRadius: '999px', cursor: 'pointer', fontSize: '13px',
90
+ border: '1px solid var(--dsw-alias-brand-primary,#4d6bfe)', background: 'rgba(77,107,254,0.16)',
91
+ color: 'var(--dsw-alias-brand-primary,#8fa8ff)', fontWeight: 600,
92
+ },
83
93
  };
84
94
 
85
95
  function HousePage() {
@@ -136,6 +146,11 @@ window.__ModuleLoader__.load({
136
146
  var isBusy = busy[0];
137
147
  var setBusy = busy[1];
138
148
 
149
+ // 2026-10-30:房子 / 识海 两个视图
150
+ var view = React.useState('house');
151
+ var currentView = view[0];
152
+ var setView = view[1];
153
+
139
154
  // 搜历史原话:压缩掉的旧对话还在会话存档里,按关键词翻回去
140
155
  var histQ = React.useState('');
141
156
  var histQuery = histQ[0];
@@ -430,7 +445,259 @@ window.__ModuleLoader__.load({
430
445
  children.push(React.createElement('div', { key: 'path', style: styles.note }, s.data.path));
431
446
  }
432
447
 
433
- return React.createElement('div', { style: styles.wrap }, children);
448
+ var tabs = React.createElement(
449
+ 'div', { key: 'tabs', style: { display: 'flex', gap: '8px' } },
450
+ React.createElement('button', { style: currentView === 'house' ? styles.tabOn : styles.tab, onClick: function () { setView('house'); } }, '房子'),
451
+ React.createElement('button', { style: currentView === 'sea' ? styles.tabOn : styles.tab, onClick: function () { setView('sea'); } }, '识海'),
452
+ );
453
+ if (currentView === 'sea') {
454
+ return React.createElement('div', { style: styles.wrap }, [tabs, React.createElement(SeaView, { key: 'sea' })]);
455
+ }
456
+ return React.createElement('div', { style: styles.wrap }, [tabs].concat(children));
457
+ }
458
+
459
+ // ── 识海 3D 星图(2026-10-30)────────────────────────────────────
460
+ // 数据来自 host 的 sea 接口(sea-graph.json):
461
+ // 原意识在中心、记忆按时间沉底、24h 内的「心光」发光、
462
+ // 点一个记忆亮起它牵出的念头链(心流)。
463
+ function startSea(canvas, panelEl, DATA) {
464
+ var ctx = canvas.getContext('2d');
465
+ var DPR = Math.min(2, window.devicePixelRatio || 1);
466
+ var W = 0, H = 0;
467
+ function measure() {
468
+ var pe = canvas.parentElement;
469
+ var w = pe ? pe.clientWidth : 800;
470
+ var h = pe ? pe.clientHeight : 500;
471
+ W = canvas.width = Math.max(320, w) * DPR;
472
+ H = canvas.height = Math.max(320, h) * DPR;
473
+ }
474
+ measure();
475
+ var ro = null;
476
+ if (typeof ResizeObserver !== 'undefined' && canvas.parentElement) {
477
+ ro = new ResizeObserver(measure);
478
+ ro.observe(canvas.parentElement);
479
+ }
480
+
481
+ var NOW = Date.now(), DAY = 86400000;
482
+ var COLOR = { memory: '#ff7ad9', vault: '#7fd7ff', ability: '#ffd166', tag: '#9b8cff', person: '#5dffc0' };
483
+ function ageDays(at) {
484
+ var t = Date.parse(at || '');
485
+ if (!t || isNaN(t)) return 45;
486
+ return Math.max(0, (NOW - t) / DAY);
487
+ }
488
+ var nodes = [], i, n;
489
+ var src = (DATA.nodes || []);
490
+ for (i = 0; i < src.length; i++) {
491
+ n = src[i];
492
+ if (n.type === 'tag' || n.type === 'person') continue;
493
+ var a = ageDays(n.at);
494
+ var ang = (i * 2.399963) % (Math.PI * 2);
495
+ var rad = 140 + Math.min(420, a * 3) + Math.random() * 60;
496
+ nodes.push({
497
+ id: n.id, type: n.type, label: n.label, text: n.text, at: n.at, source: n.source,
498
+ importance: n.importance, group: n.group, ageDays: a,
499
+ x: Math.cos(ang) * rad,
500
+ y: n.type === 'memory' ? -Math.min(460, a * 2.8) : (n.type === 'vault' ? -40 - Math.random() * 320 : -140 - Math.random() * 260),
501
+ z: Math.sin(ang) * rad,
502
+ color: COLOR[n.type] || '#8899aa',
503
+ size: (n.type === 'memory' ? 3.2 : 2.2) + Math.min(4, (Number(n.importance) || 0.5) * 4),
504
+ });
505
+ }
506
+ var core = { id: 'core', type: 'core', label: '原意识', x: 0, y: 20, z: 0, color: '#bfe3ff', size: 12 };
507
+ var byId = new Map();
508
+ for (i = 0; i < nodes.length; i++) byId.set(nodes[i].id, nodes[i]);
509
+ byId.set('core', core);
510
+
511
+ var edges = [];
512
+ for (i = 0; i < nodes.length; i++) edges.push({ source: nodes[i].id, target: 'core', kind: 'origin' });
513
+ var se = DATA.edges || [];
514
+ for (i = 0; i < se.length; i++) {
515
+ if (byId.has(se[i].source) && byId.has(se[i].target)) edges.push(se[i]);
516
+ }
517
+ var adj = new Map();
518
+ for (i = 0; i < edges.length; i++) {
519
+ var e = edges[i];
520
+ if (!adj.has(e.source)) adj.set(e.source, []);
521
+ if (!adj.has(e.target)) adj.set(e.target, []);
522
+ adj.get(e.source).push(e.target);
523
+ adj.get(e.target).push(e.source);
524
+ }
525
+
526
+ var yaw = 0.5, pitch = -0.18, zoom = 1, dragging = false, lx = 0, ly = 0;
527
+ var active = null, chain = new Set();
528
+
529
+ function onDown(ev) { dragging = true; lx = ev.clientX; ly = ev.clientY; }
530
+ function onUp() { dragging = false; }
531
+ function onMove(ev) {
532
+ if (!dragging) return;
533
+ yaw += (ev.clientX - lx) * 0.0045;
534
+ pitch += (ev.clientY - ly) * 0.0045;
535
+ pitch = Math.max(-1.35, Math.min(1.35, pitch));
536
+ lx = ev.clientX; ly = ev.clientY;
537
+ }
538
+ function onWheel(ev) {
539
+ zoom *= ev.deltaY > 0 ? 1.07 : 0.93;
540
+ zoom = Math.max(0.35, Math.min(3.2, zoom));
541
+ }
542
+ function bfs(start, depth) {
543
+ var seen = new Set([start]), q = [[start, 0]], k;
544
+ while (q.length) {
545
+ var cur = q.shift();
546
+ if (cur[1] >= depth) continue;
547
+ var list = adj.get(cur[0]) || [];
548
+ for (k = 0; k < list.length; k++) {
549
+ if (!seen.has(list[k])) { seen.add(list[k]); q.push([list[k], cur[1] + 1]); }
550
+ }
551
+ }
552
+ return seen;
553
+ }
554
+ function esc(s) {
555
+ return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
556
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c];
557
+ });
558
+ }
559
+ function renderPanel(node) {
560
+ if (!panelEl) return;
561
+ var rows = [];
562
+ function add(k, v) { if (v !== undefined && v !== '') rows.push('<div><span style="color:#6e89ab">' + k + ':</span><span style="color:#e6f2ff">' + esc(v) + '</span></div>'); }
563
+ add('类型', { memory: '记忆', vault: '记忆库', ability: '能力' }[node.type] || node.type);
564
+ add('来源', node.source);
565
+ add('时间', node.at);
566
+ add('重要度', node.importance);
567
+ add('多少天前', node.ageDays !== undefined ? Math.round(node.ageDays) + ' 天' : '');
568
+ if (node.text) rows.push('<div style="margin-top:8px;opacity:.9">' + esc(String(node.text).slice(0, 260)) + '</div>');
569
+ var rel = [];
570
+ chain.forEach(function (id) {
571
+ if (id === 'core' || id === node.id) return;
572
+ var t = byId.get(id);
573
+ if (t) rel.push(t.label);
574
+ });
575
+ if (rel.length) rows.push('<div style="margin-top:10px;color:#6e89ab">牵着:</div><div style="line-height:1.8;opacity:.9">' + rel.slice(0, 12).map(esc).join(' · ') + '</div>');
576
+ panelEl.innerHTML = '<div style="font-size:14px;color:#a8d6ff;margin-bottom:8px;font-weight:600">' + esc(node.label) + '</div>' + rows.join('');
577
+ panelEl.style.display = 'block';
578
+ }
579
+ function onClick(ev) {
580
+ var r = canvas.getBoundingClientRect();
581
+ var mx = (ev.clientX - r.left) * DPR, my = (ev.clientY - r.top) * DPR;
582
+ var best = null, bd = 20 * DPR;
583
+ for (var k = 0; k < nodes.length; k++) {
584
+ var nn = nodes[k];
585
+ if (nn._sx === undefined) continue;
586
+ var d = Math.sqrt((nn._sx - mx) * (nn._sx - mx) + (nn._sy - my) * (nn._sy - my));
587
+ if (d < bd) { bd = d; best = nn; }
588
+ }
589
+ if (best) { active = best; chain = bfs(best.id, 2); renderPanel(best); }
590
+ }
591
+
592
+ canvas.addEventListener('pointerdown', onDown);
593
+ canvas.addEventListener('pointerup', onUp);
594
+ canvas.addEventListener('pointermove', onMove);
595
+ canvas.addEventListener('wheel', onWheel, { passive: true });
596
+ canvas.addEventListener('click', onClick);
597
+
598
+ var raf = 0;
599
+ function frame() {
600
+ raf = requestAnimationFrame(frame);
601
+ ctx.fillStyle = 'rgba(3,5,10,0.24)';
602
+ ctx.fillRect(0, 0, W, H);
603
+ var cy = Math.cos(yaw), sy = Math.sin(yaw), cp = Math.cos(pitch), sp = Math.sin(pitch);
604
+ var fov = 1100 * zoom, cx = W / 2, cyy = H / 2 + 40;
605
+ var all = [core].concat(nodes), k, nn;
606
+ for (k = 0; k < all.length; k++) {
607
+ nn = all[k];
608
+ var x1 = nn.x * cy - nn.z * sy, z1 = nn.x * sy + nn.z * cy;
609
+ var y1 = nn.y * cp - z1 * sp, z2 = nn.y * sp + z1 * cp;
610
+ var sc = fov / (fov + z2 + 700);
611
+ nn._sx = cx + x1 * sc; nn._sy = cyy + y1 * sc; nn._s = sc; nn._z = z2;
612
+ }
613
+ // 原意识光晕
614
+ var gs = ctx.createRadialGradient(core._sx, core._sy, 0, core._sx, core._sy, 150 * core._s);
615
+ gs.addColorStop(0, 'rgba(160,220,255,0.32)');
616
+ gs.addColorStop(0.45, 'rgba(90,160,255,0.09)');
617
+ gs.addColorStop(1, 'rgba(0,0,0,0)');
618
+ ctx.fillStyle = gs;
619
+ ctx.beginPath(); ctx.arc(core._sx, core._sy, 150 * core._s, 0, Math.PI * 2); ctx.fill();
620
+ // 边
621
+ for (k = 0; k < edges.length; k++) {
622
+ var ed = edges[k], a = byId.get(ed.source), b = byId.get(ed.target);
623
+ if (!a || !b || a._sx === undefined || b._sx === undefined) continue;
624
+ var lit = chain.has(a.id) || chain.has(b.id);
625
+ var depth = (a._s + b._s) / 2;
626
+ if (ed.kind === 'origin') {
627
+ ctx.strokeStyle = lit ? 'rgba(255,120,220,' + (0.26 * depth) + ')' : 'rgba(70,120,200,' + (0.045 * depth) + ')';
628
+ ctx.lineWidth = lit ? 1.1 : 0.4;
629
+ } else {
630
+ ctx.strokeStyle = lit ? 'rgba(255,140,230,' + (0.5 * depth) + ')' : 'rgba(80,130,200,' + (0.12 * depth) + ')';
631
+ ctx.lineWidth = lit ? 1.3 : 0.6;
632
+ }
633
+ ctx.beginPath(); ctx.moveTo(a._sx, a._sy); ctx.lineTo(b._sx, b._sy); ctx.stroke();
634
+ }
635
+ // 节点
636
+ nodes.sort(function (x, y2) { return y2._z - x._z; });
637
+ for (k = 0; k < nodes.length; k++) {
638
+ nn = nodes[k];
639
+ var isLit = !active || chain.has(nn.id);
640
+ var recency = Math.max(0.12, Math.min(1, 1.15 - nn.ageDays / 150));
641
+ ctx.globalAlpha = (isLit ? 0.95 : 0.16) * recency * Math.max(0.3, nn._s);
642
+ var r = Math.max(1.4, nn.size * nn._s * 2.1);
643
+ if (nn.ageDays < 1 || (active && chain.has(nn.id))) {
644
+ var gg = ctx.createRadialGradient(nn._sx, nn._sy, 0, nn._sx, nn._sy, r * 4);
645
+ gg.addColorStop(0, nn.color + 'aa');
646
+ gg.addColorStop(1, 'rgba(0,0,0,0)');
647
+ ctx.fillStyle = gg;
648
+ ctx.beginPath(); ctx.arc(nn._sx, nn._sy, r * 4, 0, Math.PI * 2); ctx.fill();
649
+ }
650
+ ctx.fillStyle = nn.color;
651
+ ctx.beginPath(); ctx.arc(nn._sx, nn._sy, r, 0, Math.PI * 2); ctx.fill();
652
+ if (nn === active) { ctx.strokeStyle = '#fff'; ctx.lineWidth = 1.6; ctx.stroke(); }
653
+ }
654
+ ctx.globalAlpha = 1;
655
+ ctx.fillStyle = '#e8f6ff';
656
+ ctx.beginPath(); ctx.arc(core._sx, core._sy, 7 * core._s, 0, Math.PI * 2); ctx.fill();
657
+ }
658
+ frame();
659
+
660
+ return function stop() {
661
+ cancelAnimationFrame(raf);
662
+ canvas.removeEventListener('pointerdown', onDown);
663
+ canvas.removeEventListener('pointerup', onUp);
664
+ canvas.removeEventListener('pointermove', onMove);
665
+ canvas.removeEventListener('wheel', onWheel);
666
+ canvas.removeEventListener('click', onClick);
667
+ if (ro) ro.disconnect();
668
+ };
669
+ }
670
+
671
+ function SeaView() {
672
+ var canvasRef = React.useRef(null);
673
+ var panelRef = React.useRef(null);
674
+ var st = React.useState({ loading: true, error: null, data: null });
675
+ var s = st[0], setS = st[1];
676
+ React.useEffect(function () {
677
+ call('sea').then(function (res) {
678
+ if (!res || res.ok !== true) {
679
+ setS({ loading: false, error: (res && res.error && res.error.message) || '读不到识海数据', data: null });
680
+ return;
681
+ }
682
+ setS({ loading: false, error: null, data: res.data });
683
+ }).catch(function (err) {
684
+ setS({ loading: false, error: String(err && err.message ? err.message : err), data: null });
685
+ });
686
+ }, []);
687
+ React.useEffect(function () {
688
+ if (!s.data || !canvasRef.current) return;
689
+ var stop = startSea(canvasRef.current, panelRef.current, s.data);
690
+ return function () { if (stop) stop(); };
691
+ }, [s.data]);
692
+ var over = 'position:absolute;left:12px;top:10px;font-size:12px;opacity:.65;color:#cfe3ff';
693
+ return React.createElement(
694
+ 'div', { style: { position: 'relative', flex: '1 1 auto', minHeight: '460px', background: '#05070d', borderRadius: '10px', overflow: 'hidden' } },
695
+ React.createElement('canvas', { ref: canvasRef, style: { display: 'block', width: '100%', height: '100%' } }),
696
+ s.loading ? React.createElement('div', { style: { padding: '16px', opacity: 0.7, color: '#cfe3ff' } }, '正在读识海…') : null,
697
+ s.error ? React.createElement('div', { style: { padding: '16px', color: '#ff8f8f', fontSize: '13px' } }, s.error) : null,
698
+ s.data && s.data.stats ? React.createElement('div', { style: over }, '原意识 · ' + s.data.stats.nodes + ' 段 · ' + s.data.stats.edges + ' 条连线 · 拖拽旋转 / 滚轮缩放 / 点记忆看念头') : null,
699
+ React.createElement('div', { ref: panelRef, style: { position: 'absolute', right: '12px', top: '12px', width: '270px', maxHeight: '72%', overflow: 'auto', background: 'rgba(8,14,26,.94)', border: '1px solid #23456b', borderRadius: '10px', padding: '12px', fontSize: '12px', lineHeight: 1.7, display: 'none' } }),
700
+ );
434
701
  }
435
702
 
436
703
  var inject = ['slots'];
package/lib/host/index.js CHANGED
@@ -118,6 +118,12 @@ export function apply(ctx, config = {}) {
118
118
  typeof config.stagesPath === 'string' && config.stagesPath.length > 0
119
119
  ? config.stagesPath
120
120
  : path.join(dshHome, 'stages.md');
121
+
122
+ // 识海(3D 星图)的数据文件:scripts/sea-graph.mjs 生成,页面只读。
123
+ const seaGraphPath =
124
+ typeof config.seaGraphPath === 'string' && config.seaGraphPath.length > 0
125
+ ? config.seaGraphPath
126
+ : path.join(dshHome, 'sea-graph.json');
121
127
  const stages = createStages({
122
128
  filePath: stagesPath,
123
129
  log: (m) => ctx.logger?.info('dsh-house2: ' + m),
@@ -125,6 +131,56 @@ export function apply(ctx, config = {}) {
125
131
  stages.ensure();
126
132
  ctx.logger?.info('dsh-house2: 工段文件 = %s', stagesPath);
127
133
 
134
+ // 2026-10-30:压缩改成按 token 触发(不再挂在「任务完成」上)。
135
+ // 口径跟 dsh-session-rotate 一样:监听 session/event 里 assistant/message 的 data.usage。
136
+ // 到阈值就在注入里提醒「该交接/换会话」,不自己调压缩(DSH 没给插件公开的手动压缩接口)。
137
+ const tokenStatePath = path.join(dshHome, 'house2-token.json');
138
+ const tokenState = { sessionId: '', usedTokens: 0 };
139
+ try {
140
+ Object.assign(tokenState, JSON.parse(fs.readFileSync(tokenStatePath, 'utf8')));
141
+ } catch {}
142
+ let tokenSaveTimer = null;
143
+ const saveTokenState = () => {
144
+ if (tokenSaveTimer) return;
145
+ tokenSaveTimer = setTimeout(() => {
146
+ tokenSaveTimer = null;
147
+ try {
148
+ fs.writeFileSync(tokenStatePath, JSON.stringify(tokenState), 'utf8');
149
+ } catch {}
150
+ }, 1000);
151
+ tokenSaveTimer.unref?.();
152
+ };
153
+ const tokenThreshold = Math.max(0, Number(config.thresholdTokens) || 100000000);
154
+ try {
155
+ ctx.on('session/event', (session, event) => {
156
+ try {
157
+ const sid = String(session?.id ?? session?.sessionId ?? '');
158
+ if (sid && sid !== tokenState.sessionId) {
159
+ tokenState.sessionId = sid;
160
+ tokenState.usedTokens = 0;
161
+ saveTokenState();
162
+ }
163
+ if (event?.type !== 'assistant/message') return;
164
+ const u = event?.data?.usage;
165
+ if (!u) return;
166
+ const total = Number(u.totalTokens);
167
+ const add =
168
+ Number.isFinite(total) && total > 0
169
+ ? total
170
+ : (Number(u.inputTokens) || 0) +
171
+ (Number(u.outputTokens) || 0) +
172
+ (Number(u.cacheReadTokens) || 0) +
173
+ (Number(u.cacheWriteTokens) || 0);
174
+ if (add > 0) {
175
+ tokenState.usedTokens += add;
176
+ saveTokenState();
177
+ }
178
+ } catch {}
179
+ });
180
+ } catch (e) {
181
+ ctx.logger?.warn('dsh-house2: 挂 token 统计失败:' + (e && e.message ? e.message : e));
182
+ }
183
+
128
184
  // 版本自检:只查 npm 有没有新版,不自动安装。
129
185
  // 注意:本地开发目录的 package.json name 是 dsh-house2,而 npm 上是 scoped 包,所以写死 npmName。
130
186
  const updateChecker = createUpdateChecker({
@@ -586,7 +642,8 @@ export function apply(ctx, config = {}) {
586
642
  '写积木分两种:按能力写(`手机-接入与操作`、`画图-Agnes`,回答「怎么做」)、按任务写(`任务-调通一直发`,回答「那段对话干了什么」,一段任务一篇,按时间轴切,切完要覆盖全程、不重不漏);' +
587
643
  '细节、命令、踩过的坑,用 house_write 写进积木(knowledge 目录,默认追加,别一个坑建一个文件);' +
588
644
  '一件事真做完了(跑通、验证过),用 house_done 把它从「正在长出来的」搬到「已经完工的」——别让旧条目一直挂着;' +
589
- '清单和最近对话里找不到的东西,用 house_search 去积木里按关键词翻,翻到再答,别猜。';
645
+ '清单和最近对话里找不到的东西,用 house_search 去积木里按关键词翻,翻到再答,别猜(先按工段定位,传 stage 参数只搜它挂的那一篇);' +
646
+ '压缩和换会话不归这里管——那是按累计 token 阈值来的(session-rotate),工段只负责把任务记清楚。';
590
647
 
591
648
  function assetsAsPrompt() {
592
649
  let text = '';
@@ -595,7 +652,15 @@ export function apply(ctx, config = {}) {
595
652
  } catch {}
596
653
  const stageText = stages.renderPrompt();
597
654
  const base = text ? `【资产清单】\n${text}\n\n${HOUSE_GUIDE}` : HOUSE_GUIDE;
598
- return stageText ? `${base}\n\n${stageText}` : base;
655
+ // 2026-10-30:压缩按 token 走,到 80% 就在这里提醒一句。
656
+ const used = tokenState.usedTokens;
657
+ let tokenLine = '';
658
+ if (used > 0 && tokenThreshold > 0) {
659
+ const pct = Math.round((used / tokenThreshold) * 100);
660
+ tokenLine = `【token】已用 ${(used / 10000).toFixed(0)} 万 / 阈值 ${(tokenThreshold / 10000).toFixed(0)} 万(${pct}%)`;
661
+ if (pct >= 80) tokenLine += ' —— 该准备交接、收尾换会话了(压缩按 token 触发,不归工段管)';
662
+ }
663
+ return [base, stageText, tokenLine].filter(Boolean).join('\n\n');
599
664
  }
600
665
 
601
666
  // 2026-10-30:改成直接 ctx.systemPrompt.section(跟 dsh-affection 一样)。
@@ -902,7 +967,25 @@ export function apply(ctx, config = {}) {
902
967
  return;
903
968
  }
904
969
  if (method === 'meta') {
905
- writeJson(response, 200, { ok: true, data: { assetsPath, knowledgeDir, sessionsDir } });
970
+ writeJson(response, 200, { ok: true, data: { assetsPath, knowledgeDir, sessionsDir, stagesPath, seaGraphPath } });
971
+ return;
972
+ }
973
+
974
+ // 识海数据:3D 星图用的节点+边(由 scripts/sea-graph.mjs 生成)
975
+ if (method === 'sea') {
976
+ try {
977
+ const raw = await readFile(seaGraphPath, 'utf8');
978
+ const j = JSON.parse(raw);
979
+ writeJson(response, 200, { ok: true, data: { stats: j.stats, nodes: j.nodes, edges: j.edges, path: seaGraphPath } });
980
+ } catch (error) {
981
+ writeJson(response, 200, {
982
+ ok: false,
983
+ error: {
984
+ code: 'missing',
985
+ message: `读不到识海数据:${seaGraphPath}(先在电脑上跑 scripts/sea-graph.mjs 生成)`,
986
+ },
987
+ });
988
+ }
906
989
  return;
907
990
  }
908
991
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiugeqqqqq/dsh-house2",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "房子 2 —— 资产/能力清单 + 工段,配侧边栏页面。0.1.0 先把安装跑通(保守兼容写法),功能后续迭代。",
5
5
  "type": "module",
6
6
  "license": "MIT",