bothouse-dsh 0.1.45 → 0.1.46
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 +9 -3
- package/README.md +9 -3
- package/collab/desk/index.html +1464 -0
- package/collab/desk.mjs +177 -0
- package/collab/gate-context.mjs +13 -19
- package/collab/handoff-rules.json +175 -0
- package/collab/hub.mjs +26 -1
- package/collab/page.html +610 -63
- package/collab/personas.json +150 -0
- package/collab/plugin.mjs +38 -5
- package/collab/roster.mjs +33 -0
- package/collab/team-exit-rules.json +81 -0
- package/collab/team-mode-rules.md +33 -0
- package/collab/team-status.mjs +180 -0
- package/collab/wiki-galaxy.mjs +126 -0
- package/package.json +6 -2
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Wiki 星河:把一本 markdown wiki 画成星系。每页一颗星,[[链接]] 与相对 .md 链接是连线,
|
|
2
|
+
// 顶层目录是星团。只读:只扫配置的 wiki 目录,不写任何文件,不跟符号链接出目录。
|
|
3
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { basename, join, posix, sep } from 'node:path';
|
|
5
|
+
|
|
6
|
+
// 来源:用户要求「点一颗星能看到这页标题、描述和前几行」(交接 wiki-galaxy-1103 总目标)。
|
|
7
|
+
export const EXCERPT_LINES = 5;
|
|
8
|
+
|
|
9
|
+
const WIKI_LINK = /\[\[([^\]\n]+?)\]\]/g;
|
|
10
|
+
const MD_LINK = /\]\(([^)\s]+?\.md)(?:#[^)]*)?\)/g;
|
|
11
|
+
const HTML = 'text/html; charset=utf-8';
|
|
12
|
+
const JSON_TYPE = 'application/json; charset=utf-8';
|
|
13
|
+
|
|
14
|
+
/** 去掉 frontmatter,返回 { meta, body };只认顶层 name/description 两个键。 */
|
|
15
|
+
export function splitFrontmatter(text) {
|
|
16
|
+
const meta = {};
|
|
17
|
+
if (!text.startsWith('---\n')) return { meta, body: text };
|
|
18
|
+
const end = text.indexOf('\n---', 4);
|
|
19
|
+
if (end < 0) return { meta, body: text };
|
|
20
|
+
for (const line of text.slice(4, end).split('\n')) {
|
|
21
|
+
const m = /^(name|description):\s*(.*)$/.exec(line);
|
|
22
|
+
if (m) meta[m[1]] = m[2].trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
23
|
+
}
|
|
24
|
+
const after = text.indexOf('\n', end + 1);
|
|
25
|
+
return { meta, body: after < 0 ? '' : text.slice(after + 1) };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function walk(root, rel, out) {
|
|
29
|
+
let entries;
|
|
30
|
+
try { entries = readdirSync(join(root, rel), { withFileTypes: true }); } catch { return; }
|
|
31
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
32
|
+
for (const entry of entries) {
|
|
33
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
34
|
+
// 不跟符号链接:wiki 目录外的东西一律不读。
|
|
35
|
+
if (entry.isSymbolicLink()) continue;
|
|
36
|
+
const child = rel ? `${rel}/${entry.name}` : entry.name;
|
|
37
|
+
if (entry.isDirectory()) walk(root, child, out);
|
|
38
|
+
else if (entry.isFile() && entry.name.endsWith('.md')) out.push(child);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 扫 wiki 目录,出 { nodes, links, groups }。 */
|
|
43
|
+
export function scanWiki(root) {
|
|
44
|
+
const files = [];
|
|
45
|
+
walk(root, '', files);
|
|
46
|
+
const nodes = [];
|
|
47
|
+
const bodies = [];
|
|
48
|
+
for (const path of files) {
|
|
49
|
+
let text;
|
|
50
|
+
try { text = readFileSync(join(root, path), 'utf8'); } catch { continue; }
|
|
51
|
+
const { meta, body } = splitFrontmatter(text);
|
|
52
|
+
const heading = /^#\s+(.+)$/m.exec(body)?.[1]?.trim();
|
|
53
|
+
const parts = path.split('/');
|
|
54
|
+
const excerpt = body.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')).slice(0, EXCERPT_LINES).join('\n');
|
|
55
|
+
nodes.push({
|
|
56
|
+
id: path,
|
|
57
|
+
name: meta.name || basename(path, '.md'),
|
|
58
|
+
title: heading || meta.name || basename(path, '.md'),
|
|
59
|
+
description: meta.description ?? '',
|
|
60
|
+
group: parts.length > 1 ? parts[0] : '',
|
|
61
|
+
cluster: parts.slice(0, -1).join('/'),
|
|
62
|
+
lines: text.split('\n').length,
|
|
63
|
+
excerpt,
|
|
64
|
+
});
|
|
65
|
+
bodies.push(body);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// [[name]] 先按 frontmatter name,再按文件名;README 也能用目录名指到。
|
|
69
|
+
const byName = new Map();
|
|
70
|
+
const claim = (key, id) => { if (key && !byName.has(key)) byName.set(key, id); };
|
|
71
|
+
for (const n of nodes) claim(n.name, n.id);
|
|
72
|
+
for (const n of nodes) claim(basename(n.id, '.md'), n.id);
|
|
73
|
+
for (const n of nodes) if (basename(n.id) === 'README.md' && n.cluster) claim(basename(n.cluster), n.id);
|
|
74
|
+
const ids = new Set(nodes.map(n => n.id));
|
|
75
|
+
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
const links = [];
|
|
78
|
+
const add = (source, target) => {
|
|
79
|
+
if (!target || source === target) return;
|
|
80
|
+
const key = source < target ? `${source}\n${target}` : `${target}\n${source}`;
|
|
81
|
+
if (seen.has(key)) return;
|
|
82
|
+
seen.add(key);
|
|
83
|
+
links.push({ source, target });
|
|
84
|
+
};
|
|
85
|
+
nodes.forEach((n, i) => {
|
|
86
|
+
for (const m of bodies[i].matchAll(WIKI_LINK)) add(n.id, byName.get(m[1].split(/[|#]/)[0].trim()));
|
|
87
|
+
for (const m of bodies[i].matchAll(MD_LINK)) {
|
|
88
|
+
if (/^[a-z]+:/i.test(m[1])) continue;
|
|
89
|
+
let target;
|
|
90
|
+
try { target = posix.normalize(posix.join(posix.dirname(n.id), decodeURI(m[1]))); } catch { continue; }
|
|
91
|
+
if (ids.has(target)) add(n.id, target);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const groups = {};
|
|
96
|
+
for (const n of nodes) groups[n.group] = (groups[n.group] ?? 0) + 1;
|
|
97
|
+
return { nodes, links, groups };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function wikiGalaxyPage() {
|
|
101
|
+
return readFileSync(new URL('./wiki-galaxy/index.html', import.meta.url), 'utf8');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function send(res, status, type, body) {
|
|
105
|
+
res.writeHead(status, { 'content-type': type, 'cache-control': 'no-store' });
|
|
106
|
+
res.end(body);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** /collab/wiki-galaxy 与 /collab/api/wiki-galaxy;返回 true 表示已处理,未匹配返回 undefined。 */
|
|
110
|
+
export function wikiGalaxyRoute(req, res, url, { wikiDir }) {
|
|
111
|
+
const path = url.pathname.replace(/\/+$/, '');
|
|
112
|
+
if (path !== '/collab/wiki-galaxy' && path !== '/collab/api/wiki-galaxy') return undefined;
|
|
113
|
+
if (req.method !== 'GET') return undefined;
|
|
114
|
+
if (path === '/collab/wiki-galaxy') {
|
|
115
|
+
send(res, 200, HTML, wikiGalaxyPage()); // SPEC:HTTP 状态码,沿用 desk.mjs deskRoute
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
let graph;
|
|
119
|
+
try { graph = scanWiki(wikiDir); } catch {
|
|
120
|
+
send(res, 500, JSON_TYPE, JSON.stringify({ error: 'wiki scan failed' })); // SPEC:HTTP 状态码,沿用 desk.mjs deskRoute
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
const name = wikiDir.split(sep).filter(Boolean).pop() ?? 'wiki';
|
|
124
|
+
send(res, 200, JSON_TYPE, JSON.stringify({ name, ...graph })); // SPEC:HTTP 状态码,沿用 desk.mjs deskRoute
|
|
125
|
+
return true;
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bothouse-dsh",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.46",
|
|
4
4
|
"description": "BotHouse channels for DeepSeek Harness: Discord-style rooms where DSH models and local CLIs (Claude Code, Codex, Gemini) are members",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./collab/plugin.mjs",
|
|
@@ -25,6 +25,9 @@
|
|
|
25
25
|
"files": [
|
|
26
26
|
"collab/*.mjs",
|
|
27
27
|
"collab/page.html",
|
|
28
|
+
"collab/*.json",
|
|
29
|
+
"collab/team-mode-rules.md",
|
|
30
|
+
"collab/desk/index.html",
|
|
28
31
|
"collab/avatars/*.png",
|
|
29
32
|
"client/client.js",
|
|
30
33
|
"cordis.patch.yml",
|
|
@@ -59,7 +62,8 @@
|
|
|
59
62
|
},
|
|
60
63
|
"dependencies": {
|
|
61
64
|
"@agentclientprotocol/sdk": "1.4.0",
|
|
62
|
-
"@zed-industries/claude-code-acp": "^0.16.2"
|
|
65
|
+
"@zed-industries/claude-code-acp": "^0.16.2",
|
|
66
|
+
"ws": "8.22.1"
|
|
63
67
|
},
|
|
64
68
|
"optionalDependencies": {
|
|
65
69
|
"@agentclientprotocol/codex-acp": "^2.2.2"
|