sage-mem 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +3 -0
- package/lib/client.js +1357 -9
- package/lib/index.js +68 -1
- package/lib/typert.host.js +39 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -22,6 +22,1305 @@ window.__ModuleLoader__.load({
|
|
|
22
22
|
let react = require("react");
|
|
23
23
|
const h = react.createElement;
|
|
24
24
|
|
|
25
|
+
const StarmapRuntime = (function (require, _react) {
|
|
26
|
+
let react = _react;
|
|
27
|
+
const h = react.createElement;
|
|
28
|
+
// ── 手写 strict codec(浏览器无 zod,typert 只要求 parse 方法)──
|
|
29
|
+
const isStr = (v) => { if (typeof v !== "string") throw new Error("expected string"); };
|
|
30
|
+
const isNum = (v) => { if (typeof v !== "number") throw new Error("expected number"); };
|
|
31
|
+
const strSchema = { parse(v) { isStr(v); return v; } };
|
|
32
|
+
const starSchema = {
|
|
33
|
+
parse(v) {
|
|
34
|
+
if (v === null || typeof v !== "object") throw new Error("invalid star");
|
|
35
|
+
isStr(v.file); isStr(v.kind); isStr(v.title); isStr(v.desc); isNum(v.bytes); isNum(v.mtimeMs);
|
|
36
|
+
return v;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const listSchema = {
|
|
40
|
+
parse(v) {
|
|
41
|
+
if (v === null || typeof v !== "object") throw new Error("invalid payload");
|
|
42
|
+
isNum(v.count);
|
|
43
|
+
if (!Array.isArray(v.stars)) throw new Error("expected stars array");
|
|
44
|
+
v.stars.forEach((s) => starSchema.parse(s));
|
|
45
|
+
return v;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
const readSchema = {
|
|
49
|
+
parse(v) {
|
|
50
|
+
if (v === null || typeof v !== "object") throw new Error("invalid payload");
|
|
51
|
+
isStr(v.name); isStr(v.content);
|
|
52
|
+
return v;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const TYPERT_REMOTE = {
|
|
57
|
+
package: "sage-starmap",
|
|
58
|
+
descriptors: [
|
|
59
|
+
{
|
|
60
|
+
id: "sage-starmap#starmap/listStars",
|
|
61
|
+
service: "starmap",
|
|
62
|
+
namespace: "starmap",
|
|
63
|
+
method: "listStars",
|
|
64
|
+
invocation: { kind: "direct" },
|
|
65
|
+
parameters: [],
|
|
66
|
+
result: { mode: "strict", typeSymbol: "sage-starmap/types#StarList", schema: listSchema },
|
|
67
|
+
sourceLocation: { "file": "workspace/sage-starmap/lib/index.js", "line": 1, "column": 1 },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "sage-starmap#starmap/readFile",
|
|
71
|
+
service: "starmap",
|
|
72
|
+
namespace: "starmap",
|
|
73
|
+
method: "readFile",
|
|
74
|
+
invocation: { kind: "direct" },
|
|
75
|
+
parameters: [
|
|
76
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-starmap/types#FileName", schema: strSchema } },
|
|
77
|
+
],
|
|
78
|
+
result: { mode: "strict", typeSymbol: "sage-starmap/types#FileContent", schema: readSchema },
|
|
79
|
+
sourceLocation: { "file": "workspace/sage-starmap/lib/index.js", "line": 1, "column": 1 },
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** 拆 typert remote 的 {ok, value} 信封。 */
|
|
85
|
+
function unwrap(result) {
|
|
86
|
+
return result && result.ok ? result.value : result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** 错误详情尽量展开,避免 "[object Object]" 式黑洞。 */
|
|
90
|
+
function detail(e) {
|
|
91
|
+
if (!e) return "未知错误";
|
|
92
|
+
if (e instanceof Error) return (e.message || String(e)) + (e.stack ? " · " + e.stack.slice(0, 180) : "");
|
|
93
|
+
try {
|
|
94
|
+
const s = JSON.stringify(e);
|
|
95
|
+
return s && s !== "{}" ? s.slice(0, 200) : String(e);
|
|
96
|
+
} catch (_) {
|
|
97
|
+
return String(e);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function preview(v) {
|
|
102
|
+
try {
|
|
103
|
+
const s = JSON.stringify(v);
|
|
104
|
+
return s && s !== "{}" ? s.slice(0, 160) : String(v);
|
|
105
|
+
} catch (_) {
|
|
106
|
+
return String(v);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── 常量与纯函数 ─────────────────────────────────────────────
|
|
111
|
+
const KIND_META = {
|
|
112
|
+
user: { label: "用户", color: "#ffd27d" },
|
|
113
|
+
feedback: { label: "反馈", color: "#6fe3ff" },
|
|
114
|
+
project: { label: "项目", color: "#b18cff" },
|
|
115
|
+
reference: { label: "参考", color: "#7dffb0" },
|
|
116
|
+
special: { label: "特殊", color: "#cfd6e6" },
|
|
117
|
+
};
|
|
118
|
+
const KIND_ORDER = ["user", "feedback", "project", "reference", "special"];
|
|
119
|
+
const FRESH_MS = 7 * 86400000;
|
|
120
|
+
|
|
121
|
+
function hashStr(s) {
|
|
122
|
+
let hh = 2166136261;
|
|
123
|
+
for (let i = 0; i < s.length; i++) {
|
|
124
|
+
hh ^= s.charCodeAt(i);
|
|
125
|
+
hh = Math.imul(hh, 16777619);
|
|
126
|
+
}
|
|
127
|
+
return hh >>> 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function rand01(seed) {
|
|
131
|
+
let t = (seed + 0x6D2B79F5) >>> 0;
|
|
132
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
133
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
134
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function hexA(hex, a) {
|
|
138
|
+
const n = parseInt(hex.slice(1), 16);
|
|
139
|
+
return "rgba(" + ((n >> 16) & 255) + "," + ((n >> 8) & 255) + "," + (n & 255) + "," + a.toFixed(3) + ")";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function fmtBytes(n) {
|
|
143
|
+
if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
|
|
144
|
+
return n + " B";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function fmtDate(ms) {
|
|
148
|
+
if (!ms) return "";
|
|
149
|
+
const d = new Date(ms);
|
|
150
|
+
const p = (x) => (x < 10 ? "0" : "") + x;
|
|
151
|
+
return d.getFullYear() + "-" + p(d.getMonth() + 1) + "-" + p(d.getDate());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function fmtRel(ms) {
|
|
155
|
+
if (!ms) return "";
|
|
156
|
+
const d = Date.now() - ms;
|
|
157
|
+
if (d < 60000) return "刚刚";
|
|
158
|
+
if (d < 3600000) return Math.floor(d / 60000) + " 分钟前";
|
|
159
|
+
if (d < 86400000) return Math.floor(d / 3600000) + " 小时前";
|
|
160
|
+
const days = Math.floor(d / 86400000);
|
|
161
|
+
if (days < 30) return days + " 天前";
|
|
162
|
+
return fmtDate(ms);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function computeLayout(stars, W, H) {
|
|
166
|
+
const pts = stars.map((s) => {
|
|
167
|
+
const hh = hashStr(s.file);
|
|
168
|
+
return {
|
|
169
|
+
star: s,
|
|
170
|
+
x: 46 + rand01(hh) * (W - 92),
|
|
171
|
+
y: 42 + rand01((hh ^ 0x9e3779b9) >>> 0) * (H - 88),
|
|
172
|
+
r: 2.2 + Math.min(3.2, Math.sqrt(Math.max(s.bytes || 200, 200) / 3500)),
|
|
173
|
+
tw: rand01((hh ^ 0x85ebca6b) >>> 0),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
for (let iter = 0; iter < 80; iter++) {
|
|
177
|
+
for (let i = 0; i < pts.length; i++) {
|
|
178
|
+
for (let j = i + 1; j < pts.length; j++) {
|
|
179
|
+
const a = pts[i];
|
|
180
|
+
const b = pts[j];
|
|
181
|
+
let dx = b.x - a.x;
|
|
182
|
+
let dy = b.y - a.y;
|
|
183
|
+
let d2 = dx * dx + dy * dy;
|
|
184
|
+
const minD = a.r + b.r + 16;
|
|
185
|
+
if (d2 < minD * minD) {
|
|
186
|
+
if (d2 < 0.01) { dx = 0.5; dy = 0.3; d2 = 0.34; }
|
|
187
|
+
const d = Math.sqrt(d2);
|
|
188
|
+
const push = ((minD - d) / d) * 0.35;
|
|
189
|
+
a.x -= dx * push;
|
|
190
|
+
a.y -= dy * push;
|
|
191
|
+
b.x += dx * push;
|
|
192
|
+
b.y += dy * push;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const p of pts) {
|
|
198
|
+
p.x = Math.max(24, Math.min(W - 24, p.x));
|
|
199
|
+
p.y = Math.max(22, Math.min(H - 22, p.y));
|
|
200
|
+
}
|
|
201
|
+
return { W: W, H: H, pts: pts };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function starVisible(p, kindsOn, q) {
|
|
205
|
+
if (!kindsOn[p.star.kind]) return false;
|
|
206
|
+
if (q) {
|
|
207
|
+
const hay = (p.star.title + " " + p.star.desc + " " + p.star.file).toLowerCase();
|
|
208
|
+
if (hay.indexOf(q) < 0) return false;
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 可见性标记 + 同类最近邻星座连线。 */
|
|
214
|
+
function buildVisibility(lay, kindsOn, q) {
|
|
215
|
+
const flags = [];
|
|
216
|
+
for (let i = 0; i < lay.pts.length; i++) flags.push(starVisible(lay.pts[i], kindsOn, q));
|
|
217
|
+
const byKind = {};
|
|
218
|
+
for (let i = 0; i < lay.pts.length; i++) {
|
|
219
|
+
if (!flags[i]) continue;
|
|
220
|
+
const k = lay.pts[i].star.kind;
|
|
221
|
+
if (!byKind[k]) byKind[k] = [];
|
|
222
|
+
byKind[k].push(i);
|
|
223
|
+
}
|
|
224
|
+
const lines = [];
|
|
225
|
+
for (const k in byKind) {
|
|
226
|
+
const remaining = byKind[k].slice();
|
|
227
|
+
if (remaining.length < 2) continue;
|
|
228
|
+
let cur = remaining.shift();
|
|
229
|
+
let guard = 0;
|
|
230
|
+
while (remaining.length > 0 && guard < 300) {
|
|
231
|
+
guard++;
|
|
232
|
+
let bi = 0;
|
|
233
|
+
let bd = Infinity;
|
|
234
|
+
for (let r = 0; r < remaining.length; r++) {
|
|
235
|
+
const dx = lay.pts[remaining[r]].x - lay.pts[cur].x;
|
|
236
|
+
const dy = lay.pts[remaining[r]].y - lay.pts[cur].y;
|
|
237
|
+
const d = dx * dx + dy * dy;
|
|
238
|
+
if (d < bd) { bd = d; bi = r; }
|
|
239
|
+
}
|
|
240
|
+
const nxt = remaining.splice(bi, 1)[0];
|
|
241
|
+
lines.push([cur, nxt, k]);
|
|
242
|
+
cur = nxt;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { flags: flags, lines: lines };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── 轻量 Markdown → React(标题/粗体/行内码/[[链接]]/引用/列表/围栏/分隔线)──
|
|
249
|
+
function parseInline(text, kb) {
|
|
250
|
+
const els = [];
|
|
251
|
+
const re = /(\*\*[^*]+\*\*|`[^`]+`|\[\[[^\]]+\]\])/g;
|
|
252
|
+
let last = 0;
|
|
253
|
+
let m;
|
|
254
|
+
let i = 0;
|
|
255
|
+
while ((m = re.exec(text)) !== null) {
|
|
256
|
+
if (m.index > last) els.push(text.slice(last, m.index));
|
|
257
|
+
const tok = m[0];
|
|
258
|
+
if (tok.charCodeAt(1) === 42) els.push(h("strong", { key: kb + "b" + i }, tok.slice(2, -2)));
|
|
259
|
+
else if (tok.charCodeAt(0) === 96) els.push(h("code", { key: kb + "c" + i, className: "smap-md-code" }, tok.slice(1, -1)));
|
|
260
|
+
else els.push(h("span", { key: kb + "w" + i, className: "smap-md-wiki" }, tok.slice(2, -2)));
|
|
261
|
+
last = m.index + tok.length;
|
|
262
|
+
i++;
|
|
263
|
+
}
|
|
264
|
+
if (last < text.length) els.push(text.slice(last));
|
|
265
|
+
return els;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function renderMarkdown(text) {
|
|
269
|
+
const lines = text.split(/\r?\n/);
|
|
270
|
+
const blocks = [];
|
|
271
|
+
let para = [];
|
|
272
|
+
let list = null;
|
|
273
|
+
let code = null;
|
|
274
|
+
let key = 0;
|
|
275
|
+
function flushPara() {
|
|
276
|
+
if (para.length) {
|
|
277
|
+
blocks.push(h("p", { key: "p" + key, className: "smap-md-p" }, parseInline(para.join(" "), "p" + key)));
|
|
278
|
+
para = [];
|
|
279
|
+
key++;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function flushList() {
|
|
283
|
+
if (list && list.items.length) {
|
|
284
|
+
blocks.push(h(list.ordered ? "ol" : "ul", { key: "l" + key, className: "smap-md-list" },
|
|
285
|
+
list.items.map((it, ix) => h("li", { key: ix }, parseInline(it, "l" + key + "i" + ix)))));
|
|
286
|
+
key++;
|
|
287
|
+
}
|
|
288
|
+
list = null;
|
|
289
|
+
}
|
|
290
|
+
for (const raw of lines) {
|
|
291
|
+
const line = raw.replace(/\s+$/, "");
|
|
292
|
+
if (code !== null) {
|
|
293
|
+
if (/^```/.test(line)) {
|
|
294
|
+
blocks.push(h("pre", { key: "pre" + key, className: "smap-md-pre" }, code.join("\n")));
|
|
295
|
+
code = null;
|
|
296
|
+
key++;
|
|
297
|
+
} else code.push(raw);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (/^```/.test(line)) { flushPara(); flushList(); code = []; continue; }
|
|
301
|
+
const hh = line.match(/^(#{1,4})\s+(.*)$/);
|
|
302
|
+
if (hh) {
|
|
303
|
+
flushPara(); flushList();
|
|
304
|
+
blocks.push(h("div", { key: "h" + key, className: "smap-md-h smap-md-h" + hh[1].length }, parseInline(hh[2], "h" + key)));
|
|
305
|
+
key++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (/^(-{3,}|\*{3,})\s*$/.test(line)) {
|
|
309
|
+
flushPara(); flushList();
|
|
310
|
+
blocks.push(h("hr", { key: "hr" + key, className: "smap-md-hr" }));
|
|
311
|
+
key++;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
const q = line.match(/^>\s?(.*)$/);
|
|
315
|
+
if (q) {
|
|
316
|
+
flushPara(); flushList();
|
|
317
|
+
blocks.push(h("blockquote", { key: "q" + key, className: "smap-md-quote" }, parseInline(q[1], "q" + key)));
|
|
318
|
+
key++;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const ul = line.match(/^[-*]\s+(.*)$/);
|
|
322
|
+
const ol = line.match(/^\d+[.)]\s+(.*)$/);
|
|
323
|
+
if (ul || ol) {
|
|
324
|
+
flushPara();
|
|
325
|
+
const ordered = !ul;
|
|
326
|
+
const itemText = ul ? ul[1] : ol[1];
|
|
327
|
+
if (!list || list.ordered !== ordered) { flushList(); list = { ordered: ordered, items: [] }; }
|
|
328
|
+
list.items.push(itemText);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (/^\s*$/.test(line)) { flushPara(); flushList(); continue; }
|
|
332
|
+
para.push(line);
|
|
333
|
+
}
|
|
334
|
+
if (code !== null) {
|
|
335
|
+
blocks.push(h("pre", { key: "pre" + key, className: "smap-md-pre" }, code.join("\n")));
|
|
336
|
+
key++;
|
|
337
|
+
}
|
|
338
|
+
flushPara();
|
|
339
|
+
flushList();
|
|
340
|
+
return blocks;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── overlay 开合的模块级 store(footer 按钮与 overlay 共享)──
|
|
344
|
+
const uiStore = { open: false, subs: new Set() };
|
|
345
|
+
/** client context 由 apply 存入;Slot 组件经 props 或此兜底获取。 */
|
|
346
|
+
let appCtx = null;
|
|
347
|
+
function setOpen(v) {
|
|
348
|
+
uiStore.open = !!v;
|
|
349
|
+
uiStore.subs.forEach((fn) => fn(uiStore.open));
|
|
350
|
+
}
|
|
351
|
+
function useOpen() {
|
|
352
|
+
const s = react.useState(uiStore.open);
|
|
353
|
+
react.useEffect(() => {
|
|
354
|
+
const fn = (v) => s[1](v);
|
|
355
|
+
uiStore.subs.add(fn);
|
|
356
|
+
return () => { uiStore.subs.delete(fn); };
|
|
357
|
+
}, []);
|
|
358
|
+
return s[0];
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── 样式(data-plugin-css 约定,幂等注入)──
|
|
362
|
+
const CSS = [
|
|
363
|
+
".smap-footer-btn{display:inline-flex;align-items:center;gap:6px;border:1px solid var(--dsw-alias-border-l1,transparent);background:transparent;color:var(--dsw-alias-label-secondary,inherit);border-radius:8px;padding:4px 10px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;}",
|
|
364
|
+
".smap-fab{position:fixed;right:0;top:15%;transform:translateY(-50%);z-index:900;width:36px;height:64px;border-radius:12px 0 0 12px;border:1px solid rgba(150,170,230,.35);border-right:none;background:rgba(13,18,38,.78);color:#ffd27d;font-size:17px;cursor:pointer;display:flex;align-items:center;justify-content:center;opacity:.5;pointer-events:auto;transition:opacity .15s,background .15s,width .15s;box-shadow:-4px 0 18px rgba(0,0,0,.35);font-family:inherit;}",
|
|
365
|
+
".smap-fab:hover{opacity:1;width:46px;background:rgba(20,28,56,.95);}",
|
|
366
|
+
".smap-footer-btn:hover{color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-border-l2,#8886);}",
|
|
367
|
+
".smap-footer-icon{font-size:13px;line-height:1;}",
|
|
368
|
+
".smap-veil{position:fixed;inset:0;z-index:1000;background:rgba(4,6,16,.72);backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;pointer-events:auto;animation:smap-fade-in .18s ease-out;}",
|
|
369
|
+
".smap-modal{width:min(980px,94vw);height:min(680px,92vh);background:linear-gradient(160deg,#0d1226,#111a33);border:1px solid rgba(140,160,220,.22);border-radius:16px;box-shadow:0 24px 80px rgba(0,0,0,.55);padding:16px 18px;display:flex;flex-direction:column;overflow:hidden;pointer-events:auto;animation:smap-pop-in .22s cubic-bezier(.2,.9,.3,1.15);}",
|
|
370
|
+
"@keyframes smap-fade-in{from{opacity:0}to{opacity:1}}",
|
|
371
|
+
"@keyframes smap-pop-in{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:none}}",
|
|
372
|
+
".smap-card{color:#dbe4f5;display:flex;flex-direction:column;flex:1;min-height:0;}",
|
|
373
|
+
".smap-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap;}",
|
|
374
|
+
".smap-title{font-size:15px;font-weight:600;letter-spacing:.06em;}",
|
|
375
|
+
".smap-sub{font-size:11px;opacity:.55;}",
|
|
376
|
+
".smap-spacer{flex:1;}",
|
|
377
|
+
".smap-search{background:rgba(255,255,255,.06);border:1px solid rgba(150,170,230,.25);border-radius:8px;color:#dbe4f5;font-size:12px;padding:4px 9px;outline:none;width:160px;font-family:inherit;}",
|
|
378
|
+
".smap-search:focus{border-color:rgba(150,170,230,.55);}",
|
|
379
|
+
".smap-search::placeholder{color:rgba(219,228,245,.35);}",
|
|
380
|
+
".smap-btn{background:rgba(255,255,255,.07);border:1px solid rgba(150,170,230,.25);border-radius:8px;color:#dbe4f5;font-size:12px;padding:4px 10px;cursor:pointer;font-family:inherit;}",
|
|
381
|
+
".smap-btn:hover{background:rgba(255,255,255,.13);}",
|
|
382
|
+
".smap-wrap{position:relative;flex:1;min-height:0;}",
|
|
383
|
+
".smap-canvas{position:absolute;inset:0;width:100%;height:100%;display:block;border-radius:10px;cursor:crosshair;}",
|
|
384
|
+
".smap-tip{position:absolute;z-index:5;pointer-events:none;max-width:280px;background:rgba(10,14,30,.95);border:1px solid rgba(150,170,230,.35);border-radius:8px;padding:8px 10px;font-size:12px;line-height:1.55;box-shadow:0 6px 24px rgba(0,0,0,.45);}",
|
|
385
|
+
".smap-tip-title{font-weight:600;margin-bottom:2px;}",
|
|
386
|
+
".smap-tip-kind{opacity:.6;font-size:11px;margin-bottom:2px;}",
|
|
387
|
+
".smap-tip-desc{opacity:.85;}",
|
|
388
|
+
".smap-panel{position:absolute;top:10px;right:10px;bottom:10px;width:min(400px,54%);background:rgba(9,13,28,.97);border:1px solid rgba(150,170,230,.32);border-radius:10px;padding:12px 14px;overflow-y:auto;z-index:6;font-size:13px;line-height:1.7;box-shadow:-8px 0 30px rgba(0,0,0,.35);}",
|
|
389
|
+
".smap-panel-head{display:flex;align-items:flex-start;gap:8px;margin-bottom:4px;}",
|
|
390
|
+
".smap-panel-title{font-weight:600;font-size:14px;flex:1;}",
|
|
391
|
+
".smap-panel-meta{font-size:11px;opacity:.6;margin-bottom:8px;}",
|
|
392
|
+
".smap-meta-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px;vertical-align:-1px;}",
|
|
393
|
+
".smap-legend{display:flex;flex-wrap:wrap;gap:14px;margin-top:10px;font-size:12px;align-items:center;}",
|
|
394
|
+
".smap-lg-item{cursor:pointer;user-select:none;}",
|
|
395
|
+
".smap-lg-item.off{opacity:.32;}",
|
|
396
|
+
".smap-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px;vertical-align:-1px;}",
|
|
397
|
+
".smap-count{margin-left:auto;opacity:.55;font-size:11px;}",
|
|
398
|
+
".smap-status{padding:28px;text-align:center;opacity:.6;font-size:13px;}",
|
|
399
|
+
".smap-empty{position:absolute;left:0;right:0;top:46%;text-align:center;color:rgba(219,228,245,.42);font-size:13px;pointer-events:none;z-index:4;}",
|
|
400
|
+
".smap-replay-on{border-color:rgba(255,210,125,.55)!important;color:#ffd27d!important;}",
|
|
401
|
+
".smap-replay-date{font-size:11px;opacity:.75;color:#ffd27d;font-variant-numeric:tabular-nums;}",
|
|
402
|
+
".smap-md-h{font-weight:600;margin:12px 0 4px;}",
|
|
403
|
+
".smap-md-h1{font-size:16px;}",
|
|
404
|
+
".smap-md-h2{font-size:14.5px;}",
|
|
405
|
+
".smap-md-h3,.smap-md-h4{font-size:13.5px;opacity:.92;}",
|
|
406
|
+
".smap-md-p{margin:4px 0;}",
|
|
407
|
+
".smap-md-list{margin:4px 0;padding-left:20px;}",
|
|
408
|
+
".smap-md-code{background:rgba(255,255,255,.09);padding:1px 5px;border-radius:4px;font-size:12px;}",
|
|
409
|
+
".smap-md-pre{background:rgba(255,255,255,.06);padding:8px 10px;border-radius:8px;font-size:12px;overflow-x:auto;white-space:pre-wrap;margin:6px 0;}",
|
|
410
|
+
".smap-md-quote{border-left:3px solid rgba(150,170,230,.4);margin:6px 0;padding:2px 10px;opacity:.88;}",
|
|
411
|
+
".smap-md-wiki{color:#8ab4ff;}",
|
|
412
|
+
".smap-md-hr{border:none;border-top:1px solid rgba(150,170,230,.22);margin:10px 0;}",
|
|
413
|
+
".smap-viewtoggle{display:inline-flex;border:1px solid rgba(150,170,230,.25);border-radius:8px;overflow:hidden;}",
|
|
414
|
+
".smap-vt-btn{background:transparent;border:none;color:rgba(219,228,245,.55);font-size:12px;padding:4px 10px;cursor:pointer;font-family:inherit;}",
|
|
415
|
+
".smap-vt-btn.on{background:rgba(255,255,255,.12);color:#dbe4f5;}",
|
|
416
|
+
".smap-timeline{position:absolute;inset:0;overflow-y:auto;display:flex;flex-direction:column;gap:6px;padding:4px 6px 4px 2px;}",
|
|
417
|
+
".smap-tl-item{display:flex;align-items:baseline;gap:8px;background:rgba(255,255,255,.035);border:1px solid rgba(150,170,230,.16);border-radius:8px;padding:7px 10px;cursor:pointer;flex:none;}",
|
|
418
|
+
".smap-tl-item:hover{background:rgba(255,255,255,.08);border-color:rgba(150,170,230,.35);}",
|
|
419
|
+
".smap-tl-date{font-size:11px;opacity:.5;font-variant-numeric:tabular-nums;flex:none;}",
|
|
420
|
+
".smap-tl-title{font-size:13px;font-weight:600;flex:none;max-width:40%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}",
|
|
421
|
+
".smap-tl-desc{font-size:12px;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;}",
|
|
422
|
+
].join("\n");
|
|
423
|
+
|
|
424
|
+
function injectCss() {
|
|
425
|
+
const tagId = "sage-starmap/overlay.css";
|
|
426
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
427
|
+
const tag = document.createElement("style");
|
|
428
|
+
tag.dataset.plugin = "sage-starmap";
|
|
429
|
+
tag.dataset.pluginCss = tagId;
|
|
430
|
+
tag.textContent = CSS;
|
|
431
|
+
document.head.appendChild(tag);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── 渲染层 v0.4:WebGL 星野(程序化银河 + 衍射星芒 + additive 发光),canvas2d 兜底 ──
|
|
436
|
+
function hexToRgb(hex) {
|
|
437
|
+
const n = parseInt(hex.slice(1), 16);
|
|
438
|
+
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const SMAP_BG_VS = "attribute vec2 aPos; void main(){ gl_Position = vec4(aPos, 0.0, 1.0); }";
|
|
442
|
+
const SMAP_BG_FS = [
|
|
443
|
+
"precision highp float;",
|
|
444
|
+
"uniform vec2 uRes;",
|
|
445
|
+
"uniform float uTime;",
|
|
446
|
+
"float hash21(vec2 p){ p = fract(p * vec2(234.34, 435.345)); p += dot(p, p + 34.23); return fract(p.x * p.y); }",
|
|
447
|
+
"float vnoise(vec2 p){",
|
|
448
|
+
" vec2 i = floor(p); vec2 f = fract(p);",
|
|
449
|
+
" f = f * f * (3.0 - 2.0 * f);",
|
|
450
|
+
" float a = hash21(i); float b = hash21(i + vec2(1.0, 0.0));",
|
|
451
|
+
" float c = hash21(i + vec2(0.0, 1.0)); float e = hash21(i + vec2(1.0, 1.0));",
|
|
452
|
+
" return mix(mix(a, b, f.x), mix(c, e, f.x), f.y);",
|
|
453
|
+
"}",
|
|
454
|
+
"float fbm(vec2 p){",
|
|
455
|
+
" float v = 0.0; float amp = 0.55;",
|
|
456
|
+
" for (int i = 0; i < 4; i++){ v += amp * vnoise(p); p = p * 2.03 + vec2(17.7, 9.2); amp *= 0.5; }",
|
|
457
|
+
" return v;",
|
|
458
|
+
"}",
|
|
459
|
+
"void main(){",
|
|
460
|
+
" float fy = gl_FragCoord.y / uRes.y;",
|
|
461
|
+
" vec3 col = mix(vec3(0.063, 0.090, 0.192), vec3(0.043, 0.063, 0.149), fy);",
|
|
462
|
+
" vec2 suv = gl_FragCoord.xy / uRes;",
|
|
463
|
+
" float band = suv.x * 0.92 - suv.y * 0.62 + 0.16;",
|
|
464
|
+
" float bandMask = exp(-band * band * 13.0);",
|
|
465
|
+
" vec2 np = vec2(suv.x * 3.1 + suv.y * 1.35, suv.y * 2.15 - suv.x * 0.75);",
|
|
466
|
+
" float neb = fbm(np + vec2(uTime * 0.010, -uTime * 0.006));",
|
|
467
|
+
" vec3 dustCol = mix(vec3(0.36, 0.44, 0.78), vec3(0.72, 0.58, 0.46), fbm(np * 0.63 + 4.7));",
|
|
468
|
+
" col += dustCol * bandMask * (neb * neb) * 0.30;",
|
|
469
|
+
" float dark = fbm(np * 0.42 + 13.1);",
|
|
470
|
+
" col *= 1.0 - dark * bandMask * 0.22;",
|
|
471
|
+
" gl_FragColor = vec4(col, 1.0);",
|
|
472
|
+
"}",
|
|
473
|
+
].join("\n");
|
|
474
|
+
|
|
475
|
+
const SMAP_STAR_VS = [
|
|
476
|
+
"attribute vec2 aCorner;",
|
|
477
|
+
"attribute vec3 aMeta0;",
|
|
478
|
+
"attribute vec4 aColor;",
|
|
479
|
+
"attribute vec3 aMeta1;",
|
|
480
|
+
"attribute float aBirth;",
|
|
481
|
+
"uniform vec2 uRes;",
|
|
482
|
+
"varying vec2 vLocal;",
|
|
483
|
+
"varying vec4 vColor;",
|
|
484
|
+
"varying vec3 vMeta1;",
|
|
485
|
+
"varying float vBirth;",
|
|
486
|
+
"void main(){",
|
|
487
|
+
" vLocal = aCorner; vColor = aColor; vMeta1 = aMeta1; vBirth = aBirth;",
|
|
488
|
+
" vec2 world = aMeta0.xy + aCorner * aMeta0.z;",
|
|
489
|
+
" vec2 clip = vec2(world.x / uRes.x * 2.0 - 1.0, 1.0 - world.y / uRes.y * 2.0);",
|
|
490
|
+
" gl_Position = vec4(clip, 0.0, 1.0);",
|
|
491
|
+
"}",
|
|
492
|
+
].join("\n");
|
|
493
|
+
|
|
494
|
+
const SMAP_STAR_FS = [
|
|
495
|
+
"precision mediump float;",
|
|
496
|
+
"varying vec2 vLocal;",
|
|
497
|
+
"varying vec4 vColor;",
|
|
498
|
+
"varying vec3 vMeta1;",
|
|
499
|
+
"varying float vBirth;",
|
|
500
|
+
"uniform float uTime;",
|
|
501
|
+
"uniform float uHoverIdx;",
|
|
502
|
+
"uniform float uRevealT;",
|
|
503
|
+
"float revealFactor(){",
|
|
504
|
+
" if (vBirth < 0.0) return 1.0;",
|
|
505
|
+
" float t = clamp((uRevealT - vBirth) / 900.0, 0.0, 1.0);",
|
|
506
|
+
" return t * t * (3.0 - 2.0 * t);",
|
|
507
|
+
"}",
|
|
508
|
+
"void main(){",
|
|
509
|
+
" float d = length(vLocal);",
|
|
510
|
+
" float alpha = 0.0;",
|
|
511
|
+
" if (vMeta1.z < -0.5){",
|
|
512
|
+
" float core = smoothstep(0.34, 0.02, d);",
|
|
513
|
+
" float slow = 0.5 + 0.5 * sin(uTime * 0.8 + vMeta1.x * 6.2831853);",
|
|
514
|
+
" alpha = core * vColor.a * (0.45 + 0.55 * slow);",
|
|
515
|
+
" } else {",
|
|
516
|
+
" float tw = 0.72 + 0.28 * sin(uTime * 2.1 + vMeta1.x * 6.2831853);",
|
|
517
|
+
" float core = smoothstep(0.30, 0.02, d);",
|
|
518
|
+
" float sh = smoothstep(0.09, 0.0, abs(vLocal.y)) * smoothstep(1.0, 0.12, abs(vLocal.x));",
|
|
519
|
+
" float sv = smoothstep(0.09, 0.0, abs(vLocal.x)) * smoothstep(1.0, 0.12, abs(vLocal.y));",
|
|
520
|
+
" float spikes = max(sh, sv); spikes = spikes * spikes * spikes * 0.6;",
|
|
521
|
+
" float glow = exp(-d * 3.2) * 0.5;",
|
|
522
|
+
" alpha = core + spikes * core + glow;",
|
|
523
|
+
" if (vMeta1.y > 0.5){",
|
|
524
|
+
" float ph = fract(uTime * 0.30 + vMeta1.x * 0.61803399);",
|
|
525
|
+
" float rr = mix(0.20, 0.88, ph);",
|
|
526
|
+
" alpha += smoothstep(0.075, 0.0, abs(d - rr)) * (1.0 - ph) * 0.85;",
|
|
527
|
+
" }",
|
|
528
|
+
" if (abs(vMeta1.z - uHoverIdx) < 0.5){",
|
|
529
|
+
" alpha += smoothstep(0.10, 0.0, abs(d - 0.66)) * 0.18;",
|
|
530
|
+
" alpha += smoothstep(0.05, 0.0, abs(d - 0.66)) * 0.9;",
|
|
531
|
+
" }",
|
|
532
|
+
" alpha = clamp(alpha, 0.0, 1.15) * tw;",
|
|
533
|
+
" }",
|
|
534
|
+
" alpha *= revealFactor();",
|
|
535
|
+
" gl_FragColor = vec4(vColor.rgb, alpha);",
|
|
536
|
+
"}",
|
|
537
|
+
].join("\n");
|
|
538
|
+
|
|
539
|
+
const SMAP_LINE_VS = [
|
|
540
|
+
"attribute vec2 aPos;",
|
|
541
|
+
"attribute vec4 aColor;",
|
|
542
|
+
"attribute float aBirth;",
|
|
543
|
+
"uniform vec2 uRes;",
|
|
544
|
+
"varying vec4 vColor;",
|
|
545
|
+
"varying float vBirth;",
|
|
546
|
+
"void main(){",
|
|
547
|
+
" vColor = aColor; vBirth = aBirth;",
|
|
548
|
+
" vec2 clip = vec2(aPos.x / uRes.x * 2.0 - 1.0, 1.0 - aPos.y / uRes.y * 2.0);",
|
|
549
|
+
" gl_Position = vec4(clip, 0.0, 1.0);",
|
|
550
|
+
"}",
|
|
551
|
+
].join("\n");
|
|
552
|
+
const SMAP_LINE_FS = [
|
|
553
|
+
"precision mediump float;",
|
|
554
|
+
"varying vec4 vColor;",
|
|
555
|
+
"varying float vBirth;",
|
|
556
|
+
"uniform float uRevealT;",
|
|
557
|
+
"void main(){",
|
|
558
|
+
" if (vBirth < 0.0){ gl_FragColor = vColor; return; }",
|
|
559
|
+
" float t = clamp((uRevealT - vBirth) / 900.0, 0.0, 1.0);",
|
|
560
|
+
" t = t * t * (3.0 - 2.0 * t);",
|
|
561
|
+
" gl_FragColor = vec4(vColor.rgb, vColor.a * t);",
|
|
562
|
+
"}",
|
|
563
|
+
].join("\n");
|
|
564
|
+
|
|
565
|
+
function glShader(gl, type, src) {
|
|
566
|
+
const sh = gl.createShader(type);
|
|
567
|
+
gl.shaderSource(sh, src);
|
|
568
|
+
gl.compileShader(sh);
|
|
569
|
+
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error("shader compile: " + gl.getShaderInfoLog(sh));
|
|
570
|
+
return sh;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function glProgram(gl, vsSrc, fsSrc, attribNames, uniformNames) {
|
|
574
|
+
const p = gl.createProgram();
|
|
575
|
+
gl.attachShader(p, glShader(gl, gl.VERTEX_SHADER, vsSrc));
|
|
576
|
+
gl.attachShader(p, glShader(gl, gl.FRAGMENT_SHADER, fsSrc));
|
|
577
|
+
gl.linkProgram(p);
|
|
578
|
+
if (!gl.getProgramParameter(p, gl.LINK_STATUS)) throw new Error("program link: " + gl.getProgramInfoLog(p));
|
|
579
|
+
const out = { prog: p, attr: {}, uni: {} };
|
|
580
|
+
for (const n of attribNames) out.attr[n] = gl.getAttribLocation(p, n);
|
|
581
|
+
for (const n of uniformNames) out.uni[n] = gl.getUniformLocation(p, n);
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const SMAP_CORNERS = [-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1];
|
|
586
|
+
const SMAP_ATTR_SIZE = { aCorner: 2, aMeta0: 3, aColor: 4, aMeta1: 3, aBirth: 1 };
|
|
587
|
+
|
|
588
|
+
function pushStarQuad(arr, cx, cy, r, rgb, aBase, twPhase, fresh, idx, birth) {
|
|
589
|
+
for (let v = 0; v < 6; v++) {
|
|
590
|
+
arr.push(SMAP_CORNERS[v * 2], SMAP_CORNERS[v * 2 + 1]);
|
|
591
|
+
arr.push(cx, cy, r);
|
|
592
|
+
arr.push(rgb[0], rgb[1], rgb[2], aBase);
|
|
593
|
+
arr.push(twPhase, fresh, idx);
|
|
594
|
+
arr.push(birth);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function bindQuadAttrs(gl, po, buf, floatsPerVtx) {
|
|
599
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
600
|
+
let off = 0;
|
|
601
|
+
for (const n of ["aCorner", "aMeta0", "aColor", "aMeta1", "aBirth"]) {
|
|
602
|
+
const size = SMAP_ATTR_SIZE[n];
|
|
603
|
+
const loc = po.attr[n];
|
|
604
|
+
if (loc >= 0) {
|
|
605
|
+
gl.enableVertexAttribArray(loc);
|
|
606
|
+
gl.vertexAttribPointer(loc, size, gl.FLOAT, false, floatsPerVtx * 4, off * 4);
|
|
607
|
+
}
|
|
608
|
+
off += size;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function makeWebGLRenderer(canvasEl, layout, visBox, hoverBox) {
|
|
613
|
+
let gl = null;
|
|
614
|
+
try {
|
|
615
|
+
gl = canvasEl.getContext("webgl", { antialias: true, alpha: false, premultipliedAlpha: true });
|
|
616
|
+
} catch (_) { gl = null; }
|
|
617
|
+
if (!gl) return null;
|
|
618
|
+
const W = layout.W;
|
|
619
|
+
const H = layout.H;
|
|
620
|
+
try {
|
|
621
|
+
canvasEl.width = W * 2;
|
|
622
|
+
canvasEl.height = H * 2;
|
|
623
|
+
gl.viewport(0, 0, canvasEl.width, canvasEl.height);
|
|
624
|
+
const progBg = glProgram(gl, SMAP_BG_VS, SMAP_BG_FS, ["aPos"], ["uRes", "uTime"]);
|
|
625
|
+
const progStar = glProgram(gl, SMAP_STAR_VS, SMAP_STAR_FS, ["aCorner", "aMeta0", "aColor", "aMeta1", "aBirth"], ["uRes", "uTime", "uHoverIdx", "uRevealT"]);
|
|
626
|
+
const progLine = glProgram(gl, SMAP_LINE_VS, SMAP_LINE_FS, ["aPos", "aColor", "aBirth"], ["uRes", "uRevealT"]);
|
|
627
|
+
|
|
628
|
+
// 背景全屏两三角(clip space 直给)
|
|
629
|
+
const bgQuadBuf = gl.createBuffer();
|
|
630
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, bgQuadBuf);
|
|
631
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]), gl.STATIC_DRAW);
|
|
632
|
+
|
|
633
|
+
// 远景尘星:一次烘焙
|
|
634
|
+
const dustBuf = gl.createBuffer();
|
|
635
|
+
let dustCount = 0;
|
|
636
|
+
{
|
|
637
|
+
const arr = [];
|
|
638
|
+
for (let i = 0; i < 240; i++) {
|
|
639
|
+
const x = rand01(i * 97 + 13) * W;
|
|
640
|
+
const y = rand01(i * 31 + 7) * H;
|
|
641
|
+
const r = 2.4 + rand01(i * 17 + 5) * 2.0;
|
|
642
|
+
pushStarQuad(arr, x, y, r, [0.62, 0.70, 0.87], 0.10 + 0.30 * rand01(i * 7 + 3), rand01(i * 11 + 29), 0, -1, -1);
|
|
643
|
+
}
|
|
644
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, dustBuf);
|
|
645
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), gl.STATIC_DRAW);
|
|
646
|
+
dustCount = arr.length / 13;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const starBuf = gl.createBuffer();
|
|
650
|
+
let starCount = 0;
|
|
651
|
+
const lineBuf = gl.createBuffer();
|
|
652
|
+
let lineCount = 0;
|
|
653
|
+
// 脏标记:canvas 晚于 visibility effect 挂载时(JSX 分支决定 ref 时序),
|
|
654
|
+
// setGeometry 可能从未被调过——render 前自动补传,顺序无关。
|
|
655
|
+
let geoDirty = true;
|
|
656
|
+
|
|
657
|
+
function uploadStars() {
|
|
658
|
+
const arr = [];
|
|
659
|
+
const nowMs = Date.now();
|
|
660
|
+
for (let i = 0; i < layout.pts.length; i++) {
|
|
661
|
+
if (!visBox.flags[i]) continue;
|
|
662
|
+
const p = layout.pts[i];
|
|
663
|
+
const meta = KIND_META[p.star.kind] || KIND_META.special;
|
|
664
|
+
const fresh = p.star.mtimeMs > 0 && nowMs - p.star.mtimeMs < FRESH_MS ? 1 : 0;
|
|
665
|
+
const birth = p.star.mtimeMs > 0 ? p.star.mtimeMs : 0;
|
|
666
|
+
pushStarQuad(arr, p.x, p.y, p.r * 5.4, hexToRgb(meta.color), 1, p.tw, fresh, i, birth);
|
|
667
|
+
}
|
|
668
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, starBuf);
|
|
669
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), gl.DYNAMIC_DRAW);
|
|
670
|
+
starCount = arr.length / 13;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
let hoverLineIdx = -1;
|
|
674
|
+
function uploadLines() {
|
|
675
|
+
const arr = [];
|
|
676
|
+
for (let li = 0; li < visBox.lines.length; li++) {
|
|
677
|
+
const ln = visBox.lines[li];
|
|
678
|
+
const a = layout.pts[ln[0]];
|
|
679
|
+
const b = layout.pts[ln[1]];
|
|
680
|
+
const c = hexToRgb((KIND_META[ln[2]] || KIND_META.special).color);
|
|
681
|
+
const hot = hoverLineIdx >= 0 && (ln[0] === hoverLineIdx || ln[1] === hoverLineIdx);
|
|
682
|
+
const mul = hot ? 1 : 0.72;
|
|
683
|
+
const al = hot ? 0.6 : 0.22;
|
|
684
|
+
const birth = Math.max(a.star.mtimeMs || 0, b.star.mtimeMs || 0);
|
|
685
|
+
arr.push(a.x, a.y, c[0] * mul, c[1] * mul, c[2] * mul, al, birth);
|
|
686
|
+
arr.push(b.x, b.y, c[0] * mul, c[1] * mul, c[2] * mul, al, birth);
|
|
687
|
+
}
|
|
688
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, lineBuf);
|
|
689
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), gl.DYNAMIC_DRAW);
|
|
690
|
+
lineCount = arr.length / 7;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function drawQuad(po, buf, count) {
|
|
694
|
+
bindQuadAttrs(gl, po, buf, 13);
|
|
695
|
+
gl.drawArrays(gl.TRIANGLES, 0, count);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
let revealT = null; // null = 全亮;数字 = 诞生回放的当前时刻
|
|
699
|
+
function render(phase) {
|
|
700
|
+
if (geoDirty) { uploadStars(); uploadLines(); geoDirty = false; }
|
|
701
|
+
const rt = revealT === null ? 8.7e15 : revealT;
|
|
702
|
+
gl.viewport(0, 0, canvasEl.width, canvasEl.height);
|
|
703
|
+
// 背景:不透明
|
|
704
|
+
gl.disable(gl.BLEND);
|
|
705
|
+
gl.useProgram(progBg.prog);
|
|
706
|
+
gl.uniform2f(progBg.uni.uRes, W, H);
|
|
707
|
+
gl.uniform1f(progBg.uni.uTime, phase);
|
|
708
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, bgQuadBuf);
|
|
709
|
+
gl.enableVertexAttribArray(progBg.attr.aPos);
|
|
710
|
+
gl.vertexAttribPointer(progBg.attr.aPos, 2, gl.FLOAT, false, 0, 0);
|
|
711
|
+
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
|
712
|
+
// additive:星座线 → 尘星 → 记忆星
|
|
713
|
+
gl.enable(gl.BLEND);
|
|
714
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE);
|
|
715
|
+
if (lineCount > 0) {
|
|
716
|
+
gl.useProgram(progLine.prog);
|
|
717
|
+
gl.uniform2f(progLine.uni.uRes, W, H);
|
|
718
|
+
gl.uniform1f(progLine.uni.uRevealT, rt);
|
|
719
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, lineBuf);
|
|
720
|
+
if (progLine.attr.aPos >= 0) { gl.enableVertexAttribArray(progLine.attr.aPos); gl.vertexAttribPointer(progLine.attr.aPos, 2, gl.FLOAT, false, 28, 0); }
|
|
721
|
+
if (progLine.attr.aColor >= 0) { gl.enableVertexAttribArray(progLine.attr.aColor); gl.vertexAttribPointer(progLine.attr.aColor, 4, gl.FLOAT, false, 28, 8); }
|
|
722
|
+
if (progLine.attr.aBirth >= 0) { gl.enableVertexAttribArray(progLine.attr.aBirth); gl.vertexAttribPointer(progLine.attr.aBirth, 1, gl.FLOAT, false, 28, 24); }
|
|
723
|
+
gl.drawArrays(gl.LINES, 0, lineCount);
|
|
724
|
+
}
|
|
725
|
+
gl.useProgram(progStar.prog);
|
|
726
|
+
gl.uniform2f(progStar.uni.uRes, W, H);
|
|
727
|
+
gl.uniform1f(progStar.uni.uTime, phase);
|
|
728
|
+
gl.uniform1f(progStar.uni.uHoverIdx, hoverBox.idx);
|
|
729
|
+
gl.uniform1f(progStar.uni.uRevealT, rt);
|
|
730
|
+
drawQuad(progStar, dustBuf, dustCount);
|
|
731
|
+
if (starCount > 0) drawQuad(progStar, starBuf, starCount);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
render: render,
|
|
736
|
+
setGeometry: function () { geoDirty = true; },
|
|
737
|
+
setReveal: function (ms) { revealT = typeof ms === "number" ? ms : null; },
|
|
738
|
+
setHighlight: function (idx) {
|
|
739
|
+
const v = typeof idx === "number" ? idx : -1;
|
|
740
|
+
if (v === hoverLineIdx) return;
|
|
741
|
+
hoverLineIdx = v;
|
|
742
|
+
uploadLines();
|
|
743
|
+
},
|
|
744
|
+
destroy: function () {
|
|
745
|
+
try {
|
|
746
|
+
for (const b of [bgQuadBuf, dustBuf, starBuf, lineBuf]) gl.deleteBuffer(b);
|
|
747
|
+
for (const p of [progBg, progStar, progLine]) gl.deleteProgram(p.prog);
|
|
748
|
+
} catch (_) { /* 销毁失败可忽略 */ }
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
} catch (e) {
|
|
752
|
+
console.error("sage-starmap: webgl init failed, fallback to 2d", e);
|
|
753
|
+
try {
|
|
754
|
+
const lose = gl.getExtension("WEBGL_lose_context");
|
|
755
|
+
if (lose) lose.loseContext();
|
|
756
|
+
} catch (_) { /* ignore */ }
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
function makeCanvas2DRenderer(canvasEl, layout, visBox, hoverBox) {
|
|
762
|
+
const W = layout.W;
|
|
763
|
+
const H = layout.H;
|
|
764
|
+
const SCALE = 2;
|
|
765
|
+
canvasEl.width = W * SCALE;
|
|
766
|
+
canvasEl.height = H * SCALE;
|
|
767
|
+
const nowMs = Date.now();
|
|
768
|
+
function render(phase) {
|
|
769
|
+
const c2d = canvasEl.getContext("2d");
|
|
770
|
+
if (!c2d) return;
|
|
771
|
+
c2d.setTransform(SCALE, 0, 0, SCALE, 0, 0);
|
|
772
|
+
const bg = c2d.createLinearGradient(0, 0, 0, H);
|
|
773
|
+
bg.addColorStop(0, "#0b1026");
|
|
774
|
+
bg.addColorStop(1, "#101731");
|
|
775
|
+
c2d.fillStyle = bg;
|
|
776
|
+
c2d.fillRect(0, 0, W, H);
|
|
777
|
+
for (let i = 0; i < 150; i++) {
|
|
778
|
+
const x = rand01(i * 97 + 13) * W;
|
|
779
|
+
const y = rand01(i * 31 + 7) * H;
|
|
780
|
+
c2d.globalAlpha = 0.08 + 0.22 * rand01(i * 7 + 3);
|
|
781
|
+
c2d.fillStyle = "#9fb3dd";
|
|
782
|
+
c2d.fillRect(x, y, 1, 1);
|
|
783
|
+
}
|
|
784
|
+
c2d.globalAlpha = 1;
|
|
785
|
+
const rt = state.revealT;
|
|
786
|
+
for (const ln of visBox.lines) {
|
|
787
|
+
const a = layout.pts[ln[0]];
|
|
788
|
+
const b = layout.pts[ln[1]];
|
|
789
|
+
const meta = KIND_META[ln[2]] || KIND_META.special;
|
|
790
|
+
const hot = state.hl >= 0 && (ln[0] === state.hl || ln[1] === state.hl);
|
|
791
|
+
const birth = Math.max(a.star.mtimeMs || 0, b.star.mtimeMs || 0);
|
|
792
|
+
let lrv = 1;
|
|
793
|
+
if (rt !== null) {
|
|
794
|
+
const lt = Math.max(0, Math.min(1, (rt - birth) / 900));
|
|
795
|
+
lrv = lt * lt * (3 - 2 * lt);
|
|
796
|
+
if (lrv <= 0) continue;
|
|
797
|
+
}
|
|
798
|
+
c2d.strokeStyle = hexA(meta.color, (hot ? 0.6 : 0.22) * lrv);
|
|
799
|
+
c2d.lineWidth = 1;
|
|
800
|
+
c2d.beginPath();
|
|
801
|
+
c2d.moveTo(a.x, a.y);
|
|
802
|
+
c2d.lineTo(b.x, b.y);
|
|
803
|
+
c2d.stroke();
|
|
804
|
+
}
|
|
805
|
+
for (let i = 0; i < layout.pts.length; i++) {
|
|
806
|
+
if (!visBox.flags[i]) continue;
|
|
807
|
+
const p = layout.pts[i];
|
|
808
|
+
const meta = KIND_META[p.star.kind] || KIND_META.special;
|
|
809
|
+
let rv = 1;
|
|
810
|
+
if (rt !== null && p.star.mtimeMs > 0) {
|
|
811
|
+
const st = Math.max(0, Math.min(1, (rt - p.star.mtimeMs) / 900));
|
|
812
|
+
rv = st * st * (3 - 2 * st);
|
|
813
|
+
if (rv <= 0) continue;
|
|
814
|
+
}
|
|
815
|
+
const tw = 0.72 + 0.28 * Math.sin(phase * 2.1 + p.tw * Math.PI * 2);
|
|
816
|
+
if (p.star.mtimeMs > 0 && nowMs - p.star.mtimeMs < FRESH_MS) {
|
|
817
|
+
const pr = p.r + 5 + 3 * (0.5 + 0.5 * Math.sin(phase * 1.4 + p.tw * 6.28));
|
|
818
|
+
c2d.strokeStyle = hexA(meta.color, 0.4 * rv);
|
|
819
|
+
c2d.lineWidth = 1;
|
|
820
|
+
c2d.beginPath();
|
|
821
|
+
c2d.arc(p.x, p.y, pr, 0, Math.PI * 2);
|
|
822
|
+
c2d.stroke();
|
|
823
|
+
}
|
|
824
|
+
const glowR = p.r * 4.2;
|
|
825
|
+
const g = c2d.createRadialGradient(p.x, p.y, 0, p.x, p.y, glowR);
|
|
826
|
+
g.addColorStop(0, hexA(meta.color, 0.5 * tw * rv));
|
|
827
|
+
g.addColorStop(1, hexA(meta.color, 0));
|
|
828
|
+
c2d.fillStyle = g;
|
|
829
|
+
c2d.beginPath();
|
|
830
|
+
c2d.arc(p.x, p.y, glowR, 0, Math.PI * 2);
|
|
831
|
+
c2d.fill();
|
|
832
|
+
c2d.globalAlpha = (0.62 + 0.38 * tw) * rv;
|
|
833
|
+
c2d.fillStyle = meta.color;
|
|
834
|
+
c2d.beginPath();
|
|
835
|
+
c2d.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
|
836
|
+
c2d.fill();
|
|
837
|
+
c2d.globalAlpha = 1;
|
|
838
|
+
if (hoverBox.idx === i) {
|
|
839
|
+
c2d.strokeStyle = hexA(meta.color, 0.9 * rv);
|
|
840
|
+
c2d.lineWidth = 1;
|
|
841
|
+
c2d.beginPath();
|
|
842
|
+
c2d.arc(p.x, p.y, p.r + 5, 0, Math.PI * 2);
|
|
843
|
+
c2d.stroke();
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const state = { revealT: null, hl: -1 };
|
|
848
|
+
return {
|
|
849
|
+
render: render,
|
|
850
|
+
setGeometry: null,
|
|
851
|
+
setReveal: function (ms) { state.revealT = typeof ms === "number" ? ms : null; },
|
|
852
|
+
setHighlight: function (idx) { state.hl = typeof idx === "number" ? idx : -1; },
|
|
853
|
+
destroy: null,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function createRenderer(canvasEl, layout, visBox, hoverBox) {
|
|
858
|
+
const wgl = makeWebGLRenderer(canvasEl, layout, visBox, hoverBox);
|
|
859
|
+
if (wgl) return wgl;
|
|
860
|
+
return makeCanvas2DRenderer(canvasEl, layout, visBox, hoverBox);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// ── 星图主体(canvas + 图例 + 详情面板)──
|
|
864
|
+
function StarMap(props) {
|
|
865
|
+
const ctx = props.ctx || appCtx;
|
|
866
|
+
const onClose = props.onClose;
|
|
867
|
+
const dataS = react.useState(null);
|
|
868
|
+
const data = dataS[0];
|
|
869
|
+
const setData = dataS[1];
|
|
870
|
+
const errS = react.useState("");
|
|
871
|
+
const err = errS[0];
|
|
872
|
+
const setErr = errS[1];
|
|
873
|
+
const canvasS = react.useState(null);
|
|
874
|
+
const canvasEl = canvasS[0];
|
|
875
|
+
const setCanvasEl = canvasS[1];
|
|
876
|
+
const layoutS = react.useState(null);
|
|
877
|
+
const layout = layoutS[0];
|
|
878
|
+
const setLayout = layoutS[1];
|
|
879
|
+
const hoverS = react.useState(null);
|
|
880
|
+
const hover = hoverS[0];
|
|
881
|
+
const setHover = hoverS[1];
|
|
882
|
+
const selS = react.useState(null);
|
|
883
|
+
const selected = selS[0];
|
|
884
|
+
const setSelected = selS[1];
|
|
885
|
+
const kindsS = react.useState(null);
|
|
886
|
+
const kindsOn = kindsS[0];
|
|
887
|
+
const setKindsOn = kindsS[1];
|
|
888
|
+
const queryS = react.useState("");
|
|
889
|
+
const query = queryS[0];
|
|
890
|
+
const setQuery = queryS[1];
|
|
891
|
+
const busyS = react.useState(false);
|
|
892
|
+
const busy = busyS[0];
|
|
893
|
+
const setBusy = busyS[1];
|
|
894
|
+
const viewS = react.useState("sky");
|
|
895
|
+
const viewMode = viewS[0];
|
|
896
|
+
const setViewMode = viewS[1];
|
|
897
|
+
const emptySkyS = react.useState(false);
|
|
898
|
+
const emptySky = emptySkyS[0];
|
|
899
|
+
const setEmptySky = emptySkyS[1];
|
|
900
|
+
const replayingS = react.useState(false);
|
|
901
|
+
const replaying = replayingS[0];
|
|
902
|
+
const setReplaying = replayingS[1];
|
|
903
|
+
const replayDateS = react.useState(null);
|
|
904
|
+
const replayDate = replayDateS[0];
|
|
905
|
+
const setReplayDate = replayDateS[1];
|
|
906
|
+
|
|
907
|
+
const hoverBox = react.useRef({ idx: -1 }).current;
|
|
908
|
+
const visBox = react.useRef({ flags: [], lines: [] }).current;
|
|
909
|
+
const drawBox = react.useRef({ renderer: null, t0: 0 }).current;
|
|
910
|
+
const replayBox = react.useRef({ raf: 0, active: false }).current;
|
|
911
|
+
|
|
912
|
+
// Esc 分层:详情面板开着时先关面板(capture 阶段拦截),再按才落到 overlay 关星空。
|
|
913
|
+
react.useEffect(() => {
|
|
914
|
+
if (!selected) return undefined;
|
|
915
|
+
const onKey = (e) => {
|
|
916
|
+
if (e.key === "Escape") {
|
|
917
|
+
e.stopPropagation();
|
|
918
|
+
setSelected(null);
|
|
919
|
+
}
|
|
920
|
+
};
|
|
921
|
+
document.addEventListener("keydown", onKey, true);
|
|
922
|
+
return () => document.removeEventListener("keydown", onKey, true);
|
|
923
|
+
}, [selected]);
|
|
924
|
+
|
|
925
|
+
react.useEffect(() => {
|
|
926
|
+
let alive = true;
|
|
927
|
+
setBusy(true);
|
|
928
|
+
const remote = ctx.get("remote.starmap");
|
|
929
|
+
if (!remote) { setErr("remote.starmap 不可用(remote 未挂载)"); return undefined; }
|
|
930
|
+
let settled = false;
|
|
931
|
+
const guard = setTimeout(() => {
|
|
932
|
+
if (!alive || settled) return;
|
|
933
|
+
settled = true;
|
|
934
|
+
setBusy(false);
|
|
935
|
+
setErr("拉取超时:starmap 服务 20 秒无响应——Host 端服务大概率未激活,需要重启 DSH 让插件行重新装载");
|
|
936
|
+
}, 20000);
|
|
937
|
+
remote.listStars().then((r) => {
|
|
938
|
+
if (!alive || settled) return;
|
|
939
|
+
settled = true;
|
|
940
|
+
clearTimeout(guard);
|
|
941
|
+
setBusy(false);
|
|
942
|
+
const d = unwrap(r);
|
|
943
|
+
if (d && Array.isArray(d.stars)) {
|
|
944
|
+
setData(d);
|
|
945
|
+
const on = {};
|
|
946
|
+
for (const k of KIND_ORDER) on[k] = true;
|
|
947
|
+
setKindsOn(on);
|
|
948
|
+
} else setErr("Host 数据无效:" + preview(d));
|
|
949
|
+
}, (e) => {
|
|
950
|
+
if (!alive || settled) return;
|
|
951
|
+
settled = true;
|
|
952
|
+
clearTimeout(guard);
|
|
953
|
+
setBusy(false);
|
|
954
|
+
setErr(detail(e));
|
|
955
|
+
});
|
|
956
|
+
return () => { alive = false; clearTimeout(guard); };
|
|
957
|
+
}, []);
|
|
958
|
+
|
|
959
|
+
// 布局只依赖数据:canvas 尚未挂载也要先把 layout 算出来,
|
|
960
|
+
// 否则「layout 等 canvasEl、canvas 又等 layout」互相死锁(v0.2.0 遗留 bug,
|
|
961
|
+
// 表现为永远停在「整理星图…」)。
|
|
962
|
+
react.useEffect(() => {
|
|
963
|
+
if (!data) return;
|
|
964
|
+
setLayout(computeLayout(data.stars, 940, 520));
|
|
965
|
+
}, [data]);
|
|
966
|
+
|
|
967
|
+
react.useEffect(() => {
|
|
968
|
+
if (!data || !layout || !canvasEl) return undefined;
|
|
969
|
+
const renderer = createRenderer(canvasEl, layout, visBox, hoverBox);
|
|
970
|
+
drawBox.renderer = renderer;
|
|
971
|
+
drawBox.t0 = Date.now();
|
|
972
|
+
renderer.render(0);
|
|
973
|
+
let raf = window.requestAnimationFrame(function loop() {
|
|
974
|
+
if (drawBox.renderer) drawBox.renderer.render((Date.now() - drawBox.t0) / 1000);
|
|
975
|
+
raf = window.requestAnimationFrame(loop);
|
|
976
|
+
});
|
|
977
|
+
return () => {
|
|
978
|
+
window.cancelAnimationFrame(raf);
|
|
979
|
+
drawBox.renderer = null;
|
|
980
|
+
replayBox.active = false;
|
|
981
|
+
window.cancelAnimationFrame(replayBox.raf);
|
|
982
|
+
if (renderer.destroy) renderer.destroy();
|
|
983
|
+
};
|
|
984
|
+
}, [data, layout, canvasEl]);
|
|
985
|
+
|
|
986
|
+
react.useEffect(() => {
|
|
987
|
+
if (!data || !layout || !kindsOn) return;
|
|
988
|
+
const q = query.trim().toLowerCase();
|
|
989
|
+
const vis = buildVisibility(layout, kindsOn, q);
|
|
990
|
+
visBox.flags = vis.flags;
|
|
991
|
+
visBox.lines = vis.lines;
|
|
992
|
+
let anyVis = false;
|
|
993
|
+
for (let i = 0; i < vis.flags.length; i++) { if (vis.flags[i]) { anyVis = true; break; } }
|
|
994
|
+
setEmptySky(!anyVis);
|
|
995
|
+
if (drawBox.renderer) {
|
|
996
|
+
if (drawBox.renderer.setGeometry) drawBox.renderer.setGeometry();
|
|
997
|
+
drawBox.renderer.render((Date.now() - drawBox.t0) / 1000);
|
|
998
|
+
}
|
|
999
|
+
}, [data, layout, kindsOn, query]);
|
|
1000
|
+
|
|
1001
|
+
function hitTest(e) {
|
|
1002
|
+
if (!layout) return -1;
|
|
1003
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
1004
|
+
if (rect.width <= 0) return -1;
|
|
1005
|
+
const sx = (e.clientX - rect.left) * (layout.W / rect.width);
|
|
1006
|
+
const sy = (e.clientY - rect.top) * (layout.H / rect.height);
|
|
1007
|
+
let best = -1;
|
|
1008
|
+
let bestD = 144;
|
|
1009
|
+
for (let i = 0; i < layout.pts.length; i++) {
|
|
1010
|
+
if (!visBox.flags[i]) continue;
|
|
1011
|
+
const p = layout.pts[i];
|
|
1012
|
+
const dx = p.x - sx;
|
|
1013
|
+
const dy = p.y - sy;
|
|
1014
|
+
const d = dx * dx + dy * dy;
|
|
1015
|
+
if (d < bestD) { bestD = d; best = i; }
|
|
1016
|
+
}
|
|
1017
|
+
return best;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function onMove(e) {
|
|
1021
|
+
if (!layout) return;
|
|
1022
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
1023
|
+
const best = hitTest(e);
|
|
1024
|
+
if (best !== hoverBox.idx) {
|
|
1025
|
+
hoverBox.idx = best;
|
|
1026
|
+
if (drawBox.renderer && drawBox.renderer.setHighlight) drawBox.renderer.setHighlight(best);
|
|
1027
|
+
}
|
|
1028
|
+
if (best >= 0) {
|
|
1029
|
+
const p = layout.pts[best];
|
|
1030
|
+
const cssX = p.x * (rect.width / layout.W);
|
|
1031
|
+
const cssY = p.y * (rect.height / layout.H);
|
|
1032
|
+
// 靠右的星摘要翻到左侧,避免溢出/被详情面板压住
|
|
1033
|
+
const flip = cssX > layout.W * 0.58;
|
|
1034
|
+
const left = flip ? Math.max(8, cssX - 292) : cssX + 12;
|
|
1035
|
+
setHover({ star: p.star, left: left, top: cssY - 8 });
|
|
1036
|
+
} else setHover(null);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
function onLeave() {
|
|
1040
|
+
hoverBox.idx = -1;
|
|
1041
|
+
if (drawBox.renderer && drawBox.renderer.setHighlight) drawBox.renderer.setHighlight(-1);
|
|
1042
|
+
setHover(null);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function openStar(st) {
|
|
1046
|
+
setSelected({ star: st, text: null });
|
|
1047
|
+
const remote = ctx.get("remote.starmap");
|
|
1048
|
+
if (!remote) return;
|
|
1049
|
+
remote.readFile(st.file).then((r) => {
|
|
1050
|
+
const d = unwrap(r);
|
|
1051
|
+
setSelected((prev) => {
|
|
1052
|
+
if (prev && prev.star.file === st.file) {
|
|
1053
|
+
return { star: prev.star, text: d && typeof d.content === "string" ? d.content : "(读取失败)" };
|
|
1054
|
+
}
|
|
1055
|
+
return prev;
|
|
1056
|
+
});
|
|
1057
|
+
}, () => {
|
|
1058
|
+
setSelected((prev) => (prev && prev.star.file === st.file ? { star: prev.star, text: "(读取失败)" } : prev));
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function onClick(e) {
|
|
1063
|
+
const best = hitTest(e);
|
|
1064
|
+
if (best < 0 || !layout) return;
|
|
1065
|
+
openStar(layout.pts[best].star);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function refresh() {
|
|
1069
|
+
const remote = ctx.get("remote.starmap");
|
|
1070
|
+
if (!remote) return;
|
|
1071
|
+
setData(null);
|
|
1072
|
+
setLayout(null);
|
|
1073
|
+
setSelected(null);
|
|
1074
|
+
setErr("");
|
|
1075
|
+
setBusy(true);
|
|
1076
|
+
remote.listStars().then((r) => {
|
|
1077
|
+
setBusy(false);
|
|
1078
|
+
const d = unwrap(r);
|
|
1079
|
+
if (d && Array.isArray(d.stars)) setData(d);
|
|
1080
|
+
else setErr("Host 返回了空数据");
|
|
1081
|
+
}, (e) => {
|
|
1082
|
+
setBusy(false);
|
|
1083
|
+
setErr(String((e && e.message) || e));
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function toggleKind(k) {
|
|
1088
|
+
setKindsOn((prev) => {
|
|
1089
|
+
const next = {};
|
|
1090
|
+
for (const kk in prev) next[kk] = prev[kk];
|
|
1091
|
+
next[k] = !prev[k];
|
|
1092
|
+
return next;
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ── 诞生回放:按 mtime 顺序把星空重新点亮一遍 ──
|
|
1097
|
+
function stopReplay() {
|
|
1098
|
+
replayBox.active = false;
|
|
1099
|
+
window.cancelAnimationFrame(replayBox.raf);
|
|
1100
|
+
if (drawBox.renderer && drawBox.renderer.setReveal) drawBox.renderer.setReveal(null);
|
|
1101
|
+
setReplaying(false);
|
|
1102
|
+
setReplayDate(null);
|
|
1103
|
+
}
|
|
1104
|
+
function startReplay() {
|
|
1105
|
+
if (!data || !drawBox.renderer || !drawBox.renderer.setReveal) return;
|
|
1106
|
+
let mn = Infinity;
|
|
1107
|
+
let mx = 0;
|
|
1108
|
+
for (const s of data.stars) {
|
|
1109
|
+
const m = s.mtimeMs || 0;
|
|
1110
|
+
if (m < mn) mn = m;
|
|
1111
|
+
if (m > mx) mx = m;
|
|
1112
|
+
}
|
|
1113
|
+
if (!isFinite(mn)) return;
|
|
1114
|
+
window.cancelAnimationFrame(replayBox.raf);
|
|
1115
|
+
const span = Math.max(mx - mn, 60000) + 2400;
|
|
1116
|
+
const dur = 18000;
|
|
1117
|
+
const t0 = Date.now();
|
|
1118
|
+
replayBox.active = true;
|
|
1119
|
+
setReplaying(true);
|
|
1120
|
+
let lastLabel = 0;
|
|
1121
|
+
const tick = () => {
|
|
1122
|
+
if (!replayBox.active) return;
|
|
1123
|
+
const p = Math.min(1, (Date.now() - t0) / dur);
|
|
1124
|
+
const cur = mn - 1200 + span * p;
|
|
1125
|
+
if (drawBox.renderer && drawBox.renderer.setReveal) drawBox.renderer.setReveal(cur);
|
|
1126
|
+
if (Date.now() - lastLabel > 250) {
|
|
1127
|
+
lastLabel = Date.now();
|
|
1128
|
+
setReplayDate(Math.min(mx, cur));
|
|
1129
|
+
}
|
|
1130
|
+
if (p >= 1) { stopReplay(); return; }
|
|
1131
|
+
replayBox.raf = window.requestAnimationFrame(tick);
|
|
1132
|
+
};
|
|
1133
|
+
replayBox.raf = window.requestAnimationFrame(tick);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
let body;
|
|
1137
|
+
if (err) {
|
|
1138
|
+
body = h("div", { className: "smap-status" }, "点亮失败:" + err);
|
|
1139
|
+
} else if (!data || !layout || !kindsOn) {
|
|
1140
|
+
body = h("div", { className: "smap-status" }, busy ? "正在点亮星空…" : "整理星图…");
|
|
1141
|
+
} else {
|
|
1142
|
+
const legendKids = [];
|
|
1143
|
+
for (const k of KIND_ORDER) {
|
|
1144
|
+
const n = data.stars.filter((s) => s.kind === k).length;
|
|
1145
|
+
if (n === 0) continue;
|
|
1146
|
+
legendKids.push(h("span", {
|
|
1147
|
+
key: k,
|
|
1148
|
+
className: "smap-lg-item" + (kindsOn[k] ? "" : " off"),
|
|
1149
|
+
onClick: () => toggleKind(k),
|
|
1150
|
+
title: "点击显示/隐藏这一类",
|
|
1151
|
+
},
|
|
1152
|
+
h("span", { className: "smap-dot", style: { background: KIND_META[k].color } }),
|
|
1153
|
+
KIND_META[k].label + " " + n));
|
|
1154
|
+
}
|
|
1155
|
+
let totalBytes = 0;
|
|
1156
|
+
let newest = 0;
|
|
1157
|
+
for (const s of data.stars) {
|
|
1158
|
+
totalBytes += s.bytes || 0;
|
|
1159
|
+
if (s.mtimeMs > newest) newest = s.mtimeMs;
|
|
1160
|
+
}
|
|
1161
|
+
legendKids.push(h("span", { key: "count", className: "smap-count" },
|
|
1162
|
+
data.count + " 条 · " + fmtBytes(totalBytes) + (newest ? " · 最近更新 " + fmtDate(newest) : "") + " · 单击看全文"));
|
|
1163
|
+
|
|
1164
|
+
const tip = hover ? (() => {
|
|
1165
|
+
const meta = KIND_META[hover.star.kind] || KIND_META.special;
|
|
1166
|
+
const kids = [
|
|
1167
|
+
h("div", { key: "t", className: "smap-tip-title" }, hover.star.title),
|
|
1168
|
+
h("div", { key: "k", className: "smap-tip-kind" },
|
|
1169
|
+
meta.label + " · " + hover.star.file + " · " + fmtBytes(hover.star.bytes)),
|
|
1170
|
+
];
|
|
1171
|
+
if (hover.star.desc) {
|
|
1172
|
+
const d = hover.star.desc.length > 90 ? hover.star.desc.slice(0, 90) + "…" : hover.star.desc;
|
|
1173
|
+
kids.push(h("div", { key: "d", className: "smap-tip-desc" }, d));
|
|
1174
|
+
}
|
|
1175
|
+
return h("div", { className: "smap-tip", style: { left: hover.left, top: hover.top } }, kids);
|
|
1176
|
+
})() : null;
|
|
1177
|
+
|
|
1178
|
+
let panel = null;
|
|
1179
|
+
if (selected) {
|
|
1180
|
+
const meta = KIND_META[selected.star.kind] || KIND_META.special;
|
|
1181
|
+
const panelKids = [
|
|
1182
|
+
h("div", { key: "head", className: "smap-panel-head" },
|
|
1183
|
+
h("span", { className: "smap-panel-title" }, selected.star.title),
|
|
1184
|
+
h("button", { className: "smap-btn", onClick: () => setSelected(null), title: "关闭" }, "×")),
|
|
1185
|
+
h("div", { key: "meta", className: "smap-panel-meta" },
|
|
1186
|
+
h("span", { className: "smap-meta-dot", style: { background: meta.color } }),
|
|
1187
|
+
meta.label + " · " + selected.star.file + " · " + fmtBytes(selected.star.bytes) +
|
|
1188
|
+
(selected.star.mtimeMs ? " · " + fmtDate(selected.star.mtimeMs) : "")),
|
|
1189
|
+
];
|
|
1190
|
+
if (selected.text === null) panelKids.push(h("div", { key: "loading", className: "smap-status" }, "展开中…"));
|
|
1191
|
+
else panelKids.push(h("div", { key: "body" }, renderMarkdown(selected.text)));
|
|
1192
|
+
panel = h("div", { className: "smap-panel" }, panelKids);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
let mainView;
|
|
1196
|
+
if (viewMode === "timeline") {
|
|
1197
|
+
const q2 = query.trim().toLowerCase();
|
|
1198
|
+
const sorted = data.stars
|
|
1199
|
+
.filter((s) => starVisible({ star: s }, kindsOn, q2))
|
|
1200
|
+
.sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0) || a.file.localeCompare(b.file));
|
|
1201
|
+
mainView = h("div", { className: "smap-wrap", key: "wrap" },
|
|
1202
|
+
h("div", { className: "smap-timeline" },
|
|
1203
|
+
sorted.length === 0
|
|
1204
|
+
? h("div", { className: "smap-status" }, "这片天区安静了")
|
|
1205
|
+
: sorted.map((s) => h("div", {
|
|
1206
|
+
className: "smap-tl-item",
|
|
1207
|
+
key: s.file,
|
|
1208
|
+
onClick: () => openStar(s),
|
|
1209
|
+
title: "展开全文",
|
|
1210
|
+
},
|
|
1211
|
+
h("span", { className: "smap-meta-dot", style: { background: (KIND_META[s.kind] || KIND_META.special).color, flex: "none", alignSelf: "center" } }),
|
|
1212
|
+
h("span", { className: "smap-tl-date", title: s.mtimeMs ? fmtDate(s.mtimeMs) : "" }, s.mtimeMs ? fmtRel(s.mtimeMs) : "—"),
|
|
1213
|
+
h("span", { className: "smap-tl-title" }, s.title),
|
|
1214
|
+
h("span", { className: "smap-tl-desc" }, s.desc || "")))),
|
|
1215
|
+
panel);
|
|
1216
|
+
} else {
|
|
1217
|
+
mainView = h("div", { className: "smap-wrap", key: "wrap" },
|
|
1218
|
+
h("canvas", {
|
|
1219
|
+
className: "smap-canvas",
|
|
1220
|
+
ref: setCanvasEl,
|
|
1221
|
+
onMouseMove: onMove,
|
|
1222
|
+
onMouseLeave: onLeave,
|
|
1223
|
+
onClick: onClick,
|
|
1224
|
+
}),
|
|
1225
|
+
emptySky ? h("div", { className: "smap-empty" }, "这片天区安静了") : null,
|
|
1226
|
+
tip,
|
|
1227
|
+
panel);
|
|
1228
|
+
}
|
|
1229
|
+
body = [
|
|
1230
|
+
mainView,
|
|
1231
|
+
h("div", { className: "smap-legend", key: "legend" }, legendKids),
|
|
1232
|
+
];
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
return h("div", { className: "smap-card" },
|
|
1236
|
+
h("div", { className: "smap-head" },
|
|
1237
|
+
h("span", { className: "smap-title" }, "记忆星图"),
|
|
1238
|
+
h("span", { className: "smap-sub" }, "sage-mem memory · 每颗星是一条记忆"),
|
|
1239
|
+
h("span", { className: "smap-spacer" }),
|
|
1240
|
+
h("span", { className: "smap-viewtoggle" },
|
|
1241
|
+
h("button", { className: "smap-vt-btn" + (viewMode === "sky" ? " on" : ""), onClick: () => setViewMode("sky"), title: "空间视角:星座分布" }, "星空"),
|
|
1242
|
+
h("button", { className: "smap-vt-btn" + (viewMode === "timeline" ? " on" : ""), onClick: () => setViewMode("timeline"), title: "时间视角:最近更新在前" }, "时间线")),
|
|
1243
|
+
viewMode === "sky" ? h("button", {
|
|
1244
|
+
className: "smap-btn" + (replaying ? " smap-replay-on" : ""),
|
|
1245
|
+
onClick: () => (replaying ? stopReplay() : startReplay()),
|
|
1246
|
+
title: "按记忆写下的顺序,把这片天重新点亮一遍",
|
|
1247
|
+
}, replaying ? "停止" : "回放") : null,
|
|
1248
|
+
replaying && replayDate ? h("span", { className: "smap-replay-date" }, fmtDate(replayDate) + " · 诞生中") : null,
|
|
1249
|
+
h("input", {
|
|
1250
|
+
className: "smap-search",
|
|
1251
|
+
placeholder: "找一颗星…",
|
|
1252
|
+
value: query,
|
|
1253
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1254
|
+
}),
|
|
1255
|
+
h("button", { className: "smap-btn", onClick: refresh, title: "重新读取记忆目录" }, busy ? "…" : "刷新"),
|
|
1256
|
+
onClose ? h("button", { className: "smap-btn", onClick: onClose, title: "关闭 (Esc)" }, "关闭") : null),
|
|
1257
|
+
body);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// ── 全屏 overlay ─────────────────────────────────────────────
|
|
1261
|
+
function StarmapOverlay() {
|
|
1262
|
+
const open = useOpen();
|
|
1263
|
+
react.useEffect(() => {
|
|
1264
|
+
if (!open) return undefined;
|
|
1265
|
+
const onKey = (e) => { if (e.key === "Escape") setOpen(false); };
|
|
1266
|
+
document.addEventListener("keydown", onKey);
|
|
1267
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
1268
|
+
}, [open]);
|
|
1269
|
+
if (!open) return null;
|
|
1270
|
+
return h("div", {
|
|
1271
|
+
className: "smap-veil",
|
|
1272
|
+
onMouseDown: (e) => { if (e.target === e.currentTarget) setOpen(false); },
|
|
1273
|
+
},
|
|
1274
|
+
h("div", { className: "smap-modal" },
|
|
1275
|
+
h(StarMap, { ctx: appCtx, onClose: () => setOpen(false) })));
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// ── 窗口右缘浮标(唯一入口,不绑实例/会话/Run 卡片)────────────
|
|
1279
|
+
function StarmapEdgeTab() {
|
|
1280
|
+
const open = useOpen();
|
|
1281
|
+
if (open) return null;
|
|
1282
|
+
return h("button", {
|
|
1283
|
+
type: "button",
|
|
1284
|
+
className: "smap-fab",
|
|
1285
|
+
onClick: () => setOpen(true),
|
|
1286
|
+
title: "记忆星图 — 把 sage-mem 的记忆画成一片星空",
|
|
1287
|
+
"aria-label": "记忆星图",
|
|
1288
|
+
}, "✦");
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
const inject = ["slots", "remote"];
|
|
1292
|
+
|
|
1293
|
+
/**
|
|
1294
|
+
* Client 插件:$mount strict remote contribution(第三方 remote 不在
|
|
1295
|
+
* dsh-api-remotes 的固定列表里,必须自己挂),再注册 footer 按钮与 overlay。
|
|
1296
|
+
*/
|
|
1297
|
+
async function apply(ctx) {
|
|
1298
|
+
appCtx = ctx;
|
|
1299
|
+
try {
|
|
1300
|
+
await ctx.remote.$mount(TYPERT_REMOTE);
|
|
1301
|
+
} catch (e) {
|
|
1302
|
+
// remote 注册失败不拖死 UI:星图打开时再向用户报错
|
|
1303
|
+
console.error("sage-starmap: remote mount failed", e);
|
|
1304
|
+
}
|
|
1305
|
+
injectCss();
|
|
1306
|
+
// B 方案:星图不再占对话主界面(去掉右缘 ✦ 浮标 + 全屏 overlay),
|
|
1307
|
+
// 改为注入 sage-mem 记忆管理页的「星图」子槽(settings.memory.starmap),
|
|
1308
|
+
// 由记忆管理页的标签栏切换渲染,作为并列视图。
|
|
1309
|
+
ctx.slots.inject("settings.memory.starmap", () => {
|
|
1310
|
+
ctx.slots.register(
|
|
1311
|
+
{ name: "settings.memory.starmap", id: "sage-starmap", order: 10, label: "记忆星图" },
|
|
1312
|
+
() => h(StarMap, { ctx: appCtx }),
|
|
1313
|
+
);
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
injectCss();
|
|
1318
|
+
return { StarMap: StarMap };
|
|
1319
|
+
|
|
1320
|
+
})(require, react);
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
|
|
25
1324
|
// ── 手写 strict codec(浏览器无 zod,typert 只要求 parse 方法)──
|
|
26
1325
|
function parseSchema(shape) {
|
|
27
1326
|
return {
|
|
@@ -44,6 +1343,9 @@ window.__ModuleLoader__.load({
|
|
|
44
1343
|
const readSchema = parseSchema({ "name": isStr, "content": isStr });
|
|
45
1344
|
const writeSchema = parseSchema({ "ok": isBool, "file": isStr });
|
|
46
1345
|
const deleteSchema = parseSchema({ "ok": isBool });
|
|
1346
|
+
const starmapStarSchema = parseSchema({ "file": isStr, "kind": isStr, "title": isStr, "desc": isStr, "bytes": isNum, "mtimeMs": isNum });
|
|
1347
|
+
const starmapListSchema = { parse(v) { if (!v || typeof v !== "object") throw new Error("expected object"); if (!Array.isArray(v.stars)) throw new Error("expected stars array"); v.stars.forEach((f) => starmapStarSchema.parse(f)); return v; } };
|
|
1348
|
+
const starmapReadSchema = parseSchema({ "name": isStr, "content": isStr });
|
|
47
1349
|
|
|
48
1350
|
const TYPERT_REMOTE = {
|
|
49
1351
|
package: "sage-mem",
|
|
@@ -95,6 +1397,28 @@ window.__ModuleLoader__.load({
|
|
|
95
1397
|
result: { mode: "strict", typeSymbol: "sage-mem/types#DeleteResult", schema: deleteSchema },
|
|
96
1398
|
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
97
1399
|
},
|
|
1400
|
+
{
|
|
1401
|
+
id: "sage-mem#starmap/listStars",
|
|
1402
|
+
service: "starmap",
|
|
1403
|
+
namespace: "starmap",
|
|
1404
|
+
method: "listStars",
|
|
1405
|
+
invocation: { kind: "direct" },
|
|
1406
|
+
parameters: [],
|
|
1407
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#StarmapList", schema: starmapListSchema },
|
|
1408
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
id: "sage-mem#starmap/readFile",
|
|
1412
|
+
service: "starmap",
|
|
1413
|
+
namespace: "starmap",
|
|
1414
|
+
method: "readFile",
|
|
1415
|
+
invocation: { kind: "direct" },
|
|
1416
|
+
parameters: [
|
|
1417
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "sage-mem/types#FileName", schema: strSchema } },
|
|
1418
|
+
],
|
|
1419
|
+
result: { mode: "strict", typeSymbol: "sage-mem/types#StarmapRead", schema: starmapReadSchema },
|
|
1420
|
+
sourceLocation: { "file": "workspace/sage-mem/lib/index.js", "line": 1, "column": 1 },
|
|
1421
|
+
},
|
|
98
1422
|
],
|
|
99
1423
|
};
|
|
100
1424
|
|
|
@@ -139,6 +1463,11 @@ window.__ModuleLoader__.load({
|
|
|
139
1463
|
".smem-err{border:1px solid var(--dsw-alias-state-error-primary,#ef4444);color:var(--dsw-alias-state-error-primary,#ef4444);border-radius:8px;padding:6px 10px;font-size:12px;white-space:pre-wrap;overflow-wrap:anywhere;}",
|
|
140
1464
|
".smem-ok{color:var(--dsw-alias-state-success-primary,#16a34a);font-size:12px;}",
|
|
141
1465
|
".smem-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:10px;padding:22px 14px;text-align:center;color:var(--dsw-alias-label-secondary);font-size:12.5px;}",
|
|
1466
|
+
".smem-tabs{display:flex;gap:20px;border-bottom:1px solid var(--dsw-alias-border-l2);margin-bottom:4px;}",
|
|
1467
|
+
".smem-tab{background:none;border:0;padding:8px 2px 10px;font-size:13px;font-family:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;position:relative;}",
|
|
1468
|
+
'.smem-tab--on{color:var(--dsw-alias-label-primary);}',
|
|
1469
|
+
'.smem-tab--on:after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;border-radius:2px 2px 0 0;background:var(--dsw-alias-label-primary);}',
|
|
1470
|
+
".smem-starmap{display:flex;flex-direction:column;height:min(70vh,720px);min-height:420px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;overflow:hidden;}",
|
|
142
1471
|
].join("\n");
|
|
143
1472
|
|
|
144
1473
|
const TYPE_META = {
|
|
@@ -234,6 +1563,9 @@ window.__ModuleLoader__.load({
|
|
|
234
1563
|
const busy = sBusy[0], setBusy = sBusy[1];
|
|
235
1564
|
const sOk = react.useState(false);
|
|
236
1565
|
const dirOk = sOk[0], setDirOk = sOk[1];
|
|
1566
|
+
const sTab = react.useState("files");
|
|
1567
|
+
const tab = sTab[0], setTab = sTab[1];
|
|
1568
|
+
const renderSlot = props.renderSlot;
|
|
237
1569
|
|
|
238
1570
|
function load() {
|
|
239
1571
|
setLoading(true);
|
|
@@ -338,6 +1670,10 @@ window.__ModuleLoader__.load({
|
|
|
338
1670
|
});
|
|
339
1671
|
|
|
340
1672
|
return h("div", { className: "smem-root" }, [
|
|
1673
|
+
h("div", { className: "smem-tabs", key: "tabs" }, [
|
|
1674
|
+
h("button", { type: "button", className: "smem-tab" + (tab === "files" ? " smem-tab--on" : ""), key: "f", onClick: function () { setTab("files"); } }, "文件列表"),
|
|
1675
|
+
h("button", { type: "button", className: "smem-tab" + (tab === "star" ? " smem-tab--on" : ""), key: "s", onClick: function () { setTab("star"); } }, "记忆星图"),
|
|
1676
|
+
]),
|
|
341
1677
|
h("div", { className: "smem-head", key: "head" }, [
|
|
342
1678
|
h("span", { className: "smem-title", key: "t" }, "记忆文件(sage-mem)"),
|
|
343
1679
|
h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: dirOk ? "memory 目录可读" : "memory 目录不可读" }),
|
|
@@ -373,14 +1709,17 @@ window.__ModuleLoader__.load({
|
|
|
373
1709
|
]),
|
|
374
1710
|
])
|
|
375
1711
|
: null,
|
|
376
|
-
|
|
377
|
-
? h("div", { className: "smem-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
1712
|
+
tab === "star"
|
|
1713
|
+
? h("div", { className: "smem-starmap", key: "starmap" },
|
|
1714
|
+
h(StarmapRuntime.StarMap, { ctx: props.ctx }))
|
|
1715
|
+
: loading && items.length === 0
|
|
1716
|
+
? h("div", { className: "smem-muted", key: "loading" }, "加载中…")
|
|
1717
|
+
: filtered.length === 0
|
|
1718
|
+
? h("div", { className: "smem-empty", key: "empty" },
|
|
1719
|
+
items.length === 0
|
|
1720
|
+
? "还没有记忆文件。对话中值得记住的事会写到这里,也可以点「添加记忆」手动写入。"
|
|
1721
|
+
: "没有符合搜索条件的文件。")
|
|
1722
|
+
: h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
|
|
384
1723
|
return h(Card, {
|
|
385
1724
|
key: it.file,
|
|
386
1725
|
item: it,
|
|
@@ -419,7 +1758,16 @@ window.__ModuleLoader__.load({
|
|
|
419
1758
|
}
|
|
420
1759
|
ctx.slots.inject("settings.section", () =>
|
|
421
1760
|
ctx.slots.register(
|
|
422
|
-
{
|
|
1761
|
+
{
|
|
1762
|
+
name: "settings.section",
|
|
1763
|
+
id: "sage-mem",
|
|
1764
|
+
order: 20,
|
|
1765
|
+
label: "记忆管理",
|
|
1766
|
+
// 声明「记忆星图」子槽:sage-starmap 向它注入星空视图,
|
|
1767
|
+
// 记忆管理页用 renderSlot 渲染。声明 children 后 shell 才会
|
|
1768
|
+
// 向本 section 组件传入 renderSlot。
|
|
1769
|
+
children: { "settings.memory.starmap": { kind: "list", scope: "root" } },
|
|
1770
|
+
},
|
|
423
1771
|
(props) => react.createElement(Section, { ...props, ctx }),
|
|
424
1772
|
),
|
|
425
1773
|
);
|