@gezi-wen/dsh-mem 0.5.5
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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +180 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +1970 -0
- package/lib/index.js +558 -0
- package/lib/typert.host.js +137 -0
- package/package.json +85 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,1970 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-mem — 客户端半:DSH 设置页「记忆管理」界面(文件管理器)。
|
|
3
|
+
*
|
|
4
|
+
* 手写 ModuleLoader bundle(与官方 dsh-client-* 包的构建产物同格式):
|
|
5
|
+
* window.__ModuleLoader__.load({ id, factory }),factory 内 require('react')
|
|
6
|
+
* 可用,module.exports 导出标准 Cordis 插件(apply/inject)。
|
|
7
|
+
*
|
|
8
|
+
* 数据通道:TypertRemoteService(host 半 MemoryGateway),通过
|
|
9
|
+
* ctx.get("remote.memory") 调用文件列表/读/写/删。remote 返回 {ok, value}
|
|
10
|
+
* 信封。浏览器侧无 zod,schema 手写(codec 只要求 mode==="strict" + parse)。
|
|
11
|
+
*
|
|
12
|
+
* 功能:浏览 memory 目录的 markdown 记忆文件(类型徽章 + 描述预览)、
|
|
13
|
+
* 查看/编辑全文、两步确认删除、添加新记忆(选类型 + 描述 + 正文)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
window.__ModuleLoader__.load({
|
|
17
|
+
id: "dsh-mem",
|
|
18
|
+
factory: (require) => {
|
|
19
|
+
var module = { exports: {} };
|
|
20
|
+
var exports = module.exports;
|
|
21
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
22
|
+
let react = require("react");
|
|
23
|
+
const h = react.createElement;
|
|
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, create: () => 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, create: () => strSchema } },
|
|
77
|
+
],
|
|
78
|
+
result: { mode: "strict", typeSymbol: "sage-starmap/types#FileContent", schema: readSchema, create: () => 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" }, "dsh-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: "记忆星图 — 把 dsh-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
|
+
// 改为注入 dsh-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
|
+
|
|
1324
|
+
// ── 手写 strict codec(浏览器无 zod,typert 只要求 parse 方法)──
|
|
1325
|
+
function parseSchema(shape) {
|
|
1326
|
+
return {
|
|
1327
|
+
parse(value) {
|
|
1328
|
+
if (value === null || typeof value !== "object") throw new Error("dsh-mem: invalid payload");
|
|
1329
|
+
for (const [key, check] of Object.entries(shape)) {
|
|
1330
|
+
if (!(key in value)) throw new Error("dsh-mem: missing field " + key);
|
|
1331
|
+
check(value[key]);
|
|
1332
|
+
}
|
|
1333
|
+
return value;
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
const isStr = (v) => { if (typeof v !== "string") throw new Error("expected string"); };
|
|
1338
|
+
const isBool = (v) => { if (typeof v !== "boolean") throw new Error("expected boolean"); };
|
|
1339
|
+
const isNum = (v) => { if (typeof v !== "number") throw new Error("expected number"); };
|
|
1340
|
+
const strSchema = { parse(v) { isStr(v); return v; } };
|
|
1341
|
+
const fileSchema = parseSchema({ "file": isStr, "type": isStr, "description": isStr, "size": isNum });
|
|
1342
|
+
const listSchema = { parse(v) { if (!Array.isArray(v)) throw new Error("expected array"); v.forEach((f) => fileSchema.parse(f)); return v; } };
|
|
1343
|
+
const readSchema = parseSchema({ "name": isStr, "content": isStr });
|
|
1344
|
+
const writeSchema = parseSchema({ "ok": isBool, "file": isStr });
|
|
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 });
|
|
1349
|
+
|
|
1350
|
+
const TYPERT_REMOTE = {
|
|
1351
|
+
package: "@gezi-wen/dsh-mem",
|
|
1352
|
+
descriptors: [
|
|
1353
|
+
{
|
|
1354
|
+
id: "@gezi-wen/dsh-mem#memory/listFiles",
|
|
1355
|
+
service: "memory",
|
|
1356
|
+
namespace: "memory",
|
|
1357
|
+
method: "listFiles",
|
|
1358
|
+
invocation: { kind: "direct" },
|
|
1359
|
+
parameters: [],
|
|
1360
|
+
result: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#FileList", schema: listSchema, create: () => listSchema },
|
|
1361
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1362
|
+
},
|
|
1363
|
+
{
|
|
1364
|
+
id: "@gezi-wen/dsh-mem#memory/readFile",
|
|
1365
|
+
service: "memory",
|
|
1366
|
+
namespace: "memory",
|
|
1367
|
+
method: "readFile",
|
|
1368
|
+
invocation: { kind: "direct" },
|
|
1369
|
+
parameters: [
|
|
1370
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1371
|
+
],
|
|
1372
|
+
result: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#FileContent", schema: readSchema, create: () => readSchema },
|
|
1373
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
id: "@gezi-wen/dsh-mem#memory/writeFile",
|
|
1377
|
+
service: "memory",
|
|
1378
|
+
namespace: "memory",
|
|
1379
|
+
method: "writeFile",
|
|
1380
|
+
invocation: { kind: "direct" },
|
|
1381
|
+
parameters: [
|
|
1382
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1383
|
+
{ name: "content", wire: "content", source: "json", codec: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#FileContent", schema: strSchema, create: () => strSchema } },
|
|
1384
|
+
],
|
|
1385
|
+
result: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#WriteResult", schema: writeSchema, create: () => writeSchema },
|
|
1386
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1387
|
+
},
|
|
1388
|
+
{
|
|
1389
|
+
id: "@gezi-wen/dsh-mem#memory/deleteFile",
|
|
1390
|
+
service: "memory",
|
|
1391
|
+
namespace: "memory",
|
|
1392
|
+
method: "deleteFile",
|
|
1393
|
+
invocation: { kind: "direct" },
|
|
1394
|
+
parameters: [
|
|
1395
|
+
{ name: "name", wire: "name", source: "json", codec: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1396
|
+
],
|
|
1397
|
+
result: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#DeleteResult", schema: deleteSchema, create: () => deleteSchema },
|
|
1398
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1399
|
+
},
|
|
1400
|
+
{
|
|
1401
|
+
id: "@gezi-wen/dsh-mem#starmap/listStars",
|
|
1402
|
+
service: "starmap",
|
|
1403
|
+
namespace: "starmap",
|
|
1404
|
+
method: "listStars",
|
|
1405
|
+
invocation: { kind: "direct" },
|
|
1406
|
+
parameters: [],
|
|
1407
|
+
result: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#StarmapList", schema: starmapListSchema, create: () => starmapListSchema },
|
|
1408
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
id: "@gezi-wen/dsh-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: "@gezi-wen/dsh-mem/types#FileName", schema: strSchema, create: () => strSchema } },
|
|
1418
|
+
],
|
|
1419
|
+
result: { mode: "strict", typeSymbol: "@gezi-wen/dsh-mem/types#StarmapRead", schema: starmapReadSchema, create: () => starmapReadSchema },
|
|
1420
|
+
sourceLocation: { "file": "lib/index.js", "line": 1, "column": 1 },
|
|
1421
|
+
},
|
|
1422
|
+
],
|
|
1423
|
+
};
|
|
1424
|
+
|
|
1425
|
+
const CSS = [
|
|
1426
|
+
".smem-root{display:flex;flex-direction:column;gap:12px;font-size:13px;line-height:1.55;color:var(--dsw-alias-label-primary);max-width:760px;}",
|
|
1427
|
+
".smem-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
|
|
1428
|
+
".smem-title{font-size:15px;font-weight:600;}",
|
|
1429
|
+
".smem-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#22c55e);flex:none;}",
|
|
1430
|
+
".smem-dot--bad{background:var(--dsw-alias-state-error-primary,#ef4444);}",
|
|
1431
|
+
".smem-muted{color:var(--dsw-alias-label-secondary);font-size:12px;}",
|
|
1432
|
+
".smem-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
|
|
1433
|
+
".smem-search{flex:1;min-width:160px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:inherit;padding:5px 10px;font-size:12px;outline:none;font-family:inherit;}",
|
|
1434
|
+
".smem-search:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
|
|
1435
|
+
".smem-btn{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:8px;padding:4px 12px;font-size:12px;cursor:pointer;font-family:inherit;white-space:nowrap;}",
|
|
1436
|
+
".smem-btn:hover{border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-brand-primary,#4d76e6);}",
|
|
1437
|
+
".smem-btn:disabled{opacity:.5;cursor:default;}",
|
|
1438
|
+
".smem-btn-primary{background:var(--dsw-alias-brand-primary,#4d76e6);border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-bg-base,#fff);}",
|
|
1439
|
+
".smem-btn-primary:hover{color:var(--dsw-alias-bg-base,#fff);opacity:.88;}",
|
|
1440
|
+
".smem-btn-danger-ghost{color:var(--dsw-alias-state-error-primary,#ef4444);border-color:var(--dsw-alias-border-l2);}",
|
|
1441
|
+
".smem-btn-danger{background:var(--dsw-alias-state-error-primary,#dc2626);border-color:var(--dsw-alias-state-error-primary,#dc2626);color:#fff;}",
|
|
1442
|
+
".smem-btn-danger:hover{color:#fff;opacity:.9;}",
|
|
1443
|
+
".smem-list{display:flex;flex-direction:column;gap:10px;}",
|
|
1444
|
+
".smem-card{border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);padding:10px 12px;display:flex;flex-direction:column;gap:6px;}",
|
|
1445
|
+
".smem-card-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}",
|
|
1446
|
+
".smem-card-title{font-weight:600;flex:1;min-width:120px;overflow-wrap:anywhere;text-align:left;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;}",
|
|
1447
|
+
".smem-badge{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:1px 6px;white-space:nowrap;}",
|
|
1448
|
+
".smem-narrative{color:var(--dsw-alias-label-secondary);font-size:12.5px;white-space:pre-wrap;overflow-wrap:anywhere;}",
|
|
1449
|
+
'.smem-narrative[data-clamp="1"]{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;cursor:pointer;}',
|
|
1450
|
+
".smem-card-foot{display:flex;justify-content:flex-end;}",
|
|
1451
|
+
".smem-actions{display:flex;gap:6px;}",
|
|
1452
|
+
".smem-form{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;padding:12px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px;}",
|
|
1453
|
+
".smem-form-title{font-weight:600;font-size:13px;}",
|
|
1454
|
+
".smem-label{font-size:12px;color:var(--dsw-alias-label-secondary);}",
|
|
1455
|
+
".smem-input,.smem-textarea{width:100%;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-base);color:inherit;padding:6px 10px;font-size:13px;outline:none;font-family:inherit;}",
|
|
1456
|
+
".smem-input:focus,.smem-textarea:focus{border-color:var(--dsw-alias-brand-primary,#4d76e6);}",
|
|
1457
|
+
".smem-textarea{resize:vertical;min-height:72px;}",
|
|
1458
|
+
".smem-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}",
|
|
1459
|
+
".smem-chip{border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);border-radius:999px;padding:3px 12px;font-size:12px;cursor:pointer;font-family:inherit;}",
|
|
1460
|
+
".smem-chip:hover{border-color:var(--dsw-alias-border-l2);}",
|
|
1461
|
+
'.smem-chip[data-on="1"]{border-color:var(--dsw-alias-brand-primary,#4d76e6);color:var(--dsw-alias-brand-primary,#4d76e6);}',
|
|
1462
|
+
".smem-chips{display:flex;gap:6px;flex-wrap:wrap;}",
|
|
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;}",
|
|
1464
|
+
".smem-ok{color:var(--dsw-alias-state-success-primary,#16a34a);font-size:12px;}",
|
|
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;}",
|
|
1471
|
+
].join("\n");
|
|
1472
|
+
|
|
1473
|
+
const TYPE_META = {
|
|
1474
|
+
user: ["👤", "用户"],
|
|
1475
|
+
feedback: ["📌", "反馈"],
|
|
1476
|
+
project: ["📁", "项目"],
|
|
1477
|
+
reference: ["🔗", "参考"],
|
|
1478
|
+
};
|
|
1479
|
+
const TYPE_ORDER = ["user", "feedback", "project", "reference"];
|
|
1480
|
+
|
|
1481
|
+
// ── frontmatter 纯逻辑(顶层自包含普通函数,不碰 React state)─────
|
|
1482
|
+
//
|
|
1483
|
+
// 2026-09-25 P0 数据事故的根因全在这几行里,三条硬规矩:
|
|
1484
|
+
// 1) 绝不「重建」frontmatter —— 就地合并,除 description / type 外每一行
|
|
1485
|
+
// 逐字保留。旧版 buildFrontmatter() 只写 name/description/type 三个字段,
|
|
1486
|
+
// 保存一次就把 baseline / node_type / originSessionId / updated 全抹掉
|
|
1487
|
+
// (baseline 一丢,「每次会话必读」的记忆从此不再注入)。
|
|
1488
|
+
// 2) 类型正则必须锚行首:` node_type: memory` 里的 `type:` 否则会被先命中,
|
|
1489
|
+
// 捕获到 memory → 不在 TYPE_ORDER 里 → 兜底成 reference(全库 45 条中招)。
|
|
1490
|
+
// 3) 换行必须容忍 CRLF(Windows 编辑器碰一次就是 \r\n);回写沿用原文件的
|
|
1491
|
+
// 换行风格,从而 LF / CRLF 都能逐字节 round-trip。
|
|
1492
|
+
|
|
1493
|
+
/** 去掉值两侧成对的引号(与 host 半 parseFrontmatter 的剥法一致)。 */
|
|
1494
|
+
function stripQuotes(v) {
|
|
1495
|
+
const s = typeof v === "string" ? v.trim() : "";
|
|
1496
|
+
return s.replace(/^["']|["']$/g, "").trim();
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
/** 渲染成双引号 YAML 标量:转义反斜杠与双引号(旧版把 " 换成 ' 会改字)。 */
|
|
1500
|
+
function yamlQuote(v) {
|
|
1501
|
+
return String(v == null ? "" : v).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
/** 传整篇 content 或只传 fm 文本都能用;以 --- 开头的当整篇拆。 */
|
|
1505
|
+
function fmInner(text) {
|
|
1506
|
+
const t = typeof text === "string" ? text : "";
|
|
1507
|
+
return /^---[ \t]*\r?\n/.test(t) ? splitFrontmatter(t).fm : t;
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* 拆 frontmatter。返回 { hasFm, fm, body }:
|
|
1512
|
+
* fm = 两条 --- 之间的原始文本(不含定界行)
|
|
1513
|
+
* body = 定界之后的内容(只吃掉紧跟 --- 的那一个换行,其余原样)
|
|
1514
|
+
* 没有 frontmatter 时 hasFm=false、fm=""、body=整个 content。
|
|
1515
|
+
*/
|
|
1516
|
+
function splitFrontmatter(content) {
|
|
1517
|
+
const text = typeof content === "string" ? content : "";
|
|
1518
|
+
const m = text.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n)?/);
|
|
1519
|
+
if (!m) return { hasFm: false, fm: "", body: text };
|
|
1520
|
+
return { hasFm: true, fm: m[1], body: text.slice(m[0].length) };
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* 识别记忆类型。锚行首(^ + m),**不看 node_type**:
|
|
1525
|
+
* 1) 顶层 `type: X`
|
|
1526
|
+
* 2) metadata 块内的缩进写法 ` type: X`
|
|
1527
|
+
* 优先级与 host 半 parseFrontmatter 一致(顶层优先)。找不到返回 ""。
|
|
1528
|
+
*/
|
|
1529
|
+
function detectType(text) {
|
|
1530
|
+
const fm = fmInner(text);
|
|
1531
|
+
const top = fm.match(/^type[ \t]*:[ \t]*([^\r\n]*)/m);
|
|
1532
|
+
if (top) return stripQuotes(top[1]);
|
|
1533
|
+
const nested = fm.match(/^[ \t]+type[ \t]*:[ \t]*([^\r\n]*)/m);
|
|
1534
|
+
if (nested) return stripQuotes(nested[1]);
|
|
1535
|
+
return "";
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/**
|
|
1539
|
+
* 兜底:读 node_type。本机有两份老文件没有 type 行,node_type 直接就是类型
|
|
1540
|
+
* (project_ui-lab.md → project、user_courses-2026fall.md → user)。
|
|
1541
|
+
* 只由调用方在 detectType 返回空时使用 —— detectType 本身绝不认 node_type。
|
|
1542
|
+
*/
|
|
1543
|
+
function detectNodeType(text) {
|
|
1544
|
+
const fm = fmInner(text);
|
|
1545
|
+
const m = fm.match(/^[ \t]*node_type[ \t]*:[ \t]*([^\r\n]*)/m);
|
|
1546
|
+
return m ? stripQuotes(m[1]) : "";
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/** 取顶层 description 的值(剥掉成对引号)。 */
|
|
1550
|
+
function extractDescription(text) {
|
|
1551
|
+
const fm = fmInner(text);
|
|
1552
|
+
const m = fm.match(/^description[ \t]*:[ \t]*([^\r\n]*)/m);
|
|
1553
|
+
return m ? stripQuotes(m[1]) : "";
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
/**
|
|
1557
|
+
* 就地合并 frontmatter,返回新的完整文件内容。
|
|
1558
|
+
* patch = { type, description, addMissingType }
|
|
1559
|
+
* - description:与原文相同时整行逐字保留;不同时改写该行;原本没有该行才补一行。
|
|
1560
|
+
* - type:只改写已有的 type 行(顶层优先,其次 metadata 内缩进那一行)并保留原缩进;
|
|
1561
|
+
* 原本没有 type 行时默认一行都不加(不动 node_type-only 的老文件),
|
|
1562
|
+
* 只有 patch.addMissingType === true 才插入(插进 metadata 块或顶层)。
|
|
1563
|
+
* - 其余所有行(name / baseline / node_type / originSessionId / updated / 任意键)
|
|
1564
|
+
* 逐字保留,顺序不变。
|
|
1565
|
+
* 没有 frontmatter 的文件原样返回(新增文件走 buildFrontmatter)。
|
|
1566
|
+
*/
|
|
1567
|
+
function mergeFrontmatter(content, patch) {
|
|
1568
|
+
const src = typeof content === "string" ? content : "";
|
|
1569
|
+
const p = patch || {};
|
|
1570
|
+
const parts = splitFrontmatter(src);
|
|
1571
|
+
if (!parts.hasFm) return src;
|
|
1572
|
+
|
|
1573
|
+
const oldDesc = extractDescription(parts.fm);
|
|
1574
|
+
const oldType = detectType(parts.fm);
|
|
1575
|
+
const srcLines = parts.fm.split(/\r?\n/);
|
|
1576
|
+
|
|
1577
|
+
// 目标 type 行的下标:与 detectType 同一优先级(顶层 → 缩进)
|
|
1578
|
+
let typeIdx = -1;
|
|
1579
|
+
for (let i = 0; i < srcLines.length; i++) {
|
|
1580
|
+
if (/^type[ \t]*:/.test(srcLines[i])) { typeIdx = i; break; }
|
|
1581
|
+
}
|
|
1582
|
+
if (typeIdx < 0) {
|
|
1583
|
+
for (let i = 0; i < srcLines.length; i++) {
|
|
1584
|
+
if (/^[ \t]+type[ \t]*:/.test(srcLines[i])) { typeIdx = i; break; }
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
const wantDesc = typeof p.description === "string" && p.description !== oldDesc;
|
|
1589
|
+
const wantType = typeof p.type === "string" && p.type !== "" && p.type !== oldType;
|
|
1590
|
+
const out = [];
|
|
1591
|
+
let descDone = !wantDesc;
|
|
1592
|
+
let typeDone = !wantType || typeIdx < 0;
|
|
1593
|
+
let metadataAt = -1;
|
|
1594
|
+
for (let i = 0; i < srcLines.length; i++) {
|
|
1595
|
+
const line = srcLines[i];
|
|
1596
|
+
if (/^metadata[ \t]*:/.test(line)) metadataAt = out.length;
|
|
1597
|
+
if (!descDone && /^description[ \t]*:/.test(line)) {
|
|
1598
|
+
out.push('description: "' + yamlQuote(p.description) + '"');
|
|
1599
|
+
descDone = true;
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
if (!typeDone && i === typeIdx) {
|
|
1603
|
+
out.push((line.match(/^[ \t]*/) || [""])[0] + "type: " + p.type);
|
|
1604
|
+
typeDone = true;
|
|
1605
|
+
continue;
|
|
1606
|
+
}
|
|
1607
|
+
out.push(line);
|
|
1608
|
+
}
|
|
1609
|
+
// 原本没有 type 行 → 仅在调用方明确要求时插入
|
|
1610
|
+
if (typeIdx < 0 && p.addMissingType === true) {
|
|
1611
|
+
if (metadataAt >= 0) {
|
|
1612
|
+
let at = metadataAt + 1;
|
|
1613
|
+
let indent = " ";
|
|
1614
|
+
for (let i = metadataAt + 1; i < out.length; i++) {
|
|
1615
|
+
const mm = out[i].match(/^([ \t]+)\S/);
|
|
1616
|
+
if (!mm) break;
|
|
1617
|
+
indent = mm[1];
|
|
1618
|
+
at = i + 1;
|
|
1619
|
+
}
|
|
1620
|
+
out.splice(at, 0, indent + "type: " + p.type);
|
|
1621
|
+
} else {
|
|
1622
|
+
out.push("type: " + p.type);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
// 原本没有 description 行 → 补一行(description 本就在可改范围内)
|
|
1626
|
+
if (!descDone) {
|
|
1627
|
+
const rendered = 'description: "' + yamlQuote(p.description) + '"';
|
|
1628
|
+
let at = -1;
|
|
1629
|
+
for (let i = 0; i < out.length; i++) {
|
|
1630
|
+
if (/^name[ \t]*:/.test(out[i])) { at = i + 1; break; }
|
|
1631
|
+
}
|
|
1632
|
+
if (at >= 0) out.splice(at, 0, rendered); else out.unshift(rendered);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// 原文件是 CRLF 就照样回写 CRLF(LF 文件回写 \n)——保证逐字节 round-trip。
|
|
1636
|
+
// 组件把原文重建成 "---\n" + fm + "\n---\n" + body,所以还要从 fm / body 里看 CRLF。
|
|
1637
|
+
const eol = /^---[ \t]*\r\n/.test(src) || parts.fm.indexOf("\r\n") >= 0 || parts.body.indexOf("\r\n") >= 0 ? "\r\n" : "\n";
|
|
1638
|
+
return "---" + eol + out.join(eol) + eol + "---" + eol + parts.body;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/**
|
|
1642
|
+
* 新建记忆的 frontmatter(只用于「添加记忆」或原本没有 frontmatter 的文件;
|
|
1643
|
+
* 编辑已有文件一律走 mergeFrontmatter,绝不重建)。
|
|
1644
|
+
*/
|
|
1645
|
+
function buildFrontmatter(type, desc, fileName) {
|
|
1646
|
+
const slug = String(fileName == null ? "" : fileName).replace(/\.md$/i, "");
|
|
1647
|
+
return "---\nname: \"" + yamlQuote(slug) + "\"\ndescription: \"" + yamlQuote(desc) +
|
|
1648
|
+
"\"\nmetadata:\n node_type: memory\n type: " + type + "\n---\n\n";
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
function fmtSize(n) {
|
|
1652
|
+
if (n < 1024) return n + " B";
|
|
1653
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KB";
|
|
1654
|
+
return (n / 1024 / 1024).toFixed(1) + " MB";
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/** 拆 typert remote 的 {ok, value} 信封。 */
|
|
1658
|
+
function unwrap(result) {
|
|
1659
|
+
return result && result.ok ? result.value : result;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function Chip(props) {
|
|
1663
|
+
return h("button", {
|
|
1664
|
+
type: "button",
|
|
1665
|
+
className: "smem-chip",
|
|
1666
|
+
"data-on": props.on ? "1" : "0",
|
|
1667
|
+
onClick: props.onClick,
|
|
1668
|
+
}, props.label);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
function Card(props) {
|
|
1672
|
+
const item = props.item;
|
|
1673
|
+
const meta = TYPE_META[item.type] || TYPE_META.reference;
|
|
1674
|
+
const isOpen = !!props.expanded;
|
|
1675
|
+
return h("div", { className: "smem-card", key: item.file }, [
|
|
1676
|
+
h("div", { className: "smem-card-head", key: "head" }, [
|
|
1677
|
+
h("span", { className: "smem-badge", key: "badge", title: "类型 " + String(item.type) }, meta[0] + " " + meta[1]),
|
|
1678
|
+
h("span", { className: "smem-card-title", key: "title" }, item.file),
|
|
1679
|
+
h("span", { className: "smem-muted", key: "size" }, fmtSize(item.size)),
|
|
1680
|
+
]),
|
|
1681
|
+
item.description
|
|
1682
|
+
? h("div", {
|
|
1683
|
+
className: "smem-narrative",
|
|
1684
|
+
"data-clamp": isOpen ? "0" : "1",
|
|
1685
|
+
key: "body",
|
|
1686
|
+
onClick: function () { props.onToggle(item.file); },
|
|
1687
|
+
title: isOpen ? "点击收起" : "点击展开全文",
|
|
1688
|
+
}, String(item.description))
|
|
1689
|
+
: null,
|
|
1690
|
+
h("div", { className: "smem-card-foot", key: "foot" },
|
|
1691
|
+
h("div", { className: "smem-actions", key: "acts" },
|
|
1692
|
+
props.deleting
|
|
1693
|
+
? [
|
|
1694
|
+
h("button", { type: "button", className: "smem-btn smem-btn-danger", key: "yes", onClick: function () { props.onConfirmDelete(item.file); } }, "确认删除"),
|
|
1695
|
+
h("button", { type: "button", className: "smem-btn", key: "no", onClick: function () { props.onCancelDelete(); } }, "取消"),
|
|
1696
|
+
]
|
|
1697
|
+
: [
|
|
1698
|
+
h("button", { type: "button", className: "smem-btn", key: "edit", onClick: function () { props.onEdit(item.file); } }, "编辑"),
|
|
1699
|
+
h("button", { type: "button", className: "smem-btn smem-btn-danger-ghost", key: "del", onClick: function () { props.onAskDelete(item.file); } }, "删除"),
|
|
1700
|
+
])),
|
|
1701
|
+
]);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
function Section(props) {
|
|
1705
|
+
const remote = props.ctx.get("remote.memory");
|
|
1706
|
+
const sItems = react.useState([]);
|
|
1707
|
+
const items = sItems[0], setItems = sItems[1];
|
|
1708
|
+
const sLoading = react.useState(false);
|
|
1709
|
+
const loading = sLoading[0], setLoading = sLoading[1];
|
|
1710
|
+
const sError = react.useState(null);
|
|
1711
|
+
const error = sError[0], setError = sError[1];
|
|
1712
|
+
const sNotice = react.useState("");
|
|
1713
|
+
const notice = sNotice[0], setNotice = sNotice[1];
|
|
1714
|
+
const sQuery = react.useState("");
|
|
1715
|
+
const query = sQuery[0], setQuery = sQuery[1];
|
|
1716
|
+
const sExpanded = react.useState({});
|
|
1717
|
+
const expanded = sExpanded[0], setExpanded = sExpanded[1];
|
|
1718
|
+
const sDeleting = react.useState(null);
|
|
1719
|
+
const deleting = sDeleting[0], setDeleting = sDeleting[1];
|
|
1720
|
+
const sFormOpen = react.useState(false);
|
|
1721
|
+
const formOpen = sFormOpen[0], setFormOpen = sFormOpen[1];
|
|
1722
|
+
const sEditingFile = react.useState(null);
|
|
1723
|
+
const editingFile = sEditingFile[0], setEditingFile = sEditingFile[1];
|
|
1724
|
+
const sFormType = react.useState("feedback");
|
|
1725
|
+
const formType = sFormType[0], setFormType = sFormType[1];
|
|
1726
|
+
const sFormName = react.useState("");
|
|
1727
|
+
const formName = sFormName[0], setFormName = sFormName[1];
|
|
1728
|
+
const sFormDesc = react.useState("");
|
|
1729
|
+
const formDesc = sFormDesc[0], setFormDesc = sFormDesc[1];
|
|
1730
|
+
const sFormBody = react.useState("");
|
|
1731
|
+
const formBody = sFormBody[0], setFormBody = sFormBody[1];
|
|
1732
|
+
// 打开编辑时的原 frontmatter 原文(null = 该文件没有 frontmatter)
|
|
1733
|
+
const sFormFm = react.useState(null);
|
|
1734
|
+
const formFm = sFormFm[0], setFormFm = sFormFm[1];
|
|
1735
|
+
// 打开时展示给用户的类型(判断他是否真的改过类型)
|
|
1736
|
+
const sFormBaseType = react.useState("");
|
|
1737
|
+
const formBaseType = sFormBaseType[0], setFormBaseType = sFormBaseType[1];
|
|
1738
|
+
const sBusy = react.useState(false);
|
|
1739
|
+
const busy = sBusy[0], setBusy = sBusy[1];
|
|
1740
|
+
const sOk = react.useState(false);
|
|
1741
|
+
const dirOk = sOk[0], setDirOk = sOk[1];
|
|
1742
|
+
const sTab = react.useState("files");
|
|
1743
|
+
const tab = sTab[0], setTab = sTab[1];
|
|
1744
|
+
const renderSlot = props.renderSlot;
|
|
1745
|
+
|
|
1746
|
+
function load() {
|
|
1747
|
+
setLoading(true);
|
|
1748
|
+
setError(null);
|
|
1749
|
+
remote.listFiles()
|
|
1750
|
+
.then(function (res) {
|
|
1751
|
+
if (res && typeof res === "object" && res.ok === false) {
|
|
1752
|
+
const msg = res.error && res.error.message ? res.error.message : "remote 调用失败";
|
|
1753
|
+
throw new Error("listFiles RPC 失败: " + msg);
|
|
1754
|
+
}
|
|
1755
|
+
const list = unwrap(res);
|
|
1756
|
+
setItems(Array.isArray(list) ? list : []);
|
|
1757
|
+
setDirOk(true);
|
|
1758
|
+
})
|
|
1759
|
+
.catch(function (e) { setError(String((e && e.message) || e)); setDirOk(false); })
|
|
1760
|
+
.then(function () { setLoading(false); });
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
react.useEffect(function () { load(); }, []);
|
|
1764
|
+
|
|
1765
|
+
function closeForm() {
|
|
1766
|
+
setFormOpen(false); setEditingFile(null); setFormName(""); setFormDesc(""); setFormBody(""); setFormType("feedback");
|
|
1767
|
+
setFormFm(null); setFormBaseType("");
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
function openEdit(file) {
|
|
1771
|
+
setError(null);
|
|
1772
|
+
setBusy(true);
|
|
1773
|
+
remote.readFile(file)
|
|
1774
|
+
.then(function (res) {
|
|
1775
|
+
const data = unwrap(res);
|
|
1776
|
+
const content = (data && data.content) || "";
|
|
1777
|
+
// 解析走顶层纯函数:拆 frontmatter / 认类型 / 取描述
|
|
1778
|
+
const parts = splitFrontmatter(content);
|
|
1779
|
+
// detectType 只认 type 行(锚行首,不碰 node_type);
|
|
1780
|
+
// 老文件里 node_type 直接就是类型的,才退回 detectNodeType。
|
|
1781
|
+
const found = detectType(parts.fm) || detectNodeType(parts.fm);
|
|
1782
|
+
const shown = TYPE_ORDER.indexOf(found) >= 0 ? found : "reference";
|
|
1783
|
+
setEditingFile(file);
|
|
1784
|
+
setFormName(file);
|
|
1785
|
+
setFormType(shown);
|
|
1786
|
+
setFormBaseType(shown);
|
|
1787
|
+
setFormDesc(extractDescription(parts.fm));
|
|
1788
|
+
setFormBody(parts.body);
|
|
1789
|
+
// 关键:原 frontmatter 原文留着,保存时「就地合并」而不是重建
|
|
1790
|
+
setFormFm(parts.hasFm ? parts.fm : null);
|
|
1791
|
+
setFormOpen(true);
|
|
1792
|
+
})
|
|
1793
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
1794
|
+
.then(function () { setBusy(false); });
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function openAdd() {
|
|
1798
|
+
setError(null);
|
|
1799
|
+
setEditingFile(null);
|
|
1800
|
+
setFormName("");
|
|
1801
|
+
setFormType("feedback");
|
|
1802
|
+
setFormBaseType("feedback");
|
|
1803
|
+
setFormDesc("");
|
|
1804
|
+
setFormBody("");
|
|
1805
|
+
setFormFm(null);
|
|
1806
|
+
setFormOpen(true);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
function submit() {
|
|
1810
|
+
if (!formDesc.trim()) { setError("描述不能为空"); return; }
|
|
1811
|
+
setBusy(true); setError(null); setNotice("");
|
|
1812
|
+
const type = formType;
|
|
1813
|
+
const name = (formName.trim() || (type + "_note.md"));
|
|
1814
|
+
const safeName = name.endsWith(".md") ? name : name + ".md";
|
|
1815
|
+
const desc = formDesc.trim();
|
|
1816
|
+
let content;
|
|
1817
|
+
if (formFm === null) {
|
|
1818
|
+
// 没有 frontmatter(新增记忆 / 索引文件)→ 生成一份
|
|
1819
|
+
content = buildFrontmatter(type, desc, safeName) + formBody;
|
|
1820
|
+
} else {
|
|
1821
|
+
// 有 frontmatter → 就地合并:除 description / type 外一字不动
|
|
1822
|
+
const original = "---\n" + formFm + "\n---\n" + formBody;
|
|
1823
|
+
content = mergeFrontmatter(original, {
|
|
1824
|
+
type: type,
|
|
1825
|
+
description: desc,
|
|
1826
|
+
// 原文没有 type 行时,只有用户确实改过类型才补写一行
|
|
1827
|
+
addMissingType: type !== formBaseType,
|
|
1828
|
+
});
|
|
1829
|
+
}
|
|
1830
|
+
remote.writeFile(safeName, content)
|
|
1831
|
+
.then(function (r) {
|
|
1832
|
+
// host 侧对保留名 / 非字符串内容 / 超限一律返回 { ok: false, error },不再抛异常。
|
|
1833
|
+
if (r && r.ok === false) { setError(r.error || "写入被拒绝"); return; }
|
|
1834
|
+
closeForm();
|
|
1835
|
+
setNotice(editingFile ? "已更新 " + safeName : "已保存 " + safeName);
|
|
1836
|
+
load();
|
|
1837
|
+
})
|
|
1838
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
1839
|
+
.then(function () { setBusy(false); });
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function confirmDelete(file) {
|
|
1843
|
+
setBusy(true); setError(null); setNotice("");
|
|
1844
|
+
remote.deleteFile(file)
|
|
1845
|
+
.then(function (r) {
|
|
1846
|
+
if (r && r.ok === false) { setError(r.error || "删除被拒绝"); return; }
|
|
1847
|
+
setDeleting(null);
|
|
1848
|
+
setNotice("已删除 " + file);
|
|
1849
|
+
load();
|
|
1850
|
+
})
|
|
1851
|
+
.catch(function (e) { setError(String((e && e.message) || e)); })
|
|
1852
|
+
.then(function () { setBusy(false); });
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const q = query.trim().toLowerCase();
|
|
1856
|
+
const filtered = items.filter(function (it) {
|
|
1857
|
+
if (!q) return true;
|
|
1858
|
+
const hay = ((it.file || "") + "\n" + (it.description || "") + "\n" + (it.type || "")).toLowerCase();
|
|
1859
|
+
return hay.indexOf(q) >= 0;
|
|
1860
|
+
});
|
|
1861
|
+
|
|
1862
|
+
return h("div", { className: "smem-root" }, [
|
|
1863
|
+
h("div", { className: "smem-tabs", key: "tabs" }, [
|
|
1864
|
+
h("button", { type: "button", className: "smem-tab" + (tab === "files" ? " smem-tab--on" : ""), key: "f", onClick: function () { setTab("files"); } }, "文件列表"),
|
|
1865
|
+
h("button", { type: "button", className: "smem-tab" + (tab === "star" ? " smem-tab--on" : ""), key: "s", onClick: function () { setTab("star"); } }, "记忆星图"),
|
|
1866
|
+
]),
|
|
1867
|
+
h("div", { className: "smem-head", key: "head" }, [
|
|
1868
|
+
h("span", { className: "smem-title", key: "t" }, "记忆文件(dsh-mem)"),
|
|
1869
|
+
h("span", { className: "smem-dot" + (dirOk ? "" : " smem-dot--bad"), key: "dot", title: dirOk ? "memory 目录可读" : "memory 目录不可读" }),
|
|
1870
|
+
h("span", { className: "smem-muted", key: "st" }, dirOk ? "目录可读" : "目录不可读"),
|
|
1871
|
+
h("span", { className: "smem-badge", key: "cnt" }, "共 " + String(items.length) + " 个文件"),
|
|
1872
|
+
]),
|
|
1873
|
+
h("div", { className: "smem-toolbar", key: "bar" }, [
|
|
1874
|
+
h("input", { className: "smem-search", key: "q", value: query, placeholder: "搜索文件名 / 描述 / 类型…", onChange: function (e) { setQuery(e.target.value); } }),
|
|
1875
|
+
h("button", { type: "button", className: "smem-btn", key: "refresh", disabled: busy || loading, onClick: load }, "刷新"),
|
|
1876
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "add", onClick: openAdd }, "+ 添加记忆"),
|
|
1877
|
+
]),
|
|
1878
|
+
error ? h("div", { className: "smem-err", key: "err" }, "⚠ " + error) : null,
|
|
1879
|
+
notice && !error ? h("div", { className: "smem-ok", key: "ok" }, "✓ " + notice) : null,
|
|
1880
|
+
formOpen
|
|
1881
|
+
? h("div", { className: "smem-form", key: "form" }, [
|
|
1882
|
+
h("div", { className: "smem-form-title", key: "ft" }, editingFile ? "编辑 " + editingFile : "添加记忆"),
|
|
1883
|
+
h("div", { className: "smem-row", key: "cat" }, [
|
|
1884
|
+
h("span", { className: "smem-label", key: "l" }, "类型:"),
|
|
1885
|
+
h("div", { className: "smem-chips", key: "chips" }, TYPE_ORDER.map(function (t) {
|
|
1886
|
+
const meta = TYPE_META[t];
|
|
1887
|
+
return h(Chip, { key: t, label: meta[0] + " " + meta[1], on: formType === t, onClick: function () { setFormType(t); } });
|
|
1888
|
+
})),
|
|
1889
|
+
]),
|
|
1890
|
+
h("label", { className: "smem-label", key: "ln" }, "文件名(.md,留空自动 " + formType + "_note.md)"),
|
|
1891
|
+
h("input", { className: "smem-input", key: "tn", value: formName, onChange: function (e) { setFormName(e.target.value); }, placeholder: "例如 project_traveler.md" }),
|
|
1892
|
+
h("label", { className: "smem-label", key: "ld" }, "描述(写进 frontmatter,检索与列表都靠它)"),
|
|
1893
|
+
h("input", { className: "smem-input", key: "td", value: formDesc, onChange: function (e) { setFormDesc(e.target.value); }, placeholder: "一句话说清这条记忆是什么" }),
|
|
1894
|
+
h("label", { className: "smem-label", key: "lb" }, "正文(可选,frontmatter 自动生成)"),
|
|
1895
|
+
h("textarea", { className: "smem-textarea", key: "tb", rows: 6, value: formBody, onChange: function (e) { setFormBody(e.target.value); }, placeholder: "要长期记住的细节" }),
|
|
1896
|
+
h("div", { className: "smem-row", key: "btns" }, [
|
|
1897
|
+
h("button", { type: "button", className: "smem-btn smem-btn-primary", key: "save", disabled: busy, onClick: submit }, busy ? "保存中…" : "保存"),
|
|
1898
|
+
h("button", { type: "button", className: "smem-btn", key: "cancel", disabled: busy, onClick: closeForm }, "取消"),
|
|
1899
|
+
]),
|
|
1900
|
+
])
|
|
1901
|
+
: null,
|
|
1902
|
+
tab === "star"
|
|
1903
|
+
? h("div", { className: "smem-starmap", key: "starmap" },
|
|
1904
|
+
h(StarmapRuntime.StarMap, { ctx: props.ctx }))
|
|
1905
|
+
: loading && items.length === 0
|
|
1906
|
+
? h("div", { className: "smem-muted", key: "loading" }, "加载中…")
|
|
1907
|
+
: filtered.length === 0
|
|
1908
|
+
? h("div", { className: "smem-empty", key: "empty" },
|
|
1909
|
+
items.length === 0
|
|
1910
|
+
? "还没有记忆文件。对话中值得记住的事会写到这里,也可以点「添加记忆」手动写入。"
|
|
1911
|
+
: "没有符合搜索条件的文件。")
|
|
1912
|
+
: h("div", { className: "smem-list", key: "list" }, filtered.map(function (it) {
|
|
1913
|
+
return h(Card, {
|
|
1914
|
+
key: it.file,
|
|
1915
|
+
item: it,
|
|
1916
|
+
expanded: !!expanded[it.file],
|
|
1917
|
+
deleting: deleting === it.file,
|
|
1918
|
+
onToggle: function (file) {
|
|
1919
|
+
const nx = {};
|
|
1920
|
+
nx[file] = !expanded[file];
|
|
1921
|
+
setExpanded(Object.assign({}, expanded, nx));
|
|
1922
|
+
},
|
|
1923
|
+
onEdit: openEdit,
|
|
1924
|
+
onAskDelete: function (file) { setDeleting(file); },
|
|
1925
|
+
onCancelDelete: function () { setDeleting(null); },
|
|
1926
|
+
onConfirmDelete: confirmDelete,
|
|
1927
|
+
});
|
|
1928
|
+
})),
|
|
1929
|
+
]);
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
const inject = ["slots", "remote"];
|
|
1933
|
+
|
|
1934
|
+
/**
|
|
1935
|
+
* Client 插件:$mount strict remote contribution(第三方 remote 不在
|
|
1936
|
+
* dsh-api-remotes 的固定列表里,必须自己挂),再注册设置页签。
|
|
1937
|
+
*/
|
|
1938
|
+
async function apply(ctx) {
|
|
1939
|
+
await ctx.remote.$mount(TYPERT_REMOTE);
|
|
1940
|
+
// 样式注入(参照官方 client bundle 的 data-plugin-css 约定,幂等)
|
|
1941
|
+
const tagId = "dsh-mem/settings.css";
|
|
1942
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
1943
|
+
const tag = document.createElement("style");
|
|
1944
|
+
tag.dataset.plugin = "dsh-mem";
|
|
1945
|
+
tag.dataset.pluginCss = tagId;
|
|
1946
|
+
tag.textContent = CSS;
|
|
1947
|
+
document.head.appendChild(tag);
|
|
1948
|
+
}
|
|
1949
|
+
ctx.slots.inject("settings.section", () =>
|
|
1950
|
+
ctx.slots.register(
|
|
1951
|
+
{
|
|
1952
|
+
name: "settings.section",
|
|
1953
|
+
id: "dsh-mem",
|
|
1954
|
+
order: 20,
|
|
1955
|
+
label: "记忆管理",
|
|
1956
|
+
// 声明「记忆星图」子槽:sage-starmap 向它注入星空视图,
|
|
1957
|
+
// 记忆管理页用 renderSlot 渲染。声明 children 后 shell 才会
|
|
1958
|
+
// 向本 section 组件传入 renderSlot。
|
|
1959
|
+
children: { "settings.memory.starmap": { kind: "list", scope: "root" } },
|
|
1960
|
+
},
|
|
1961
|
+
(props) => react.createElement(Section, { ...props, ctx }),
|
|
1962
|
+
),
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
exports.apply = apply;
|
|
1967
|
+
exports.inject = inject;
|
|
1968
|
+
return module.exports;
|
|
1969
|
+
}
|
|
1970
|
+
});
|