From 3c25d8b8a44a1c5f37ef315b70192641ba1b080f Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 17:02:39 +0800 Subject: [PATCH 01/23] feat(desktop): bundle pinned SkillHub DSH plugin --- dsh-plugin-desktop/THIRD_PARTY_NOTICES.md | 5 +++++ dsh-plugin-desktop/package.json | 2 +- .../scripts/prepare-workdsh-runtime.mjs | 19 ++++++++++++++++++- .../verify-product-plugin-inventory.mjs | 4 ++++ 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/dsh-plugin-desktop/THIRD_PARTY_NOTICES.md b/dsh-plugin-desktop/THIRD_PARTY_NOTICES.md index 8c7d8e0d46..d9a67b28f7 100644 --- a/dsh-plugin-desktop/THIRD_PARTY_NOTICES.md +++ b/dsh-plugin-desktop/THIRD_PARTY_NOTICES.md @@ -11,5 +11,10 @@ third-party notices are maintained by the upstream project: WorkDSH bundles and their dependencies retain their own license terms, which must be checked from the exact release artifacts used for an installer. +The SkillHub and DSH plugin catalogue is provided by +[`@cocofhu/skillhub`](https://www.npmjs.com/package/@cocofhu/skillhub), +pinned at 0.2.16 in the Desktop Profile. It is a third-party project licensed +under MIT; its source and license are at . + This file intentionally does not freeze a dependency inventory from an older DSH release. Check the bundled Profile and its license files when publishing. diff --git a/dsh-plugin-desktop/package.json b/dsh-plugin-desktop/package.json index 279e9d2cea..dab06232bf 100644 --- a/dsh-plugin-desktop/package.json +++ b/dsh-plugin-desktop/package.json @@ -1,6 +1,6 @@ { "name": "dsh-plugin-desktop", - "version": "2.0.5", + "version": "2.0.6", "description": "WorkDSH Electron carrier for a pinned DeepSeek Harness runtime Profile", "license": "MIT", "publishConfig": { diff --git a/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs b/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs index 07fcb9f381..43136c7c4a 100644 --- a/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs +++ b/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs @@ -15,7 +15,9 @@ const destination = join(output, 'profiles', 'workdsh') const packageCache = join(output, 'package-cache') const cli = join(destination, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js') const releaseMarker = '.workdsh-desktop-release.json' -const profileLayout = 'five-product-plugins-v1' +const profileLayout = 'five-product-plugins-skillhub-v1' +const SKILLHUB_PACKAGE = '@cocofhu/skillhub' +const SKILLHUB_VERSION = '0.2.16' const productPackages = new Set(PRODUCT_PACKAGES) function run(command, args, options = {}) { @@ -183,6 +185,14 @@ async function installReleasedProfile(output) { // layer, while only the five product bundles are directly manageable. run(process.execPath, [pnpmCli, '--dir', destination, 'install', '--lockfile-only', '--offline']) run(process.execPath, [pnpmCli, '--dir', destination, 'install', '--frozen-lockfile', '--offline']) + // Keep the third-party SkillHub integration in the same DSH Profile. It + // supplies SkillHub search and a DSH plugin catalogue without a second host. + run(process.execPath, [pnpmCli, '--dir', destination, 'add', '--save-exact', `${SKILLHUB_PACKAGE}@${SKILLHUB_VERSION}`]) + const installedSkillHub = JSON.parse(readFileSync(join(destination, 'node_modules', SKILLHUB_PACKAGE, 'package.json'), 'utf8')) + if (installedSkillHub.version !== SKILLHUB_VERSION) throw new Error('Pinned SkillHub plugin version is missing') + const profileWithSkillHub = JSON.parse(readFileSync(profilePath, 'utf8')) + profileWithSkillHub.dsh.profile.bundles = [...new Set([...profileWithSkillHub.dsh.profile.bundles, SKILLHUB_PACKAGE])] + writeFileSync(profilePath, JSON.stringify(profileWithSkillHub, null, 2) + '\n') const config = spawnSync(process.execPath, [cli, '--profile', 'workdsh', '--dump-config'], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, env: { ...process.env, DSH_HOME: output }, }) @@ -191,6 +201,7 @@ async function installReleasedProfile(output) { for (const id of ['workdsh-installation-probe', 'workdsh-identity-local', 'workdsh-access', 'workdsh-audit', 'workdsh-office']) { if (!config.stdout.includes(`id: ${id}`)) throw new Error(`Internal WorkDSH service is missing: ${id}`) } + if (!config.stdout.includes('id: skillhub')) throw new Error('SkillHub plugin is missing from the WorkDSH Profile') run(process.execPath, [join(desktopRoot, 'scripts', 'verify-product-plugin-inventory.mjs'), output], { env: { ...process.env, DSH_HOME: output }, }) @@ -219,6 +230,10 @@ const internalPatch = profile => { const isPreparedProfile = candidate => { if (installedDshVersion(candidate) !== DSH_VERSION) return false if (!releasePackages.every(name => existsSync(join(candidate, 'node_modules', name, 'package.json')))) return false + try { + const skillHub = JSON.parse(readFileSync(join(candidate, 'node_modules', SKILLHUB_PACKAGE, 'package.json'), 'utf8')) + if (skillHub.version !== SKILLHUB_VERSION) return false + } catch { return false } try { const marker = JSON.parse(readFileSync(join(candidate, releaseMarker), 'utf8')) if (marker.release !== WORKDSH_VERSION || marker.harness !== DSH_VERSION || marker.layout !== profileLayout) return false @@ -240,6 +255,8 @@ const isPreparedProfile = candidate => { const manifest = JSON.parse(readFileSync(resolve(candidate, '..', '..', 'package-cache', 'release-manifest.json'), 'utf8')) const selected = profile.dsh?.profile?.bundles ?? [] return [...productPackages].every(name => selected.includes(name)) && + selected.includes(SKILLHUB_PACKAGE) && + profile.dependencies?.[SKILLHUB_PACKAGE] === SKILLHUB_VERSION && supportPackages.every(name => !selected.includes(name)) && supportPackages.every(name => !Object.hasOwn(profile.dependencies ?? {}, name)) && readFileSync(join(candidate, 'cordis.patch.yml'), 'utf8').startsWith(internalPatch(candidate)) && diff --git a/dsh-plugin-desktop/scripts/verify-product-plugin-inventory.mjs b/dsh-plugin-desktop/scripts/verify-product-plugin-inventory.mjs index b78577b0f9..80e43adf71 100644 --- a/dsh-plugin-desktop/scripts/verify-product-plugin-inventory.mjs +++ b/dsh-plugin-desktop/scripts/verify-product-plugin-inventory.mjs @@ -38,6 +38,10 @@ try { throw new Error(`WorkDSH product bundle is inactive or invalid: ${bundle.name}: ${JSON.stringify(bundle)}`) } } + const skillHub = (await ctx.pluginManager.listBundles()).find(row => row.name === '@cocofhu/skillhub') + if (!skillHub?.enabled || !skillHub.installed || skillHub.error) { + throw new Error(`SkillHub DSH plugin is inactive or invalid: ${JSON.stringify(skillHub)}`) + } console.log(`Verified plugin manager exposes exactly five WorkDSH product bundles: ${names.join(', ')}`) } finally { try { From f07e8882da807d6d114c6b5ebf89488e5d8e04f8 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 17:20:35 +0800 Subject: [PATCH 02/23] fix(skills): isolate WorkDSH skill roots and show real categories --- dsh-plugin-desktop/src/workdsh-main.ts | 1 + workdsh-web/package.json | 2 +- workdsh-web/packages/bundle/package.json | 2 +- .../packages/plugins/skills/package.json | 2 +- .../skills/src/client/SkillHubPanel.tsx | 70 +++++++++++++++++++ .../plugins/skills/src/client/SkillsPanel.tsx | 18 +++-- .../plugins/skills/src/client/styles.ts | 6 ++ .../plugins/skills/src/services/manager.ts | 2 +- workdsh-web/scripts/build-skill-catalog.mjs | 4 +- workdsh-web/scripts/install-preview.mjs | 2 +- workdsh-web/scripts/start-preview.mjs | 5 +- .../tests/integration/skill-manager.test.mjs | 26 +++++++ 12 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx diff --git a/dsh-plugin-desktop/src/workdsh-main.ts b/dsh-plugin-desktop/src/workdsh-main.ts index a5f8fa7766..bd110ff368 100644 --- a/dsh-plugin-desktop/src/workdsh-main.ts +++ b/dsh-plugin-desktop/src/workdsh-main.ts @@ -169,6 +169,7 @@ function startRuntime(home: string, profileDir: string): void { env: { ...process.env, DSH_HOME: home, + DSH_AGENTS_HOME: join(home, 'agents'), DSH_BUNDLED_PRIMARY_RUNTIME: bundledPrimaryRuntime(), DSH_ELECTRON_EXECUTABLE: process.execPath, ELECTRON_RUN_AS_NODE: undefined, diff --git a/workdsh-web/package.json b/workdsh-web/package.json index ac3712e51a..ee5ede2530 100644 --- a/workdsh-web/package.json +++ b/workdsh-web/package.json @@ -1,6 +1,6 @@ { "name": "workdsh", - "version": "0.1.0-alpha.13", + "version": "0.1.0-alpha.14", "private": true, "type": "module", "description": "WorkDSH planning and plugin workspace", diff --git a/workdsh-web/packages/bundle/package.json b/workdsh-web/packages/bundle/package.json index ddf40f6413..69e20aa0b4 100644 --- a/workdsh-web/packages/bundle/package.json +++ b/workdsh-web/packages/bundle/package.json @@ -1,6 +1,6 @@ { "name": "workdsh-bundle", - "version": "0.1.0-alpha.52", + "version": "0.1.0-alpha.53", "private": true, "type": "module", "description": "WorkDSH default Harness bundle and client composition", diff --git a/workdsh-web/packages/plugins/skills/package.json b/workdsh-web/packages/plugins/skills/package.json index 00b515bb32..7efb0b2e39 100644 --- a/workdsh-web/packages/plugins/skills/package.json +++ b/workdsh-web/packages/plugins/skills/package.json @@ -1,6 +1,6 @@ { "name": "workdsh-plugin-skills", - "version": "0.1.0-alpha.33", + "version": "0.1.0-alpha.34", "private": false, "type": "module", "exports": { diff --git a/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx new file mode 100644 index 0000000000..1736ac5c29 --- /dev/null +++ b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { Button } from 'workdsh-ui'; + +type SkillHubCard = { + slug: string; + name: string; + description: string; + categoryLabel?: string; + version?: string; + owner?: string; + installed?: boolean; +}; + +type SearchResponse = { ok: boolean; items?: SkillHubCard[]; total?: number; error?: string }; + +async function skillHub(method: string, payload: Record, signal?: AbortSignal): Promise { + const response = await fetch(new URL('./skillhub', document.baseURI), { + method: 'POST', credentials: 'same-origin', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ method, ...payload }), signal, + }); + const result = await response.json() as T; + if (!response.ok || !result.ok) throw new Error(result.error || `SkillHub 请求失败 (${response.status})`); + return result; +} + +/** Reuse the installed DSH plugin's search and verified installation path. */ +export function SkillHubPanel({ query, onInstalled }: { query: string; onInstalled: () => Promise }) { + const [cards, setCards] = useState([]); + const [total, setTotal] = useState(0); + const [busy, setBusy] = useState(true); + const [error, setError] = useState(''); + const [installing, setInstalling] = useState(''); + const [revision, setRevision] = useState(0); + + useEffect(() => { + const controller = new AbortController(); + const timer = window.setTimeout(() => { + setBusy(true); setError(''); + void skillHub('search', { query: query.trim(), limit: 24 }, controller.signal) + .then(result => { setCards([...new Map((result.items ?? []).map(item => [item.slug, item])).values()]); setTotal(result.total ?? 0); }) + .catch(cause => { if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : 'SkillHub 暂不可用'); }) + .finally(() => { if (!controller.signal.aborted) setBusy(false); }); + }, 250); + return () => { window.clearTimeout(timer); controller.abort(); }; + }, [query, revision]); + + const install = async (card: SkillHubCard) => { + setInstalling(card.slug); setError(''); + try { + await skillHub('install', { slug: card.slug }); + await onInstalled(); + setRevision(value => value + 1); + } catch (cause) { setError(cause instanceof Error ? cause.message : '安装失败,请重试。'); } + finally { setInstalling(''); } + }; + + return
+

SkillHub {total}

来源:SkillHub · 安装到本机 DSH 技能目录
+ {error &&

{error}

} + {busy ?

正在读取 SkillHub…

: cards.length ?
{cards.map(card => + )}
:

没有找到匹配的技能。

} +
; +} diff --git a/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx b/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx index 9f1fb3c2cd..1271d8959d 100644 --- a/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx +++ b/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx @@ -7,6 +7,7 @@ import type { ManagedSkillDetail, ManagedSkillResource, ManagedSkillSummary, Ski import type { SkillTaskKind } from './drafts.js'; import type { SkillManagementClient } from './management.js'; import { ImportSkillModal } from './ImportSkillModal.js'; +import { SkillHubPanel } from './SkillHubPanel.js'; import { skillsActionsCss, skillsCss, skillsMarketCss } from './styles.js'; type SkillsPanelInjected = { @@ -65,6 +66,7 @@ export function SkillsPanel({ toggleNavigation, management, startSkillTask, star const [selectedNames, setSelectedNames] = useState([]); const [confirmBatchUninstall, setConfirmBatchUninstall] = useState(false); const [view, setView] = useState<'market' | 'installed'>('market'); + const [marketSource, setMarketSource] = useState<'local' | 'skillhub'>('local'); const [installedQuery, setInstalledQuery] = useState(''); const search = useRef(null); const addMenu = useRef(null); @@ -205,12 +207,15 @@ export function SkillsPanel({ toggleNavigation, management, startSkillTask, star }; const entries = catalog?.entries ?? []; - const categories = catalog?.categories ?? []; const normalized = query.trim().toLowerCase(); const matches = (text: string) => text.toLowerCase().includes(normalized); - const inCategory = (values?: readonly string[]) => category === ALL || Boolean(values?.includes(category)); - const available = entries.filter(entry => !entry.installed && inCategory(entry.categories) && (matches(entry.name) || matches(entry.title) || matches(entry.description))); - const filtered = skills.filter(skill => inCategory(skill.categories) && matches(`${skill.name} ${skill.title ?? ''} ${skill.localizedDescription ?? skill.description} ${skill.whenToUse ?? ''}`)); + const matchingEntries = entries.filter(entry => !entry.installed && (matches(entry.name) || matches(entry.title) || matches(entry.description))); + const matchingSkills = skills.filter(skill => matches(`${skill.name} ${skill.title ?? ''} ${skill.localizedDescription ?? skill.description} ${skill.whenToUse ?? ''}`)); + const categories = [...new Set([...matchingEntries, ...matchingSkills].flatMap(item => item.categories ?? []))].sort((a, b) => a.localeCompare(b, 'zh-CN')); + const selectedCategory = categories.includes(category) ? category : ALL; + const inCategory = (values?: readonly string[]) => selectedCategory === ALL || Boolean(values?.includes(selectedCategory)); + const available = matchingEntries.filter(entry => inCategory(entry.categories)); + const filtered = matchingSkills.filter(skill => inCategory(skill.categories)); const installedFiltered = skills.filter(skill => `${skill.name} ${skill.title ?? ''} ${skill.localizedDescription ?? skill.description} ${skill.whenToUse ?? ''}`.toLowerCase().includes(installedQuery.trim().toLowerCase())); const capabilityTabs = [['experts', '专家'], ['skills', '技能'], ['connectors', '连接器']] as const; const capabilityKey: Record = { experts: 'workdsh-experts', skills: 'workdsh-skills', connectors: 'workdsh-connectors' }; @@ -258,7 +263,9 @@ export function SkillsPanel({ toggleNavigation, management, startSkillTask, star
{addMenuOpen &&
}

技能市场

- + + {marketSource === 'skillhub' ? : <> + {catalog?.status === 'invalid' &&

技能目录暂时不可用,已安装的技能仍可使用。

} {batchMode && batchBar} {countsLine} @@ -274,6 +281,7 @@ export function SkillsPanel({ toggleNavigation, management, startSkillTask, star
{filtered.map(renderSkillCard)}
: null} } + } setPreview(undefined)}> {preview &&

{preview.title}

{preview.name}

diff --git a/workdsh-web/packages/plugins/skills/src/client/styles.ts b/workdsh-web/packages/plugins/skills/src/client/styles.ts index 55a208026e..7753cd6481 100644 --- a/workdsh-web/packages/plugins/skills/src/client/styles.ts +++ b/workdsh-web/packages/plugins/skills/src/client/styles.ts @@ -105,6 +105,12 @@ ${controlsCss} export const skillsMarketCss = `${modalCss} @layer workdsh-business { +.wd-skills .skill-source-tabs{display:flex;gap:6px;margin:0 0 20px;border-bottom:1px solid var(--dsw-alias-border-l2)} +.wd-skills .skill-source-tabs button{padding:10px 16px;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit} +.wd-skills .skill-source-tabs button.active{border-bottom-color:var(--dsw-alias-interactive-primary);color:var(--dsw-alias-label-primary);font-weight:600} +.wd-skills .skillhub-market .card-open{text-decoration:none;color:inherit} +.wd-skills .skillhub-meta{display:block;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px} + .wd-skills .skill-icon{width:46px;flex:none;object-fit:cover} diff --git a/workdsh-web/packages/plugins/skills/src/services/manager.ts b/workdsh-web/packages/plugins/skills/src/services/manager.ts index 1d7b9de44b..3ede6f222c 100644 --- a/workdsh-web/packages/plugins/skills/src/services/manager.ts +++ b/workdsh-web/packages/plugins/skills/src/services/manager.ts @@ -162,7 +162,7 @@ export class SkillManager extends Service implements SkillManagementService { constructor(ctx: Context) { super(ctx, 'workdshSkills'); const dshHome = resolve(process.env.DSH_HOME ?? join(homedir(), '.dsh')); - const agentsHome = resolve(process.env.DSH_AGENTS_HOME ?? join(homedir(), '.agents')); + const agentsHome = resolve(process.env.DSH_AGENTS_HOME ?? join(dshHome, 'agents')); this.activeRoots = [join(agentsHome, 'skills'), join(dshHome, 'skills')]; this.disabledRoot = join(agentsHome, '.workdsh-disabled', 'skills'); this.trashRoot = join(agentsHome, '.workdsh-trash', 'skills'); diff --git a/workdsh-web/scripts/build-skill-catalog.mjs b/workdsh-web/scripts/build-skill-catalog.mjs index e9b1fcbe00..ee6625db7d 100644 --- a/workdsh-web/scripts/build-skill-catalog.mjs +++ b/workdsh-web/scripts/build-skill-catalog.mjs @@ -10,7 +10,7 @@ // // Usage: // node scripts/build-skill-catalog.mjs --source /path/to/skills-marketplace -// node scripts/build-skill-catalog.mjs --source ... --target ~/.agents/.workdsh-catalog --dry-run +// node scripts/build-skill-catalog.mjs --source ... --target ~/.dsh/agents/.workdsh-catalog --dry-run import { createHash } from 'node:crypto'; import { copyFile, lstat, mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'; import { homedir } from 'node:os'; @@ -47,7 +47,7 @@ function parseArguments(argv) { if (!options.source) throw new Error('缺少 --source:请指向包含 .codebuddy-skill/marketplace.json、skills/ 与 icons/ 的镜像目录。'); options.source = resolve(options.source.replace(/^~(?=\/|$)/, homedir())); if (!options.target) { - const agentsHome = resolve(process.env.DSH_AGENTS_HOME ?? join(homedir(), '.agents')); + const agentsHome = resolve(process.env.DSH_AGENTS_HOME ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'agents')); options.target = join(agentsHome, '.workdsh-catalog'); } options.target = resolve(options.target.replace(/^~(?=\/|$)/, homedir())); diff --git a/workdsh-web/scripts/install-preview.mjs b/workdsh-web/scripts/install-preview.mjs index 868a534d62..ce68afe03c 100644 --- a/workdsh-web/scripts/install-preview.mjs +++ b/workdsh-web/scripts/install-preview.mjs @@ -23,7 +23,7 @@ const artifacts = join(root, '.artifacts'); // pnpm includes local archive paths in its store index filename. Keep the // immutable archive path short even when this checkout is a nested worktree. const previewPacks = resolve(process.env.WORKDSH_PREVIEW_PACKS_HOME ?? join(homedir(), '.cache/workdsh-preview-packs')); -const env = { ...process.env, DSH_HOME: home, PATH: `${join(root, 'node_modules/.bin')}:${dirname(process.execPath)}:${process.env.PATH}` }; +const env = { ...process.env, DSH_HOME: home, DSH_AGENTS_HOME: process.env.DSH_AGENTS_HOME ?? join(home, 'agents'), PATH: `${join(root, 'node_modules/.bin')}:${dirname(process.execPath)}:${process.env.PATH}` }; const exec = promisify(execFile); const run = async (tool, args) => { await exec(process.execPath, [join(root, 'node_modules', tool), ...args], { cwd: root, env, timeout: 600_000, maxBuffer: 8 * 1024 * 1024 }); diff --git a/workdsh-web/scripts/start-preview.mjs b/workdsh-web/scripts/start-preview.mjs index 2b17d431b2..753be38907 100644 --- a/workdsh-web/scripts/start-preview.mjs +++ b/workdsh-web/scripts/start-preview.mjs @@ -1,14 +1,13 @@ import { spawn } from 'node:child_process'; import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; import { createRequire } from 'node:module'; -import { homedir } from 'node:os'; -import { dirname, resolve } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { withoutRedundantAgentTeamProfile } from './preview-agent-team.mjs'; const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const previewHome = process.env.WORKDSH_PREVIEW_HOME ?? resolve(root, '.test-runtime/preview'); -const agentsHome = process.env.DSH_AGENTS_HOME ?? resolve(homedir(), '.agents'); +const agentsHome = process.env.DSH_AGENTS_HOME ?? join(previewHome, 'agents'); const port = process.env.WORKDSH_PREVIEW_PORT ?? '18989'; // Preview currently needs a larger startup heap; this does not fix the underlying growth. const heapMb = process.env.WORKDSH_PREVIEW_HEAP_MB ?? '8192'; diff --git a/workdsh-web/tests/integration/skill-manager.test.mjs b/workdsh-web/tests/integration/skill-manager.test.mjs index 0f44152845..62f6408b98 100644 --- a/workdsh-web/tests/integration/skill-manager.test.mjs +++ b/workdsh-web/tests/integration/skill-manager.test.mjs @@ -12,6 +12,32 @@ import { SkillManager } from '../../packages/plugins/skills/dist/index.js'; const skillPackageRequire = createRequire(new URL('../../packages/plugins/skills/package.json', import.meta.url)); const { zipSync } = skillPackageRequire('fflate'); +test('default managed skills stay inside the WorkDSH home', async () => { + const root = await mkdtemp(join(tmpdir(), 'workdsh-skill-private-home-')); + const dshHome = join(root, 'dsh'); + const agentsHome = join(dshHome, 'agents'); + const originalAgentsHome = process.env.DSH_AGENTS_HOME; + const originalDshHome = process.env.DSH_HOME; + const ctx = new Context(); + try { + process.env.DSH_HOME = dshHome; + delete process.env.DSH_AGENTS_HOME; + await mkdir(join(agentsHome, 'skills', 'private-skill'), { recursive: true }); + await writeFile(join(agentsHome, 'skills', 'private-skill', 'SKILL.md'), '---\nname: private-skill\ndescription: Private WorkDSH skill\n---\n'); + await ctx.plugin(SkillRegistry); + await ctx.plugin(filesystem, { dshHome, agentsHome, watch: false }); + new SkillManager(ctx); + const skills = await ctx.workdshSkills.list(); + assert.equal(skills.find(skill => skill.name === 'private-skill')?.manageable, true); + assert.equal((await readdir(join(root))).includes('.agents'), false); + } finally { + await ctx.fiber.dispose(); + if (originalAgentsHome === undefined) delete process.env.DSH_AGENTS_HOME; else process.env.DSH_AGENTS_HOME = originalAgentsHome; + if (originalDshHome === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = originalDshHome; + await rm(root, { recursive: true, force: true }); + } +}); + test('canonical skill paths remain manageable through a home alias without selecting a shadowed local copy', async () => { const root = await mkdtemp(join(tmpdir(), 'workdsh-skill-canonical-')); const physicalHome = join(root, 'physical-agents'); From 70f17290bc141c0d308e58f4b04a9a09f7ba4d13 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 17:29:09 +0800 Subject: [PATCH 03/23] feat(skills): paginate SkillHub and render catalog icons --- .../skills/src/client/SkillHubPanel.tsx | 24 +++++++++++++++---- .../plugins/skills/src/client/styles.ts | 14 ++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx index 1736ac5c29..38dca79d6c 100644 --- a/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx +++ b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx @@ -9,10 +9,18 @@ type SkillHubCard = { categoryLabel?: string; version?: string; owner?: string; + iconUrl?: string; + pageUrl?: string; installed?: boolean; }; type SearchResponse = { ok: boolean; items?: SkillHubCard[]; total?: number; error?: string }; +const PAGE_SIZE = 24; + +function SkillHubIcon({ card }: { card: SkillHubCard }) { + const iconUrl = card.iconUrl && /^https:\/\//i.test(card.iconUrl) ? card.iconUrl : undefined; + return ; +} async function skillHub(method: string, payload: Record, signal?: AbortSignal): Promise { const response = await fetch(new URL('./skillhub', document.baseURI), { @@ -33,18 +41,21 @@ export function SkillHubPanel({ query, onInstalled }: { query: string; onInstall const [error, setError] = useState(''); const [installing, setInstalling] = useState(''); const [revision, setRevision] = useState(0); + const [page, setPage] = useState(0); + + useEffect(() => { setPage(0); }, [query]); useEffect(() => { const controller = new AbortController(); const timer = window.setTimeout(() => { setBusy(true); setError(''); - void skillHub('search', { query: query.trim(), limit: 24 }, controller.signal) + void skillHub('search', { query: query.trim(), limit: PAGE_SIZE, offset: page * PAGE_SIZE }, controller.signal) .then(result => { setCards([...new Map((result.items ?? []).map(item => [item.slug, item])).values()]); setTotal(result.total ?? 0); }) .catch(cause => { if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : 'SkillHub 暂不可用'); }) .finally(() => { if (!controller.signal.aborted) setBusy(false); }); }, 250); return () => { window.clearTimeout(timer); controller.abort(); }; - }, [query, revision]); + }, [query, page, revision]); const install = async (card: SkillHubCard) => { setInstalling(card.slug); setError(''); @@ -56,15 +67,20 @@ export function SkillHubPanel({ query, onInstalled }: { query: string; onInstall finally { setInstalling(''); } }; + const pageCount = Math.ceil(total / PAGE_SIZE); + const firstPage = Math.max(0, Math.min(page - 2, pageCount - 5)); + const pageNumbers = Array.from({ length: Math.min(5, pageCount) }, (_, index) => firstPage + index); + return

SkillHub {total}

来源:SkillHub · 安装到本机 DSH 技能目录
{error &&

{error}

} {busy ?

正在读取 SkillHub…

: cards.length ?
{cards.map(card => )}
:

没有找到匹配的技能。

} +
)} : !error &&

没有找到匹配的技能。

} + {!busy && pageCount > 1 && } ; } diff --git a/workdsh-web/packages/plugins/skills/src/client/styles.ts b/workdsh-web/packages/plugins/skills/src/client/styles.ts index 7753cd6481..0af6df4b8d 100644 --- a/workdsh-web/packages/plugins/skills/src/client/styles.ts +++ b/workdsh-web/packages/plugins/skills/src/client/styles.ts @@ -105,11 +105,19 @@ ${controlsCss} export const skillsMarketCss = `${modalCss} @layer workdsh-business { -.wd-skills .skill-source-tabs{display:flex;gap:6px;margin:0 0 20px;border-bottom:1px solid var(--dsw-alias-border-l2)} -.wd-skills .skill-source-tabs button{padding:10px 16px;border:0;border-bottom:2px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit} -.wd-skills .skill-source-tabs button.active{border-bottom-color:var(--dsw-alias-interactive-primary);color:var(--dsw-alias-label-primary);font-weight:600} +.wd-skills .section-head:has(+ .skill-source-tabs){margin-bottom:0;border-bottom:0} +.wd-skills .skill-source-tabs{display:flex;gap:24px;margin:0 0 20px;border-bottom:1px solid var(--dsw-alias-border-l2)} +.wd-skills .skill-source-tabs button{padding:10px 2px;border:0;border-bottom:2px solid transparent;border-radius:0;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font:inherit} +.wd-skills .skill-source-tabs button:hover:not(:disabled){background:transparent;color:var(--dsw-alias-label-primary)} +.wd-skills .skill-source-tabs button.active{border-bottom-color:var(--dsw-alias-brand-primary);background:transparent;color:var(--dsw-alias-label-primary);font-weight:600} .wd-skills .skillhub-market .card-open{text-decoration:none;color:inherit} +.wd-skills .skillhub-icon{position:relative;display:grid;place-items:center;width:46px;height:46px;flex:none;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-brand-text);font-weight:700;font-size:19px} +.wd-skills .skillhub-icon img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:var(--dsw-alias-bg-layer-2)} +.wd-skills .skillhub-icon img[hidden]{display:none} .wd-skills .skillhub-meta{display:block;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px} +.wd-skills .skillhub-pagination{display:flex;justify-content:center;align-items:center;gap:8px;flex-wrap:wrap;margin:28px 0 12px} +.wd-skills .skillhub-pagination button{min-width:36px;background:transparent} +.wd-skills .skillhub-pagination button.active{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);font-weight:600} .wd-skills .skill-icon{width:46px;flex:none;object-fit:cover} From 303bf3773dc6eb0bbe9fdfd67a08712ec520bf5c Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 17:35:10 +0800 Subject: [PATCH 04/23] fix(skills): keep SkillHub context after installation --- .../plugins/skills/src/client/SkillHubPanel.tsx | 14 ++++++++------ .../plugins/skills/src/client/SkillsPanel.tsx | 2 +- .../packages/plugins/skills/src/client/styles.ts | 5 +++++ 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx index 38dca79d6c..dbe904f86e 100644 --- a/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx +++ b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx @@ -34,13 +34,13 @@ async function skillHub(method: strin } /** Reuse the installed DSH plugin's search and verified installation path. */ -export function SkillHubPanel({ query, onInstalled }: { query: string; onInstalled: () => Promise }) { +export function SkillHubPanel({ query, onInstalled, onOpenInstalled }: { query: string; onInstalled: () => Promise; onOpenInstalled: () => void }) { const [cards, setCards] = useState([]); const [total, setTotal] = useState(0); const [busy, setBusy] = useState(true); const [error, setError] = useState(''); const [installing, setInstalling] = useState(''); - const [revision, setRevision] = useState(0); + const [installedName, setInstalledName] = useState(''); const [page, setPage] = useState(0); useEffect(() => { setPage(0); }, [query]); @@ -55,14 +55,15 @@ export function SkillHubPanel({ query, onInstalled }: { query: string; onInstall .finally(() => { if (!controller.signal.aborted) setBusy(false); }); }, 250); return () => { window.clearTimeout(timer); controller.abort(); }; - }, [query, page, revision]); + }, [query, page]); const install = async (card: SkillHubCard) => { setInstalling(card.slug); setError(''); try { await skillHub('install', { slug: card.slug }); - await onInstalled(); - setRevision(value => value + 1); + setCards(current => current.map(item => item.slug === card.slug ? { ...item, installed: true } : item)); + setInstalledName(card.name); + void onInstalled(); } catch (cause) { setError(cause instanceof Error ? cause.message : '安装失败,请重试。'); } finally { setInstalling(''); } }; @@ -73,11 +74,12 @@ export function SkillHubPanel({ query, onInstalled }: { query: string; onInstall return

SkillHub {total}

来源:SkillHub · 安装到本机 DSH 技能目录
+ {installedName &&
已安装「{installedName}」。
} {error &&

{error}

} {busy ?

正在读取 SkillHub…

: cards.length ?
{cards.map(card =>

{card.description}

版本 {card.version || '未标注'} · 许可证请查看来源页 )} : !error &&

没有找到匹配的技能。

} diff --git a/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx b/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx index 1271d8959d..e67e9d4120 100644 --- a/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx +++ b/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx @@ -264,7 +264,7 @@ export function SkillsPanel({ toggleNavigation, management, startSkillTask, star

技能市场

- {marketSource === 'skillhub' ? : <> + {marketSource === 'skillhub' ? : <> {catalog?.status === 'invalid' &&

技能目录暂时不可用,已安装的技能仍可使用。

} {batchMode && batchBar} diff --git a/workdsh-web/packages/plugins/skills/src/client/styles.ts b/workdsh-web/packages/plugins/skills/src/client/styles.ts index 0af6df4b8d..cd535d1bce 100644 --- a/workdsh-web/packages/plugins/skills/src/client/styles.ts +++ b/workdsh-web/packages/plugins/skills/src/client/styles.ts @@ -115,6 +115,11 @@ export const skillsMarketCss = `${modalCss} .wd-skills .skillhub-icon img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:var(--dsw-alias-bg-layer-2)} .wd-skills .skillhub-icon img[hidden]{display:none} .wd-skills .skillhub-meta{display:block;margin-top:8px;color:var(--dsw-alias-label-tertiary);font-size:11px} +.wd-skills .skillhub-market .install.is-status{width:auto;min-width:58px;padding:0 8px;border-radius:8px;font-size:12px} +.wd-skills .skillhub-market .install.is-installed:disabled{color:var(--dsw-alias-state-success-primary);opacity:1} +.wd-skills .skillhub-success{display:flex;align-items:center;gap:12px;margin:0 0 18px;padding:10px 14px;border:1px solid var(--dsw-alias-state-success-primary);border-radius:10px;background:var(--dsw-alias-bg-layer-2);font-size:13px} +.wd-skills .skillhub-success span{flex:1} +.wd-skills .skillhub-success .skillhub-dismiss{border:0;background:transparent;font-size:18px} .wd-skills .skillhub-pagination{display:flex;justify-content:center;align-items:center;gap:8px;flex-wrap:wrap;margin:28px 0 12px} .wd-skills .skillhub-pagination button{min-width:36px;background:transparent} .wd-skills .skillhub-pagination button.active{border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-text);font-weight:600} From 911015592c1904c2556f1864cb71698c935d6e7e Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 17:39:10 +0800 Subject: [PATCH 05/23] fix(skills): normalize SkillHub skill names after installation --- workdsh-web/packages/contracts/src/skills.ts | 3 ++- .../skills/src/client/SkillHubPanel.tsx | 4 ++- .../plugins/skills/src/client/SkillsPanel.tsx | 2 +- .../plugins/skills/src/client/management.ts | 1 + .../skills/src/services/connection-api.ts | 2 ++ .../plugins/skills/src/services/manager.ts | 27 +++++++++++++++++++ .../tests/integration/skill-manager.test.mjs | 27 +++++++++++++++++++ 7 files changed, 63 insertions(+), 3 deletions(-) diff --git a/workdsh-web/packages/contracts/src/skills.ts b/workdsh-web/packages/contracts/src/skills.ts index bee1c9d098..c29870666a 100644 --- a/workdsh-web/packages/contracts/src/skills.ts +++ b/workdsh-web/packages/contracts/src/skills.ts @@ -161,7 +161,7 @@ export interface StagedSkillImport { readonly expiresAt: string; } -export type SkillManagementEndpoint = 'list' | 'detail' | 'update' | 'resource' | 'write-resource' | 'set-enabled' | 'dependency-impact' | 'uninstall' | 'batch' | 'trash-list' | 'restore' | 'commit-import' | 'discard-import' | 'catalog' | 'install-catalog'; +export type SkillManagementEndpoint = 'list' | 'detail' | 'update' | 'resource' | 'write-resource' | 'set-enabled' | 'dependency-impact' | 'uninstall' | 'batch' | 'trash-list' | 'restore' | 'commit-import' | 'discard-import' | 'catalog' | 'install-catalog' | 'normalize-skillhub'; export interface SkillManagementFailure { readonly code: string; readonly message: string; } export type SkillManagementResult = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: SkillManagementFailure }; @@ -179,6 +179,7 @@ export interface SkillManagementService extends SkillRevisionProvider { detail(name: string, signal?: AbortSignal): Promise; readResource(name: string, path: string): Promise; update(request: SkillWriteRequest): Promise; + normalizeSkillHub(name: string): Promise; writeResource(request: SkillResourceWriteRequest): Promise; validateDocument(document: string, expectedName?: string): SkillValidationResult; saveDraft(request: SkillDraftWriteRequest): Promise; diff --git a/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx index dbe904f86e..ff0cf57146 100644 --- a/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx +++ b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; import { Button } from 'workdsh-ui'; +import type { SkillManagementClient } from './management.js'; type SkillHubCard = { slug: string; @@ -34,7 +35,7 @@ async function skillHub(method: strin } /** Reuse the installed DSH plugin's search and verified installation path. */ -export function SkillHubPanel({ query, onInstalled, onOpenInstalled }: { query: string; onInstalled: () => Promise; onOpenInstalled: () => void }) { +export function SkillHubPanel({ query, management, onInstalled, onOpenInstalled }: { query: string; management: SkillManagementClient; onInstalled: () => Promise; onOpenInstalled: () => void }) { const [cards, setCards] = useState([]); const [total, setTotal] = useState(0); const [busy, setBusy] = useState(true); @@ -61,6 +62,7 @@ export function SkillHubPanel({ query, onInstalled, onOpenInstalled }: { query: setInstalling(card.slug); setError(''); try { await skillHub('install', { slug: card.slug }); + await management.normalizeSkillHub(card.slug); setCards(current => current.map(item => item.slug === card.slug ? { ...item, installed: true } : item)); setInstalledName(card.name); void onInstalled(); diff --git a/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx b/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx index e67e9d4120..6460c4e99e 100644 --- a/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx +++ b/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx @@ -264,7 +264,7 @@ export function SkillsPanel({ toggleNavigation, management, startSkillTask, star

技能市场

- {marketSource === 'skillhub' ? : <> + {marketSource === 'skillhub' ? : <> {catalog?.status === 'invalid' &&

技能目录暂时不可用,已安装的技能仍可使用。

} {batchMode && batchBar} diff --git a/workdsh-web/packages/plugins/skills/src/client/management.ts b/workdsh-web/packages/plugins/skills/src/client/management.ts index 5977d2e163..ae3e0432b9 100644 --- a/workdsh-web/packages/plugins/skills/src/client/management.ts +++ b/workdsh-web/packages/plugins/skills/src/client/management.ts @@ -49,6 +49,7 @@ export function createSkillManagementClient(ctx: Context, lifetime?: AbortSignal catalog: () => invoke('catalog', {}), installFromCatalog: (name: string) => invoke('install-catalog', { name }), detail: (name: string) => invoke('detail', { name }), + normalizeSkillHub: (name: string) => invoke('normalize-skillhub', { name }), update: (request: SkillWriteRequest) => invoke('update', request), resource: (name: string, resourcePath: string) => invoke('resource', { name, path: resourcePath }), writeResource: (request: SkillResourceWriteRequest) => invoke('write-resource', request), diff --git a/workdsh-web/packages/plugins/skills/src/services/connection-api.ts b/workdsh-web/packages/plugins/skills/src/services/connection-api.ts index 52d67486ce..ffb7e9bcbb 100644 --- a/workdsh-web/packages/plugins/skills/src/services/connection-api.ts +++ b/workdsh-web/packages/plugins/skills/src/services/connection-api.ts @@ -54,6 +54,7 @@ function publicFailure(error: unknown): ConnectionRpcResult { 'skill/catalog-entry-unknown': '本地技能目录中没有该技能。', 'skill/catalog-entry-over-limit': '该技能的体积或文件数超过导入上限,不能从目录安装。', 'skill/catalog-payload-missing': '本地技能目录缺少该技能的安装负载,请重新生成目录。', + 'skill/skillhub-invalid-document': 'SkillHub 技能文档还有其他格式错误,无法启用。', }; return fail(code, messages[code] ?? '技能操作失败,请重试。'); } @@ -91,6 +92,7 @@ async function dispatch(manager: SkillManagementService, rawEndpoint: unknown, p const detail = await manager.detail(name, signal); return detail ? ok(detail) : fail('skill/not-found', '未找到该技能。'); } + if (endpoint === 'normalize-skillhub') return ok(await manager.normalizeSkillHub(name)); if (endpoint === 'update') { const input = record(payload); if (typeof input?.document !== 'string' || input.document.length > 1024 * 1024 || typeof input.expectedRevision !== 'string') { diff --git a/workdsh-web/packages/plugins/skills/src/services/manager.ts b/workdsh-web/packages/plugins/skills/src/services/manager.ts index 3ede6f222c..3f74b5b920 100644 --- a/workdsh-web/packages/plugins/skills/src/services/manager.ts +++ b/workdsh-web/packages/plugins/skills/src/services/manager.ts @@ -289,6 +289,33 @@ export class SkillManager extends Service implements SkillManagementService { }); } + /** SkillHub installs by catalog slug, while some published SKILL.md files use another name. */ + async normalizeSkillHub(name: string): Promise { + this.assertName(name); + return this.withSkillLock(name, async () => { + const file = join(this.activeRoots[1], name, 'SKILL.md'); + const entry = await this.activeEntry(name); + if (!entry?.directoryBundle || entry.file !== file) throw new Error('skill/not-manageable'); + const original = await readFile(file, 'utf8'); + const validation = this.validateDocument(original, name); + if (!validation.valid) { + if (validation.diagnostics.some(diagnostic => diagnostic.code !== 'name-mismatch')) throw new Error('skill/skillhub-invalid-document'); + const header = original.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); + if (!header) throw new Error('skill/skillhub-invalid-document'); + const lines = header[1].split(/\r?\n/); + const index = lines.findIndex(line => /^name\s*:/.test(line)); + if (index < 0 || lines.filter(line => /^name\s*:/.test(line)).length !== 1) throw new Error('skill/skillhub-invalid-document'); + lines[index] = `name: ${name}`; + const normalized = original.replace(header[1], lines.join(header[0].includes('\r\n') ? '\r\n' : '\n')); + if (!this.validateDocument(normalized, name).valid) throw new Error('skill/skillhub-invalid-document'); + await this.atomicWrite(file, normalized); + } + const detail = await this.detail(name); + if (!detail || detail.state === 'invalid') throw new Error('skill/reload-failed'); + return detail; + }); + } + validateDocument(document: string, expectedName?: string): SkillValidationResult { const diagnostics: SkillDiagnostic[] = []; if (Buffer.byteLength(document) > maximumDocumentBytes) diagnostics.push({ code: 'document-too-large', message: 'SKILL.md 超过 1 MiB 上限。', path: 'SKILL.md' }); diff --git a/workdsh-web/tests/integration/skill-manager.test.mjs b/workdsh-web/tests/integration/skill-manager.test.mjs index 62f6408b98..07f0e2ad16 100644 --- a/workdsh-web/tests/integration/skill-manager.test.mjs +++ b/workdsh-web/tests/integration/skill-manager.test.mjs @@ -38,6 +38,33 @@ test('default managed skills stay inside the WorkDSH home', async () => { } }); +test('SkillHub slug installation normalizes only a mismatched frontmatter name', async () => { + const root = await mkdtemp(join(tmpdir(), 'workdsh-skillhub-normalize-')); + const dshHome = join(root, 'dsh'); + const originalDshHome = process.env.DSH_HOME; + const originalAgentsHome = process.env.DSH_AGENTS_HOME; + const ctx = new Context(); + try { + process.env.DSH_HOME = dshHome; + delete process.env.DSH_AGENTS_HOME; + const file = join(dshHome, 'skills', 'published-slug', 'SKILL.md'); + await mkdir(join(dshHome, 'skills', 'published-slug'), { recursive: true }); + await writeFile(file, '---\nname: original-name\ndescription: Published skill\n---\nInstructions\n'); + await ctx.plugin(SkillRegistry); + await ctx.plugin(filesystem, { dshHome, agentsHome: join(dshHome, 'agents'), watch: false }); + new SkillManager(ctx); + assert.equal((await ctx.workdshSkills.list()).find(row => row.name === 'published-slug')?.state, 'invalid'); + await ctx.workdshSkills.normalizeSkillHub('published-slug'); + assert.match(await readFile(file, 'utf8'), /^---\nname: published-slug\n/); + assert.equal((await ctx.workdshSkills.list()).find(row => row.name === 'published-slug')?.state, 'enabled'); + } finally { + await ctx.fiber.dispose(); + if (originalDshHome === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = originalDshHome; + if (originalAgentsHome === undefined) delete process.env.DSH_AGENTS_HOME; else process.env.DSH_AGENTS_HOME = originalAgentsHome; + await rm(root, { recursive: true, force: true }); + } +}); + test('canonical skill paths remain manageable through a home alias without selecting a shadowed local copy', async () => { const root = await mkdtemp(join(tmpdir(), 'workdsh-skill-canonical-')); const physicalHome = join(root, 'physical-agents'); From 8cde2c12405892600c36199ee63a6c344d6fe203 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 17:43:22 +0800 Subject: [PATCH 06/23] Revert "fix(skills): normalize SkillHub skill names after installation" This reverts commit 911015592c1904c2556f1864cb71698c935d6e7e. --- workdsh-web/packages/contracts/src/skills.ts | 3 +-- .../skills/src/client/SkillHubPanel.tsx | 4 +-- .../plugins/skills/src/client/SkillsPanel.tsx | 2 +- .../plugins/skills/src/client/management.ts | 1 - .../skills/src/services/connection-api.ts | 2 -- .../plugins/skills/src/services/manager.ts | 27 ------------------- .../tests/integration/skill-manager.test.mjs | 27 ------------------- 7 files changed, 3 insertions(+), 63 deletions(-) diff --git a/workdsh-web/packages/contracts/src/skills.ts b/workdsh-web/packages/contracts/src/skills.ts index c29870666a..bee1c9d098 100644 --- a/workdsh-web/packages/contracts/src/skills.ts +++ b/workdsh-web/packages/contracts/src/skills.ts @@ -161,7 +161,7 @@ export interface StagedSkillImport { readonly expiresAt: string; } -export type SkillManagementEndpoint = 'list' | 'detail' | 'update' | 'resource' | 'write-resource' | 'set-enabled' | 'dependency-impact' | 'uninstall' | 'batch' | 'trash-list' | 'restore' | 'commit-import' | 'discard-import' | 'catalog' | 'install-catalog' | 'normalize-skillhub'; +export type SkillManagementEndpoint = 'list' | 'detail' | 'update' | 'resource' | 'write-resource' | 'set-enabled' | 'dependency-impact' | 'uninstall' | 'batch' | 'trash-list' | 'restore' | 'commit-import' | 'discard-import' | 'catalog' | 'install-catalog'; export interface SkillManagementFailure { readonly code: string; readonly message: string; } export type SkillManagementResult = { readonly ok: true; readonly value: T } | { readonly ok: false; readonly error: SkillManagementFailure }; @@ -179,7 +179,6 @@ export interface SkillManagementService extends SkillRevisionProvider { detail(name: string, signal?: AbortSignal): Promise; readResource(name: string, path: string): Promise; update(request: SkillWriteRequest): Promise; - normalizeSkillHub(name: string): Promise; writeResource(request: SkillResourceWriteRequest): Promise; validateDocument(document: string, expectedName?: string): SkillValidationResult; saveDraft(request: SkillDraftWriteRequest): Promise; diff --git a/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx index ff0cf57146..dbe904f86e 100644 --- a/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx +++ b/workdsh-web/packages/plugins/skills/src/client/SkillHubPanel.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; import { Button } from 'workdsh-ui'; -import type { SkillManagementClient } from './management.js'; type SkillHubCard = { slug: string; @@ -35,7 +34,7 @@ async function skillHub(method: strin } /** Reuse the installed DSH plugin's search and verified installation path. */ -export function SkillHubPanel({ query, management, onInstalled, onOpenInstalled }: { query: string; management: SkillManagementClient; onInstalled: () => Promise; onOpenInstalled: () => void }) { +export function SkillHubPanel({ query, onInstalled, onOpenInstalled }: { query: string; onInstalled: () => Promise; onOpenInstalled: () => void }) { const [cards, setCards] = useState([]); const [total, setTotal] = useState(0); const [busy, setBusy] = useState(true); @@ -62,7 +61,6 @@ export function SkillHubPanel({ query, management, onInstalled, onOpenInstalled setInstalling(card.slug); setError(''); try { await skillHub('install', { slug: card.slug }); - await management.normalizeSkillHub(card.slug); setCards(current => current.map(item => item.slug === card.slug ? { ...item, installed: true } : item)); setInstalledName(card.name); void onInstalled(); diff --git a/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx b/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx index 6460c4e99e..e67e9d4120 100644 --- a/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx +++ b/workdsh-web/packages/plugins/skills/src/client/SkillsPanel.tsx @@ -264,7 +264,7 @@ export function SkillsPanel({ toggleNavigation, management, startSkillTask, star

技能市场

- {marketSource === 'skillhub' ? : <> + {marketSource === 'skillhub' ? : <> {catalog?.status === 'invalid' &&

技能目录暂时不可用,已安装的技能仍可使用。

} {batchMode && batchBar} diff --git a/workdsh-web/packages/plugins/skills/src/client/management.ts b/workdsh-web/packages/plugins/skills/src/client/management.ts index ae3e0432b9..5977d2e163 100644 --- a/workdsh-web/packages/plugins/skills/src/client/management.ts +++ b/workdsh-web/packages/plugins/skills/src/client/management.ts @@ -49,7 +49,6 @@ export function createSkillManagementClient(ctx: Context, lifetime?: AbortSignal catalog: () => invoke('catalog', {}), installFromCatalog: (name: string) => invoke('install-catalog', { name }), detail: (name: string) => invoke('detail', { name }), - normalizeSkillHub: (name: string) => invoke('normalize-skillhub', { name }), update: (request: SkillWriteRequest) => invoke('update', request), resource: (name: string, resourcePath: string) => invoke('resource', { name, path: resourcePath }), writeResource: (request: SkillResourceWriteRequest) => invoke('write-resource', request), diff --git a/workdsh-web/packages/plugins/skills/src/services/connection-api.ts b/workdsh-web/packages/plugins/skills/src/services/connection-api.ts index ffb7e9bcbb..52d67486ce 100644 --- a/workdsh-web/packages/plugins/skills/src/services/connection-api.ts +++ b/workdsh-web/packages/plugins/skills/src/services/connection-api.ts @@ -54,7 +54,6 @@ function publicFailure(error: unknown): ConnectionRpcResult { 'skill/catalog-entry-unknown': '本地技能目录中没有该技能。', 'skill/catalog-entry-over-limit': '该技能的体积或文件数超过导入上限,不能从目录安装。', 'skill/catalog-payload-missing': '本地技能目录缺少该技能的安装负载,请重新生成目录。', - 'skill/skillhub-invalid-document': 'SkillHub 技能文档还有其他格式错误,无法启用。', }; return fail(code, messages[code] ?? '技能操作失败,请重试。'); } @@ -92,7 +91,6 @@ async function dispatch(manager: SkillManagementService, rawEndpoint: unknown, p const detail = await manager.detail(name, signal); return detail ? ok(detail) : fail('skill/not-found', '未找到该技能。'); } - if (endpoint === 'normalize-skillhub') return ok(await manager.normalizeSkillHub(name)); if (endpoint === 'update') { const input = record(payload); if (typeof input?.document !== 'string' || input.document.length > 1024 * 1024 || typeof input.expectedRevision !== 'string') { diff --git a/workdsh-web/packages/plugins/skills/src/services/manager.ts b/workdsh-web/packages/plugins/skills/src/services/manager.ts index 3f74b5b920..3ede6f222c 100644 --- a/workdsh-web/packages/plugins/skills/src/services/manager.ts +++ b/workdsh-web/packages/plugins/skills/src/services/manager.ts @@ -289,33 +289,6 @@ export class SkillManager extends Service implements SkillManagementService { }); } - /** SkillHub installs by catalog slug, while some published SKILL.md files use another name. */ - async normalizeSkillHub(name: string): Promise { - this.assertName(name); - return this.withSkillLock(name, async () => { - const file = join(this.activeRoots[1], name, 'SKILL.md'); - const entry = await this.activeEntry(name); - if (!entry?.directoryBundle || entry.file !== file) throw new Error('skill/not-manageable'); - const original = await readFile(file, 'utf8'); - const validation = this.validateDocument(original, name); - if (!validation.valid) { - if (validation.diagnostics.some(diagnostic => diagnostic.code !== 'name-mismatch')) throw new Error('skill/skillhub-invalid-document'); - const header = original.match(/^---\s*\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/); - if (!header) throw new Error('skill/skillhub-invalid-document'); - const lines = header[1].split(/\r?\n/); - const index = lines.findIndex(line => /^name\s*:/.test(line)); - if (index < 0 || lines.filter(line => /^name\s*:/.test(line)).length !== 1) throw new Error('skill/skillhub-invalid-document'); - lines[index] = `name: ${name}`; - const normalized = original.replace(header[1], lines.join(header[0].includes('\r\n') ? '\r\n' : '\n')); - if (!this.validateDocument(normalized, name).valid) throw new Error('skill/skillhub-invalid-document'); - await this.atomicWrite(file, normalized); - } - const detail = await this.detail(name); - if (!detail || detail.state === 'invalid') throw new Error('skill/reload-failed'); - return detail; - }); - } - validateDocument(document: string, expectedName?: string): SkillValidationResult { const diagnostics: SkillDiagnostic[] = []; if (Buffer.byteLength(document) > maximumDocumentBytes) diagnostics.push({ code: 'document-too-large', message: 'SKILL.md 超过 1 MiB 上限。', path: 'SKILL.md' }); diff --git a/workdsh-web/tests/integration/skill-manager.test.mjs b/workdsh-web/tests/integration/skill-manager.test.mjs index 07f0e2ad16..62f6408b98 100644 --- a/workdsh-web/tests/integration/skill-manager.test.mjs +++ b/workdsh-web/tests/integration/skill-manager.test.mjs @@ -38,33 +38,6 @@ test('default managed skills stay inside the WorkDSH home', async () => { } }); -test('SkillHub slug installation normalizes only a mismatched frontmatter name', async () => { - const root = await mkdtemp(join(tmpdir(), 'workdsh-skillhub-normalize-')); - const dshHome = join(root, 'dsh'); - const originalDshHome = process.env.DSH_HOME; - const originalAgentsHome = process.env.DSH_AGENTS_HOME; - const ctx = new Context(); - try { - process.env.DSH_HOME = dshHome; - delete process.env.DSH_AGENTS_HOME; - const file = join(dshHome, 'skills', 'published-slug', 'SKILL.md'); - await mkdir(join(dshHome, 'skills', 'published-slug'), { recursive: true }); - await writeFile(file, '---\nname: original-name\ndescription: Published skill\n---\nInstructions\n'); - await ctx.plugin(SkillRegistry); - await ctx.plugin(filesystem, { dshHome, agentsHome: join(dshHome, 'agents'), watch: false }); - new SkillManager(ctx); - assert.equal((await ctx.workdshSkills.list()).find(row => row.name === 'published-slug')?.state, 'invalid'); - await ctx.workdshSkills.normalizeSkillHub('published-slug'); - assert.match(await readFile(file, 'utf8'), /^---\nname: published-slug\n/); - assert.equal((await ctx.workdshSkills.list()).find(row => row.name === 'published-slug')?.state, 'enabled'); - } finally { - await ctx.fiber.dispose(); - if (originalDshHome === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = originalDshHome; - if (originalAgentsHome === undefined) delete process.env.DSH_AGENTS_HOME; else process.env.DSH_AGENTS_HOME = originalAgentsHome; - await rm(root, { recursive: true, force: true }); - } -}); - test('canonical skill paths remain manageable through a home alias without selecting a shadowed local copy', async () => { const root = await mkdtemp(join(tmpdir(), 'workdsh-skill-canonical-')); const physicalHome = join(root, 'physical-agents'); From dd5d1ca0037826c51695a8a4c5a2ec66e2cd6295 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 17:48:19 +0800 Subject: [PATCH 07/23] fix(skills): manage SkillHub entries by official DSH skill name --- .../plugins/skills/src/services/manager.ts | 38 ++++++++++++++++--- .../tests/integration/skill-manager.test.mjs | 36 +++++++++++++++++- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/workdsh-web/packages/plugins/skills/src/services/manager.ts b/workdsh-web/packages/plugins/skills/src/services/manager.ts index 3ede6f222c..79e38de5c4 100644 --- a/workdsh-web/packages/plugins/skills/src/services/manager.ts +++ b/workdsh-web/packages/plugins/skills/src/services/manager.ts @@ -183,13 +183,14 @@ export class SkillManager extends Service implements SkillManagementService { signal?.throwIfAborted(); const skills = await this.ctx.skills.list({ signal }); const rows: ManagedSkillSummary[] = []; + const registeredFiles = new Set(); for (const skill of skills) { const manageable = this.isManagedSummary(skill); if (manageable) { const definition = await this.ctx.skills.get(skill.name, { signal }); const path = definition?.path; if (!path) continue; - try { if (!await this.isSafeManagedFile(path)) continue; } + try { if (!await this.isSafeManagedFile(path)) continue; registeredFiles.add(await realpath(path)); } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT' || (error as Error).message === 'skill/path-symlink') continue; throw error; } } rows.push({ name: skill.name, description: skill.description, whenToUse: skill.whenToUse, @@ -204,8 +205,9 @@ export class SkillManager extends Service implements SkillManagementService { try { active = await this.activeEntry(name); } catch (error) { if ((error as Error).message === 'skill/path-symlink') continue; throw error; } if (!active) continue; + if (registeredFiles.has(await realpath(active.file))) continue; const document = await readFile(active.file, 'utf8'); - const validation = this.validateDocument(document, name); + const validation = this.validateDocument(document); rows.push({ name, description: validation.description ?? '技能文件需要修复', @@ -246,7 +248,7 @@ export class SkillManager extends Service implements SkillManagementService { const active = await this.activeEntry(name); if (active) { const document = await readFile(active.file, 'utf8'); - const validation = this.validateDocument(document, name); + const validation = this.validateDocument(document); return { name, description: validation.description ?? '技能文件需要修复', @@ -298,7 +300,9 @@ export class SkillManager extends Service implements SkillManagementService { const name = typeof metadata?.name === 'string' ? metadata.name.trim() : undefined; const description = typeof metadata?.description === 'string' ? metadata.description.trim() : undefined; if (!name || !skillNamePattern.test(name) || !isSkillName(name)) diagnostics.push({ code: 'invalid-name', message: 'name 必须是合法的 kebab-case 技能名称。', path: 'SKILL.md' }); - if (expectedName && name && name !== expectedName) diagnostics.push({ code: 'name-mismatch', message: `frontmatter name 必须与技能目录 ${expectedName} 一致。`, path: 'SKILL.md' }); + // The official DSH loader identifies a skill by frontmatter name, not its directory. + // A marketplace slug may differ; only authoring flows enforce an expected name. + if (expectedName && name && name !== expectedName) diagnostics.push({ code: 'name-mismatch', message: `frontmatter name 必须是 ${expectedName}。`, path: 'SKILL.md' }); if (!description) diagnostics.push({ code: 'description-required', message: 'description 不能为空。', path: 'SKILL.md' }); const body = metadata ? document.replace(/^---\s*\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, '').trim() : ''; if (metadata && !body) diagnostics.push({ code: 'instructions-required', message: 'frontmatter 后必须包含可执行的技能说明。', path: 'SKILL.md' }); @@ -451,7 +455,7 @@ export class SkillManager extends Service implements SkillManagementService { if (!current?.directoryPath || (current.state !== 'enabled' && current.state !== 'invalid')) throw new Error('skill/not-manageable'); const active = await this.activeEntry(name); if (!active) throw new Error('skill/not-manageable'); - const target = join(this.disabledRoot, basename(active.entry)); + const target = join(this.disabledRoot, active.directoryBundle ? name : `${name}.md`); await this.assertAbsent(target); await mkdir(this.disabledRoot, { recursive: true }); await this.writeJson(this.originPath(name), { name, originalEntry: active.entry, directoryBundle: active.directoryBundle } satisfies DisabledOrigin); try { await rename(active.entry, target); } @@ -591,7 +595,7 @@ export class SkillManager extends Service implements SkillManagementService { modelInvocable: definition.invocation.modelInvocable, state: 'readonly', manageable: false, resources: [], }; if (!definition.path) return readonlyDetail; - const active = await this.activeEntry(definition.name); + const active = await this.managedEntryForPath(definition.path); if (!active) return readonlyDetail; // Harness exposes a canonical instruction path; configured roots may use // an OS alias (e.g. /var -> /private/var). Compare actual files only after @@ -649,6 +653,28 @@ export class SkillManager extends Service implements SkillManagementService { } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } } } + const definition = await this.ctx.skills.get(name); + if (definition?.path) return this.managedEntryForPath(definition.path); + return undefined; + } + + private async managedEntryForPath(path: string): Promise<{ entry: string; file: string; directoryBundle: boolean } | undefined> { + if (!await this.isSafeManagedFile(path)) return undefined; + const canonical = await realpath(path); + for (const root of this.activeRoots) { + let canonicalRoot: string; + try { canonicalRoot = await realpath(root); } + catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue; throw error; } + const part = relative(canonicalRoot, canonical); + const segments = part.split(sep); + const directoryBundle = segments.length === 2 && segments[1] === 'SKILL.md' && skillNamePattern.test(segments[0]); + const standalone = segments.length === 1 && segments[0].endsWith('.md') && skillNamePattern.test(segments[0].slice(0, -3)); + if (!directoryBundle && !standalone) continue; + const entry = join(root, segments[0]); + const file = directoryBundle ? join(entry, 'SKILL.md') : entry; + if ((await lstat(entry)).isSymbolicLink() || (await lstat(file)).isSymbolicLink()) throw new Error('skill/path-symlink'); + if (await realpath(file) === canonical) return { entry, file, directoryBundle }; + } return undefined; } diff --git a/workdsh-web/tests/integration/skill-manager.test.mjs b/workdsh-web/tests/integration/skill-manager.test.mjs index 62f6408b98..31c4e814f0 100644 --- a/workdsh-web/tests/integration/skill-manager.test.mjs +++ b/workdsh-web/tests/integration/skill-manager.test.mjs @@ -38,6 +38,38 @@ test('default managed skills stay inside the WorkDSH home', async () => { } }); +test('SkillHub directory slug may differ from the official DSH skill name', async () => { + const root = await mkdtemp(join(tmpdir(), 'workdsh-skillhub-name-')); + const dshHome = join(root, 'dsh'); const agentsHome = join(dshHome, 'agents'); + const originalDshHome = process.env.DSH_HOME; const originalAgentsHome = process.env.DSH_AGENTS_HOME; + const file = join(dshHome, 'skills', 'marketplace-slug', 'SKILL.md'); + const document = '---\nname: original-name\ndescription: Marketplace skill\n---\nInstructions\n'; + const ctx = new Context(); + try { + process.env.DSH_HOME = dshHome; delete process.env.DSH_AGENTS_HOME; + await mkdir(join(dshHome, 'skills', 'marketplace-slug'), { recursive: true }); + await writeFile(file, document); + await ctx.plugin(SkillRegistry); + await ctx.plugin(filesystem, { dshHome, agentsHome, watch: false }); + new SkillManager(ctx); + const rows = await ctx.workdshSkills.list(); + assert.equal(rows.some(row => row.name === 'marketplace-slug'), false); + assert.equal(rows.find(row => row.name === 'original-name')?.state, 'enabled'); + assert.equal((await ctx.workdshSkills.detail('original-name'))?.manageable, true); + assert.equal(await readFile(file, 'utf8'), document); + await ctx.workdshSkills.setEnabled('original-name', false); + assert.equal((await ctx.workdshSkills.list()).find(row => row.name === 'original-name')?.state, 'disabled'); + await ctx.workdshSkills.setEnabled('original-name', true); + assert.equal((await ctx.workdshSkills.list()).find(row => row.name === 'original-name')?.state, 'enabled'); + assert.equal(await readFile(file, 'utf8'), document); + } finally { + await ctx.fiber.dispose(); + if (originalDshHome === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = originalDshHome; + if (originalAgentsHome === undefined) delete process.env.DSH_AGENTS_HOME; else process.env.DSH_AGENTS_HOME = originalAgentsHome; + await rm(root, { recursive: true, force: true }); + } +}); + test('canonical skill paths remain manageable through a home alias without selecting a shadowed local copy', async () => { const root = await mkdtemp(join(tmpdir(), 'workdsh-skill-canonical-')); const physicalHome = join(root, 'physical-agents'); @@ -154,10 +186,10 @@ test('skill manager surfaces invalid local skills with actionable diagnostics', const summary = (await ctx.workdshSkills.list()).find(row => row.name === 'broken-skill'); assert.equal(summary.state, 'invalid'); assert.equal(summary.modelInvocable, false); - assert.deepEqual(summary.diagnostics.map(item => item.code), ['name-mismatch', 'description-required', 'instructions-required']); + assert.deepEqual(summary.diagnostics.map(item => item.code), ['description-required', 'instructions-required']); const detail = await ctx.workdshSkills.detail('broken-skill'); assert.equal(detail.state, 'invalid'); - assert.match(detail.diagnostics[0].message, /技能目录/); + assert.match(detail.diagnostics[0].message, /description/); const fixed = '---\nname: broken-skill\ndescription: Repaired skill\n---\nUse these repaired instructions.\n'; const repaired = await ctx.workdshSkills.update({ name: 'broken-skill', document: fixed, expectedRevision: detail.revision }); assert.equal(repaired.state, 'enabled'); From bc73b8d9977d971f852004652b86b710cc29da3f Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 17:52:44 +0800 Subject: [PATCH 08/23] fix(skills): align installed validation with upstream loader --- workdsh-web/packages/plugins/skills/src/services/manager.ts | 4 ++-- workdsh-web/tests/integration/skill-manager.test.mjs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/workdsh-web/packages/plugins/skills/src/services/manager.ts b/workdsh-web/packages/plugins/skills/src/services/manager.ts index 79e38de5c4..6677ad3aa5 100644 --- a/workdsh-web/packages/plugins/skills/src/services/manager.ts +++ b/workdsh-web/packages/plugins/skills/src/services/manager.ts @@ -293,7 +293,7 @@ export class SkillManager extends Service implements SkillManagementService { validateDocument(document: string, expectedName?: string): SkillValidationResult { const diagnostics: SkillDiagnostic[] = []; - if (Buffer.byteLength(document) > maximumDocumentBytes) diagnostics.push({ code: 'document-too-large', message: 'SKILL.md 超过 1 MiB 上限。', path: 'SKILL.md' }); + if (expectedName && Buffer.byteLength(document) > maximumDocumentBytes) diagnostics.push({ code: 'document-too-large', message: 'SKILL.md 超过 1 MiB 上限。', path: 'SKILL.md' }); let metadata: Record | undefined; try { metadata = frontmatter(document); } catch { diagnostics.push({ code: 'invalid-frontmatter', message: 'SKILL.md 必须以有效的 YAML frontmatter 开头。', path: 'SKILL.md' }); } @@ -305,7 +305,7 @@ export class SkillManager extends Service implements SkillManagementService { if (expectedName && name && name !== expectedName) diagnostics.push({ code: 'name-mismatch', message: `frontmatter name 必须是 ${expectedName}。`, path: 'SKILL.md' }); if (!description) diagnostics.push({ code: 'description-required', message: 'description 不能为空。', path: 'SKILL.md' }); const body = metadata ? document.replace(/^---\s*\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/, '').trim() : ''; - if (metadata && !body) diagnostics.push({ code: 'instructions-required', message: 'frontmatter 后必须包含可执行的技能说明。', path: 'SKILL.md' }); + if (expectedName && metadata && !body) diagnostics.push({ code: 'instructions-required', message: 'frontmatter 后必须包含可执行的技能说明。', path: 'SKILL.md' }); return { valid: diagnostics.length === 0, ...(name ? { name } : {}), ...(description ? { description } : {}), diagnostics }; } diff --git a/workdsh-web/tests/integration/skill-manager.test.mjs b/workdsh-web/tests/integration/skill-manager.test.mjs index 31c4e814f0..604b91d267 100644 --- a/workdsh-web/tests/integration/skill-manager.test.mjs +++ b/workdsh-web/tests/integration/skill-manager.test.mjs @@ -186,7 +186,7 @@ test('skill manager surfaces invalid local skills with actionable diagnostics', const summary = (await ctx.workdshSkills.list()).find(row => row.name === 'broken-skill'); assert.equal(summary.state, 'invalid'); assert.equal(summary.modelInvocable, false); - assert.deepEqual(summary.diagnostics.map(item => item.code), ['description-required', 'instructions-required']); + assert.deepEqual(summary.diagnostics.map(item => item.code), ['description-required']); const detail = await ctx.workdshSkills.detail('broken-skill'); assert.equal(detail.state, 'invalid'); assert.match(detail.diagnostics[0].message, /description/); From 2b47f874a167d82d70bf166a7c303210ec49b370 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 17:55:47 +0800 Subject: [PATCH 09/23] fix(skills): resolve local SkillHub entries by declared name --- .../plugins/skills/src/services/manager.ts | 18 +++++++++++- .../tests/integration/skill-manager.test.mjs | 28 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/workdsh-web/packages/plugins/skills/src/services/manager.ts b/workdsh-web/packages/plugins/skills/src/services/manager.ts index 6677ad3aa5..4d7ea07750 100644 --- a/workdsh-web/packages/plugins/skills/src/services/manager.ts +++ b/workdsh-web/packages/plugins/skills/src/services/manager.ts @@ -208,8 +208,10 @@ export class SkillManager extends Service implements SkillManagementService { if (registeredFiles.has(await realpath(active.file))) continue; const document = await readFile(active.file, 'utf8'); const validation = this.validateDocument(document); + const displayName = validation.valid ? validation.name ?? name : name; + if (rows.some(row => row.name === displayName)) continue; rows.push({ - name, + name: displayName, description: validation.description ?? '技能文件需要修复', whenToUse: frontmatterValue(document, 'when-to-use'), modelInvocable: validation.valid, @@ -655,6 +657,20 @@ export class SkillManager extends Service implements SkillManagementService { } const definition = await this.ctx.skills.get(name); if (definition?.path) return this.managedEntryForPath(definition.path); + // Web Profile loads filesystem skills in Agent compositions rather than the + // top-level registry. Resolve the installed skill's declared name locally. + for (const root of this.activeRoots) { + await mkdir(root, { recursive: true }); + for (const row of await readdir(root, { withFileTypes: true })) { + if (!row.isDirectory() || !skillNamePattern.test(row.name)) continue; + const entry = join(root, row.name); + const file = join(entry, 'SKILL.md'); + try { + if ((await lstat(entry)).isSymbolicLink() || (await lstat(file)).isSymbolicLink() || !await this.isSafeManagedFile(file)) continue; + if (frontmatterValue(await readFile(file, 'utf8'), 'name') === name) return { entry, file, directoryBundle: true }; + } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; } + } + } return undefined; } diff --git a/workdsh-web/tests/integration/skill-manager.test.mjs b/workdsh-web/tests/integration/skill-manager.test.mjs index 604b91d267..2592889072 100644 --- a/workdsh-web/tests/integration/skill-manager.test.mjs +++ b/workdsh-web/tests/integration/skill-manager.test.mjs @@ -70,6 +70,34 @@ test('SkillHub directory slug may differ from the official DSH skill name', asyn } }); +test('Web Profile lists SkillHub skills by declared name without a root filesystem provider', async () => { + const root = await mkdtemp(join(tmpdir(), 'workdsh-skillhub-web-name-')); + const dshHome = join(root, 'dsh'); + const originalDshHome = process.env.DSH_HOME; const originalAgentsHome = process.env.DSH_AGENTS_HOME; + const file = join(dshHome, 'skills', 'marketplace-slug', 'SKILL.md'); + const ctx = new Context(); + try { + process.env.DSH_HOME = dshHome; delete process.env.DSH_AGENTS_HOME; + await mkdir(join(dshHome, 'skills', 'marketplace-slug'), { recursive: true }); + await writeFile(file, '---\nname: declared-name\ndescription: SkillHub example\n---\nInstructions\n'); + await ctx.plugin(SkillRegistry); + new SkillManager(ctx); + const rows = await ctx.workdshSkills.list(); + assert.equal(rows.some(row => row.name === 'marketplace-slug'), false); + assert.equal(rows.find(row => row.name === 'declared-name')?.state, 'enabled'); + assert.equal((await ctx.workdshSkills.detail('declared-name'))?.manageable, true); + await ctx.workdshSkills.setEnabled('declared-name', false); + assert.equal((await ctx.workdshSkills.list()).find(row => row.name === 'declared-name')?.state, 'disabled'); + await ctx.workdshSkills.setEnabled('declared-name', true); + assert.equal((await ctx.workdshSkills.list()).find(row => row.name === 'declared-name')?.state, 'enabled'); + } finally { + await ctx.fiber.dispose(); + if (originalDshHome === undefined) delete process.env.DSH_HOME; else process.env.DSH_HOME = originalDshHome; + if (originalAgentsHome === undefined) delete process.env.DSH_AGENTS_HOME; else process.env.DSH_AGENTS_HOME = originalAgentsHome; + await rm(root, { recursive: true, force: true }); + } +}); + test('canonical skill paths remain manageable through a home alias without selecting a shadowed local copy', async () => { const root = await mkdtemp(join(tmpdir(), 'workdsh-skill-canonical-')); const physicalHome = join(root, 'physical-agents'); From e033c37f69f021f656c54ad4234943a07daf1dbd Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 18:07:55 +0800 Subject: [PATCH 10/23] fix: expose WorkDSH plugin descriptions to DSH manager --- workdsh-web/packages/bundle/locale/en.json | 6 ++++++ workdsh-web/packages/bundle/locale/zh.json | 6 ++++++ workdsh-web/packages/bundle/package.json | 9 ++++++--- workdsh-web/packages/plugins/access/locale/en.json | 6 ++++++ workdsh-web/packages/plugins/access/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/access/package.json | 10 +++++++--- workdsh-web/packages/plugins/activity/locale/en.json | 6 ++++++ workdsh-web/packages/plugins/activity/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/activity/package.json | 9 ++++++--- workdsh-web/packages/plugins/audit/locale/en.json | 6 ++++++ workdsh-web/packages/plugins/audit/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/audit/package.json | 10 +++++++--- .../packages/plugins/connectors/locale/en.json | 6 ++++++ .../packages/plugins/connectors/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/connectors/package.json | 9 ++++++--- workdsh-web/packages/plugins/experts/locale/en.json | 6 ++++++ workdsh-web/packages/plugins/experts/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/experts/package.json | 9 ++++++--- workdsh-web/packages/plugins/library/locale/en.json | 6 ++++++ workdsh-web/packages/plugins/library/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/library/package.json | 9 ++++++--- workdsh-web/packages/plugins/office/locale/en.json | 6 ++++++ workdsh-web/packages/plugins/office/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/office/package.json | 10 +++++++--- workdsh-web/packages/plugins/projects/locale/en.json | 6 ++++++ workdsh-web/packages/plugins/projects/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/projects/package.json | 9 ++++++--- workdsh-web/packages/plugins/skills/locale/en.json | 6 ++++++ workdsh-web/packages/plugins/skills/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/skills/package.json | 9 ++++++--- .../packages/plugins/workbench/locale/en.json | 6 ++++++ .../packages/plugins/workbench/locale/zh.json | 6 ++++++ workdsh-web/packages/plugins/workbench/package.json | 10 +++++++--- .../providers/browser-session/locale/en.json | 6 ++++++ .../providers/browser-session/locale/zh.json | 6 ++++++ .../packages/providers/browser-session/package.json | 12 +++++++++--- .../packages/providers/identity-local/locale/en.json | 6 ++++++ .../packages/providers/identity-local/locale/zh.json | 6 ++++++ .../packages/providers/identity-local/package.json | 10 +++++++--- 39 files changed, 242 insertions(+), 39 deletions(-) create mode 100644 workdsh-web/packages/bundle/locale/en.json create mode 100644 workdsh-web/packages/bundle/locale/zh.json create mode 100644 workdsh-web/packages/plugins/access/locale/en.json create mode 100644 workdsh-web/packages/plugins/access/locale/zh.json create mode 100644 workdsh-web/packages/plugins/activity/locale/en.json create mode 100644 workdsh-web/packages/plugins/activity/locale/zh.json create mode 100644 workdsh-web/packages/plugins/audit/locale/en.json create mode 100644 workdsh-web/packages/plugins/audit/locale/zh.json create mode 100644 workdsh-web/packages/plugins/connectors/locale/en.json create mode 100644 workdsh-web/packages/plugins/connectors/locale/zh.json create mode 100644 workdsh-web/packages/plugins/experts/locale/en.json create mode 100644 workdsh-web/packages/plugins/experts/locale/zh.json create mode 100644 workdsh-web/packages/plugins/library/locale/en.json create mode 100644 workdsh-web/packages/plugins/library/locale/zh.json create mode 100644 workdsh-web/packages/plugins/office/locale/en.json create mode 100644 workdsh-web/packages/plugins/office/locale/zh.json create mode 100644 workdsh-web/packages/plugins/projects/locale/en.json create mode 100644 workdsh-web/packages/plugins/projects/locale/zh.json create mode 100644 workdsh-web/packages/plugins/skills/locale/en.json create mode 100644 workdsh-web/packages/plugins/skills/locale/zh.json create mode 100644 workdsh-web/packages/plugins/workbench/locale/en.json create mode 100644 workdsh-web/packages/plugins/workbench/locale/zh.json create mode 100644 workdsh-web/packages/providers/browser-session/locale/en.json create mode 100644 workdsh-web/packages/providers/browser-session/locale/zh.json create mode 100644 workdsh-web/packages/providers/identity-local/locale/en.json create mode 100644 workdsh-web/packages/providers/identity-local/locale/zh.json diff --git a/workdsh-web/packages/bundle/locale/en.json b/workdsh-web/packages/bundle/locale/en.json new file mode 100644 index 0000000000..d7435586a6 --- /dev/null +++ b/workdsh-web/packages/bundle/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "WorkDSH application bundle", + "description": "Composes the WorkDSH workspace, capabilities and default DSH plugins." + } +} diff --git a/workdsh-web/packages/bundle/locale/zh.json b/workdsh-web/packages/bundle/locale/zh.json new file mode 100644 index 0000000000..5c3b82bb4f --- /dev/null +++ b/workdsh-web/packages/bundle/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "WorkDSH 应用组合包", + "description": "组合工作区、能力中心和默认 DSH 插件。" + } +} diff --git a/workdsh-web/packages/bundle/package.json b/workdsh-web/packages/bundle/package.json index 69e20aa0b4..520955aef8 100644 --- a/workdsh-web/packages/bundle/package.json +++ b/workdsh-web/packages/bundle/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-alpha.53", "private": true, "type": "module", - "description": "WorkDSH default Harness bundle and client composition", + "description": "Composes the WorkDSH workspace, capabilities and default DSH plugins.", "exports": { "./probe": { "types": "./dist/probe.d.ts", @@ -16,13 +16,16 @@ ".": { "types": "./dist/probe.d.ts", "default": "./dist/probe.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "cordis.patch.yml", "README.md", - "CHANGELOG.md" + "CHANGELOG.md", + "locale/*.json" ], "dsh": { "bundle": { diff --git a/workdsh-web/packages/plugins/access/locale/en.json b/workdsh-web/packages/plugins/access/locale/en.json new file mode 100644 index 0000000000..0b6c2aa03c --- /dev/null +++ b/workdsh-web/packages/plugins/access/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Resource access control", + "description": "Controls access to local resources, sessions and tools." + } +} diff --git a/workdsh-web/packages/plugins/access/locale/zh.json b/workdsh-web/packages/plugins/access/locale/zh.json new file mode 100644 index 0000000000..205a8cba74 --- /dev/null +++ b/workdsh-web/packages/plugins/access/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "资源访问控制", + "description": "管理本地资源、会话和工具的访问权限。" + } +} diff --git a/workdsh-web/packages/plugins/access/package.json b/workdsh-web/packages/plugins/access/package.json index af207e688c..8a4849bc2d 100644 --- a/workdsh-web/packages/plugins/access/package.json +++ b/workdsh-web/packages/plugins/access/package.json @@ -15,13 +15,16 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "cordis.patch.yml", "dist", "README.md", - "CHANGELOG.md" + "CHANGELOG.md", + "locale/*.json" ], "dsh": { "bundle": { @@ -50,5 +53,6 @@ "@deepseek-ai/dsh-session": "0.1.7-rc.2", "@deepseek-ai/dsh-tools": "0.1.7-rc.2", "workdsh-contracts": "workspace:*" - } + }, + "description": "Controls access to local resources, sessions and tools." } diff --git a/workdsh-web/packages/plugins/activity/locale/en.json b/workdsh-web/packages/plugins/activity/locale/en.json new file mode 100644 index 0000000000..09027a82f9 --- /dev/null +++ b/workdsh-web/packages/plugins/activity/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Session activity", + "description": "Shows session activity and agent collaboration status." + } +} diff --git a/workdsh-web/packages/plugins/activity/locale/zh.json b/workdsh-web/packages/plugins/activity/locale/zh.json new file mode 100644 index 0000000000..69a13fd7de --- /dev/null +++ b/workdsh-web/packages/plugins/activity/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "会话活动", + "description": "展示会话活动和智能体协作状态。" + } +} diff --git a/workdsh-web/packages/plugins/activity/package.json b/workdsh-web/packages/plugins/activity/package.json index 060da4b64e..264db11f0d 100644 --- a/workdsh-web/packages/plugins/activity/package.json +++ b/workdsh-web/packages/plugins/activity/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-alpha.6", "type": "module", "private": false, - "description": "Compact animated activity presentation for standard DSH sessions", + "description": "Shows session activity and agent collaboration status.", "exports": { ".": { "types": "./dist/index.d.ts", @@ -16,14 +16,17 @@ "./presentation": { "types": "./dist/activity.d.ts", "default": "./dist/presentation.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "README.md", "cordis.patch.yml", "DESIGN.md", - "CHANGELOG.md" + "CHANGELOG.md", + "locale/*.json" ], "scripts": { "build": "corepack pnpm --filter workdsh-contracts build && tsc -p tsconfig.json && node ../../../scripts/build-activity.mjs", diff --git a/workdsh-web/packages/plugins/audit/locale/en.json b/workdsh-web/packages/plugins/audit/locale/en.json new file mode 100644 index 0000000000..368cd94496 --- /dev/null +++ b/workdsh-web/packages/plugins/audit/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Audit log", + "description": "Records local management and execution events for audit." + } +} diff --git a/workdsh-web/packages/plugins/audit/locale/zh.json b/workdsh-web/packages/plugins/audit/locale/zh.json new file mode 100644 index 0000000000..e348e53f11 --- /dev/null +++ b/workdsh-web/packages/plugins/audit/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "审计记录", + "description": "记录本地管理与执行事件,供审计使用。" + } +} diff --git a/workdsh-web/packages/plugins/audit/package.json b/workdsh-web/packages/plugins/audit/package.json index a3fe7cb902..d95debff00 100644 --- a/workdsh-web/packages/plugins/audit/package.json +++ b/workdsh-web/packages/plugins/audit/package.json @@ -7,13 +7,16 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "cordis.patch.yml", "dist", "README.md", - "CHANGELOG.md" + "CHANGELOG.md", + "locale/*.json" ], "dsh": { "bundle": { @@ -36,5 +39,6 @@ "@deepseek-ai/dsh-storage-domain": "0.1.7-rc.2", "@types/node": "22.19.0", "workdsh-contracts": "workspace:*" - } + }, + "description": "Records local management and execution events for audit." } diff --git a/workdsh-web/packages/plugins/connectors/locale/en.json b/workdsh-web/packages/plugins/connectors/locale/en.json new file mode 100644 index 0000000000..92107cd814 --- /dev/null +++ b/workdsh-web/packages/plugins/connectors/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Connectors", + "description": "Manages MCP connectors and per-conversation tool selection." + } +} diff --git a/workdsh-web/packages/plugins/connectors/locale/zh.json b/workdsh-web/packages/plugins/connectors/locale/zh.json new file mode 100644 index 0000000000..1e27d0f401 --- /dev/null +++ b/workdsh-web/packages/plugins/connectors/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "连接器", + "description": "管理 MCP 连接器及每个对话可用的工具。" + } +} diff --git a/workdsh-web/packages/plugins/connectors/package.json b/workdsh-web/packages/plugins/connectors/package.json index f79239479b..f7688b1d44 100644 --- a/workdsh-web/packages/plugins/connectors/package.json +++ b/workdsh-web/packages/plugins/connectors/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-alpha.4", "private": false, "type": "module", - "description": "MCP connector management and a verified local example for WorkDSH", + "description": "Manages MCP connectors and per-conversation tool selection.", "exports": { ".": { "types": "./dist/index.d.ts", @@ -12,13 +12,16 @@ "./client": { "types": "./dist/client.d.ts", "default": "./dist/client.browser.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "README.md", "CHANGELOG.md", - "cordis.patch.yml" + "cordis.patch.yml", + "locale/*.json" ], "scripts": { "build": "corepack pnpm --filter workdsh-ui build && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node ../../../scripts/build-connectors.mjs", diff --git a/workdsh-web/packages/plugins/experts/locale/en.json b/workdsh-web/packages/plugins/experts/locale/en.json new file mode 100644 index 0000000000..f915ffc665 --- /dev/null +++ b/workdsh-web/packages/plugins/experts/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Experts", + "description": "Manages reusable experts and expert teams in WorkDSH." + } +} diff --git a/workdsh-web/packages/plugins/experts/locale/zh.json b/workdsh-web/packages/plugins/experts/locale/zh.json new file mode 100644 index 0000000000..5f06d6f0b3 --- /dev/null +++ b/workdsh-web/packages/plugins/experts/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "专家", + "description": "管理 WorkDSH 中可复用的专家与专家团。" + } +} diff --git a/workdsh-web/packages/plugins/experts/package.json b/workdsh-web/packages/plugins/experts/package.json index 508f440961..c777a7bf9d 100644 --- a/workdsh-web/packages/plugins/experts/package.json +++ b/workdsh-web/packages/plugins/experts/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-alpha.9", "private": false, "type": "module", - "description": "Local expert management plugin for DeepSeek Harness (D04 / P1-02, expert module 0.1)", + "description": "Manages reusable experts and expert teams in WorkDSH.", "exports": { ".": { "types": "./dist/index.d.ts", @@ -12,14 +12,17 @@ "./client": { "types": "./dist/client.d.ts", "default": "./dist/client.browser.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "resources", "README.md", "CHANGELOG.md", - "cordis.patch.yml" + "cordis.patch.yml", + "locale/*.json" ], "scripts": { "build": "corepack pnpm --filter workdsh-contracts build && corepack pnpm --filter workdsh-ui build && node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json && node ../../../scripts/build-experts.mjs", diff --git a/workdsh-web/packages/plugins/library/locale/en.json b/workdsh-web/packages/plugins/library/locale/en.json new file mode 100644 index 0000000000..763019a38f --- /dev/null +++ b/workdsh-web/packages/plugins/library/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Library", + "description": "Organizes local documents, revisions and task resources." + } +} diff --git a/workdsh-web/packages/plugins/library/locale/zh.json b/workdsh-web/packages/plugins/library/locale/zh.json new file mode 100644 index 0000000000..6a18365416 --- /dev/null +++ b/workdsh-web/packages/plugins/library/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "资料库", + "description": "整理本地文档、修订版本和任务资料。" + } +} diff --git a/workdsh-web/packages/plugins/library/package.json b/workdsh-web/packages/plugins/library/package.json index f61c294320..067256d975 100644 --- a/workdsh-web/packages/plugins/library/package.json +++ b/workdsh-web/packages/plugins/library/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-alpha.5", "private": false, "type": "module", - "description": "Local asset library, deterministic document conversion and task reuse for WorkDSH", + "description": "Organizes local documents, revisions and task resources.", "exports": { ".": { "types": "./dist/index.d.ts", @@ -12,13 +12,16 @@ "./client": { "types": "./dist/client.d.ts", "default": "./dist/client.browser.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "README.md", "CHANGELOG.md", - "cordis.patch.yml" + "cordis.patch.yml", + "locale/*.json" ], "scripts": { "build": "corepack pnpm --filter workdsh-contracts build && corepack pnpm --filter workdsh-ui build && tsc -p tsconfig.json && node ../../../scripts/build-library.mjs", diff --git a/workdsh-web/packages/plugins/office/locale/en.json b/workdsh-web/packages/plugins/office/locale/en.json new file mode 100644 index 0000000000..5c87077e87 --- /dev/null +++ b/workdsh-web/packages/plugins/office/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Office documents", + "description": "Creates and edits document working copies in WorkDSH." + } +} diff --git a/workdsh-web/packages/plugins/office/locale/zh.json b/workdsh-web/packages/plugins/office/locale/zh.json new file mode 100644 index 0000000000..7d9611207e --- /dev/null +++ b/workdsh-web/packages/plugins/office/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "办公文档", + "description": "在 WorkDSH 中创建和编辑文档工作副本。" + } +} diff --git a/workdsh-web/packages/plugins/office/package.json b/workdsh-web/packages/plugins/office/package.json index 8b3df66bc9..974e73cb5e 100644 --- a/workdsh-web/packages/plugins/office/package.json +++ b/workdsh-web/packages/plugins/office/package.json @@ -9,14 +9,17 @@ }, "./client": { "default": "./dist/client.browser.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "cordis.patch.yml", "README.md", "THIRD-PARTY-NOTICES.md", - "CHANGELOG.md" + "CHANGELOG.md", + "locale/*.json" ], "scripts": { "build": "node ../../../scripts/build-office.mjs", @@ -108,5 +111,6 @@ "@deepseek-ai/dsh-fs": { "optional": true } - } + }, + "description": "Creates and edits document working copies in WorkDSH." } diff --git a/workdsh-web/packages/plugins/projects/locale/en.json b/workdsh-web/packages/plugins/projects/locale/en.json new file mode 100644 index 0000000000..c49c740d2f --- /dev/null +++ b/workdsh-web/packages/plugins/projects/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Projects", + "description": "Organizes tasks, configurations and assets by project." + } +} diff --git a/workdsh-web/packages/plugins/projects/locale/zh.json b/workdsh-web/packages/plugins/projects/locale/zh.json new file mode 100644 index 0000000000..b5c5de453c --- /dev/null +++ b/workdsh-web/packages/plugins/projects/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "项目", + "description": "按项目组织任务、配置和资料。" + } +} diff --git a/workdsh-web/packages/plugins/projects/package.json b/workdsh-web/packages/plugins/projects/package.json index 7b0a095839..229953fe3f 100644 --- a/workdsh-web/packages/plugins/projects/package.json +++ b/workdsh-web/packages/plugins/projects/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-alpha.4", "private": false, "type": "module", - "description": "Project workspace, planning, configuration and asset references for WorkDSH", + "description": "Organizes tasks, configurations and assets by project.", "exports": { ".": { "types": "./dist/index.d.ts", @@ -12,13 +12,16 @@ "./client": { "types": "./dist/client.d.ts", "default": "./dist/client.browser.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "README.md", "CHANGELOG.md", - "cordis.patch.yml" + "cordis.patch.yml", + "locale/*.json" ], "scripts": { "build": "corepack pnpm --filter workdsh-contracts build && corepack pnpm --filter workdsh-ui build && tsc -p tsconfig.json && node ../../../scripts/build-projects.mjs", diff --git a/workdsh-web/packages/plugins/skills/locale/en.json b/workdsh-web/packages/plugins/skills/locale/en.json new file mode 100644 index 0000000000..bb1830b37b --- /dev/null +++ b/workdsh-web/packages/plugins/skills/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Skills", + "description": "Manages installed skills, local skill packages and SkillHub browsing." + } +} diff --git a/workdsh-web/packages/plugins/skills/locale/zh.json b/workdsh-web/packages/plugins/skills/locale/zh.json new file mode 100644 index 0000000000..0abaf1766f --- /dev/null +++ b/workdsh-web/packages/plugins/skills/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "技能", + "description": "管理已安装技能、本地技能包和 SkillHub 浏览。" + } +} diff --git a/workdsh-web/packages/plugins/skills/package.json b/workdsh-web/packages/plugins/skills/package.json index 7efb0b2e39..a5fbce1207 100644 --- a/workdsh-web/packages/plugins/skills/package.json +++ b/workdsh-web/packages/plugins/skills/package.json @@ -11,14 +11,17 @@ "./client": { "types": "./dist/client.d.ts", "default": "./dist/client.browser.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "resources", "README.md", "CHANGELOG.md", - "cordis.patch.yml" + "cordis.patch.yml", + "locale/*.json" ], "scripts": { "build": "node ../../../scripts/generate-builtin-skills.mjs && corepack pnpm --filter workdsh-contracts build && corepack pnpm --filter workdsh-ui build && tsc -p tsconfig.json && node ../../../scripts/build-skills.mjs", @@ -57,7 +60,7 @@ "fflate": "0.8.3", "yaml": "2.9.0" }, - "description": "Local skill management plugin for DeepSeek Harness", + "description": "Manages installed skills, local skill packages and SkillHub browsing.", "dsh": { "bundle": { "patch": "./cordis.patch.yml" diff --git a/workdsh-web/packages/plugins/workbench/locale/en.json b/workdsh-web/packages/plugins/workbench/locale/en.json new file mode 100644 index 0000000000..924900122f --- /dev/null +++ b/workdsh-web/packages/plugins/workbench/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Workbench", + "description": "Adds WorkDSH navigation and workspace entry points to the DSH sidebar." + } +} diff --git a/workdsh-web/packages/plugins/workbench/locale/zh.json b/workdsh-web/packages/plugins/workbench/locale/zh.json new file mode 100644 index 0000000000..16a2ee77c5 --- /dev/null +++ b/workdsh-web/packages/plugins/workbench/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "工作台", + "description": "在 DSH 侧栏加入 WorkDSH 导航与工作区入口。" + } +} diff --git a/workdsh-web/packages/plugins/workbench/package.json b/workdsh-web/packages/plugins/workbench/package.json index 3311135096..ac3f5da0a5 100644 --- a/workdsh-web/packages/plugins/workbench/package.json +++ b/workdsh-web/packages/plugins/workbench/package.json @@ -7,12 +7,15 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "README.md", - "CHANGELOG.md" + "CHANGELOG.md", + "locale/*.json" ], "scripts": { "build": "tsc -p tsconfig.json", @@ -38,5 +41,6 @@ "@deepseek-ai/dsh-client-ui-session": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-chat": "0.1.7-rc.2", "@deepseek-ai/dsh-tool-todo": "0.1.7-rc.2" - } + }, + "description": "Adds WorkDSH navigation and workspace entry points to the DSH sidebar." } diff --git a/workdsh-web/packages/providers/browser-session/locale/en.json b/workdsh-web/packages/providers/browser-session/locale/en.json new file mode 100644 index 0000000000..9271df611a --- /dev/null +++ b/workdsh-web/packages/providers/browser-session/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Browser session", + "description": "Connects the current agent session to a managed browser." + } +} diff --git a/workdsh-web/packages/providers/browser-session/locale/zh.json b/workdsh-web/packages/providers/browser-session/locale/zh.json new file mode 100644 index 0000000000..df6fb4cdbc --- /dev/null +++ b/workdsh-web/packages/providers/browser-session/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "浏览器会话", + "description": "将当前智能体会话连接到受管浏览器。" + } +} diff --git a/workdsh-web/packages/providers/browser-session/package.json b/workdsh-web/packages/providers/browser-session/package.json index 6b3b6d2962..989d1727eb 100644 --- a/workdsh-web/packages/providers/browser-session/package.json +++ b/workdsh-web/packages/providers/browser-session/package.json @@ -3,7 +3,7 @@ "version": "0.1.0-alpha.2", "private": true, "type": "module", - "description": "Session-owned Chromium browser bridge for WorkDSH", + "description": "Connects the current agent session to a managed browser.", "exports": { ".": { "types": "./dist/index.d.ts", @@ -12,9 +12,15 @@ "./client": { "types": "./dist/client.d.ts", "default": "./dist/client.browser.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, - "files": ["dist", "README.md"], + "files": [ + "dist", + "README.md", + "locale/*.json" + ], "scripts": { "build": "tsc -p tsconfig.json && node ../../../scripts/build-browser-session.mjs", "typecheck": "tsc -p tsconfig.json --noEmit", diff --git a/workdsh-web/packages/providers/identity-local/locale/en.json b/workdsh-web/packages/providers/identity-local/locale/en.json new file mode 100644 index 0000000000..c92fd1b220 --- /dev/null +++ b/workdsh-web/packages/providers/identity-local/locale/en.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "Local identity", + "description": "Provides trusted local identity and personal workspace ownership." + } +} diff --git a/workdsh-web/packages/providers/identity-local/locale/zh.json b/workdsh-web/packages/providers/identity-local/locale/zh.json new file mode 100644 index 0000000000..b24c7b5320 --- /dev/null +++ b/workdsh-web/packages/providers/identity-local/locale/zh.json @@ -0,0 +1,6 @@ +{ + "meta": { + "title": "本地身份", + "description": "提供可信本地身份与个人工作区归属信息。" + } +} diff --git a/workdsh-web/packages/providers/identity-local/package.json b/workdsh-web/packages/providers/identity-local/package.json index 4fbe8342c7..4e6bf72cf5 100644 --- a/workdsh-web/packages/providers/identity-local/package.json +++ b/workdsh-web/packages/providers/identity-local/package.json @@ -7,13 +7,16 @@ ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" - } + }, + "./package.json": "./package.json", + "./locale/*.json": "./locale/*.json" }, "files": [ "dist", "README.md", "CHANGELOG.md", - "cordis.patch.yml" + "cordis.patch.yml", + "locale/*.json" ], "dsh": { "bundle": { @@ -37,5 +40,6 @@ "@deepseek-ai/dsh-storage-domain": "0.1.7-rc.2", "@types/node": "22.19.0", "workdsh-contracts": "workspace:*" - } + }, + "description": "Provides trusted local identity and personal workspace ownership." } From 42ea7266b8dcc41d20f206edca11e0fb3a0e16b4 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 18:12:10 +0800 Subject: [PATCH 11/23] fix: retain package names in plugin manager --- workdsh-web/packages/bundle/locale/en.json | 1 - workdsh-web/packages/bundle/locale/zh.json | 1 - workdsh-web/packages/plugins/access/locale/en.json | 1 - workdsh-web/packages/plugins/access/locale/zh.json | 1 - workdsh-web/packages/plugins/activity/locale/en.json | 1 - workdsh-web/packages/plugins/activity/locale/zh.json | 1 - workdsh-web/packages/plugins/audit/locale/en.json | 1 - workdsh-web/packages/plugins/audit/locale/zh.json | 1 - workdsh-web/packages/plugins/connectors/locale/en.json | 1 - workdsh-web/packages/plugins/connectors/locale/zh.json | 1 - workdsh-web/packages/plugins/experts/locale/en.json | 1 - workdsh-web/packages/plugins/experts/locale/zh.json | 1 - workdsh-web/packages/plugins/library/locale/en.json | 1 - workdsh-web/packages/plugins/library/locale/zh.json | 1 - workdsh-web/packages/plugins/office/locale/en.json | 1 - workdsh-web/packages/plugins/office/locale/zh.json | 1 - workdsh-web/packages/plugins/projects/locale/en.json | 1 - workdsh-web/packages/plugins/projects/locale/zh.json | 1 - workdsh-web/packages/plugins/skills/locale/en.json | 1 - workdsh-web/packages/plugins/skills/locale/zh.json | 1 - workdsh-web/packages/plugins/workbench/locale/en.json | 1 - workdsh-web/packages/plugins/workbench/locale/zh.json | 1 - workdsh-web/packages/providers/browser-session/locale/en.json | 1 - workdsh-web/packages/providers/browser-session/locale/zh.json | 1 - workdsh-web/packages/providers/identity-local/locale/en.json | 1 - workdsh-web/packages/providers/identity-local/locale/zh.json | 1 - 26 files changed, 26 deletions(-) diff --git a/workdsh-web/packages/bundle/locale/en.json b/workdsh-web/packages/bundle/locale/en.json index d7435586a6..7c728b1c45 100644 --- a/workdsh-web/packages/bundle/locale/en.json +++ b/workdsh-web/packages/bundle/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "WorkDSH application bundle", "description": "Composes the WorkDSH workspace, capabilities and default DSH plugins." } } diff --git a/workdsh-web/packages/bundle/locale/zh.json b/workdsh-web/packages/bundle/locale/zh.json index 5c3b82bb4f..8bbaa3de94 100644 --- a/workdsh-web/packages/bundle/locale/zh.json +++ b/workdsh-web/packages/bundle/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "WorkDSH 应用组合包", "description": "组合工作区、能力中心和默认 DSH 插件。" } } diff --git a/workdsh-web/packages/plugins/access/locale/en.json b/workdsh-web/packages/plugins/access/locale/en.json index 0b6c2aa03c..0a302ac879 100644 --- a/workdsh-web/packages/plugins/access/locale/en.json +++ b/workdsh-web/packages/plugins/access/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Resource access control", "description": "Controls access to local resources, sessions and tools." } } diff --git a/workdsh-web/packages/plugins/access/locale/zh.json b/workdsh-web/packages/plugins/access/locale/zh.json index 205a8cba74..6cdbcc2156 100644 --- a/workdsh-web/packages/plugins/access/locale/zh.json +++ b/workdsh-web/packages/plugins/access/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "资源访问控制", "description": "管理本地资源、会话和工具的访问权限。" } } diff --git a/workdsh-web/packages/plugins/activity/locale/en.json b/workdsh-web/packages/plugins/activity/locale/en.json index 09027a82f9..12e4de2d0e 100644 --- a/workdsh-web/packages/plugins/activity/locale/en.json +++ b/workdsh-web/packages/plugins/activity/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Session activity", "description": "Shows session activity and agent collaboration status." } } diff --git a/workdsh-web/packages/plugins/activity/locale/zh.json b/workdsh-web/packages/plugins/activity/locale/zh.json index 69a13fd7de..eb3062aaf9 100644 --- a/workdsh-web/packages/plugins/activity/locale/zh.json +++ b/workdsh-web/packages/plugins/activity/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "会话活动", "description": "展示会话活动和智能体协作状态。" } } diff --git a/workdsh-web/packages/plugins/audit/locale/en.json b/workdsh-web/packages/plugins/audit/locale/en.json index 368cd94496..32499e5116 100644 --- a/workdsh-web/packages/plugins/audit/locale/en.json +++ b/workdsh-web/packages/plugins/audit/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Audit log", "description": "Records local management and execution events for audit." } } diff --git a/workdsh-web/packages/plugins/audit/locale/zh.json b/workdsh-web/packages/plugins/audit/locale/zh.json index e348e53f11..49e79d667f 100644 --- a/workdsh-web/packages/plugins/audit/locale/zh.json +++ b/workdsh-web/packages/plugins/audit/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "审计记录", "description": "记录本地管理与执行事件,供审计使用。" } } diff --git a/workdsh-web/packages/plugins/connectors/locale/en.json b/workdsh-web/packages/plugins/connectors/locale/en.json index 92107cd814..fd3012d852 100644 --- a/workdsh-web/packages/plugins/connectors/locale/en.json +++ b/workdsh-web/packages/plugins/connectors/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Connectors", "description": "Manages MCP connectors and per-conversation tool selection." } } diff --git a/workdsh-web/packages/plugins/connectors/locale/zh.json b/workdsh-web/packages/plugins/connectors/locale/zh.json index 1e27d0f401..a4d04711b7 100644 --- a/workdsh-web/packages/plugins/connectors/locale/zh.json +++ b/workdsh-web/packages/plugins/connectors/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "连接器", "description": "管理 MCP 连接器及每个对话可用的工具。" } } diff --git a/workdsh-web/packages/plugins/experts/locale/en.json b/workdsh-web/packages/plugins/experts/locale/en.json index f915ffc665..e92bc84f38 100644 --- a/workdsh-web/packages/plugins/experts/locale/en.json +++ b/workdsh-web/packages/plugins/experts/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Experts", "description": "Manages reusable experts and expert teams in WorkDSH." } } diff --git a/workdsh-web/packages/plugins/experts/locale/zh.json b/workdsh-web/packages/plugins/experts/locale/zh.json index 5f06d6f0b3..5a64d23494 100644 --- a/workdsh-web/packages/plugins/experts/locale/zh.json +++ b/workdsh-web/packages/plugins/experts/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "专家", "description": "管理 WorkDSH 中可复用的专家与专家团。" } } diff --git a/workdsh-web/packages/plugins/library/locale/en.json b/workdsh-web/packages/plugins/library/locale/en.json index 763019a38f..95083e0a62 100644 --- a/workdsh-web/packages/plugins/library/locale/en.json +++ b/workdsh-web/packages/plugins/library/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Library", "description": "Organizes local documents, revisions and task resources." } } diff --git a/workdsh-web/packages/plugins/library/locale/zh.json b/workdsh-web/packages/plugins/library/locale/zh.json index 6a18365416..61387bd062 100644 --- a/workdsh-web/packages/plugins/library/locale/zh.json +++ b/workdsh-web/packages/plugins/library/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "资料库", "description": "整理本地文档、修订版本和任务资料。" } } diff --git a/workdsh-web/packages/plugins/office/locale/en.json b/workdsh-web/packages/plugins/office/locale/en.json index 5c87077e87..c910f13864 100644 --- a/workdsh-web/packages/plugins/office/locale/en.json +++ b/workdsh-web/packages/plugins/office/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Office documents", "description": "Creates and edits document working copies in WorkDSH." } } diff --git a/workdsh-web/packages/plugins/office/locale/zh.json b/workdsh-web/packages/plugins/office/locale/zh.json index 7d9611207e..493a5bdf24 100644 --- a/workdsh-web/packages/plugins/office/locale/zh.json +++ b/workdsh-web/packages/plugins/office/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "办公文档", "description": "在 WorkDSH 中创建和编辑文档工作副本。" } } diff --git a/workdsh-web/packages/plugins/projects/locale/en.json b/workdsh-web/packages/plugins/projects/locale/en.json index c49c740d2f..98e4ed8cb0 100644 --- a/workdsh-web/packages/plugins/projects/locale/en.json +++ b/workdsh-web/packages/plugins/projects/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Projects", "description": "Organizes tasks, configurations and assets by project." } } diff --git a/workdsh-web/packages/plugins/projects/locale/zh.json b/workdsh-web/packages/plugins/projects/locale/zh.json index b5c5de453c..90470d4d5b 100644 --- a/workdsh-web/packages/plugins/projects/locale/zh.json +++ b/workdsh-web/packages/plugins/projects/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "项目", "description": "按项目组织任务、配置和资料。" } } diff --git a/workdsh-web/packages/plugins/skills/locale/en.json b/workdsh-web/packages/plugins/skills/locale/en.json index bb1830b37b..24046b0c02 100644 --- a/workdsh-web/packages/plugins/skills/locale/en.json +++ b/workdsh-web/packages/plugins/skills/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Skills", "description": "Manages installed skills, local skill packages and SkillHub browsing." } } diff --git a/workdsh-web/packages/plugins/skills/locale/zh.json b/workdsh-web/packages/plugins/skills/locale/zh.json index 0abaf1766f..2bb873ff53 100644 --- a/workdsh-web/packages/plugins/skills/locale/zh.json +++ b/workdsh-web/packages/plugins/skills/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "技能", "description": "管理已安装技能、本地技能包和 SkillHub 浏览。" } } diff --git a/workdsh-web/packages/plugins/workbench/locale/en.json b/workdsh-web/packages/plugins/workbench/locale/en.json index 924900122f..6ed007d667 100644 --- a/workdsh-web/packages/plugins/workbench/locale/en.json +++ b/workdsh-web/packages/plugins/workbench/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Workbench", "description": "Adds WorkDSH navigation and workspace entry points to the DSH sidebar." } } diff --git a/workdsh-web/packages/plugins/workbench/locale/zh.json b/workdsh-web/packages/plugins/workbench/locale/zh.json index 16a2ee77c5..5bace524fe 100644 --- a/workdsh-web/packages/plugins/workbench/locale/zh.json +++ b/workdsh-web/packages/plugins/workbench/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "工作台", "description": "在 DSH 侧栏加入 WorkDSH 导航与工作区入口。" } } diff --git a/workdsh-web/packages/providers/browser-session/locale/en.json b/workdsh-web/packages/providers/browser-session/locale/en.json index 9271df611a..c553fc0269 100644 --- a/workdsh-web/packages/providers/browser-session/locale/en.json +++ b/workdsh-web/packages/providers/browser-session/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Browser session", "description": "Connects the current agent session to a managed browser." } } diff --git a/workdsh-web/packages/providers/browser-session/locale/zh.json b/workdsh-web/packages/providers/browser-session/locale/zh.json index df6fb4cdbc..5ed2a7bac4 100644 --- a/workdsh-web/packages/providers/browser-session/locale/zh.json +++ b/workdsh-web/packages/providers/browser-session/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "浏览器会话", "description": "将当前智能体会话连接到受管浏览器。" } } diff --git a/workdsh-web/packages/providers/identity-local/locale/en.json b/workdsh-web/packages/providers/identity-local/locale/en.json index c92fd1b220..0cf73caafa 100644 --- a/workdsh-web/packages/providers/identity-local/locale/en.json +++ b/workdsh-web/packages/providers/identity-local/locale/en.json @@ -1,6 +1,5 @@ { "meta": { - "title": "Local identity", "description": "Provides trusted local identity and personal workspace ownership." } } diff --git a/workdsh-web/packages/providers/identity-local/locale/zh.json b/workdsh-web/packages/providers/identity-local/locale/zh.json index b24c7b5320..50be1ee2a1 100644 --- a/workdsh-web/packages/providers/identity-local/locale/zh.json +++ b/workdsh-web/packages/providers/identity-local/locale/zh.json @@ -1,6 +1,5 @@ { "meta": { - "title": "本地身份", "description": "提供可信本地身份与个人工作区归属信息。" } } From 03232a590c56116279b06b22f17fc0a61073e87d Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 18:30:00 +0800 Subject: [PATCH 12/23] feat: add community plugin discovery entry to DSH manager --- workdsh-web/packages/bundle/package.json | 1 + .../src/client/components/CommunityPlugins.tsx | 15 +++++++++++++++ .../packages/bundle/src/client/harness/client.ts | 6 ++++++ workdsh-web/pnpm-lock.yaml | 3 +++ 4 files changed, 25 insertions(+) create mode 100644 workdsh-web/packages/bundle/src/client/components/CommunityPlugins.tsx diff --git a/workdsh-web/packages/bundle/package.json b/workdsh-web/packages/bundle/package.json index 520955aef8..e546f2a1f9 100644 --- a/workdsh-web/packages/bundle/package.json +++ b/workdsh-web/packages/bundle/package.json @@ -61,6 +61,7 @@ "@deepseek-ai/dsh-client-connection": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-layout": "0.1.7-rc.2", + "@deepseek-ai/dsh-client-ui-plugin-manager": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-sidebar": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.7-rc.2", diff --git a/workdsh-web/packages/bundle/src/client/components/CommunityPlugins.tsx b/workdsh-web/packages/bundle/src/client/components/CommunityPlugins.tsx new file mode 100644 index 0000000000..b49bca4d24 --- /dev/null +++ b/workdsh-web/packages/bundle/src/client/components/CommunityPlugins.tsx @@ -0,0 +1,15 @@ +import type { ReactNode } from 'react'; + +/** This is a discovery link, not a second plugin installer or a copied catalog. */ +export function CommunityPlugins({ view }: { readonly view: 'summary' | 'page' }): ReactNode { + if (view === 'summary') return 'WorkDSH 提供的第三方 DSH 插件发现入口;不是 DSH 官方推荐。'; + return ( +
+

在 dshmarket 浏览社区插件,确认作者、来源、版本及兼容性后,复制包名或仓库地址,再使用此页面右上角的“添加插件”安装。

+

dshmarket 是独立的第三方网站。打开后适用该网站的内容和隐私政策;WorkDSH 不会自动安装其中的插件。

+ + 前往 dshmarket 浏览社区插件 ↗ + +
+ ); +} diff --git a/workdsh-web/packages/bundle/src/client/harness/client.ts b/workdsh-web/packages/bundle/src/client/harness/client.ts index 3725239d6a..180cddf8c8 100644 --- a/workdsh-web/packages/bundle/src/client/harness/client.ts +++ b/workdsh-web/packages/bundle/src/client/harness/client.ts @@ -1,7 +1,9 @@ import { ShellAppearance } from '../components/ShellAppearance.js'; +import { CommunityPlugins } from '../components/CommunityPlugins.js'; import type { Context } from '@deepseek-ai/cordis'; import type {} from '@deepseek-ai/dsh-api-remotes/client'; import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'; +import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'; import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'; import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client'; import type {} from '@deepseek-ai/dsh-client-ui-session/client'; @@ -26,6 +28,10 @@ const productViews: Readonly> = { }; export function apply(ctx: Context): void { + ctx.slots.inject('plugins.item', () => ctx.slots.register({ + name: 'plugins.item', id: 'workdsh-community-plugins', order: 100, + label: 'WorkDSH · 发现社区插件', + }, CommunityPlugins)); let legacyBrowserEnabled = false; ctx.effect(() => { const controller = new AbortController(); diff --git a/workdsh-web/pnpm-lock.yaml b/workdsh-web/pnpm-lock.yaml index 9c638c52eb..d570c0a66e 100644 --- a/workdsh-web/pnpm-lock.yaml +++ b/workdsh-web/pnpm-lock.yaml @@ -440,6 +440,9 @@ importers: '@deepseek-ai/dsh-client-ui-layout': specifier: 0.1.7-rc.2 version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.3) + '@deepseek-ai/dsh-client-ui-plugin-manager': + specifier: 0.1.7-rc.2 + version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.3) '@deepseek-ai/dsh-client-ui-renderer': specifier: 0.1.7-rc.2 version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.3) From 1ff24f2e58ec7ee134834c62ddea2fb919bb3c37 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 18:44:50 +0800 Subject: [PATCH 13/23] Revert "feat: add community plugin discovery entry to DSH manager" This reverts commit 03232a590c56116279b06b22f17fc0a61073e87d. --- workdsh-web/packages/bundle/package.json | 1 - .../src/client/components/CommunityPlugins.tsx | 15 --------------- .../packages/bundle/src/client/harness/client.ts | 6 ------ workdsh-web/pnpm-lock.yaml | 3 --- 4 files changed, 25 deletions(-) delete mode 100644 workdsh-web/packages/bundle/src/client/components/CommunityPlugins.tsx diff --git a/workdsh-web/packages/bundle/package.json b/workdsh-web/packages/bundle/package.json index e546f2a1f9..520955aef8 100644 --- a/workdsh-web/packages/bundle/package.json +++ b/workdsh-web/packages/bundle/package.json @@ -61,7 +61,6 @@ "@deepseek-ai/dsh-client-connection": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-layout": "0.1.7-rc.2", - "@deepseek-ai/dsh-client-ui-plugin-manager": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-sidebar": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.7-rc.2", diff --git a/workdsh-web/packages/bundle/src/client/components/CommunityPlugins.tsx b/workdsh-web/packages/bundle/src/client/components/CommunityPlugins.tsx deleted file mode 100644 index b49bca4d24..0000000000 --- a/workdsh-web/packages/bundle/src/client/components/CommunityPlugins.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { ReactNode } from 'react'; - -/** This is a discovery link, not a second plugin installer or a copied catalog. */ -export function CommunityPlugins({ view }: { readonly view: 'summary' | 'page' }): ReactNode { - if (view === 'summary') return 'WorkDSH 提供的第三方 DSH 插件发现入口;不是 DSH 官方推荐。'; - return ( -
-

在 dshmarket 浏览社区插件,确认作者、来源、版本及兼容性后,复制包名或仓库地址,再使用此页面右上角的“添加插件”安装。

-

dshmarket 是独立的第三方网站。打开后适用该网站的内容和隐私政策;WorkDSH 不会自动安装其中的插件。

- - 前往 dshmarket 浏览社区插件 ↗ - -
- ); -} diff --git a/workdsh-web/packages/bundle/src/client/harness/client.ts b/workdsh-web/packages/bundle/src/client/harness/client.ts index 180cddf8c8..3725239d6a 100644 --- a/workdsh-web/packages/bundle/src/client/harness/client.ts +++ b/workdsh-web/packages/bundle/src/client/harness/client.ts @@ -1,9 +1,7 @@ import { ShellAppearance } from '../components/ShellAppearance.js'; -import { CommunityPlugins } from '../components/CommunityPlugins.js'; import type { Context } from '@deepseek-ai/cordis'; import type {} from '@deepseek-ai/dsh-api-remotes/client'; import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'; -import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'; import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'; import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client'; import type {} from '@deepseek-ai/dsh-client-ui-session/client'; @@ -28,10 +26,6 @@ const productViews: Readonly> = { }; export function apply(ctx: Context): void { - ctx.slots.inject('plugins.item', () => ctx.slots.register({ - name: 'plugins.item', id: 'workdsh-community-plugins', order: 100, - label: 'WorkDSH · 发现社区插件', - }, CommunityPlugins)); let legacyBrowserEnabled = false; ctx.effect(() => { const controller = new AbortController(); diff --git a/workdsh-web/pnpm-lock.yaml b/workdsh-web/pnpm-lock.yaml index d570c0a66e..9c638c52eb 100644 --- a/workdsh-web/pnpm-lock.yaml +++ b/workdsh-web/pnpm-lock.yaml @@ -440,9 +440,6 @@ importers: '@deepseek-ai/dsh-client-ui-layout': specifier: 0.1.7-rc.2 version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.3) - '@deepseek-ai/dsh-client-ui-plugin-manager': - specifier: 0.1.7-rc.2 - version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.3) '@deepseek-ai/dsh-client-ui-renderer': specifier: 0.1.7-rc.2 version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.3) From 123e900e6107358f8a72408a1236a9df4515f91c Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 19:00:18 +0800 Subject: [PATCH 14/23] feat: embed dshmarket in existing plugin manager --- .../scripts/prepare-workdsh-runtime.mjs | 15 ++++++++++++--- .../scripts/verify-product-plugin-inventory.mjs | 4 ++++ workdsh-web/packages/bundle/package.json | 1 + .../src/client/components/CommunityMarket.tsx | 15 +++++++++++++++ .../packages/bundle/src/client/harness/client.ts | 15 +++++++++++++++ workdsh-web/pnpm-lock.yaml | 3 +++ workdsh-web/scripts/install-preview.mjs | 5 ++++- 7 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx diff --git a/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs b/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs index 43136c7c4a..0bc142ac76 100644 --- a/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs +++ b/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs @@ -15,9 +15,11 @@ const destination = join(output, 'profiles', 'workdsh') const packageCache = join(output, 'package-cache') const cli = join(destination, 'node_modules', '@deepseek-ai', 'dsh', 'lib', 'bin.js') const releaseMarker = '.workdsh-desktop-release.json' -const profileLayout = 'five-product-plugins-skillhub-v1' +const profileLayout = 'five-product-plugins-skillhub-dshmarket-v1' const SKILLHUB_PACKAGE = '@cocofhu/skillhub' const SKILLHUB_VERSION = '0.2.16' +const MARKET_PACKAGE = 'dshmarket' +const MARKET_VERSION = '1.66.1' const productPackages = new Set(PRODUCT_PACKAGES) function run(command, args, options = {}) { @@ -187,11 +189,13 @@ async function installReleasedProfile(output) { run(process.execPath, [pnpmCli, '--dir', destination, 'install', '--frozen-lockfile', '--offline']) // Keep the third-party SkillHub integration in the same DSH Profile. It // supplies SkillHub search and a DSH plugin catalogue without a second host. - run(process.execPath, [pnpmCli, '--dir', destination, 'add', '--save-exact', `${SKILLHUB_PACKAGE}@${SKILLHUB_VERSION}`]) + run(process.execPath, [pnpmCli, '--dir', destination, 'add', '--save-exact', `${SKILLHUB_PACKAGE}@${SKILLHUB_VERSION}`, `${MARKET_PACKAGE}@${MARKET_VERSION}`]) const installedSkillHub = JSON.parse(readFileSync(join(destination, 'node_modules', SKILLHUB_PACKAGE, 'package.json'), 'utf8')) if (installedSkillHub.version !== SKILLHUB_VERSION) throw new Error('Pinned SkillHub plugin version is missing') + const installedMarket = JSON.parse(readFileSync(join(destination, 'node_modules', MARKET_PACKAGE, 'package.json'), 'utf8')) + if (installedMarket.version !== MARKET_VERSION) throw new Error('Pinned dshmarket plugin version is missing') const profileWithSkillHub = JSON.parse(readFileSync(profilePath, 'utf8')) - profileWithSkillHub.dsh.profile.bundles = [...new Set([...profileWithSkillHub.dsh.profile.bundles, SKILLHUB_PACKAGE])] + profileWithSkillHub.dsh.profile.bundles = [...new Set([...profileWithSkillHub.dsh.profile.bundles, SKILLHUB_PACKAGE, MARKET_PACKAGE])] writeFileSync(profilePath, JSON.stringify(profileWithSkillHub, null, 2) + '\n') const config = spawnSync(process.execPath, [cli, '--profile', 'workdsh', '--dump-config'], { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, env: { ...process.env, DSH_HOME: output }, @@ -202,6 +206,7 @@ async function installReleasedProfile(output) { if (!config.stdout.includes(`id: ${id}`)) throw new Error(`Internal WorkDSH service is missing: ${id}`) } if (!config.stdout.includes('id: skillhub')) throw new Error('SkillHub plugin is missing from the WorkDSH Profile') + if (!config.stdout.includes('id: dsh-market')) throw new Error('dshmarket plugin is missing from the WorkDSH Profile') run(process.execPath, [join(desktopRoot, 'scripts', 'verify-product-plugin-inventory.mjs'), output], { env: { ...process.env, DSH_HOME: output }, }) @@ -233,6 +238,8 @@ const isPreparedProfile = candidate => { try { const skillHub = JSON.parse(readFileSync(join(candidate, 'node_modules', SKILLHUB_PACKAGE, 'package.json'), 'utf8')) if (skillHub.version !== SKILLHUB_VERSION) return false + const market = JSON.parse(readFileSync(join(candidate, 'node_modules', MARKET_PACKAGE, 'package.json'), 'utf8')) + if (market.version !== MARKET_VERSION) return false } catch { return false } try { const marker = JSON.parse(readFileSync(join(candidate, releaseMarker), 'utf8')) @@ -256,7 +263,9 @@ const isPreparedProfile = candidate => { const selected = profile.dsh?.profile?.bundles ?? [] return [...productPackages].every(name => selected.includes(name)) && selected.includes(SKILLHUB_PACKAGE) && + selected.includes(MARKET_PACKAGE) && profile.dependencies?.[SKILLHUB_PACKAGE] === SKILLHUB_VERSION && + profile.dependencies?.[MARKET_PACKAGE] === MARKET_VERSION && supportPackages.every(name => !selected.includes(name)) && supportPackages.every(name => !Object.hasOwn(profile.dependencies ?? {}, name)) && readFileSync(join(candidate, 'cordis.patch.yml'), 'utf8').startsWith(internalPatch(candidate)) && diff --git a/dsh-plugin-desktop/scripts/verify-product-plugin-inventory.mjs b/dsh-plugin-desktop/scripts/verify-product-plugin-inventory.mjs index 80e43adf71..cedcba4207 100644 --- a/dsh-plugin-desktop/scripts/verify-product-plugin-inventory.mjs +++ b/dsh-plugin-desktop/scripts/verify-product-plugin-inventory.mjs @@ -42,6 +42,10 @@ try { if (!skillHub?.enabled || !skillHub.installed || skillHub.error) { throw new Error(`SkillHub DSH plugin is inactive or invalid: ${JSON.stringify(skillHub)}`) } + const market = (await ctx.pluginManager.listBundles()).find(row => row.name === 'dshmarket') + if (!market?.enabled || !market.installed || market.error) { + throw new Error(`dshmarket DSH plugin is inactive or invalid: ${JSON.stringify(market)}`) + } console.log(`Verified plugin manager exposes exactly five WorkDSH product bundles: ${names.join(', ')}`) } finally { try { diff --git a/workdsh-web/packages/bundle/package.json b/workdsh-web/packages/bundle/package.json index 520955aef8..e546f2a1f9 100644 --- a/workdsh-web/packages/bundle/package.json +++ b/workdsh-web/packages/bundle/package.json @@ -61,6 +61,7 @@ "@deepseek-ai/dsh-client-connection": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-layout": "0.1.7-rc.2", + "@deepseek-ai/dsh-client-ui-plugin-manager": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-sidebar": "0.1.7-rc.2", "@deepseek-ai/dsh-client-ui-sidebar-right": "0.1.7-rc.2", diff --git a/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx b/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx new file mode 100644 index 0000000000..c7ffcce024 --- /dev/null +++ b/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx @@ -0,0 +1,15 @@ +import type { ReactNode } from 'react'; + +/** dshmarket explicitly provides its complete panel for embedding by a host. */ +export interface MarketHost { + readonly version: number; + readonly render: (props?: Record) => ReactNode; + readonly setSettingsVisible: (visible: boolean) => void; +} + +export function communityMarketView(market: MarketHost) { + return function CommunityMarket({ view }: { readonly view: 'summary' | 'page' }): ReactNode { + if (view === 'summary') return '浏览、搜索和安装 DSH 社区插件;由第三方 dsh-market 提供。'; + return market.render(); + }; +} diff --git a/workdsh-web/packages/bundle/src/client/harness/client.ts b/workdsh-web/packages/bundle/src/client/harness/client.ts index 3725239d6a..26fe9f08ec 100644 --- a/workdsh-web/packages/bundle/src/client/harness/client.ts +++ b/workdsh-web/packages/bundle/src/client/harness/client.ts @@ -1,7 +1,9 @@ import { ShellAppearance } from '../components/ShellAppearance.js'; +import { communityMarketView, type MarketHost } from '../components/CommunityMarket.js'; import type { Context } from '@deepseek-ai/cordis'; import type {} from '@deepseek-ai/dsh-api-remotes/client'; import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'; +import type {} from '@deepseek-ai/dsh-client-ui-plugin-manager/client'; import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'; import type {} from '@deepseek-ai/dsh-client-ui-sidebar-right/client'; import type {} from '@deepseek-ai/dsh-client-ui-session/client'; @@ -13,6 +15,10 @@ import { DiagnosticsPanel, type Inventory } from '../components/DiagnosticsPanel import { NavigationLocation } from '../components/NavigationLocation.js'; import { AgentBrowserPage, agentBrowserKind, readAgentBrowserFrame } from '../components/AgentBrowserPage.js'; +declare module '@deepseek-ai/cordis' { + interface Context { market: MarketHost; } +} + declare module '@deepseek-ai/dsh-client-ui-sidebar-right/client' { interface SidebarRightTabParamsMap { 'workdsh-agent-browser': Record; } } @@ -26,6 +32,15 @@ const productViews: Readonly> = { }; export function apply(ctx: Context): void { + ctx.inject(['market'], scope => { + if (scope.market.version !== 1) return; + scope.market.setSettingsVisible(false); + scope.effect(() => () => scope.market.setSettingsVisible(true), 'workdsh.community-market.settings-visibility'); + scope.slots.inject('plugins.item', () => scope.slots.register({ + name: 'plugins.item', id: 'workdsh-community-market', order: 100, + label: '插件市场 · dsh-market', + }, communityMarketView(scope.market))); + }); let legacyBrowserEnabled = false; ctx.effect(() => { const controller = new AbortController(); diff --git a/workdsh-web/pnpm-lock.yaml b/workdsh-web/pnpm-lock.yaml index 9c638c52eb..d570c0a66e 100644 --- a/workdsh-web/pnpm-lock.yaml +++ b/workdsh-web/pnpm-lock.yaml @@ -440,6 +440,9 @@ importers: '@deepseek-ai/dsh-client-ui-layout': specifier: 0.1.7-rc.2 version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.3) + '@deepseek-ai/dsh-client-ui-plugin-manager': + specifier: 0.1.7-rc.2 + version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.3) '@deepseek-ai/dsh-client-ui-renderer': specifier: 0.1.7-rc.2 version: 0.1.7-rc.2(@deepseek-ai/cordis@4.0.3) diff --git a/workdsh-web/scripts/install-preview.mjs b/workdsh-web/scripts/install-preview.mjs index ce68afe03c..642574c751 100644 --- a/workdsh-web/scripts/install-preview.mjs +++ b/workdsh-web/scripts/install-preview.mjs @@ -16,6 +16,8 @@ if (typeof baseVersion !== 'string') throw new Error('Missing pinned @deepseek-a if (typeof webAppVersion !== 'string') throw new Error('Missing pinned @deepseek-ai/dsh-web-app version in package.json pnpm.overrides.'); const baseSpec = `@deepseek-ai/dsh-base@${baseVersion}`; const webAppSpec = `@deepseek-ai/dsh-web-app@${webAppVersion}`; +const marketVersion = '1.66.1'; +const marketSpec = `dshmarket@${marketVersion}`; const cliVersion = JSON.parse(await readFile(join(root, 'node_modules/@deepseek-ai/dsh/package.json'), 'utf8')).version; if (cliVersion !== baseVersion) throw new Error('Preview CLI and Base must use the same pinned version.'); const home = resolve(process.env.WORKDSH_PREVIEW_HOME ?? join(root, '.test-runtime/preview')); @@ -68,9 +70,10 @@ if (normalizedManifest) { const currentDependencies = JSON.parse(await readFile(previewManifestPath, 'utf8')).dependencies ?? {}; const layersMatch = currentDependencies['@deepseek-ai/dsh-base'] === baseVersion && currentDependencies['@deepseek-ai/dsh-web-app'] === webAppVersion + && currentDependencies.dshmarket === marketVersion && packages.every(({ manifest }, index) => currentDependencies[manifest.name] === `file:${tarballs[index]}`); if (!layersMatch) { - await run('@deepseek-ai/dsh/lib/bin.js', ['plugin', '--profile', 'preview', 'add', baseSpec, webAppSpec, ...tarballs]); + await run('@deepseek-ai/dsh/lib/bin.js', ['plugin', '--profile', 'preview', 'add', baseSpec, webAppSpec, marketSpec, ...tarballs]); } // Boot and ConfigEditor share module-local registration in dsh-app-boot. // Keep the official CLI in the Profile dependency graph as well: launching the From e86a40426fe6d5fa34483e21f7e87e64777ae690 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 19:05:31 +0800 Subject: [PATCH 15/23] ui: surface community plugin discovery on first screen --- .../packages/bundle/src/client/components/CommunityMarket.tsx | 2 +- workdsh-web/packages/bundle/src/client/harness/client.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx b/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx index c7ffcce024..bc8c67fcf9 100644 --- a/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx +++ b/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx @@ -9,7 +9,7 @@ export interface MarketHost { export function communityMarketView(market: MarketHost) { return function CommunityMarket({ view }: { readonly view: 'summary' | 'page' }): ReactNode { - if (view === 'summary') return '浏览、搜索和安装 DSH 社区插件;由第三方 dsh-market 提供。'; + if (view === 'summary') return '进入 dsh-market 插件市场,搜索并安装 DSH 社区插件。'; return market.render(); }; } diff --git a/workdsh-web/packages/bundle/src/client/harness/client.ts b/workdsh-web/packages/bundle/src/client/harness/client.ts index 26fe9f08ec..041239718c 100644 --- a/workdsh-web/packages/bundle/src/client/harness/client.ts +++ b/workdsh-web/packages/bundle/src/client/harness/client.ts @@ -37,8 +37,8 @@ export function apply(ctx: Context): void { scope.market.setSettingsVisible(false); scope.effect(() => () => scope.market.setSettingsVisible(true), 'workdsh.community-market.settings-visibility'); scope.slots.inject('plugins.item', () => scope.slots.register({ - name: 'plugins.item', id: 'workdsh-community-market', order: 100, - label: '插件市场 · dsh-market', + name: 'plugins.item', id: 'workdsh-community-market', order: -100, + label: '发现社区插件', }, communityMarketView(scope.market))); }); let legacyBrowserEnabled = false; From 22c33267233436dd560baaf18f36f87d4d467a59 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 19:15:50 +0800 Subject: [PATCH 16/23] ui: highlight community market discovery on first view --- .../src/client/components/CommunityMarket.tsx | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx b/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx index bc8c67fcf9..0e358f7842 100644 --- a/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx +++ b/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx @@ -1,4 +1,32 @@ -import type { ReactNode } from 'react'; +import { useEffect, useRef, type ReactNode } from 'react'; + +const discoverySeenKey = 'workdsh.community-market.discovery-seen'; + +function DiscoverySummary(): ReactNode { + const badge = useRef(null); + useEffect(() => { + const node = badge.current; + if (!node || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + try { if (window.localStorage.getItem(discoverySeenKey)) return; } + catch { return; } + const observer = new IntersectionObserver(entries => { + if (!entries.some(entry => entry.isIntersecting)) return; + observer.disconnect(); + node.animate([ + { transform: 'scale(1)', boxShadow: '0 0 0 0 var(--dsw-alias-state-business-primary)' }, + { transform: 'scale(1.08)', boxShadow: '0 0 0 7px transparent', offset: 0.5 }, + { transform: 'scale(1)', boxShadow: '0 0 0 0 transparent' }, + ], { duration: 1000, easing: 'ease-out' }); + try { window.localStorage.setItem(discoverySeenKey, '1'); } catch { /* Storage is optional. */ } + }); + observer.observe(node); + return () => observer.disconnect(); + }, []); + return + 第三方市场 → + 搜索并安装 DSH 社区插件 + ; +} /** dshmarket explicitly provides its complete panel for embedding by a host. */ export interface MarketHost { @@ -9,7 +37,7 @@ export interface MarketHost { export function communityMarketView(market: MarketHost) { return function CommunityMarket({ view }: { readonly view: 'summary' | 'page' }): ReactNode { - if (view === 'summary') return '进入 dsh-market 插件市场,搜索并安装 DSH 社区插件。'; + if (view === 'summary') return ; return market.render(); }; } From 4cb8de378af3fbc58be0b7fb8d5bac7574e349f6 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 19:19:08 +0800 Subject: [PATCH 17/23] ui: tint community plugin discovery card --- .../src/client/components/CommunityMarket.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx b/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx index 0e358f7842..4e41855cfd 100644 --- a/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx +++ b/workdsh-web/packages/bundle/src/client/components/CommunityMarket.tsx @@ -1,9 +1,27 @@ import { useEffect, useRef, type ReactNode } from 'react'; const discoverySeenKey = 'workdsh.community-market.discovery-seen'; +const discoveryCardStyle = ` +[data-plugin-item="workdsh-community-market"] { + background: linear-gradient(90deg, + color-mix(in srgb, var(--dsw-alias-state-business-primary) 17%, transparent), + color-mix(in srgb, var(--dsw-alias-state-business-primary) 6%, transparent)); + box-shadow: inset 3px 0 0 var(--dsw-alias-state-business-primary); + outline: 1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 28%, transparent); +} +[data-plugin-item="workdsh-community-market"]:hover { + background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 21%, transparent); +} +`; function DiscoverySummary(): ReactNode { const badge = useRef(null); + useEffect(() => { + const style = document.createElement('style'); + style.textContent = discoveryCardStyle; + document.head.append(style); + return () => style.remove(); + }, []); useEffect(() => { const node = badge.current; if (!node || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; From beb5849f531e94b8d4db72c95701702b808f8fe3 Mon Sep 17 00:00:00 2001 From: techflag <562635045@qq.com> Date: Sat, 26 Sep 2026 19:49:41 +0800 Subject: [PATCH 18/23] release: prepare SkillHub and DSH market desktop alpha --- .github/workflows/ci.yml | 20 ++++----- README.i18n.yaml | 4 +- README.md | 38 +++++++++--------- README.zh-CN.md | 38 +++++++++--------- dsh-plugin-desktop/THIRD_PARTY_NOTICES.md | 17 ++++++-- dsh-plugin-desktop/package.json | 2 +- .../scripts/prepare-workdsh-runtime.mjs | 2 +- workdsh-web/README.md | 23 ++++------- workdsh-web/README.zh-CN.md | 23 ++++------- workdsh-web/RELEASE-NOTES.md | 7 ++++ .../screenshots/workdsh-dshmarket-2026-09.png | Bin 0 -> 590444 bytes .../screenshots/workdsh-skillhub-2026-09.png | Bin 0 -> 767329 bytes .../workdsh-skills-alpha8-dark.png | Bin 374956 -> 0 bytes workdsh-web/scripts/pack-project-release.mjs | 6 +-- 14 files changed, 92 insertions(+), 88 deletions(-) create mode 100644 workdsh-web/RELEASE-NOTES.md create mode 100644 workdsh-web/assets/screenshots/workdsh-dshmarket-2026-09.png create mode 100644 workdsh-web/assets/screenshots/workdsh-skillhub-2026-09.png delete mode 100644 workdsh-web/assets/screenshots/workdsh-skills-alpha8-dark.png diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f405bc024..12bdd19c49 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -284,6 +284,11 @@ jobs: node-version: 22.23.2 - name: Require one DSH version before publication run: node scripts/verify-desktop-dsh-alignment.mjs + - name: Require release tag to match Desktop package version + env: + TAG: ${{ github.ref_name }} + run: | + node -e "const version = require('./dsh-plugin-desktop/package.json').version; if (process.env.TAG !== 'desktop-v' + version) { throw new Error('Release tag does not match Desktop package version: ' + process.env.TAG + ' != desktop-v' + version) }" - name: Download desktop packages uses: actions/download-artifact@v5 with: @@ -308,17 +313,14 @@ jobs: run: | VERSION="${TAG#desktop-v}" cat > release-notes.md <<'EOF' - WorkDSH Desktop 提供 Windows x64 安装版和 macOS Intel x64 与 Apple Silicon arm64 独立 DMG。 + WorkDSH Desktop 将 WorkBuddy 风格的项目、资料、专家、技能和连接器工作台与 DeepSeek Harness 插件能力结合。 - - 桌面产品名称统一为 WorkDSH - - 安装包内置完整 WorkDSH 运行时,启动后可直接使用项目、资料库、专家、技能和连接器 - - 内置 Node 和 Python;Agent 使用 Playwright 浏览网页时,可在同一会话的右侧栏查看并操作页面 - - 应用、Dock、托盘和安装包图标统一为 WorkDSH 蓝色 W 标识 - - README 包含项目、资料库和桌面端下载说明 - - Windows 包在原生 Windows Runner 构建和验证 - - macOS 分别提供 Intel x64 和 Apple Silicon arm64 未签名预览包 + - 技能页接入 SkillHub 目录,可查看技能图标、来源、版本与安装入口;本地技能仍可单独管理。 + - 现有插件页可直接发现 DSH 社区插件,并打开第三方 dsh-market 目录进行搜索和安装。 + - Desktop 使用锁定的官方 DSH 0.1.7-rc.2 Profile;内置 Node.js 和 Python,不额外打包浏览器。 + - 提供 Windows x64 安装包,以及 macOS Intel x64、Apple Silicon arm64 未签名 DMG。 - 这是 Alpha 预发布。请在升级前备份本地配置和数据;未签名包可能触发系统安全提示。所有文件的 SHA-256 见 `SHA256SUMS`。 + SkillHub 和 dsh-market 是独立第三方来源;目录内容并非全部预装或经 WorkDSH 审核。安装前请检查许可、依赖和版本兼容性。本次为 Alpha 预发布,升级前请备份本地配置和数据。所有文件的 SHA-256 见 `SHA256SUMS`。 EOF gh release create "$TAG" release-assets/* \ --repo "$GITHUB_REPOSITORY" \ diff --git a/README.i18n.yaml b/README.i18n.yaml index 64e97dd3ad..b353f00b72 100644 --- a/README.i18n.yaml +++ b/README.i18n.yaml @@ -1,5 +1,5 @@ # Bilingual-pair consistency record: the git blob hash of each side as of the last # confirmed-consistent state. Both languages carry equal authority. Update both files # and re-record their hashes after editing either side. -README.md: 0d11f05e25608c5c34d30eb0b95d4e3212393f14 -README.zh-CN.md: 6d7025fa93be348b50387f5ebb1606c465e5bab4 +README.md: 72453b59bdb0c0cd63d5ff8058df121b6f64c35c +README.zh-CN.md: 3aabc96ad9047087264b662e1f98ba8185e8cee2 diff --git a/README.md b/README.md index 0d11f05e25..72453b59bd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@

WorkDSH logo

WorkDSH

-

A WorkBuddy-style workspace with the DSH plugin ecosystem.

-

Inspired by WorkBuddy, WorkDSH brings projects, material, experts, skills, and connectors to the desktop—and extends them through DeepSeek Harness plugins.

+

A WorkBuddy-style workspace where skills, experts, and plugins shape new workflows.

+

WorkDSH brings material, experts, skills, and connectors into one workspace, with the SkillHub catalog and installable DSH community plugins.

Download Desktop · Explore the workflow · User guide · 简体中文

-[![Desktop release](https://img.shields.io/badge/Desktop-2.0.5--alpha.21-176BFF)](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.5-alpha.21) [![GitHub stars](https://img.shields.io/github/stars/techflag/workdsh?label=stars)](https://github.com/techflag/workdsh) [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![Desktop release](https://img.shields.io/badge/Desktop-2.0.6--alpha.1-176BFF)](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.1) [![GitHub stars](https://img.shields.io/github/stars/techflag/workdsh?label=stars)](https://github.com/techflag/workdsh) [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE) ![WorkDSH projects home with project templates and the complete desktop sidebar](workdsh-web/assets/screenshots/workdsh-projects-alpha8-dark.png) @@ -12,7 +12,7 @@ ## A WorkBuddy-style experience, an open DSH ecosystem -WorkDSH takes WorkBuddy's way of organizing work as its model. It recreates the core path from projects and document references to expert and skill collaboration, while retaining DeepSeek Harness models, tools, and sessions so the desktop workspace can grow through DSH plugins. +WorkDSH draws on WorkBuddy's way of organizing projects, material, experts, skills, and connectors, and implements those pages and management features on DeepSeek Harness. DSH is a complete agent application in its own right; it composes models, tools, skill support, and UI through plugins. WorkDSH adds its workspace features through the same mechanism. | What you need to do | Where WorkDSH helps | | --- | --- | @@ -40,31 +40,31 @@ This path is WorkDSH's product direction. End-to-end validation of project docum -## WorkBuddy-style Skills meet DSH plugins +## Skills and plugins -This is the other half of WorkDSH: **the workflow draws on WorkBuddy; extensions use DSH**. These ecosystems serve different purposes: +It helps to distinguish **the content people use** from **the software extension that manages it**. A skill usually consists of instructions and resources containing `SKILL.md`; it can be installed directly into the local DSH Skills directory and is not necessarily a plugin. WorkDSH's skill manager is a DSH plugin. An expert configuration is not itself a plugin either; a WorkDSH plugin manages experts. Other DSH plugins can add tools or UI features directly. The plugin system can therefore support skills, experts, and software functions without making every skill or expert a separate plugin. -| | WorkBuddy-style Skill | DSH plugin | -| --- | --- | --- | -| What it adds | Reusable instructions, scripts, references, and resources | New tools, services, and workspace capabilities | -| How it works | Import a file or ZIP containing `SKILL.md`, review it, then confirm installation | Load and compose plugins against the current DSH version | -| In WorkDSH | Search, enable, and manage a local skill directory; migrate WorkBuddy-style and community Skills | Projects, library, experts, skills, and connectors are themselves extensions; third-party plugins can be adapted | +DSH plugins can be individual tools or combine UI, services, and other resources into a larger application scenario. For example, a data-management plugin could add data views and processing tools, then work with skills and experts across a workflow. This illustrates what the plugin model can support; it does not mean this release bundles such a data-management system. Users can discover and install skills through SkillHub and find DSH community plugins through dsh-market. Before installing a plugin, check what it provides and whether it supports the current DSH version. -WorkBuddy-style Skills come from a broad ecosystem. WorkDSH offers a compatible import path, **not a claim that every Skill is tested or preinstalled**. Skills with scripts, external services, or special dependencies need individual testing; third-party DSH plugins need version-specific validation too. [Skill management](workdsh-web/packages/plugins/skills/README.md) · [Plugin development](docs/plugin-development.en.md) · [Ecosystem manifesto](docs/plugin-ecosystem.en.md) +WorkDSH connects two independently maintained catalogs: [SkillHub](https://skillhub.cn/) for Skills and [dsh-market](https://dshmarket.com/zh/) for DSH plugins. SkillHub entries show their source and version, and open the source page for license information before installation. The DSH catalog is provided by the third-party dsh-market plugin inside the existing plugin page. Catalog entries are **not all preinstalled, reviewed, or endorsed by WorkDSH**; check each item's license, dependencies, and DSH compatibility. [Skill management](workdsh-web/packages/plugins/skills/README.md) · [Plugin development](docs/plugin-development.en.md) · [Ecosystem manifesto](docs/plugin-ecosystem.en.md) -![WorkDSH skill catalog with local entries, categories, and install actions](workdsh-web/assets/screenshots/workdsh-skills-alpha8-dark.png) +![SkillHub catalog inside WorkDSH, showing skill icons, versions, sources, and install actions](workdsh-web/assets/screenshots/workdsh-skillhub-2026-09.png) -The installable entries shown here come from the demonstration machine's local skill directory. They are neither bundled with the installer nor an officially hosted online marketplace. +Live SkillHub results; catalog size and entries change over time. The local session shown is a demonstration environment. + +![DSH community plugin catalog opened from WorkDSH's existing plugin page](workdsh-web/assets/screenshots/workdsh-dshmarket-2026-09.png) + +The third-party dsh-market plugin supplies discovery and installation. The screenshot does not imply that catalog plugins are bundled with WorkDSH. ## Download Desktop -The current public desktop installer release is **2.0.5-alpha.21**. These links point directly to files in its [GitHub Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.5-alpha.21): +The planned desktop installer release is **2.0.6-alpha.1**. Its download links will become available after the [GitHub Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.1) is published: | Platform | Download | | --- | --- | -| Windows x64 | [WorkDSH Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.5-x64-Setup.exe) | -| macOS Apple Silicon | [WorkDSH arm64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.5-arm64.dmg) | -| macOS Intel | [WorkDSH x64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.5-x64.dmg) | +| Windows x64 | [WorkDSH Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.6-alpha.1-x64-Setup.exe) | +| macOS Apple Silicon | [WorkDSH arm64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.6-alpha.1-arm64.dmg) | +| macOS Intel | [WorkDSH x64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.6-alpha.1-x64.dmg) | Desktop installers include Node.js and Python runtimes by default, so users do not need to install them separately. This is an **Alpha release**: end-to-end project document references, expert execution, and different Office formats are still being validated. The macOS DMGs are unsigned; download updates from [Releases](https://github.com/techflag/workdsh/releases). Start with the [user guide](docs/user-guide.en.md) and [FAQ](docs/faq.en.md). @@ -82,7 +82,7 @@ Run checks with `corepack yarn check`. [Contributing](CONTRIBUTING.en.md) ## Community and acknowledgements -WorkDSH builds on the open-source [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) project. Feedback and contributions: [GitHub Issues](https://github.com/techflag/workdsh/issues) · [Contributing](CONTRIBUTING.en.md) +Thanks to the maintainers and contributors of [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness), the foundation of our runtime and plugin system; [Tencent SkillHub](https://github.com/Tencent/skillhub), whose public catalog API powers skill discovery; [@cocofhu/skillhub](https://github.com/cocofhu/skillhub), the bundled DSH SkillHub plugin; and [dsh-market](https://github.com/dsh-market/dsh-market) with the [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) catalog, which power community plugin discovery. WorkBuddy inspired the workspace design. The bundled skill-creator adaptation retains its [Apache-2.0 notice](workdsh-web/packages/plugins/skills/resources/skills/workdsh-skill-creator/NOTICE.md). Feedback and contributions: [GitHub Issues](https://github.com/techflag/workdsh/issues) · [Contributing](CONTRIBUTING.en.md) WorkDSH uses the [MIT License](LICENSE). It is an independent community project and is not affiliated with, partnered with, authorized by, or endorsed by DeepSeek or WorkBuddy. Those names appear only to describe technical origins, compatibility, and design references. Upstream contributors shown on GitHub are inherited from synchronized commit history; this does not imply that they maintain this repository. diff --git a/README.zh-CN.md b/README.zh-CN.md index 6d7025fa93..3aabc96ad9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,10 +1,10 @@

WorkDSH 标志

WorkDSH

-

WorkBuddy 式工作台,连接 DSH 插件生态。

-

以 WorkBuddy 为蓝本,把项目、资料、专家、技能和连接器带到桌面;用 DeepSeek Harness 插件扩展工作能力。

+

WorkBuddy 式工作台,让技能、专家与插件组成更多工作场景。

+

WorkDSH 将资料、专家、技能和连接器带入同一工作台;接入 SkillHub 技能目录,并支持安装 DSH 社区插件。

下载桌面版 · 了解工作流 · 使用指南 · English

-[![Desktop release](https://img.shields.io/badge/Desktop-2.0.5--alpha.21-176BFF)](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.5-alpha.21) [![GitHub stars](https://img.shields.io/github/stars/techflag/workdsh?label=stars)](https://github.com/techflag/workdsh) [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE) +[![Desktop release](https://img.shields.io/badge/Desktop-2.0.6--alpha.1-176BFF)](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.1) [![GitHub stars](https://img.shields.io/github/stars/techflag/workdsh?label=stars)](https://github.com/techflag/workdsh) [![MIT License](https://img.shields.io/badge/license-MIT-green)](LICENSE) ![WorkDSH 项目主页:项目、模板与完整桌面侧栏](workdsh-web/assets/screenshots/workdsh-projects-alpha8-dark.png) @@ -12,7 +12,7 @@ ## WorkBuddy 式体验,DSH 开放生态 -WorkDSH 以 WorkBuddy 的工作方式为蓝本,复刻从项目组织、资料引用到专家和技能协作的核心路径。它同时保留 DeepSeek Harness 的模型、工具和会话能力,让熟悉的桌面工作台可以继续通过 DSH 插件扩展。 +WorkDSH 参考 WorkBuddy 按项目组织资料、专家、技能和连接器的工作方式,并在 DeepSeek Harness 上实现这些页面和管理功能。DSH 本身也是完整的 Agent 软件;它通过插件组合模型、工具、技能支持、界面等功能,WorkDSH 则在这套机制上加入自己的工作台功能。 | 你要完成的事 | WorkDSH 提供的入口 | | --- | --- | @@ -40,31 +40,31 @@ WorkDSH 以 WorkBuddy 的工作方式为蓝本,复刻从项目组织、资料 -## 面向 WorkBuddy Skill,接入 DSH 插件 +## 技能与插件生态 -这是 WorkDSH 的另一半特色:**工作方式参考 WorkBuddy,能力扩展沿用 DSH**。两种生态承担不同的角色: +这里要区分**用户使用的内容**和**实现它的软件扩展**:一个技能通常是包含 `SKILL.md` 的说明与资源,可以直接安装到本机 DSH 技能目录,它本身不一定是插件;WorkDSH 的技能管理器才是 DSH 插件。专家配置也不等于插件,管理专家的功能由 WorkDSH 插件实现。DSH 插件还可以直接增加工具或界面功能。因而插件体系能承载技能、专家及软件功能,但不能把每一个技能或专家都算成一个插件。 -| | WorkBuddy 风格 Skill | DSH 插件 | -| --- | --- | --- | -| 带来什么 | 可复用的工作说明、脚本、参考资料和资源 | 新的工具、服务与工作台能力 | -| 如何使用 | 导入包含 `SKILL.md` 的文件或 ZIP,预检后确认安装 | 按当前 DSH 版本加载和组合插件 | -| 在 WorkDSH 中 | 本地技能目录可搜索、启停和管理;可迁入 WorkBuddy 风格及社区 Skill | 项目、资料库、专家、技能、连接器本身也是扩展能力,并可适配第三方插件 | +DSH 插件可以是单项工具,也可以组合界面、服务与其他资源,形成更完整的应用场景。例如,数据管理插件可以提供数据页面和处理工具,再与技能、专家配合完成一套流程;这是插件体系允许的扩展方向,不表示当前版本已内置这样的数据管理系统。用户可以从 SkillHub 发现和安装技能,也可以通过 dsh-market 寻找 DSH 社区插件。安装前应看插件具体提供什么,以及是否兼容当前 DSH 版本。 -WorkBuddy 风格的 Skill 来源广泛,WorkDSH 提供兼容的导入入口,**不把“可导入”说成“全部已验证或预装”**。含脚本、外部服务或特殊依赖的 Skill 要逐个测试;第三方 DSH 插件也需按当前版本验证。[技能管理](workdsh-web/packages/plugins/skills/README.md) · [插件开发](docs/plugin-development.md) · [生态倡议](docs/plugin-ecosystem.md) +WorkDSH 接入两个独立维护的目录:[SkillHub](https://skillhub.cn/) 提供 Skill,[dsh-market](https://dshmarket.com/zh/) 提供 DSH 插件。SkillHub 条目展示来源和版本,许可证信息可到来源页核对;现有插件页通过第三方 dsh-market 插件打开社区目录。目录中的内容**并非全部预装、经 WorkDSH 审核或获得 WorkDSH 背书**,安装前应查看许可证、依赖和 DSH 版本兼容性。[技能管理](workdsh-web/packages/plugins/skills/README.md) · [插件开发](docs/plugin-development.md) · [生态倡议](docs/plugin-ecosystem.md) -![WorkDSH 技能市场:本地目录、分类和可安装技能](workdsh-web/assets/screenshots/workdsh-skills-alpha8-dark.png) +![WorkDSH 中的 SkillHub 技能目录:图标、版本、来源与安装入口](workdsh-web/assets/screenshots/workdsh-skillhub-2026-09.png) -截图中的可安装条目来自演示机的本地技能目录,不代表安装包自带或官方托管的在线市场。 +SkillHub 实时目录,条目和数量会变化;截图所示本地会话为演示环境。 + +![从 WorkDSH 现有插件页打开的 DSH 社区插件市场](workdsh-web/assets/screenshots/workdsh-dshmarket-2026-09.png) + +社区插件的发现和安装由第三方 dsh-market 插件提供;截图不代表目录中的插件已随 WorkDSH 安装包提供。 ## 下载桌面版 -当前公开桌面安装包为 **2.0.5-alpha.21**。以下链接直接指向 [GitHub Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.5-alpha.21) 中的文件: +计划发布的桌面安装包版本为 **2.0.6-alpha.1**。发布 [GitHub Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.1) 后,以下下载链接才会生效: | 平台 | 下载 | | --- | --- | -| Windows x64 | [WorkDSH Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.5-x64-Setup.exe) | -| macOS Apple Silicon | [WorkDSH arm64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.5-arm64.dmg) | -| macOS Intel | [WorkDSH x64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.5-x64.dmg) | +| Windows x64 | [WorkDSH Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.6-alpha.1-x64-Setup.exe) | +| macOS Apple Silicon | [WorkDSH arm64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.6-alpha.1-arm64.dmg) | +| macOS Intel | [WorkDSH x64.dmg](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.6-alpha.1-x64.dmg) | Desktop 安装包默认内置 Node.js 和 Python 运行时,普通用户无需单独安装。当前为 **Alpha 版**:项目资料引用、专家执行及不同 Office 格式的端到端体验仍在验收中。macOS DMG 未签名;更新请从 [Releases](https://github.com/techflag/workdsh/releases) 下载。开始使用前请阅读[用户指南](docs/user-guide.md)和[常见问题](docs/faq.md)。 @@ -82,7 +82,7 @@ corepack yarn dev ## 社区与致谢 -WorkDSH 基于开源项目 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 构建。反馈与参与:[GitHub Issues](https://github.com/techflag/workdsh/issues) · [参与贡献](CONTRIBUTING.md) +感谢 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 社区提供运行时与插件底座;[腾讯 SkillHub](https://github.com/Tencent/skillhub) 提供公开的技能目录 API;[@cocofhu/skillhub](https://github.com/cocofhu/skillhub) 提供随桌面版集成的 DSH SkillHub 插件;[dsh-market](https://github.com/dsh-market/dsh-market) 与 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 社区提供插件市场与目录。WorkBuddy 是工作台设计参考。内置 skill-creator 的改编保留了原 [Apache-2.0 来源说明](workdsh-web/packages/plugins/skills/resources/skills/workdsh-skill-creator/NOTICE.md)。反馈与参与:[GitHub Issues](https://github.com/techflag/workdsh/issues) · [参与贡献](CONTRIBUTING.md) WorkDSH 采用 [MIT License](LICENSE),是独立社区项目,与 DeepSeek 或 WorkBuddy 不存在隶属、合作、授权或背书关系。相关名称仅用于说明技术来源、兼容性与设计参考。GitHub Contributors 中的上游贡献者来自继承和同步的提交历史,不表示其参与本仓库维护。 diff --git a/dsh-plugin-desktop/THIRD_PARTY_NOTICES.md b/dsh-plugin-desktop/THIRD_PARTY_NOTICES.md index d9a67b28f7..22878489c3 100644 --- a/dsh-plugin-desktop/THIRD_PARTY_NOTICES.md +++ b/dsh-plugin-desktop/THIRD_PARTY_NOTICES.md @@ -11,10 +11,19 @@ third-party notices are maintained by the upstream project: WorkDSH bundles and their dependencies retain their own license terms, which must be checked from the exact release artifacts used for an installer. -The SkillHub and DSH plugin catalogue is provided by -[`@cocofhu/skillhub`](https://www.npmjs.com/package/@cocofhu/skillhub), -pinned at 0.2.16 in the Desktop Profile. It is a third-party project licensed -under MIT; its source and license are at . +The Desktop Profile bundles two separate third-party plugins: + +- [`@cocofhu/skillhub`](https://www.npmjs.com/package/@cocofhu/skillhub) + version 0.2.16 for SkillHub integration. Source and MIT license: + . +- [`dshmarket`](https://www.npmjs.com/package/dshmarket) version 1.66.1 for + DSH community plugin discovery. Source and MIT license: + . + +The SkillHub catalog and API are maintained separately by +[`Tencent/skillhub`](https://github.com/Tencent/skillhub). The plugin catalog +used by dshmarket is maintained by +[`awesome-dsh-plugin`](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin). This file intentionally does not freeze a dependency inventory from an older DSH release. Check the bundled Profile and its license files when publishing. diff --git a/dsh-plugin-desktop/package.json b/dsh-plugin-desktop/package.json index dab06232bf..aa0b403272 100644 --- a/dsh-plugin-desktop/package.json +++ b/dsh-plugin-desktop/package.json @@ -1,6 +1,6 @@ { "name": "dsh-plugin-desktop", - "version": "2.0.6", + "version": "2.0.6-alpha.1", "description": "WorkDSH Electron carrier for a pinned DeepSeek Harness runtime Profile", "license": "MIT", "publishConfig": { diff --git a/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs b/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs index 0bc142ac76..c21fce4764 100644 --- a/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs +++ b/dsh-plugin-desktop/scripts/prepare-workdsh-runtime.mjs @@ -8,7 +8,7 @@ import { DSH_VERSION } from './runtime-version.mjs' import { PRODUCT_PACKAGES, RELEASE_PACKAGES } from './workdsh-package-boundary.mjs' import { verifyPackageDshReferences, verifyProfileRelease } from './verify-profile-release.mjs' -const WORKDSH_VERSION = '0.1.0-alpha.13' +const WORKDSH_VERSION = '0.1.0-alpha.14' const desktopRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') const output = join(desktopRoot, 'build', 'workdsh-runtime') const destination = join(output, 'profiles', 'workdsh') diff --git a/workdsh-web/README.md b/workdsh-web/README.md index 69655cd50c..5a792bc3c8 100644 --- a/workdsh-web/README.md +++ b/workdsh-web/README.md @@ -5,19 +5,19 @@ WorkDSH is an open-source AI workspace built on the official DeepSeek Harness. Organize conversations, material and capabilities in projects, reuse your local Library, and review editable deliverables alongside the task. -**Desktop v2.0.5-alpha.21 · Web/plugins v0.1.0-alpha.13 · Alpha preview** +**Desktop v2.0.6-alpha.1 · Web/plugins v0.1.0-alpha.14 · Alpha preview** -[Desktop downloads](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.5-alpha.21) · [Web/plugin download](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.13) · [Release notes](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.13) · [Quick start](#quick-start) · [Website](https://techflag.github.io/workdsh/) · [Gitee mirror](https://gitee.com/techflag/workdsh) +[Desktop downloads](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.1) · [Web/plugin download](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.14) · [Release notes](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.14) · [Quick start](#quick-start) · [Website](https://techflag.github.io/workdsh/) · [Gitee mirror](https://gitee.com/techflag/workdsh) ## Download WorkDSH Desktop | System | Installer | | --- | --- | -| Windows x64 | [Download Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.5-x64-Setup.exe) | -| macOS Apple Silicon | [Download arm64 DMG](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.5-arm64.dmg) | -| macOS Intel | [Download x64 DMG](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.5-x64.dmg) | +| Windows x64 | [Download Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.6-alpha.1-x64-Setup.exe) | +| macOS Apple Silicon | [Download arm64 DMG](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.6-alpha.1-arm64.dmg) | +| macOS Intel | [Download x64 DMG](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.6-alpha.1-x64.dmg) | -These Alpha installers bundle WorkDSH v0.1.0-alpha.13 and the official Harness 0.1.7-rc.2 Profile, including Node and Python. The task browser reuses Electron; no second browser binary is bundled. macOS DMGs are unsigned previews; checksums are in the [Desktop release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.5-alpha.21). +These Alpha installers bundle WorkDSH v0.1.0-alpha.14 and the official Harness 0.1.7-rc.2 Profile, including Node and Python. The task browser reuses Electron; no second browser binary is bundled. macOS DMGs are unsigned previews; checksums are in the [Desktop release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.1). ![WorkDSH project home — full application in dark mode](assets/screenshots/workdsh-projects-alpha8-dark.png) @@ -32,7 +32,7 @@ These Alpha installers bundle WorkDSH v0.1.0-alpha.13 and the official Harness 0 | Office deliverables | Preview and edit supported document, presentation, spreadsheet, HTML and PDF working copies. Format fidelity varies. | | Activity | Inspect native task and child-agent activity; an expert-team label alone does not mean multiple agents are executing. | -The alpha.13 bundle includes **12 installable modules**, including Projects, Library and the managed browser session provider, built against the published Harness 0.1.7-rc.2 APIs. Native attachment, input and send behavior remain owned by Harness. In Desktop, Agent browser work appears in the right sidebar on the same Electron page used by its tools. +The alpha.14 bundle includes **12 installable modules**, including Projects, Library and the managed browser session provider, built against the published Harness 0.1.7-rc.2 APIs. Native attachment, input and send behavior remain owned by Harness. In Desktop, Agent browser work appears in the right sidebar on the same Electron page used by its tools. ## Screenshots @@ -45,13 +45,6 @@ Full application captures from a local workspace; example projects, installed sk -
-Skills — local installed catalog - -![WorkDSH skills — full application](assets/screenshots/workdsh-skills-alpha8-dark.png) - -
-
Office example — conversation and HTML deliverable @@ -75,7 +68,7 @@ Earlier local preview showing the artifact workflow; it is not an alpha.8 accept Requirements: Node.js `^22.19.0 || >=24.0.0`, Corepack/pnpm and the official `dsh` CLI **0.1.7-rc.2**. -1. Download all 12 `.tgz` packages, `release-manifest.json`, `SHA256SUMS` and `install-workdsh.mjs` from the [alpha.13 release](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.13) into one directory. +1. Download all 12 `.tgz` packages, `release-manifest.json`, `SHA256SUMS` and `install-workdsh.mjs` from the [alpha.14 release](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.14) into one directory. 2. For an upgrade, stop the target Profile and keep a recoverable backup of its configuration and data. 3. Run from the download directory: diff --git a/workdsh-web/README.zh-CN.md b/workdsh-web/README.zh-CN.md index d313b86dec..a5072162e2 100644 --- a/workdsh-web/README.zh-CN.md +++ b/workdsh-web/README.zh-CN.md @@ -5,19 +5,19 @@ WorkDSH 是基于官方 DeepSeek Harness 的开源 AI 工作台。把对话、资料与能力组织到项目里,复用本地资料库,在任务旁查看和编辑交付成果。 -**桌面版 v2.0.5-alpha.21 · Web/插件 v0.1.0-alpha.13 · Alpha 预览版** +**桌面版 v2.0.6-alpha.1 · Web/插件 v0.1.0-alpha.14 · Alpha 预览版** -[桌面版下载](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.5-alpha.21) · [Web/插件下载](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.13) · [更新说明](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.13) · [快速开始](#快速开始) · [官网](https://techflag.github.io/workdsh/) · [Gitee 镜像](https://gitee.com/techflag/workdsh) +[桌面版下载](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.1) · [Web/插件下载](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.14) · [更新说明](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.14) · [快速开始](#快速开始) · [官网](https://techflag.github.io/workdsh/) · [Gitee 镜像](https://gitee.com/techflag/workdsh) ## 下载 WorkDSH 桌面版 | 系统 | 安装包 | | --- | --- | -| Windows x64 | [下载 Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.5-x64-Setup.exe) | -| macOS Apple 芯片 | [下载 arm64 DMG](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.5-arm64.dmg) | -| macOS Intel | [下载 x64 DMG](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.5-alpha.21/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.5-x64.dmg) | +| Windows x64 | [下载 Setup.exe](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-windows-x64--WorkDSH-2.0.6-alpha.1-x64-Setup.exe) | +| macOS Apple 芯片 | [下载 arm64 DMG](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-macos-arm64--WorkDSH-2.0.6-alpha.1-arm64.dmg) | +| macOS Intel | [下载 x64 DMG](https://github.com/techflag/workdsh/releases/download/desktop-v2.0.6-alpha.1/dsh-plugin-desktop-macos-x64--WorkDSH-2.0.6-alpha.1-x64.dmg) | -这批 Alpha 安装包内置 WorkDSH v0.1.0-alpha.13 和官方 Harness 0.1.7-rc.2 Profile,包含 Node 与 Python。任务浏览器复用 Electron,不额外打包浏览器。macOS DMG 是未签名预览包;校验文件见[桌面版 Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.5-alpha.21)。 +这批 Alpha 安装包内置 WorkDSH v0.1.0-alpha.14 和官方 Harness 0.1.7-rc.2 Profile,包含 Node 与 Python。任务浏览器复用 Electron,不额外打包浏览器。macOS DMG 是未签名预览包;校验文件见[桌面版 Release](https://github.com/techflag/workdsh/releases/tag/desktop-v2.0.6-alpha.1)。 ![WorkDSH 深色项目主页,包含完整侧栏](assets/screenshots/workdsh-projects-alpha8-dark.png) @@ -32,7 +32,7 @@ WorkDSH 是基于官方 DeepSeek Harness 的开源 AI 工作台。把对话、 | Office 成果 | 预览和编辑支持范围内的文档、演示文稿、表格、HTML 与 PDF 工作副本;不同格式的保真范围有差异。 | | 活动记录 | 查看原生任务与子代理活动;显示专家团名称不代表多个成员已经执行。 | -alpha.13 整包包含 **12 个可安装模块**,包括项目、资料库和受管浏览器会话,并适配 Harness 0.1.7-rc.2 的公开接口。Desktop 中 Agent 操作的网页显示在右侧任务浏览器,与工具操作同一 Electron 页面。附件、输入和发送继续使用 Harness 原生能力。 +alpha.14 整包包含 **12 个可安装模块**,包括项目、资料库和受管浏览器会话,并适配 Harness 0.1.7-rc.2 的公开接口。Desktop 中 Agent 操作的网页显示在右侧任务浏览器,与工具操作同一 Electron 页面。附件、输入和发送继续使用 Harness 原生能力。 ## 页面截图 @@ -45,13 +45,6 @@ alpha.13 整包包含 **12 个可安装模块**,包括项目、资料库和受
-
-技能:本地已安装目录 - -![WorkDSH 技能页面,包含完整侧栏](assets/screenshots/workdsh-skills-alpha8-dark.png) - -
-
Office 示例:对话与 HTML 成果 @@ -75,7 +68,7 @@ alpha.13 整包包含 **12 个可安装模块**,包括项目、资料库和受 环境要求:Node.js `^22.19.0 || >=24.0.0`、Corepack/pnpm,以及官方 **0.1.7-rc.2** 版本的 `dsh` CLI。 -1. 从 [alpha.13 Release](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.13) 下载全部 12 个 `.tgz`、`release-manifest.json`、`SHA256SUMS` 和 `install-workdsh.mjs`,放入同一目录。 +1. 从 [alpha.14 Release](https://github.com/techflag/workdsh/releases/tag/v0.1.0-alpha.14) 下载全部 12 个 `.tgz`、`release-manifest.json`、`SHA256SUMS` 和 `install-workdsh.mjs`,放入同一目录。 2. 升级已有 Profile 时,先停止运行,并保留配置和数据的可恢复备份。 3. 在下载目录执行: diff --git a/workdsh-web/RELEASE-NOTES.md b/workdsh-web/RELEASE-NOTES.md new file mode 100644 index 0000000000..b8118774bb --- /dev/null +++ b/workdsh-web/RELEASE-NOTES.md @@ -0,0 +1,7 @@ +# WorkDSH Web and plugins v0.1.0-alpha.14 + +This release packages the current WorkDSH Profile for DeepSeek Harness 0.1.7-rc.2. It adds a SkillHub catalog to the skill page, with skill icons, search, pagination, source links, version display and managed installation. The existing DSH plugin page can open the separately maintained dsh-market catalog without modifying official Harness source. + +The release contains 12 WorkDSH packages, the install script, a manifest and SHA-256 checksums. SkillHub and dsh-market are third-party services or plugins; individual catalog entries are not bundled or approved by WorkDSH. Check each item's license, dependencies and DSH compatibility before installation. + +This is an Alpha release. Back up local profiles before upgrading. The desktop installer is published separately from this Web and plugin archive. diff --git a/workdsh-web/assets/screenshots/workdsh-dshmarket-2026-09.png b/workdsh-web/assets/screenshots/workdsh-dshmarket-2026-09.png new file mode 100644 index 0000000000000000000000000000000000000000..7c4dfae49baa84372418d2bfab2a9b223ac3d5a3 GIT binary patch literal 590444 zcmeFZcT|&E*FKCWqM%{}6a{oFNR18Y1aT~g5CsBKqaa19h;)*O4x@m84N+Q9P^9+` z2_i*62uQCH5JHC#(i8F>Y%}lsJ~Pbo$M?r?t#K`uHz9X&pMCbZ_O)y9Mg6lI1;hk+ zcz8DIocq;)hexo5hi9$hy0zeyur)3_!GEhf49;rvWH#@f0KfRyUe>YG)8ml=*XwxJ ztPh#Xk{S*2sJf1Za&i$~|zlSV$PrU&?+ z!#hf#pCf)*_jV6FT6{B~+}m}Ra|WNkIe3KHZEd|ZYsWUj_Lv=t!Aprsj>lW8-(E`8 zdC(dvW^?NG4)f`XtNeUxUT>Vib=7*r9`mqMe){gkkR@$rvz1d;1$A)lMAVQkilrv+ zN1M%4KF+&l-R3=7x3#uyeVZCh_${;Z^;YmJ&#LdQ|ILNgDZvdpT5zun^V1!k>3sW` zzj^!r`BOr$;JOwi694$Z`sFA1KY#z5R}zk{TE%RL{rJm&ciU~Az=H~VvcYPpx`K@UJ)wVtd*A=`?+IN=M*l+Y zim?B$4PA-j{A)v3qyT{pYs74CV{N;(L|Ssr$94=o%sN~4?v%-J$g}^Ro7L`bDLrC& zL7*AZ>wL?|vu`gh{RBQvGBjSpt;1qC;brSlR)3&dXy@GegGZ9ijFaB+OFk$X z^h+Bw%q*KN&6&z96Cui36d!iQGRCZNnPDgI@}I@&DarRB@fx&he#wA~M?1S!5sZeY zM9VU2O`{(%uE<1L3rFpBs*~`azGRV?8OA3lQvFogaCo48uJ((!R9%0G$5gZ#$tTS? z!@4{BY7WuLzoygNP294~D@-9yt?q1|T}^w6;Yf#xRq*2D%7GqT-nL{|mczhIVV?qX zal$ZG4ZXzPam?;Q4x+x+HSx^pu!SwTiXSOB4I*5Xi#I)64_A+S__fOBk-fET`6!vXwPHmQC>-cEOt4<}@ zB%Y#{j0c~!ZhjJx{*>Hqkz`)vN=hT2m!0CyF^F}1q81lD9CgqU8ng$9rG=;iHWAPz zp9|5)Q?8}k5tCXIZMp`$WH*_>;Xni~@aZi5T^fFiZ*6hyEJV4ogdcr~-iP3d74`e+ zevDW2Pr26~d)T`AzVgU}W7Z?nqruS`Gto|Gq7*D&p}9*rG)-1@tV-UkB*D(_odisUDvrXGCM?pEyk@MxdYrpQI}e1|Q4&Us0tsH=5o z3FL9X>rwHzY`p!>z>kOFe%w(NVl|G-xf0-xgFJg-L~9`tn8p^!CPmx#qF1?mF=kyWp8a?i-DfnsJBV!sz*T{Gr4PyUcBchs+Dpx$YYvq6q}| zy2qZ5UMK2h;o-(52EiX^SdJi(($6naiiaDj{w67LoPX@X#)Jt`# z%vI0^LxTiGpABHNS)zyl_a5ZhpGQ6#qcx|Av`LQh4r}fy+NOA`&${LFc+Q|7CEhME zOeuKg-pu9+K0JKj#Nf5IX})&7sHyxZ%Jj~CXJ&Uy817ryXKdf@VCnI>Z4%;#*3L)G z1zv7(xn~yju(tQw5be0VskuN?G7kz~;Mu3{jgs@DmJb{F-q(c3;Zs|aXW*>{_mM

U5~%jPB~b4H=et=yD!p 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