bothouse-dsh 0.1.64 → 0.1.66
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/collab/avatars/brand-claude.png +0 -0
- package/collab/avatars/brand-deepseek.png +0 -0
- package/collab/avatars/brand-gemini.png +0 -0
- package/collab/avatars/brand-grok.png +0 -0
- package/collab/avatars/brand-minimax.png +0 -0
- package/collab/avatars/brand-openai.png +0 -0
- package/collab/avatars.mjs +4 -1
- package/collab/desk/index.html +499 -76
- package/collab/desk.mjs +21 -1
- package/node_modules/bothouse-dsh-channels/client.js +116 -27
- package/package.json +1 -1
package/collab/desk.mjs
CHANGED
|
@@ -138,6 +138,26 @@ export function deskActiveTasks({ boardDir }) {
|
|
|
138
138
|
return [...last.values()].filter(entry => entry.kind === 'created' || entry.kind === 'assigned');
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/** 暂停着的任务(最后一条是 paused,且在 24 小时内):桌面页让那个小人举手、主角色那盏灯变红。 */
|
|
142
|
+
export function deskPausedTasks({ boardDir, now = Date.now() } = {}) {
|
|
143
|
+
const last = new Map();
|
|
144
|
+
for (const entry of readBoardEvents(boardDir)) {
|
|
145
|
+
const id = String(entry.id ?? '').trim();
|
|
146
|
+
if (id && entry.kind) last.set(id, entry);
|
|
147
|
+
}
|
|
148
|
+
return [...last.values()].filter(entry => entry.kind === 'paused' && now - Date.parse(entry.ts) < 24 * 3600 * 1000);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 交回了、还没验收的任务(最后一条是 returned,且在 24 小时内):桌面页在主角色桌上叠成一摞文件。 */
|
|
152
|
+
export function deskReturnedTasks({ boardDir, now = Date.now() } = {}) {
|
|
153
|
+
const last = new Map();
|
|
154
|
+
for (const entry of readBoardEvents(boardDir)) {
|
|
155
|
+
const id = String(entry.id ?? '').trim();
|
|
156
|
+
if (id && entry.kind) last.set(id, entry);
|
|
157
|
+
}
|
|
158
|
+
return [...last.values()].filter(entry => entry.kind === 'returned' && now - Date.parse(entry.ts) < 24 * 3600 * 1000);
|
|
159
|
+
}
|
|
160
|
+
|
|
141
161
|
/** 桌面页:C 路提供的 dsh/collab/desk/index.html;还没落地时给一个能看事件流的占位页。 */
|
|
142
162
|
export function deskPage() {
|
|
143
163
|
try { return readFileSync(new URL('./desk/index.html', import.meta.url), 'utf8'); } catch { return PLACEHOLDER_PAGE; }
|
|
@@ -194,7 +214,7 @@ async function namesRoute(req, res, namesFile) {
|
|
|
194
214
|
export function deskRoute(req, res, url, { store, boardDir, members = [], namesFile } = {}) {
|
|
195
215
|
if (url.pathname.replace(/\/+$/, '') === '/collab/api/desk/names' && (req.method === 'GET' || req.method === 'PUT')) return namesRoute(req, res, namesFile);
|
|
196
216
|
if (url.pathname.replace(/\/+$/, '') === '/collab/api/desk/state' && req.method === 'GET') {
|
|
197
|
-
sendJson(res, 200, { tasks: deskActiveTasks({ boardDir }) });
|
|
217
|
+
sendJson(res, 200, { tasks: deskActiveTasks({ boardDir }), returned: deskReturnedTasks({ boardDir }), paused: deskPausedTasks({ boardDir }) });
|
|
198
218
|
return true;
|
|
199
219
|
}
|
|
200
220
|
if (url.pathname.replace(/\/+$/, '') !== '/collab/desk' && url.pathname.replace(/\/+$/, '') !== '/collab/api/desk/replay') return undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-client-source-sha256:
|
|
1
|
+
// dsh-client-source-sha256: 0dae018f21980115a8987a880c994fe82307bbb1aaf1d67b3742cbf6548371e1
|
|
2
2
|
// dsh-style-sha256: 9ec6f9be158431d36e8c658f1b8def48515a0e80fe35ad14ccfa7202308bb0cd
|
|
3
3
|
window.__ModuleLoader__.load({
|
|
4
4
|
id: "bothouse-dsh-channels",
|
|
@@ -19775,6 +19775,7 @@ function HighlightedMarkdown({ text, labels, members, className, inline, dataCar
|
|
|
19775
19775
|
/**
|
|
19776
19776
|
* Built-in member avatars served by the collab plugin at `collab/avatars/<slug>.png`:
|
|
19777
19777
|
* the four B-version character portraits (Claude / GPT / DeepSeek / Gemini) + Grok + lanxi.
|
|
19778
|
+
* These are the slugs a member can pick for itself (MemberDialog).
|
|
19778
19779
|
*/
|
|
19779
19780
|
const AVATARS = [
|
|
19780
19781
|
"orange",
|
|
@@ -19784,39 +19785,63 @@ const AVATARS = [
|
|
|
19784
19785
|
"lanxi",
|
|
19785
19786
|
"grok"
|
|
19786
19787
|
];
|
|
19788
|
+
const DEFAULT_AVATAR_STYLE = "brand";
|
|
19789
|
+
const AVATAR_SETS = {
|
|
19790
|
+
brand: {
|
|
19791
|
+
claude: "brand-claude",
|
|
19792
|
+
gpt: "brand-openai",
|
|
19793
|
+
deepseek: "brand-deepseek",
|
|
19794
|
+
gemini: "brand-gemini",
|
|
19795
|
+
grok: "brand-grok",
|
|
19796
|
+
minimax: "brand-minimax"
|
|
19797
|
+
},
|
|
19798
|
+
chibi: {
|
|
19799
|
+
claude: "orange",
|
|
19800
|
+
gpt: "dragon",
|
|
19801
|
+
deepseek: "whale",
|
|
19802
|
+
gemini: "rainbow",
|
|
19803
|
+
grok: "grok",
|
|
19804
|
+
minimax: "lanxi"
|
|
19805
|
+
}
|
|
19806
|
+
};
|
|
19807
|
+
/** Every image the plugin serves under collab/avatars/ (both sets). */
|
|
19808
|
+
const SERVED = [...AVATARS, ...Object.values(AVATAR_SETS.brand)];
|
|
19809
|
+
function normalizeAvatarStyle(v) {
|
|
19810
|
+
return v === "chibi" ? "chibi" : DEFAULT_AVATAR_STYLE;
|
|
19811
|
+
}
|
|
19787
19812
|
/**
|
|
19788
|
-
* Maps member role/model/cli/name to a built-in avatar slug:
|
|
19789
|
-
* -
|
|
19790
|
-
* - GPT / Codex -> dragon
|
|
19791
|
-
* - DeepSeek -> whale
|
|
19792
|
-
* - Gemini -> rainbow
|
|
19793
|
-
* - Grok -> grok
|
|
19813
|
+
* Maps member role/model/cli/name to a built-in avatar slug in the given set (default brand):
|
|
19814
|
+
* Claude, GPT / Codex, DeepSeek, Gemini, Grok, MiniMax. A member's own built-in pick wins.
|
|
19794
19815
|
*/
|
|
19795
|
-
function matchAvatarSlug(opts) {
|
|
19796
|
-
if (opts.avatar &&
|
|
19816
|
+
function matchAvatarSlug(opts, style = DEFAULT_AVATAR_STYLE) {
|
|
19817
|
+
if (opts.avatar && SERVED.includes(opts.avatar)) return opts.avatar;
|
|
19818
|
+
const family = matchFamily(opts);
|
|
19819
|
+
return family === void 0 ? void 0 : AVATAR_SETS[style][family];
|
|
19820
|
+
}
|
|
19821
|
+
function matchFamily(opts) {
|
|
19797
19822
|
const model = (opts.model || "").toLowerCase();
|
|
19798
19823
|
const cli = (opts.cli || "").toLowerCase();
|
|
19799
19824
|
const name = (opts.name || opts.id || "").toLowerCase();
|
|
19800
19825
|
if (model) {
|
|
19801
|
-
if (model.includes("claude") || model.includes("opus") || model.includes("sonnet") || model.includes("haiku")) return "
|
|
19802
|
-
if (model.includes("gpt") || model.includes("codex") || model.includes("o1") || model.includes("o3") || model.includes("o4") || model.includes("chatgpt")) return "
|
|
19803
|
-
if (model.includes("deepseek") || model.includes("r1") || model.includes("v3")) return "
|
|
19804
|
-
if (model.includes("gemini")) return "
|
|
19826
|
+
if (model.includes("claude") || model.includes("opus") || model.includes("sonnet") || model.includes("haiku")) return "claude";
|
|
19827
|
+
if (model.includes("gpt") || model.includes("codex") || model.includes("o1") || model.includes("o3") || model.includes("o4") || model.includes("chatgpt")) return "gpt";
|
|
19828
|
+
if (model.includes("deepseek") || model.includes("r1") || model.includes("v3")) return "deepseek";
|
|
19829
|
+
if (model.includes("gemini")) return "gemini";
|
|
19805
19830
|
if (model.includes("grok") || model.includes("xai")) return "grok";
|
|
19806
19831
|
}
|
|
19807
19832
|
if (cli) {
|
|
19808
|
-
if (cli === "claude" || cli === "claudecode") return "
|
|
19809
|
-
if (cli === "codex") return "
|
|
19810
|
-
if (cli === "deepseek" || cli === "dsh") return "
|
|
19811
|
-
if (cli === "gemini" || cli === "antigravity") return "
|
|
19833
|
+
if (cli === "claude" || cli === "claudecode") return "claude";
|
|
19834
|
+
if (cli === "codex") return "gpt";
|
|
19835
|
+
if (cli === "deepseek" || cli === "dsh") return "deepseek";
|
|
19836
|
+
if (cli === "gemini" || cli === "antigravity") return "gemini";
|
|
19812
19837
|
if (cli === "grok") return "grok";
|
|
19813
19838
|
}
|
|
19814
19839
|
if (name) {
|
|
19815
|
-
if (name === "橘" || name === "c酱" || name === "cjiang" || name === "claude") return "
|
|
19816
|
-
if (name === "龙娘" || name === "g哥" || name === "gge" || name === "codex" || name === "gpt") return "
|
|
19817
|
-
if (name === "鲸鱼娘" || name === "肥鱼" || name === "deep" || name === "deepseek" || name === "dsh") return "
|
|
19818
|
-
if (name === "彩虹猫" || name === "大豆包" || name === "gjiang" || name === "gemini") return "
|
|
19819
|
-
if (name === "岚汐" || name === "lanxi" || name === "minimax") return "
|
|
19840
|
+
if (name === "橘" || name === "c酱" || name === "cjiang" || name === "claude") return "claude";
|
|
19841
|
+
if (name === "龙娘" || name === "g哥" || name === "gge" || name === "codex" || name === "gpt") return "gpt";
|
|
19842
|
+
if (name === "鲸鱼娘" || name === "肥鱼" || name === "deep" || name === "deepseek" || name === "dsh") return "deepseek";
|
|
19843
|
+
if (name === "彩虹猫" || name === "大豆包" || name === "gjiang" || name === "gemini") return "gemini";
|
|
19844
|
+
if (name === "岚汐" || name === "lanxi" || name === "minimax") return "minimax";
|
|
19820
19845
|
if (name === "grok" || name.includes("grok")) return "grok";
|
|
19821
19846
|
}
|
|
19822
19847
|
}
|
|
@@ -19826,21 +19851,84 @@ function matchAvatarSlug(opts) {
|
|
|
19826
19851
|
* @param url - #236 path mapper of the panel (a paired computer's panel reads its avatars).
|
|
19827
19852
|
* @returns a URL relative to the document base, the absolute URL, or undefined for the initial badge.
|
|
19828
19853
|
*/
|
|
19829
|
-
function avatarSrc(avatar, url = (path) => path) {
|
|
19854
|
+
function avatarSrc(avatar, url = (path) => path, style = DEFAULT_AVATAR_STYLE) {
|
|
19830
19855
|
if (avatar === void 0 || avatar === "") return void 0;
|
|
19831
19856
|
if (typeof avatar === "string") {
|
|
19832
19857
|
if (/^upload:[a-z0-9-]+\.png$/.test(avatar)) return url(`collab/avatars/uploads/${avatar.slice(7)}`);
|
|
19833
19858
|
if (/^https?:\/\//.test(avatar)) return avatar;
|
|
19834
|
-
if (
|
|
19835
|
-
const matched = matchAvatarSlug({ name: avatar });
|
|
19859
|
+
if (SERVED.includes(avatar)) return url(`collab/avatars/${avatar}.png`);
|
|
19860
|
+
const matched = matchAvatarSlug({ name: avatar }, style);
|
|
19836
19861
|
return matched ? url(`collab/avatars/${matched}.png`) : void 0;
|
|
19837
19862
|
}
|
|
19838
19863
|
const a = avatar.avatar;
|
|
19839
19864
|
if (a && /^upload:[a-z0-9-]+\.png$/.test(a)) return url(`collab/avatars/uploads/${a.slice(7)}`);
|
|
19840
19865
|
if (a && /^https?:\/\//.test(a)) return a;
|
|
19841
|
-
const slug = matchAvatarSlug(avatar);
|
|
19866
|
+
const slug = matchAvatarSlug(avatar, style);
|
|
19842
19867
|
return slug ? url(`collab/avatars/${slug}.png`) : void 0;
|
|
19843
19868
|
}
|
|
19869
|
+
// src/client/avatar-style.ts
|
|
19870
|
+
/**
|
|
19871
|
+
* Which built-in avatar set members without their own avatar show (#602). The value lives in the
|
|
19872
|
+
* bothouse config ([display] avatar_style), read through this DSH's panel forwarder
|
|
19873
|
+
* (dsh/collab/panel.mjs, `collab/panel/api/v1/settings`), so the channel page and the web panel
|
|
19874
|
+
* show the same set. No bothouse, not the owner, or any error: the default brand set.
|
|
19875
|
+
* The embedded panel announces a switch on the `bothouse-avatar-style` BroadcastChannel
|
|
19876
|
+
* (bridge/web/src/lib/avatarMap.ts), so the page follows without a reload.
|
|
19877
|
+
*/
|
|
19878
|
+
const AVATAR_STYLE_CHANNEL = "bothouse-avatar-style";
|
|
19879
|
+
const SETTINGS_PATH = "collab/panel/api/v1/settings";
|
|
19880
|
+
const targets = /* @__PURE__ */ new Map();
|
|
19881
|
+
function stateFor(url) {
|
|
19882
|
+
const path = url(SETTINGS_PATH);
|
|
19883
|
+
let state = targets.get(path);
|
|
19884
|
+
if (!state) {
|
|
19885
|
+
state = {
|
|
19886
|
+
current: DEFAULT_AVATAR_STYLE,
|
|
19887
|
+
started: false,
|
|
19888
|
+
listeners: /* @__PURE__ */ new Set()
|
|
19889
|
+
};
|
|
19890
|
+
targets.set(path, state);
|
|
19891
|
+
}
|
|
19892
|
+
return state;
|
|
19893
|
+
}
|
|
19894
|
+
function setAvatarStyle(style, url = localCollabUrl) {
|
|
19895
|
+
const state = stateFor(url);
|
|
19896
|
+
if (style === state.current) return;
|
|
19897
|
+
state.current = style;
|
|
19898
|
+
for (const fn of state.listeners) fn();
|
|
19899
|
+
}
|
|
19900
|
+
/** Reads the stored set once and listens for switches from the panel. */
|
|
19901
|
+
function startAvatarStyle(fetchImpl = globalThis.fetch, url = localCollabUrl) {
|
|
19902
|
+
const state = stateFor(url);
|
|
19903
|
+
if (state.started) return;
|
|
19904
|
+
state.started = true;
|
|
19905
|
+
const path = url(SETTINGS_PATH);
|
|
19906
|
+
const link = path.match(/(?:^|\/)collab\/remote\/([^/]+)\/panel\//)?.[1];
|
|
19907
|
+
try {
|
|
19908
|
+
const ch = new BroadcastChannel(link ? `${AVATAR_STYLE_CHANNEL}:${link}` : AVATAR_STYLE_CHANNEL);
|
|
19909
|
+
state.channel = ch;
|
|
19910
|
+
ch.onmessage = (e) => {
|
|
19911
|
+
setAvatarStyle(normalizeAvatarStyle(e.data), url);
|
|
19912
|
+
};
|
|
19913
|
+
} catch (error) {}
|
|
19914
|
+
if (typeof fetchImpl !== "function") return;
|
|
19915
|
+
fetchImpl(path, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : void 0).then((s) => {
|
|
19916
|
+
if (s?.data !== void 0) setAvatarStyle(normalizeAvatarStyle(s.data.avatar_style), url);
|
|
19917
|
+
}).catch((error) => {});
|
|
19918
|
+
}
|
|
19919
|
+
function useAvatarStyle() {
|
|
19920
|
+
const url = useCollabUrl();
|
|
19921
|
+
const state = stateFor(url);
|
|
19922
|
+
(0, react.useEffect)(() => {
|
|
19923
|
+
startAvatarStyle(globalThis.fetch, url);
|
|
19924
|
+
}, [url]);
|
|
19925
|
+
return (0, react.useSyncExternalStore)((fn) => {
|
|
19926
|
+
state.listeners.add(fn);
|
|
19927
|
+
return () => {
|
|
19928
|
+
state.listeners.delete(fn);
|
|
19929
|
+
};
|
|
19930
|
+
}, () => state.current, () => state.current);
|
|
19931
|
+
}
|
|
19844
19932
|
// src/client/Avatar.tsx
|
|
19845
19933
|
/** Member portrait: a built-in or bot-announced image over a colored initial badge. */
|
|
19846
19934
|
const PALETTE = [
|
|
@@ -19861,13 +19949,14 @@ function color(id) {
|
|
|
19861
19949
|
/** Member portrait, or a colored initial badge when there is none or the image fails to load. */
|
|
19862
19950
|
function Avatar({ id, name, avatar, avatarVersion, small, cli, model }) {
|
|
19863
19951
|
const url = useCollabUrl();
|
|
19952
|
+
const style = useAvatarStyle();
|
|
19864
19953
|
const base = avatarSrc({
|
|
19865
19954
|
avatar,
|
|
19866
19955
|
name,
|
|
19867
19956
|
cli,
|
|
19868
19957
|
model,
|
|
19869
19958
|
id
|
|
19870
|
-
}, url);
|
|
19959
|
+
}, url, style);
|
|
19871
19960
|
const src = base && avatar?.startsWith("upload:") && avatarVersion ? `${base}?v=${avatarVersion}` : base;
|
|
19872
19961
|
const [broken, setBroken] = (0, react.useState)(void 0);
|
|
19873
19962
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bothouse-dsh",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.66",
|
|
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",
|