bothouse-dsh 0.1.50 → 0.1.52
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/README.en.md +1 -1
- package/README.md +2 -2
- package/collab/cli-member.mjs +9 -1
- package/collab/devices.html +319 -0
- package/collab/dsh-member.mjs +2 -1
- package/collab/pages-migrate.mjs +50 -0
- package/collab/plugin.mjs +11 -1
- package/collab/team-mode-rules.md +5 -5
- package/collab/wiki-galaxy/index.html +345 -0
- package/cordis.patch.yml +15 -2
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/{client → node_modules/bothouse-dsh-channels}/client.js +3 -99
- package/node_modules/bothouse-dsh-channels/index.js +2 -0
- package/node_modules/bothouse-dsh-channels/locale/en.json +6 -0
- package/node_modules/bothouse-dsh-channels/locale/zh.json +6 -0
- package/node_modules/bothouse-dsh-channels/package.json +29 -0
- package/node_modules/bothouse-dsh-desk/client.js +36 -0
- package/node_modules/bothouse-dsh-desk/index.js +2 -0
- package/node_modules/bothouse-dsh-desk/locale/en.json +6 -0
- package/node_modules/bothouse-dsh-desk/locale/zh.json +6 -0
- package/node_modules/bothouse-dsh-desk/package.json +34 -0
- package/node_modules/bothouse-dsh-devices/client.js +44 -0
- package/node_modules/bothouse-dsh-devices/index.js +2 -0
- package/node_modules/bothouse-dsh-devices/locale/en.json +6 -0
- package/node_modules/bothouse-dsh-devices/locale/zh.json +6 -0
- package/node_modules/bothouse-dsh-devices/package.json +34 -0
- package/node_modules/bothouse-dsh-panel/client.js +42 -0
- package/node_modules/bothouse-dsh-panel/index.js +2 -0
- package/node_modules/bothouse-dsh-panel/locale/en.json +6 -0
- package/node_modules/bothouse-dsh-panel/locale/zh.json +6 -0
- package/node_modules/bothouse-dsh-panel/package.json +34 -0
- package/package.json +15 -16
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<meta name="color-scheme" content="dark">
|
|
7
|
+
<title>Wiki 星河 · CoNest</title>
|
|
8
|
+
<link rel="icon" href="data:,">
|
|
9
|
+
<style>
|
|
10
|
+
:root{--text:#e9eef8;--muted:#8d9bb4;--line:#ffffff1a;--panel:#0b1020cc}
|
|
11
|
+
*{box-sizing:border-box}
|
|
12
|
+
html,body{margin:0;height:100%;background:#02030a;color:var(--text);font:14px/1.5 system-ui,-apple-system,"PingFang SC","Segoe UI",sans-serif;overflow:hidden;-webkit-font-smoothing:antialiased}
|
|
13
|
+
canvas{position:fixed;inset:0;width:100%;height:100%;display:block;cursor:grab;touch-action:none}
|
|
14
|
+
canvas.drag{cursor:grabbing}
|
|
15
|
+
.top{position:fixed;left:22px;top:18px;right:22px;display:flex;align-items:flex-start;justify-content:space-between;gap:16px;pointer-events:none}
|
|
16
|
+
.brand h1{margin:0;font-size:26px;font-weight:700;letter-spacing:3px;background:linear-gradient(90deg,#fff6d8,#ffd27a 40%,#9ad7ff);-webkit-background-clip:text;background-clip:text;color:transparent;text-shadow:0 0 28px #ffd27a33}
|
|
17
|
+
.brand p{margin:4px 0 0;color:var(--muted);font:12px ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:1px}
|
|
18
|
+
.links{display:flex;gap:8px;pointer-events:auto}
|
|
19
|
+
.links a,.links button{border:1px solid var(--line);background:#0b1020aa;color:#cfd9ea;border-radius:99px;padding:6px 14px;font-size:12px;text-decoration:none;cursor:pointer;backdrop-filter:blur(6px)}
|
|
20
|
+
.links a:hover,.links button:hover{border-color:#ffffff40;color:#fff}
|
|
21
|
+
.legend{position:fixed;left:18px;bottom:16px;display:flex;flex-wrap:wrap;gap:6px;max-width:min(720px,calc(100vw - 36px))}
|
|
22
|
+
.legend span.chip{display:flex;align-items:center;gap:7px;border:1px solid var(--line);background:#0b1020aa;color:#cfd9ea;border-radius:99px;padding:4px 11px 4px 9px;font-size:12px;cursor:default;backdrop-filter:blur(6px)}
|
|
23
|
+
.legend span.chip i{width:9px;height:9px;border-radius:50%;background:currentColor;box-shadow:0 0 10px currentColor}
|
|
24
|
+
.legend span.chip b{font-weight:500;color:var(--text)}
|
|
25
|
+
.legend span.chip span{color:var(--muted);font:11px ui-monospace,Menlo,monospace}
|
|
26
|
+
.card{position:fixed;right:18px;top:78px;width:min(360px,calc(100vw - 36px));max-height:calc(100dvh - 150px);overflow:auto;border:1px solid var(--line);border-radius:16px;background:var(--panel);backdrop-filter:blur(14px);padding:16px 18px;box-shadow:0 20px 60px #000a;animation:in .25s ease-out}
|
|
27
|
+
.card[hidden]{display:none}
|
|
28
|
+
@keyframes in{from{opacity:0;transform:translateY(6px)}}
|
|
29
|
+
.card .grp{display:inline-flex;align-items:center;gap:6px;font-size:11px;letter-spacing:1px;color:var(--muted)}
|
|
30
|
+
.card .grp i{width:8px;height:8px;border-radius:50%;background:currentColor;box-shadow:0 0 8px currentColor}
|
|
31
|
+
.card h2{margin:6px 0 2px;font-size:18px;line-height:1.35}
|
|
32
|
+
.card .path{font:11px ui-monospace,Menlo,monospace;color:#6f7d96;word-break:break-all}
|
|
33
|
+
.card .desc{margin:10px 0 0;color:#cdd6e6}
|
|
34
|
+
.card pre{margin:10px 0 0;white-space:pre-wrap;font:12px/1.6 system-ui,"PingFang SC",sans-serif;color:#9fb0c9;border-left:2px solid #ffffff22;padding-left:10px}
|
|
35
|
+
.card .meta{margin-top:10px;font:11px ui-monospace,Menlo,monospace;color:var(--muted)}
|
|
36
|
+
.card h3{margin:14px 0 6px;font-size:11px;letter-spacing:2px;color:var(--muted);font-weight:600}
|
|
37
|
+
.card ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:3px}
|
|
38
|
+
.card li button{all:unset;display:flex;align-items:center;gap:7px;cursor:pointer;font-size:12px;color:#cfd9ea;padding:3px 6px;border-radius:7px;width:100%;box-sizing:border-box}
|
|
39
|
+
.card li button:hover{background:#ffffff0d}
|
|
40
|
+
.card li i{width:6px;height:6px;border-radius:50%;background:currentColor;flex:0 0 auto}
|
|
41
|
+
.card .x{position:absolute;right:10px;top:8px;all:unset;cursor:pointer;color:var(--muted);font-size:18px;position:absolute}
|
|
42
|
+
.msg{position:fixed;inset:0;display:grid;place-items:center;color:var(--muted);font-size:13px;letter-spacing:2px;pointer-events:none}
|
|
43
|
+
.hint{position:fixed;right:18px;bottom:18px;color:#5d6a82;font-size:11px;letter-spacing:1px;pointer-events:none}
|
|
44
|
+
@media (max-width:640px){.brand h1{font-size:20px}.hint{display:none}.card{top:auto;bottom:70px;max-height:45dvh}}
|
|
45
|
+
</style>
|
|
46
|
+
</head>
|
|
47
|
+
<body>
|
|
48
|
+
<canvas id="sky"></canvas>
|
|
49
|
+
<div class="top">
|
|
50
|
+
<div class="brand"><h1>WIKI 星河</h1><p id="stats">正在点亮星星…</p></div>
|
|
51
|
+
<div class="links"><a href="/collab/desk">团队工作台</a><a href="/collab">频道</a></div>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="legend" id="legend"></div>
|
|
54
|
+
<aside class="card" id="card" hidden></aside>
|
|
55
|
+
<div class="msg" id="msg"></div>
|
|
56
|
+
<div class="hint">拖动旋转 · 滚轮缩放 · 点星看页</div>
|
|
57
|
+
<script>
|
|
58
|
+
// 经面板 /dsh/ 转发时页面挂在前缀下:接口和链接都按当前地址带上前缀,直连时前缀为空。
|
|
59
|
+
const ROOT = (location.pathname.match(/^(.*?)\/collab(?:\/|$)/) || ['', ''])[1];
|
|
60
|
+
document.querySelectorAll('a[href^="/collab"]').forEach(a => a.setAttribute('href', ROOT + a.getAttribute('href')));
|
|
61
|
+
'use strict';
|
|
62
|
+
// 视觉取向参照 leesingx/dsh-Memory-Galaxy(旋臂星系)与 Agentage Galaxy(按文件夹分色星团)。
|
|
63
|
+
const PALETTE = { '': '#fff1c9', ai: '#ff7ab6', bots: '#7dd3fc', hopover: '#b79cff', infra: '#4ade80', mac: '#e5e7eb', misc: '#fbbf24', projects: '#fb923c', refs: '#60a5fa', tooling: '#5eead4' };
|
|
64
|
+
const GROUP_NAME = { '': '核心' };
|
|
65
|
+
const canvas = document.getElementById('sky');
|
|
66
|
+
const ctx = canvas.getContext('2d');
|
|
67
|
+
const $ = id => document.getElementById(id);
|
|
68
|
+
|
|
69
|
+
let W = 0, H = 0, DPR = 1;
|
|
70
|
+
function resize() {
|
|
71
|
+
DPR = Math.min(window.devicePixelRatio || 1, 2);
|
|
72
|
+
W = innerWidth; H = innerHeight;
|
|
73
|
+
canvas.width = W * DPR; canvas.height = H * DPR;
|
|
74
|
+
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
75
|
+
}
|
|
76
|
+
addEventListener('resize', resize); resize();
|
|
77
|
+
|
|
78
|
+
// 确定性的伪随机:同一本 wiki 每次打开是同一片天。
|
|
79
|
+
function hash(str) { let h = 2166136261; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; }
|
|
80
|
+
function rng(seed) { let s = seed || 1; return () => { s ^= s << 13; s ^= s >>> 17; s ^= s << 5; return ((s >>> 0) % 1e6) / 1e6; }; }
|
|
81
|
+
function gauss(r) { return Math.sqrt(-2 * Math.log(r() + 1e-9)) * Math.cos(2 * Math.PI * r()); }
|
|
82
|
+
function colorOf(group) {
|
|
83
|
+
if (PALETTE[group]) return PALETTE[group];
|
|
84
|
+
return `hsl(${hash(group) % 360} 85% 70%)`;
|
|
85
|
+
}
|
|
86
|
+
function rgba(hex, a) {
|
|
87
|
+
if (hex.startsWith('hsl')) return hex.replace(')', ` / ${a})`);
|
|
88
|
+
const n = parseInt(hex.slice(1), 16);
|
|
89
|
+
return `rgba(${n >> 16 & 255},${n >> 8 & 255},${n & 255},${a})`;
|
|
90
|
+
}
|
|
91
|
+
function sprite(color) {
|
|
92
|
+
const c = document.createElement('canvas'); c.width = c.height = 64;
|
|
93
|
+
const g = c.getContext('2d');
|
|
94
|
+
const grad = g.createRadialGradient(32, 32, 0, 32, 32, 32);
|
|
95
|
+
grad.addColorStop(0, '#ffffff'); grad.addColorStop(0.12, color); grad.addColorStop(0.35, rgba(color, 0.35)); grad.addColorStop(1, rgba(color, 0));
|
|
96
|
+
g.fillStyle = grad; g.fillRect(0, 0, 64, 64);
|
|
97
|
+
return c;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let nodes = [], links = [], byId = new Map(), groups = [], sprites = {}, nebula = [], dust = [];
|
|
101
|
+
let focusGroup = null, selected = null, hovered = null;
|
|
102
|
+
const cam = { yaw: 0.6, pitch: 1.02, dist: 1750, targetDist: 1750, spin: true, vyaw: 0.012 };
|
|
103
|
+
const born = performance.now();
|
|
104
|
+
|
|
105
|
+
function layout(data) {
|
|
106
|
+
nodes = data.nodes.map(n => ({ ...n, deg: 0, nbr: [] }));
|
|
107
|
+
byId = new Map(nodes.map(n => [n.id, n]));
|
|
108
|
+
links = data.links.map(l => ({ s: byId.get(l.source), t: byId.get(l.target) })).filter(l => l.s && l.t);
|
|
109
|
+
for (const l of links) { l.s.deg++; l.t.deg++; l.s.nbr.push(l.t); l.t.nbr.push(l.s); }
|
|
110
|
+
groups = Object.entries(data.groups).sort((a, b) => (a[0] === '') - (b[0] === '') || b[1] - a[1]).map(([id, count]) => ({ id, count, color: colorOf(id) }));
|
|
111
|
+
for (const g of groups) sprites[g.id] = sprite(g.color);
|
|
112
|
+
const arms = groups.filter(g => g.id !== '');
|
|
113
|
+
const R0 = 170, R1 = 900;
|
|
114
|
+
arms.forEach((g, gi) => {
|
|
115
|
+
g.angle = (gi / arms.length) * Math.PI * 2;
|
|
116
|
+
const members = nodes.filter(n => n.group === g.id);
|
|
117
|
+
const clusters = [...new Set(members.map(n => n.cluster.split('/').slice(0, 2).join('/')))].sort();
|
|
118
|
+
for (const n of members) {
|
|
119
|
+
const r = rng(hash(n.id));
|
|
120
|
+
const key = n.cluster.split('/').slice(0, 2).join('/');
|
|
121
|
+
const isIndex = n.id === `${g.id}/README.md`;
|
|
122
|
+
const t = isIndex ? 0 : (clusters.indexOf(key) + 0.5 + gauss(r) * 0.28) / clusters.length;
|
|
123
|
+
const rad = R0 + Math.max(0, t) * (R1 - R0) + gauss(r) * 26;
|
|
124
|
+
const ang = g.angle + rad * 0.0042 + gauss(r) * (0.07 + 0.05 * (1 - t));
|
|
125
|
+
n.x = Math.cos(ang) * rad; n.z = Math.sin(ang) * rad;
|
|
126
|
+
n.y = gauss(r) * (10 + 46 * Math.max(0, 1 - t));
|
|
127
|
+
}
|
|
128
|
+
g.tip = { x: Math.cos(g.angle + R1 * 0.0042) * (R1 + 90), y: 0, z: Math.sin(g.angle + R1 * 0.0042) * (R1 + 90) };
|
|
129
|
+
});
|
|
130
|
+
for (const n of nodes.filter(n => n.group === '')) {
|
|
131
|
+
const r = rng(hash(n.id));
|
|
132
|
+
const a = r() * Math.PI * 2, d = 18 + r() * 50;
|
|
133
|
+
n.x = Math.cos(a) * d; n.z = Math.sin(a) * d; n.y = gauss(r) * 14;
|
|
134
|
+
}
|
|
135
|
+
for (const n of nodes) {
|
|
136
|
+
n.size = 2.2 + Math.sqrt(n.deg) * 1.9;
|
|
137
|
+
n.color = colorOf(n.group);
|
|
138
|
+
n.r = Math.hypot(n.x, n.z);
|
|
139
|
+
n.tw = rng(hash(n.id) ^ 99)() * Math.PI * 2;
|
|
140
|
+
}
|
|
141
|
+
// 星云:沿每条旋臂撒一团团的暗光,和每颗星同色。
|
|
142
|
+
const r = rng(7);
|
|
143
|
+
nebula = nodes.filter((_, i) => i % 4 === 0 && _.group !== '').map(n => ({ x: n.x + gauss(r) * 40, y: n.y * 0.5, z: n.z + gauss(r) * 40, color: n.color, s: 70 + r() * 90 }));
|
|
144
|
+
// 星尘:旋臂之间的细碎背景星,让盘面不空。
|
|
145
|
+
dust = Array.from({ length: 2600 }, () => {
|
|
146
|
+
const g = arms[Math.floor(r() * arms.length)] ?? { angle: 0 };
|
|
147
|
+
const rad = 60 + Math.pow(r(), 0.7) * (R1 + 160);
|
|
148
|
+
const ang = g.angle + rad * 0.0042 + gauss(r) * 0.35;
|
|
149
|
+
return { x: Math.cos(ang) * rad, y: gauss(r) * 18, z: Math.sin(ang) * rad, a: 0.15 + r() * 0.5 };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const sky = (() => { const r = rng(42); return Array.from({ length: 900 }, () => ({ x: r(), y: r(), a: 0.1 + r() * 0.6, s: r() < 0.08 ? 1.6 : 0.8, tw: r() * 6 })); })();
|
|
154
|
+
|
|
155
|
+
let cy = 1, sy = 0, cp = 1, sp = 0;
|
|
156
|
+
function project(p) {
|
|
157
|
+
const x1 = p.x * cy - p.z * sy;
|
|
158
|
+
const z1 = p.x * sy + p.z * cy;
|
|
159
|
+
const y2 = p.y * cp - z1 * sp;
|
|
160
|
+
const z2 = p.y * sp + z1 * cp;
|
|
161
|
+
const depth = cam.dist + z2;
|
|
162
|
+
if (depth < 40) return null;
|
|
163
|
+
const k = Math.min(W, H) * 0.95 / depth;
|
|
164
|
+
return { x: W / 2 + x1 * k, y: H / 2 + y2 * k, k, depth };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function related(n) { return selected && (n === selected || selected.nbr.includes(n)); }
|
|
168
|
+
function dimmed(n) {
|
|
169
|
+
if (selected) return !related(n);
|
|
170
|
+
return focusGroup !== null && n.group !== focusGroup;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function frame(now) {
|
|
174
|
+
const age = (now - born) / 1000;
|
|
175
|
+
if (cam.spin && !dragging) cam.yaw += cam.vyaw / 60;
|
|
176
|
+
cam.dist += (cam.targetDist - cam.dist) * 0.06;
|
|
177
|
+
cy = Math.cos(cam.yaw); sy = Math.sin(cam.yaw); cp = Math.cos(cam.pitch); sp = Math.sin(cam.pitch);
|
|
178
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
179
|
+
ctx.fillStyle = '#02030a'; ctx.fillRect(0, 0, W, H);
|
|
180
|
+
// 远处的天幕
|
|
181
|
+
for (const s of sky) {
|
|
182
|
+
const x = ((s.x * W + cam.yaw * 12) % W + W) % W;
|
|
183
|
+
ctx.fillStyle = `rgba(200,215,255,${s.a * (0.6 + 0.4 * Math.sin(age * 1.3 + s.tw))})`;
|
|
184
|
+
ctx.fillRect(x, s.y * H, s.s, s.s);
|
|
185
|
+
}
|
|
186
|
+
ctx.globalCompositeOperation = 'lighter';
|
|
187
|
+
const reveal = Infinity;
|
|
188
|
+
// 银心
|
|
189
|
+
const core = project({ x: 0, y: 0, z: 0 });
|
|
190
|
+
if (core) {
|
|
191
|
+
const rr = 260 * core.k;
|
|
192
|
+
const g = ctx.createRadialGradient(core.x, core.y, 0, core.x, core.y, rr);
|
|
193
|
+
g.addColorStop(0, 'rgba(255,240,205,0.55)'); g.addColorStop(0.25, 'rgba(255,200,140,0.18)'); g.addColorStop(1, 'rgba(255,180,120,0)');
|
|
194
|
+
ctx.fillStyle = g; ctx.fillRect(core.x - rr, core.y - rr, rr * 2, rr * 2);
|
|
195
|
+
}
|
|
196
|
+
for (const b of nebula) {
|
|
197
|
+
const p = project(b); if (!p) continue;
|
|
198
|
+
if (Math.hypot(b.x, b.z) > reveal) continue;
|
|
199
|
+
const rr = b.s * p.k;
|
|
200
|
+
const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, rr);
|
|
201
|
+
const a = focusGroup !== null || selected ? 0.025 : 0.05;
|
|
202
|
+
g.addColorStop(0, rgba(b.color, a)); g.addColorStop(1, rgba(b.color, 0));
|
|
203
|
+
ctx.fillStyle = g; ctx.fillRect(p.x - rr, p.y - rr, rr * 2, rr * 2);
|
|
204
|
+
}
|
|
205
|
+
ctx.fillStyle = 'rgba(210,220,255,0.5)';
|
|
206
|
+
for (const d of dust) {
|
|
207
|
+
if (Math.hypot(d.x, d.z) > reveal) continue;
|
|
208
|
+
const p = project(d); if (!p) continue;
|
|
209
|
+
ctx.globalAlpha = d.a; ctx.fillRect(p.x, p.y, 1, 1);
|
|
210
|
+
}
|
|
211
|
+
ctx.globalAlpha = 1;
|
|
212
|
+
// 星轨:[[链接]]
|
|
213
|
+
for (const n of nodes) n.p = n.r <= reveal ? project(n) : null;
|
|
214
|
+
ctx.lineWidth = 0.6;
|
|
215
|
+
for (const l of links) {
|
|
216
|
+
const a = l.s.p, b = l.t.p; if (!a || !b) continue;
|
|
217
|
+
const hot = selected && (l.s === selected || l.t === selected);
|
|
218
|
+
const grp = focusGroup !== null && (l.s.group === focusGroup || l.t.group === focusGroup);
|
|
219
|
+
let alpha = selected ? (hot ? 0.75 : 0.015) : focusGroup !== null ? (grp ? 0.22 : 0.012) : 0.07;
|
|
220
|
+
ctx.strokeStyle = rgba(l.s.color, alpha);
|
|
221
|
+
ctx.lineWidth = hot ? 1.3 : 0.6;
|
|
222
|
+
ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke();
|
|
223
|
+
}
|
|
224
|
+
// 星
|
|
225
|
+
for (const n of nodes) {
|
|
226
|
+
const p = n.p; if (!p) continue;
|
|
227
|
+
const dim = dimmed(n);
|
|
228
|
+
const tw = 0.82 + 0.18 * Math.sin(age * 2 + n.tw);
|
|
229
|
+
const s = n.size * p.k * 6.5 * (n === selected ? 1.8 : n === hovered ? 1.4 : 1) * tw;
|
|
230
|
+
ctx.globalAlpha = dim ? 0.12 : 1;
|
|
231
|
+
ctx.drawImage(sprites[n.group], p.x - s, p.y - s, s * 2, s * 2);
|
|
232
|
+
}
|
|
233
|
+
ctx.globalAlpha = 1;
|
|
234
|
+
ctx.globalCompositeOperation = 'source-over';
|
|
235
|
+
// 星团名和最亮的几颗星的名字
|
|
236
|
+
ctx.textAlign = 'center';
|
|
237
|
+
for (const g of groups) {
|
|
238
|
+
if (!g.tip) continue;
|
|
239
|
+
const p = project(g.tip); if (!p) continue;
|
|
240
|
+
ctx.font = '600 13px system-ui,"PingFang SC",sans-serif';
|
|
241
|
+
ctx.fillStyle = rgba(g.color.startsWith('#') ? g.color : '#ffffff', focusGroup === null || focusGroup === g.id ? 0.9 : 0.25);
|
|
242
|
+
ctx.fillText((GROUP_NAME[g.id] ?? g.id).toUpperCase(), p.x, p.y);
|
|
243
|
+
}
|
|
244
|
+
const labelled = selected ? [selected, ...[...new Set(selected.nbr)].sort((a, b) => b.deg - a.deg).slice(0, 14)] : hubs;
|
|
245
|
+
ctx.font = '12px system-ui,"PingFang SC",sans-serif';
|
|
246
|
+
for (const n of labelled) {
|
|
247
|
+
const p = n.p; if (!p) continue;
|
|
248
|
+
ctx.fillStyle = n === selected ? '#ffffff' : 'rgba(220,230,250,0.75)';
|
|
249
|
+
ctx.fillText(short(n.title), p.x, p.y - n.size * p.k * 6.5 - 6);
|
|
250
|
+
}
|
|
251
|
+
requestAnimationFrame(frame);
|
|
252
|
+
}
|
|
253
|
+
let hubs = [];
|
|
254
|
+
function short(t) { return t.length > 18 ? t.slice(0, 17) + '…' : t; }
|
|
255
|
+
|
|
256
|
+
// 交互:拖动转、滚轮/双指缩放、点星看页
|
|
257
|
+
let dragging = false, moved = 0, last = null, pinch = null, idleTimer;
|
|
258
|
+
const pointers = new Map();
|
|
259
|
+
function pause() { clearTimeout(idleTimer); }
|
|
260
|
+
canvas.addEventListener('pointerdown', e => { canvas.setPointerCapture(e.pointerId); pointers.set(e.pointerId, e); dragging = true; moved = 0; last = e; canvas.classList.add('drag'); pause(); });
|
|
261
|
+
canvas.addEventListener('pointermove', e => {
|
|
262
|
+
if (pointers.has(e.pointerId)) pointers.set(e.pointerId, e);
|
|
263
|
+
if (pointers.size === 2) {
|
|
264
|
+
const [a, b] = [...pointers.values()];
|
|
265
|
+
const d = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
|
266
|
+
if (pinch) cam.targetDist = clampDist(cam.targetDist * pinch / d);
|
|
267
|
+
pinch = d; moved += 10; return;
|
|
268
|
+
}
|
|
269
|
+
if (dragging && last) {
|
|
270
|
+
const dx = e.clientX - last.clientX, dy = e.clientY - last.clientY;
|
|
271
|
+
moved += Math.abs(dx) + Math.abs(dy);
|
|
272
|
+
cam.yaw -= dx * 0.005;
|
|
273
|
+
cam.pitch = Math.max(0.15, Math.min(1.5, cam.pitch - dy * 0.004));
|
|
274
|
+
last = e; return;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
function up(e) {
|
|
278
|
+
pointers.delete(e.pointerId); if (pointers.size < 2) pinch = null;
|
|
279
|
+
if (pointers.size) return;
|
|
280
|
+
dragging = false; canvas.classList.remove('drag');
|
|
281
|
+
if (moved < 5) select(pick(e.clientX, e.clientY));
|
|
282
|
+
last = null;
|
|
283
|
+
}
|
|
284
|
+
canvas.addEventListener('pointerup', up);
|
|
285
|
+
canvas.addEventListener('pointercancel', up);
|
|
286
|
+
canvas.addEventListener('wheel', e => { e.preventDefault(); cam.targetDist = clampDist(cam.targetDist * Math.exp(e.deltaY * 0.0012)); }, { passive: false });
|
|
287
|
+
function clampDist(d) { return Math.max(260, Math.min(3200, d)); }
|
|
288
|
+
function pick(x, y) {
|
|
289
|
+
let best = null, bd = 14 * 14;
|
|
290
|
+
for (const n of nodes) {
|
|
291
|
+
if (!n.p) continue;
|
|
292
|
+
const d = (n.p.x - x) ** 2 + (n.p.y - y) ** 2;
|
|
293
|
+
if (d < bd) { bd = d; best = n; }
|
|
294
|
+
}
|
|
295
|
+
return best;
|
|
296
|
+
}
|
|
297
|
+
addEventListener('keydown', e => { if (e.key === 'Escape') { select(null); } });
|
|
298
|
+
|
|
299
|
+
function esc(s) { return String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]); }
|
|
300
|
+
function select(n) {
|
|
301
|
+
selected = n;
|
|
302
|
+
const card = $('card');
|
|
303
|
+
if (!n) { card.hidden = true; return; }
|
|
304
|
+
const nb = [...new Set(n.nbr)].sort((a, b) => b.deg - a.deg);
|
|
305
|
+
card.innerHTML = `<button class="x" aria-label="关闭">×</button>
|
|
306
|
+
<div class="grp" style="color:${n.color}"><i></i>${esc((GROUP_NAME[n.group] ?? n.group).toUpperCase())}</div>
|
|
307
|
+
<h2>${esc(n.title)}</h2><div class="path">${esc(n.id)}</div>
|
|
308
|
+
${n.description ? `<p class="desc">${esc(n.description)}</p>` : ''}
|
|
309
|
+
${n.excerpt ? `<pre>${esc(n.excerpt)}</pre>` : ''}
|
|
310
|
+
<div class="meta">${n.lines} 行 · ${nb.length} 条星轨</div>
|
|
311
|
+
${nb.length ? `<h3>连着的星</h3><ul>${nb.map((m, i) => `<li><button data-i="${i}" style="color:${m.color}"><i></i><span style="color:#cfd9ea">${esc(m.title)}</span></button></li>`).join('')}</ul>` : ''}`;
|
|
312
|
+
card.hidden = false;
|
|
313
|
+
card.querySelector('.x').onclick = () => select(null);
|
|
314
|
+
card.querySelectorAll('li button').forEach(b => { b.onclick = () => select(nb[Number(b.dataset.i)]); });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function load() {
|
|
318
|
+
let data;
|
|
319
|
+
try {
|
|
320
|
+
const res = await fetch(ROOT + '/collab/api/wiki-galaxy', { credentials: 'same-origin' });
|
|
321
|
+
if (!res.ok) throw new Error(String(res.status));
|
|
322
|
+
data = await res.json();
|
|
323
|
+
} catch (error) {
|
|
324
|
+
$('msg').textContent = `读不到 wiki(${error.message})`;
|
|
325
|
+
$('stats').textContent = '';
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (!data.nodes.length) { $('msg').textContent = 'wiki 目录里还没有页面'; $('stats').textContent = ''; return; }
|
|
329
|
+
layout(data);
|
|
330
|
+
hubs = [...nodes].sort((a, b) => b.deg - a.deg).slice(0, 8);
|
|
331
|
+
$('stats').textContent = `${nodes.length} 颗星 · ${links.length} 条星轨 · ${groups.length} 个星团`;
|
|
332
|
+
const legend = $('legend');
|
|
333
|
+
for (const g of groups) {
|
|
334
|
+
const b = document.createElement('span');
|
|
335
|
+
b.className = 'chip';
|
|
336
|
+
b.dataset.g = g.id; b.style.color = g.color;
|
|
337
|
+
b.innerHTML = `<i></i><b>${esc(GROUP_NAME[g.id] ?? g.id)}</b><span>${g.count}</span>`;
|
|
338
|
+
legend.append(b);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
load();
|
|
342
|
+
requestAnimationFrame(frame);
|
|
343
|
+
</script>
|
|
344
|
+
</body>
|
|
345
|
+
</html>
|
package/cordis.patch.yml
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
-
# Applied by `dsh plugin add bothouse-dsh
|
|
1
|
+
# Applied by `dsh plugin add bothouse-dsh`. One row runs the collab host; each page is its own
|
|
2
|
+
# row with its own browser half (bundled packages), so the Plugins page switches each one (#1044).
|
|
3
|
+
# Channels is on by default; team desk, bothouse panel and other computers start off.
|
|
2
4
|
# Paths default to $CONEST_COLLAB_DATA / $CONEST_COLLAB_WORKSPACE, else ~/.conest-collab and the cwd.
|
|
3
5
|
- insert:
|
|
4
6
|
- id: bothouse-dsh
|
|
5
7
|
name: bothouse-dsh
|
|
6
8
|
config:
|
|
7
|
-
dataDir: !!js process.env.CONEST_COLLAB_DATA || (process.
|
|
9
|
+
dataDir: !!js process.env.CONEST_COLLAB_DATA || process.getBuiltinModule('node:path').join(process.getBuiltinModule('node:os').homedir(), '.conest-collab')
|
|
8
10
|
workspacePath: !!js process.env.CONEST_COLLAB_WORKSPACE || process.cwd()
|
|
11
|
+
- id: bothouse-channels
|
|
12
|
+
name: bothouse-dsh-channels
|
|
13
|
+
- id: bothouse-desk
|
|
14
|
+
name: bothouse-dsh-desk
|
|
15
|
+
disabled: true
|
|
16
|
+
- id: bothouse-panel
|
|
17
|
+
name: bothouse-dsh-panel
|
|
18
|
+
disabled: true
|
|
19
|
+
- id: bothouse-devices
|
|
20
|
+
name: bothouse-dsh-devices
|
|
21
|
+
disabled: true
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "bothouse",
|
|
4
|
+
"description": "DSH models and local Claude Code or Codex in one group. The first row, \"bothouse\", is the background service the others need; leave it on. Channels, team desk, bothouse panel and other computers switch on separately."
|
|
5
|
+
}
|
|
6
|
+
}
|