feat(ui): show plugin version on brand hover

This commit is contained in:
xmanrui 2026-08-25 13:15:27 +08:00
parent 7211534aef
commit b42fc411c2
5 changed files with 173 additions and 7 deletions

View file

@ -35,6 +35,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
var index_exports = {};
__export(index_exports, {
IMSettingsTab: () => IMSettingsTab,
IM_PLUGIN_VERSION: () => IM_PLUGIN_VERSION,
LoopbackRecoveryNotice: () => LoopbackRecoveryNotice,
apply: () => apply,
inject: () => inject,
@ -43,6 +44,115 @@ __export(index_exports, {
module.exports = __toCommonJS(index_exports);
var React20 = __toESM(require("react"), 1);
// package.json
var package_default = {
name: "@xmanrui/dsh-im",
version: "2.4.0",
description: "\u628A\u4E5D\u79CD IM \u673A\u5668\u4EBA\u548C\u516C\u7F51 AI Office \u63A5\u5165\u672C\u673A DeepSeek Harness\u3002 Connect nine IM channels and a public AI Office to a local DeepSeek Harness.",
keywords: [
"deepseek-harness",
"dsh",
"dsh-plugin",
"ai-agent",
"im",
"instant-messaging",
"chatbot",
"feishu",
"lark",
"wechat",
"wecom",
"dingtalk",
"qq",
"slack",
"telegram",
"discord",
"whatsapp",
"ai-office"
],
author: {
name: "xmanrui",
url: "https://github.com/xmanrui"
},
contributors: [
{
name: "C3H3-AI",
url: "https://github.com/C3H3-AI"
}
],
license: "MIT",
type: "module",
repository: {
type: "git",
url: "git+https://github.com/xmanrui/dsh-im.git"
},
homepage: "https://github.com/xmanrui/dsh-im#readme",
bugs: "https://github.com/xmanrui/dsh-im/issues",
publishConfig: {
access: "public"
},
bin: {
"dsh-im": "bin/dsh-im.mjs"
},
main: "./lib/index.js",
exports: {
".": "./lib/index.js",
"./client": "./lib/client.js",
"./package.json": "./package.json"
},
files: [
"assets",
"bin",
"cordis.patch.yml",
"lib",
"plugin-src",
"scripts",
"src",
"LICENSE",
"README.md",
"README.en.md",
"THIRD_PARTY_NOTICES.md"
],
dsh: {
bundle: {
patch: "./cordis.patch.yml"
},
client: {
inject: [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-settings",
"@deepseek-ai/dsh-client-ui-slots",
"@deepseek-ai/dsh-client-locale"
],
platform: "web"
}
},
scripts: {
build: "node plugin-src/client/build.mjs && node plugin-src/host/build.mjs",
test: "node --test test/*.test.mjs test/channels/*/*.test.mjs",
check: "npm test && npm run build && node scripts/verify-package.mjs"
},
engines: {
node: ">=22.19"
},
dependencies: {
"@tencent-connect/qqbot-connector": "1.2.0",
"@tencent-connect/qqbot-nodejs": "1.0.4",
"@wecom/aibot-node-sdk": "1.0.7",
"dingtalk-stream": "2.1.4",
qrcode: "1.5.4"
},
devDependencies: {
"@larksuiteoapi/node-sdk": "1.73.0",
"@whiskeysockets/baileys": "7.0.0-rc14",
esbuild: "0.25.9",
"https-proxy-agent": "5.0.1",
react: "18.3.1",
"react-dom": "18.3.1",
"react-test-renderer": "18.3.1"
}
};
// plugin-src/client/channel-logos.js
var React = __toESM(require("react"), 1);
var h = React.createElement;
@ -199,6 +309,7 @@ var EN = Object.freeze({
"IM\u673A\u5668\u4EBA\u8BBE\u7F6E": "IM bot settings",
"IM \u6E20\u9053": "IM channels",
"\u8BA9 DeepSeek Harness \u89E6\u624B\u53EF\u53CA": "DeepSeek Harness, always within reach",
"\u5F53\u524D\u7248\u672C": "Current version",
"AI Office": "AI Office",
"\uFF08\u5B9E\u9A8C\u529F\u80FD\uFF09": "(Experimental)",
"AI Office \u8BBE\u7F6E": "AI Office settings",
@ -10131,9 +10242,13 @@ var CSS11 = String.raw`
}
.dim-page *, .dim-page *::before, .dim-page *::after { box-sizing: border-box; }
.dim-title { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 18px; }
.dim-brand { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
.dim-brand { position: relative; min-width: 0; width: max-content; max-width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; margin: -2px -6px; padding: 2px 6px; border-radius: 8px; cursor: help; }
.dim-brand:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
.dim-brandName { color: var(--dsw-alias-label-primary, #1f2329); font-size: 20px; line-height: 24px; font-weight: 800; letter-spacing: .04em; }
.dim-title p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; font-weight: 500; white-space: nowrap; }
.dim-versionTooltip { position: absolute; left: 0; bottom: calc(100% + 8px); z-index: 20; width: max-content; max-width: min(220px, 80vw); display: inline-flex; align-items: center; gap: 6px; padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
.dim-versionTooltip strong { color: var(--dsw-alias-label-primary, #1f2329); font: 600 11px/16px ui-monospace, SFMono-Regular, Menlo, monospace; }
.dim-brand:hover .dim-versionTooltip, .dim-brand:focus .dim-versionTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
.dim-githubAction { position: relative; display: inline-flex; flex: none; }
.dim-githubLink { min-height: 30px; display: inline-flex; align-items: center; gap: 5px; flex: none; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); font-size: 12px; line-height: normal; font-weight: 560; text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
.dim-githubLink:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }
@ -10425,6 +10540,7 @@ function installImStyles() {
// plugin-src/client/index.js
var name = "im-settings";
var inject = ["slots", "connection", "locale", "workspaces"];
var IM_PLUGIN_VERSION = package_default.version;
var CHANNELS = Object.freeze([
{ id: "weixin", label: "\u5FAE\u4FE1" },
{ id: "feishu", label: "\u98DE\u4E66" },
@ -10549,6 +10665,7 @@ function IMSettingsTab({
}) {
const [selected, setSelected] = React20.useState("weixin");
const [loopbackRecovery, setLoopbackRecovery] = React20.useState(null);
const versionTooltipId = React20.useId();
const githubTooltipId = React20.useId();
const active = CHANNELS.find((channel4) => channel4.id === selected) ?? CHANNELS[0];
const reportLoopbackRecovery = React20.useCallback((recovery) => {
@ -10593,9 +10710,23 @@ function IMSettingsTab({
{ className: "dim-title" },
h2(
"div",
{ className: "dim-brand" },
{
className: "dim-brand",
tabIndex: 0,
"aria-describedby": versionTooltipId
},
h2("strong", { className: "dim-brandName" }, "DSH-IM"),
h2("p", null, "\u8BA9 DeepSeek Harness \u89E6\u624B\u53EF\u53CA")
h2("p", null, "\u8BA9 DeepSeek Harness \u89E6\u624B\u53EF\u53CA"),
h2(
"span",
{
id: versionTooltipId,
className: "dim-versionTooltip",
role: "tooltip"
},
h2("span", null, "\u5F53\u524D\u7248\u672C"),
h2("strong", null, `v${IM_PLUGIN_VERSION}`)
)
),
h2(
"span",

View file

@ -8,6 +8,7 @@ const EN = Object.freeze({
'IM机器人设置': 'IM bot settings',
'IM 渠道': 'IM channels',
'让 DeepSeek Harness 触手可及': 'DeepSeek Harness, always within reach',
'当前版本': 'Current version',
'AI Office': 'AI Office',
'(实验功能)': '(Experimental)',
'AI Office 设置': 'AI Office settings',

View file

@ -1,4 +1,5 @@
import * as React from 'react';
import manifest from '../../package.json' with { type: 'json' };
import {
DingtalkLogoGlyph,
@ -51,6 +52,7 @@ import { WorkspaceDirectoryPickerContext } from './workspace-editor.js';
export const name = 'im-settings';
export const inject = ['slots', 'connection', 'locale', 'workspaces'];
export const IM_PLUGIN_VERSION = manifest.version;
const CHANNELS = Object.freeze([
{ id: 'weixin', label: '微信' },
@ -159,6 +161,7 @@ export function IMSettingsTab({
}) {
const [selected, setSelected] = React.useState('weixin');
const [loopbackRecovery, setLoopbackRecovery] = React.useState(null);
const versionTooltipId = React.useId();
const githubTooltipId = React.useId();
const active = CHANNELS.find((channel) => channel.id === selected) ?? CHANNELS[0];
const reportLoopbackRecovery = React.useCallback((recovery) => {
@ -195,9 +198,20 @@ export function IMSettingsTab({
return h(WorkspaceDirectoryPickerContext.Provider, { value: workspaceDirectoryPicker },
h('section', { className: 'dim-page', 'aria-label': 'IM机器人设置' },
h('header', { className: 'dim-title' },
h('div', { className: 'dim-brand' },
h('div', {
className: 'dim-brand',
tabIndex: 0,
'aria-describedby': versionTooltipId,
},
h('strong', { className: 'dim-brandName' }, 'DSH-IM'),
h('p', null, '让 DeepSeek Harness 触手可及')),
h('p', null, '让 DeepSeek Harness 触手可及'),
h('span', {
id: versionTooltipId,
className: 'dim-versionTooltip',
role: 'tooltip',
},
h('span', null, '当前版本'),
h('strong', null, `v${IM_PLUGIN_VERSION}`))),
h('span', { className: 'dim-githubAction' },
h('a', {
className: 'dim-githubLink',

View file

@ -12,9 +12,13 @@ const CSS = String.raw`
}
.dim-page *, .dim-page *::before, .dim-page *::after { box-sizing: border-box; }
.dim-title { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 0 0 18px; }
.dim-brand { min-width: 0; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; }
.dim-brand { position: relative; min-width: 0; width: max-content; max-width: 100%; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; margin: -2px -6px; padding: 2px 6px; border-radius: 8px; cursor: help; }
.dim-brand:focus-visible { outline: 2px solid color-mix(in srgb, var(--dim-blue) 70%, white); outline-offset: 2px; }
.dim-brandName { color: var(--dsw-alias-label-primary, #1f2329); font-size: 20px; line-height: 24px; font-weight: 800; letter-spacing: .04em; }
.dim-title p { margin: 0; color: var(--dsw-alias-label-secondary, #646a73); font-size: 12px; line-height: 18px; font-weight: 500; white-space: nowrap; }
.dim-versionTooltip { position: absolute; left: 0; bottom: calc(100% + 8px); z-index: 20; width: max-content; max-width: min(220px, 80vw); display: inline-flex; align-items: center; gap: 6px; padding: 6px 9px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 7px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-3, #fff); box-shadow: 0 8px 24px rgb(31 35 41 / 14%); font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateY(3px); pointer-events: none; transition: opacity .15s ease, transform .15s ease, visibility .15s ease; }
.dim-versionTooltip strong { color: var(--dsw-alias-label-primary, #1f2329); font: 600 11px/16px ui-monospace, SFMono-Regular, Menlo, monospace; }
.dim-brand:hover .dim-versionTooltip, .dim-brand:focus .dim-versionTooltip { opacity: 1; visibility: visible; transform: translateY(0); }
.dim-githubAction { position: relative; display: inline-flex; flex: none; }
.dim-githubLink { min-height: 30px; display: inline-flex; align-items: center; gap: 5px; flex: none; padding: 0 10px; border: 1px solid var(--dsw-alias-border-l2, #dfe1e5); border-radius: 8px; color: var(--dsw-alias-label-secondary, #646a73); background: var(--dsw-alias-bg-layer-1, #fff); font-size: 12px; line-height: normal; font-weight: 560; text-decoration: none; transition: border-color .15s ease, color .15s ease, background .15s ease; }
.dim-githubLink:hover { border-color: #aeb3bb; color: var(--dsw-alias-label-primary, #1f2329); background: var(--dsw-alias-interactive-bg-hover, #f7f8fa); }

View file

@ -7,6 +7,7 @@ import { renderToStaticMarkup } from 'react-dom/server';
import {
apply as applyClient,
IM_PLUGIN_VERSION,
IMSettingsTab,
inject as clientInject,
} from '../plugin-src/client/index.js';
@ -98,6 +99,10 @@ const QQ_SOURCE_URL = new URL(
);
test('IM settings renders nine IM channels plus the AI Office connector', async () => {
const { default: packageMetadata } = await import('../package.json', {
with: { type: 'json' },
});
const { version: packageVersion } = packageMetadata;
const styles = await readFile(STYLES_URL, 'utf8');
const markup = renderToStaticMarkup(React.createElement(IMSettingsTab, {
feishuRpcCall: async () => ({ ok: true, value: {} }),
@ -115,7 +120,12 @@ test('IM settings renders nine IM channels plus the AI Office connector', async
assert.match(markup, /IM机器人/);
assert.match(markup, /让 DeepSeek Harness 触手可及/);
assert.match(markup, /class="dim-brand"/);
assert.equal(IM_PLUGIN_VERSION, packageVersion);
assert.match(markup, /class="dim-brand" tabindex="0" aria-describedby="[^"]+"/);
assert.match(markup, /<strong class="dim-brandName">DSH-IM<\/strong>/);
assert.match(markup, new RegExp(
`class="dim-versionTooltip" role="tooltip"><span>当前版本<\\/span><strong>v${packageVersion.replaceAll('.', '\\.')}`,
));
assert.doesNotMatch(markup, /dim-brandLogo|<img/);
assert.match(markup, /href="https:\/\/github\.com\/xmanrui\/dsh-im"/);
assert.match(markup, /target="_blank"/);
@ -125,8 +135,11 @@ test('IM settings renders nine IM channels plus the AI Office connector', async
assert.match(markup, /role="tooltip"[^>]*>帮助与反馈 · 前往 GitHub</);
assert.match(styles, /\.dim-title \{[^}]*margin: 0 0 18px;/);
assert.match(styles, /\.dim-title p \{[^}]*color: var\(--dsw-alias-label-secondary, #646a73\);[^}]*font-size: 12px;[^}]*font-weight: 500;/);
assert.match(styles, /\.dim-brand \{[^}]*display: flex;[^}]*flex-direction: column;[^}]*align-items: flex-start;[^}]*gap: 1px;/);
assert.match(styles, /\.dim-brand \{[^}]*position: relative;[^}]*display: flex;[^}]*flex-direction: column;[^}]*align-items: flex-start;[^}]*gap: 1px;[^}]*cursor: help;/);
assert.doesNotMatch(styles, /\.dim-brand:hover\s*\{[^}]*background:/);
assert.match(styles, /\.dim-brandName \{[^}]*font-size: 20px;[^}]*font-weight: 800;[^}]*letter-spacing: \.04em;/);
assert.match(styles, /\.dim-versionTooltip \{[^}]*bottom: calc\(100% \+ 8px\);[^}]*display: inline-flex;[^}]*opacity: 0;[^}]*visibility: hidden;/);
assert.match(styles, /\.dim-brand:hover \.dim-versionTooltip, \.dim-brand:focus \.dim-versionTooltip \{[^}]*opacity: 1;[^}]*visibility: visible;/);
assert.doesNotMatch(styles, /\.dim-brandLogo/);
assert.match(styles, /\.dim-githubLink \{[^}]*border: 1px solid var\(--dsw-alias-border-l2, #dfe1e5\);[^}]*text-decoration: none;/);
assert.match(styles, /\.dim-githubTooltip \{[^}]*bottom: calc\(100% \+ 8px\);[^}]*transform: translateY\(3px\);/);
@ -724,6 +737,9 @@ test('client registers a live bilingual locale seat and directory picker for the
injected,
));
assert.match(markup, /DeepSeek Harness, always within reach/);
assert.match(markup, new RegExp(
`<span>Current version<\\/span><strong>v${IM_PLUGIN_VERSION.replaceAll('.', '\\.')}`,
));
assert.match(markup, /Help &amp; feedback · Open GitHub/);
assert.match(markup, />WeChat<|>Feishu<|>DingTalk<|>WeCom</);
assert.match(markup, />QQ<[^]*>Slack<[^]*>Telegram<[^]*>Discord<[^]*>WhatsApp</);