From d58cbc583b3daad9095a997e3eb83f5059bc67fc Mon Sep 17 00:00:00 2001 From: Aisland Date: Sun, 16 Aug 2026 23:34:22 +0800 Subject: [PATCH] =?UTF-8?q?checkpoint:=20dsh-worktable=20v2=20=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E7=89=88=EF=BC=88PRD=20=C2=A710=20=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E5=AE=9A=E6=A1=88=E5=B9=B6=E5=AE=9E=E7=8E=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 + 00_index/README.md | 10 + 01_content/build.mjs | 61 ++ 01_content/cordis.patch.yml | 4 + 01_content/dsh.plugin.json | 12 + 01_content/package-lock.json | 512 ++++++++++++ 01_content/package.json | 56 ++ 01_content/src/client/index.tsx | 736 ++++++++++++++++++ 01_content/src/client/locales.ts | 94 +++ 01_content/src/client/styles.ts | 73 ++ 01_content/src/index.ts | 24 + 02_process/PRD.md | 232 ++++++ 02_process/drafts/.gitkeep | 1 + 02_process/research/.gitkeep | 1 + 02_process/worklogs/.gitkeep | 1 + .../worklogs/2026-08-16_worktable-v2.md | 156 ++++ 04_test/.gitkeep | 1 + AGENTS.md | 28 + LICENSE | 22 + README.md | 42 + 20 files changed, 2073 insertions(+) create mode 100644 .gitignore create mode 100644 00_index/README.md create mode 100644 01_content/build.mjs create mode 100644 01_content/cordis.patch.yml create mode 100644 01_content/dsh.plugin.json create mode 100644 01_content/package-lock.json create mode 100644 01_content/package.json create mode 100644 01_content/src/client/index.tsx create mode 100644 01_content/src/client/locales.ts create mode 100644 01_content/src/client/styles.ts create mode 100644 01_content/src/index.ts create mode 100644 02_process/PRD.md create mode 100644 02_process/drafts/.gitkeep create mode 100644 02_process/research/.gitkeep create mode 100644 02_process/worklogs/.gitkeep create mode 100644 02_process/worklogs/2026-08-16_worktable-v2.md create mode 100644 04_test/.gitkeep create mode 100644 AGENTS.md create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f27e7e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +lib/ +*.log +03_local/ +.DS_Store +Thumbs.db + diff --git a/00_index/README.md b/00_index/README.md new file mode 100644 index 0000000..2e04133 --- /dev/null +++ b/00_index/README.md @@ -0,0 +1,10 @@ +# dsh-worktable 项目索引 + +- [README](../README.md) — 项目概述与目录结构 +- [PRD](../02_process/PRD.md) — 需求与设计(含接手分界线 §11) +- [AGENTS](../AGENTS.md) — 项目规则 +- [01_content](../01_content) — 插件包 +- [02_process](../02_process) — 过程文档 +- [03_local](../03_local) — 本机脚本/日志(不入库) +- [04_test](../04_test) — 测试产物 + diff --git a/01_content/build.mjs b/01_content/build.mjs new file mode 100644 index 0000000..3b93788 --- /dev/null +++ b/01_content/build.mjs @@ -0,0 +1,61 @@ +/** + * dsh-worktable 构建: + * - lib/index.js 服务端 ESM(cordis 插件,健康路由) + * - lib/client.js 客户端单文件 CJS(window.__ModuleLoader__.load 握手; + * react / @deepseek-ai/* 由宿主模块系统提供,保持 external) + * esbuild 优先用本包 node_modules 的平台二进制,其次复用相邻项目 vendor。 + */ +import { execFileSync } from 'node:child_process' +import { existsSync, mkdirSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const here = dirname(fileURLToPath(import.meta.url)) +mkdirSync(join(here, 'lib'), { recursive: true }) + +const esbuildCandidates = [ + join(here, 'node_modules', '@esbuild', 'win32-x64', 'esbuild.exe'), + join(here, '..', '..', 'dsh-travelatlas', 'node_modules', '@esbuild', 'win32-x64', 'esbuild.exe'), +] +const esbuildBin = esbuildCandidates.find((p) => existsSync(p)) +if (!esbuildBin) { + console.error('[dsh-worktable build] 找不到 esbuild.exe:请先在 01_content 运行 npm install') + process.exit(1) +} +console.log('[dsh-worktable build] esbuild: ' + esbuildBin) + +const clientBanner = { + js: "window.__ModuleLoader__.load({ id: 'dsh-worktable', factory: (require) => { var module = { exports: {} }; var exports = module.exports;", +} +const clientFooter = { js: 'return module.exports; } });' } + +const jobs = [ + { + entryPoints: [join(here, 'src/index.ts')], outfile: 'lib/index.js', + platform: 'node', format: 'esm', target: ['node22'], + external: ['@deepseek-ai/*', 'node:*'], + }, + { + entryPoints: [join(here, 'src/client/index.tsx')], outfile: 'lib/client.js', + platform: 'browser', format: 'cjs', target: ['es2022'], jsx: 'automatic', + external: ['@deepseek-ai/*', 'react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'scheduler'], + banner: clientBanner, footer: clientFooter, + }, +] + +for (const job of jobs) { + const args = [ + ...job.entryPoints, + '--bundle', '--sourcemap', '--log-level=info', + '--platform=' + job.platform, + '--format=' + job.format, + '--target=' + job.target.join(','), + '--outfile=' + join(here, job.outfile), + ...job.external.map((e) => '--external:' + e), + ] + if (job.jsx) args.push('--jsx=' + job.jsx) + if (job.banner) args.push('--banner:js=' + job.banner.js) + if (job.footer) args.push('--footer:js=' + job.footer.js) + execFileSync(esbuildBin, args, { stdio: 'inherit' }) +} +console.log('[dsh-worktable build] done: lib/index.js, lib/client.js') diff --git a/01_content/cordis.patch.yml b/01_content/cordis.patch.yml new file mode 100644 index 0000000..6273c16 --- /dev/null +++ b/01_content/cordis.patch.yml @@ -0,0 +1,4 @@ +# dsh-worktable 服务端插件:注册 /api/worktable/health 健康路由 +- insert: + - id: dsh-worktable + name: dsh-worktable diff --git a/01_content/dsh.plugin.json b/01_content/dsh.plugin.json new file mode 100644 index 0000000..c6d5db9 --- /dev/null +++ b/01_content/dsh.plugin.json @@ -0,0 +1,12 @@ +{ + "name": "dsh-worktable", + "description": "工作台:DSH 侧边栏的 agent 级项目容器,收纳如 TravelAtlas 等自装项目", + "version": "0.1.0", + "entry": { + "name": "dsh-worktable", + "inject": [] + }, + "client": { + "platform": "web" + } +} diff --git a/01_content/package-lock.json b/01_content/package-lock.json new file mode 100644 index 0000000..ff6ad2d --- /dev/null +++ b/01_content/package-lock.json @@ -0,0 +1,512 @@ +{ + "name": "dsh-worktable", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dsh-worktable", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "esbuild": "^0.25.0" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "*", + "@deepseek-ai/dsh-host-webserver": "*" + }, + "peerDependenciesMeta": { + "@deepseek-ai/cordis": { + "optional": true + }, + "@deepseek-ai/dsh-host-webserver": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + } + } +} diff --git a/01_content/package.json b/01_content/package.json new file mode 100644 index 0000000..bb753aa --- /dev/null +++ b/01_content/package.json @@ -0,0 +1,56 @@ +{ + "name": "dsh-worktable", + "description": "工作台 Worktable:DeepSeek Harness 侧边栏的 agent 级项目容器(应用抽屉)", + "version": "0.1.0", + "license": "MIT", + "type": "module", + "main": "lib/index.js", + "exports": { + ".": "./lib/index.js", + "./client": "./lib/client.js", + "./package.json": "./package.json" + }, + "files": [ + "lib", + "cordis.patch.yml", + "dsh.plugin.json", + "README.md", + "LICENSE" + ], + "dsh": { + "bundle": { + "patch": "./cordis.patch.yml" + }, + "client": { + "platform": "web", + "inject": [ + "@deepseek-ai/dsh-client-ui-slots", + "@deepseek-ai/dsh-client-ui-primitives", + "@deepseek-ai/dsh-client-locale" + ] + } + }, + "scripts": { + "build": "node build.mjs", + "check": "node --check lib/index.js && node --check lib/client.js" + }, + "peerDependencies": { + "@deepseek-ai/cordis": "*", + "@deepseek-ai/dsh-host-webserver": "*", + "@deepseek-ai/dsh-client-locale": "*" + }, + "peerDependenciesMeta": { + "@deepseek-ai/cordis": { + "optional": true + }, + "@deepseek-ai/dsh-host-webserver": { + "optional": true + }, + "@deepseek-ai/dsh-client-locale": { + "optional": true + } + }, + "devDependencies": { + "esbuild": "^0.25.0" + } +} diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx new file mode 100644 index 0000000..c80acb4 --- /dev/null +++ b/01_content/src/client/index.tsx @@ -0,0 +1,736 @@ +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { css } from './styles' +import { NS, zh, en, type WorktableKey } from './locales' + +/** + * dsh-worktable 客户端(v2):侧边栏底部「工作台」区块。 + * 结构:分隔线 → [≡ 手柄][工作台][搜索/视图选项/添加+] → 项目卡片区(子座位)。 + * v2 新增(PRD §10 定案): + * - 卡片规范 v2 渐进上报协议:owner props 下发 order/hidden/nameOverrides, + * 卡片可选上报 reportMeta/reportUsed;v1 卡片零改动兼容(按注册序排在最前)。 + * - 视图选项只留「排序:手动/最近」(分类已按用户定案取消)。 + * - 「管理项目…」编辑模式:改名/隐藏/排序(拖拽 + ↑↓),全存 localStorage。 + * - 「+」:接入指引 + 本地快捷方式(名称/图标/链接,点击新标签打开)。 + * - 完整 zh/en 词典接入 dsh-client-locale(NS 'worktable')。 + * 持久化:dsh.worktable.view.v1(视图)+ dsh.worktable.projects.v1(项目元状态)。 + */ + +type OrderBy = 'manual' | 'recent' +type DockMode = 'footer' | 'float' + +type ViewState = { + query: string + searchOpen: boolean + orderBy: OrderBy + dock: DockMode + floatTop: number | null +} + +/** 卡片上报的项目元信息(协议 v2)。 */ +export type ProjectMeta = { id: string; name: string; icon?: string } +/** 本地快捷方式(仅存 localStorage)。 */ +export type Shortcut = { id: string; name: string; icon: string; href: string } + +type ProjectsState = { + /** 手动排序的 id 序列(新注册 id 未出现在其中时追加在尾部)。 */ + order: string[] + /** id → 最近使用时间戳(毫秒),来源:卡片点击时 reportUsed。 */ + lastUsed: Record + /** 被隐藏的项目 id 集。 */ + hidden: string[] + /** 显示名覆盖(编辑模式改名;空值删除覆盖)。 */ + nameOverrides: Record + /** 本地快捷方式条目。 */ + shortcuts: Shortcut[] +} + +const PERSIST_KEY = 'dsh.worktable.view.v1' +const PROJECTS_KEY = 'dsh.worktable.projects.v1' +const MIN_TOP = 56 +const SNAP_PX = 32 +/** 插件市场外链(GitHub 仓库,已核实可访问;PRD 提及的 dshfind.com 未验证,不用死链)。 */ +const MARKET_URL = 'https://github.com/hikariming/dshfind' +/** 已报到卡片的 CSS order 偏移:未上报(order=0)的 v1 卡片永远排在已上报卡片之前。 */ +const ORDER_OFFSET = 1000 +/** 落点判定余量:松手时指针越出有效落点区(超出底部/顶部/侧边)即视为「无有效落点」。 */ +const OVER_BOTTOM_PX = 24 +const OVER_TOP_PX = 24 +const OVER_SIDE_PX = 80 + +const DEFAULT_VIEW: ViewState = { + query: '', + searchOpen: false, + orderBy: 'manual', + dock: 'footer', + floatTop: null, +} + +const DEFAULT_PROJECTS: ProjectsState = { + order: [], + lastUsed: {}, + hidden: [], + nameOverrides: {}, + shortcuts: [], +} + +function loadView(): ViewState { + try { + const raw = localStorage.getItem(PERSIST_KEY) + if (!raw) return { ...DEFAULT_VIEW } + const p = JSON.parse(raw) + // 显式挑字段:旧版遗留的 groupBy 等未知字段直接忽略。 + return { + query: typeof p.query === 'string' ? p.query : '', + searchOpen: p.searchOpen === true, + orderBy: p.orderBy === 'recent' ? 'recent' : 'manual', + dock: p.dock === 'float' ? 'float' : 'footer', + floatTop: typeof p.floatTop === 'number' ? p.floatTop : null, + } + } catch { + return { ...DEFAULT_VIEW } + } +} + +function loadProjects(): ProjectsState { + try { + const raw = localStorage.getItem(PROJECTS_KEY) + if (!raw) return { ...DEFAULT_PROJECTS } + const p = JSON.parse(raw) + return { + order: Array.isArray(p.order) ? p.order.filter((x: unknown): x is string => typeof x === 'string') : [], + lastUsed: p.lastUsed && typeof p.lastUsed === 'object' ? p.lastUsed : {}, + hidden: Array.isArray(p.hidden) ? p.hidden.filter((x: unknown): x is string => typeof x === 'string') : [], + nameOverrides: p.nameOverrides && typeof p.nameOverrides === 'object' ? p.nameOverrides : {}, + shortcuts: Array.isArray(p.shortcuts) + ? p.shortcuts.filter((s: any) => s && typeof s.id === 'string' && typeof s.name === 'string' && typeof s.href === 'string') + : [], + } + } catch { + return { ...DEFAULT_PROJECTS } + } +} + +const clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi) + +type FloatRect = { top: number } + +/** 从组件挂载点向上找 sidebar 容器:className 含 SidebarRoot/sidebar,或标签 aside/nav,到 body 为止。 */ +function findSidebar(start: HTMLElement | null): HTMLElement | null { + let el: HTMLElement | null = start + while (el && el !== document.body) { + const tag = el.tagName.toLowerCase() + if (tag === 'aside' || tag === 'nav') return el + if (typeof el.className === 'string' && /SidebarRoot|sidebar/i.test(el.className)) return el + el = el.parentElement + } + return null +} + +/** 子座位注册 id 序列(模块级 store;apply 里订阅 slots 变化写入)。 */ +const registryStore: { ids: string[]; listeners: Set<() => void> } = { ids: [], listeners: new Set() } + +function WorktableSection(props: any) { + const wide = props.wide !== false + const renderProjectSlot = typeof props.renderSlot === 'function' ? props.renderSlot : null + /** locale 座席 t;宿主未安装 locale 服务时回退 zh 词典(保持独立可用)。 */ + const t = (key: WorktableKey, params?: Record): string => { + if (typeof props.t === 'function') { + try { + return props.t(key, params) + } catch { + /* 回退 zh */ + } + } + let s: string = zh[key] ?? key + if (params) for (const [k, v] of Object.entries(params)) s = s.replace('{' + k + '}', v) + return s + } + + const [view, setView] = useState(loadView) + const [projects, setProjects] = useState(loadProjects) + const [metas, setMetas] = useState>({}) + const [registeredIds, setRegisteredIds] = useState(() => [...registryStore.ids]) + const [managing, setManaging] = useState(false) + const [addOpen, setAddOpen] = useState(false) + const [viewOptionsOpen, setViewOptionsOpen] = useState(false) + const [scName, setScName] = useState('') + const [scIcon, setScIcon] = useState('') + const [scHref, setScHref] = useState('') + const [scError, setScError] = useState(false) + const [float, setFloat] = useState(() => + view.dock === 'float' && view.floatTop != null ? { top: view.floatTop } : null, + ) + const rootRef = useRef(null) + const dragRef = useRef<{ startY: number; startX: number; startRect: DOMRect; dragging: boolean; prevFloat: FloatRect | null } | null>(null) + const dragIdRef = useRef(null) + const [railRect, setRailRect] = useState<{ left: number; width: number } | null>(null) + const [bottomInset, setBottomInset] = useState(0) + const bottomInsetRef = useRef(0) + const [floatGeo, setFloatGeo] = useState<{ left: number; width: number | null } | null>(null) + const floatRef = useRef(null) + + const persistView = (patch: Partial) => { + setView((prev) => { + const next = { ...prev, ...patch } + try { localStorage.setItem(PERSIST_KEY, JSON.stringify(next)) } catch {} + return next + }) + } + + const persistProjects = (patch: Partial | ((prev: ProjectsState) => ProjectsState)) => { + setProjects((prev) => { + const next = typeof patch === 'function' ? patch(prev) : { ...prev, ...patch } + try { localStorage.setItem(PROJECTS_KEY, JSON.stringify(next)) } catch {} + return next + }) + } + + // 子座位注册变化 → 刷新 id 序列 + useEffect(() => { + const sync = () => setRegisteredIds([...registryStore.ids]) + registryStore.listeners.add(sync) + return () => { registryStore.listeners.delete(sync) } + }, []) + + // 侧边栏折叠/展开:保持原停靠位置(不再回弹 footer);折叠态由项目图标框承载。 + // 折叠且浮动时:等折叠动画结束(320/750ms 双次重测,取收敛后的几何)再以 fixed 定位 + // 图标框到拖前高度——避免在过渡帧上测得展开态宽度导致图标框偏离窄栏中心。 + const measureRailRect = useCallback(() => { + const el = rootRef.current + if (!el) return + const r = el.getBoundingClientRect() + if (r.width > 0) { + setRailRect((prev) => (prev && Math.abs(prev.left - r.left) < 1 && Math.abs(prev.width - r.width) < 1 ? prev : { left: r.left, width: r.width })) + } + }, []) + + useLayoutEffect(() => { + if (!wide) { + const t1 = window.setTimeout(measureRailRect, 320) + const t2 = window.setTimeout(measureRailRect, 750) + return () => { window.clearTimeout(t1); window.clearTimeout(t2) } + } + }, [wide, measureRailRect]) + + // ── 悬浮窗宽度/水平定位:与 sidebar 几何联动(ResizeObserver 实时跟随,无轮询) ── + // dockWidth = sidebar 宽 − paddingLeft − paddingRight − 40(每边内缩 20) + // left = sidebar 左边缘视口坐标 + paddingLeft + 20 + // 降级:找不到 sidebar 或宽度 ≤0 → left 固定 14px,宽度不设内联(交 CSS min/max 处理) + const measureFloatGeo = useCallback(() => { + const sidebar = findSidebar(rootRef.current) + if (!sidebar) { + setFloatGeo({ left: 14, width: null }) + return + } + const rect = sidebar.getBoundingClientRect() + const cs = getComputedStyle(sidebar) + const padLeft = parseFloat(cs.paddingLeft) || 0 + const padRight = parseFloat(cs.paddingRight) || 0 + const margin = 20 + const width = rect.width - padLeft - padRight - margin * 2 + if (width > 0) { + setFloatGeo({ left: Math.round(rect.left + padLeft + margin), width: Math.round(width) }) + } else { + setFloatGeo({ left: 14, width: null }) + } + }, []) + + useLayoutEffect(() => { + measureFloatGeo() + const sidebar = findSidebar(rootRef.current) + if (!sidebar) return + const ro = new ResizeObserver(() => measureFloatGeo()) + ro.observe(sidebar) + return () => ro.disconnect() + }, [measureFloatGeo]) + + // 窗口尺寸变化 → 回弹默认停靠(浮动的列宽已失效) + useEffect(() => { + const onResize = () => { + setFloat(null); persistView({ dock: 'footer' }) + } + window.addEventListener('resize', onResize) + return () => window.removeEventListener('resize', onResize) + }, []) + + // ── 协议 v2 回调(引用稳定,避免卡片 reportMeta effect 循环)── + const reportMeta = useCallback((meta: ProjectMeta) => { + if (!meta || typeof meta.id !== 'string' || !meta.id) return + setMetas((prev) => { + const cur = prev[meta.id] + if (cur && cur.name === meta.name && cur.icon === meta.icon) return prev + return { ...prev, [meta.id]: { id: meta.id, name: typeof meta.name === 'string' ? meta.name : meta.id, icon: meta.icon } } + }) + }, []) + + const reportUsed = useCallback((id: string) => { + if (typeof id !== 'string' || !id) return + const now = Date.now() + persistProjects((prev) => { + if (prev.lastUsed[id] === now) return prev + return { ...prev, lastUsed: { ...prev.lastUsed, [id]: now } } + }) + }, []) + + // ── 有效排序 ── + // 手动:持久化 order(过滤已卸载 id)→ 新注册 id 追加尾部; + // 最近:有 lastUsed 的按时间降序在前,其余按手动序在后。 + const effectiveOrder = useMemo(() => { + const known = new Set(registeredIds) + const stored = projects.order.filter((id) => known.has(id)) + const rest = registeredIds.filter((id) => !stored.includes(id)) + let list = [...stored, ...rest] + if (view.orderBy === 'recent') { + const hot = list.filter((id) => projects.lastUsed[id] != null) + const cold = list.filter((id) => projects.lastUsed[id] == null) + hot.sort((a, b) => (projects.lastUsed[b] ?? 0) - (projects.lastUsed[a] ?? 0)) + list = [...hot, ...cold] + } + return list + }, [registeredIds, projects.order, projects.lastUsed, view.orderBy]) + + const ownerProps = { + query: view.query.trim(), + wide, + managing, + order: effectiveOrder, + hidden: projects.hidden, + nameOverrides: projects.nameOverrides, + reportMeta, + reportUsed, + } + + // ── 拖动(≡ 手柄,与 v1 相同)── + const onHandlePointerDown = (e: any) => { + const root = rootRef.current + if (!root) return + const startRect = root.getBoundingClientRect() + dragRef.current = { startY: e.clientY, startX: e.clientX, startRect, dragging: false, prevFloat: float } + try { (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId) } catch {} + setViewOptionsOpen(false); setAddOpen(false) + } + + const onHandlePointerMove = (e: any) => { + const d = dragRef.current + if (!d) return + const dy = e.clientY - d.startY + if (!d.dragging && Math.abs(dy) < 6) return + d.dragging = true + // 浮动上限按区块实际高度计算:停靠位紧邻其下方,向上拖即可自然落位 + const maxTop = Math.max(MIN_TOP, window.innerHeight - d.startRect.height - 12) + setFloat({ top: clamp(d.startRect.top + dy, MIN_TOP, maxTop) }) + } + + const onHandlePointerUp = (e: any) => { + const d = dragRef.current + dragRef.current = null + if (!d || !d.dragging) return + // 落点判定:以「松手瞬间指针位置」判断是否有有效落点(快速甩出范围 = 无落点) + const maxTop = Math.max(MIN_TOP, window.innerHeight - d.startRect.height - 12) + const rawTop = d.startRect.top + (e.clientY - d.startY) + const outOfRange = + rawTop > maxTop + OVER_BOTTOM_PX || + rawTop < MIN_TOP - OVER_TOP_PX || + e.clientX < d.startRect.left - OVER_SIDE_PX || + e.clientX > d.startRect.right + OVER_SIDE_PX + if (outOfRange) { + // 无有效落点:回归拖前位置(不持久化任何变化) + setFloat(d.prevFloat) + return + } + setFloat((cur) => { + if (!cur) return null + if (Math.abs(cur.top - d.startRect.top) < SNAP_PX) { + persistView({ dock: 'footer', floatTop: null }) + return null + } + persistView({ dock: 'float', floatTop: cur.top }) + return cur + }) + } + + const resetDock = () => { + setFloat(null); persistView({ dock: 'footer', floatTop: null }) + } + + // Esc 关闭搜索 + const onSearchKeyDown = (e: any) => { + if (e.key === 'Escape') { + persistView({ searchOpen: false, query: '' }) + } + } + + // ── 编辑模式动作 ── + const moveBy = (id: string, delta: number) => { + const list = [...effectiveOrder] + const i = list.indexOf(id) + const j = clamp(i + delta, 0, list.length - 1) + if (i < 0 || i === j) return + list.splice(i, 1) + list.splice(j, 0, id) + persistProjects({ order: list }) + } + + const moveTo = (id: string, targetId: string) => { + if (id === targetId) return + const list = [...effectiveOrder] + const from = list.indexOf(id) + const to = list.indexOf(targetId) + if (from < 0 || to < 0) return + list.splice(from, 1) + list.splice(to, 0, id) + persistProjects({ order: list }) + } + + const toggleHidden = (id: string) => { + persistProjects((prev) => { + const hidden = prev.hidden.includes(id) + ? prev.hidden.filter((x) => x !== id) + : [...prev.hidden, id] + return { ...prev, hidden } + }) + } + + const renameProject = (id: string, name: string) => { + persistProjects((prev) => { + const next = { ...prev.nameOverrides } + if (name.trim()) next[id] = name + else delete next[id] + return { ...prev, nameOverrides: next } + }) + } + + const resetProjects = () => { + persistProjects((prev) => ({ ...prev, order: [], hidden: [], nameOverrides: {} })) + } + + // ── 快捷方式 ── + const addShortcut = () => { + const name = scName.trim() + const href = scHref.trim() + if (!name || !/^https?:\/\//i.test(href)) { setScError(true); return } + const shortcut: Shortcut = { id: 'sc-' + Date.now().toString(36), name, icon: scIcon.trim() || '🔗', href } + persistProjects((prev) => ({ ...prev, shortcuts: [...prev.shortcuts, shortcut] })) + setScName(''); setScIcon(''); setScHref(''); setScError(false) + } + + const removeShortcut = (id: string) => { + persistProjects((prev) => ({ ...prev, shortcuts: prev.shortcuts.filter((s) => s.id !== id) })) + } + + const query = view.query.trim() + const queryLower = query.toLowerCase() + const visibleShortcuts = projects.shortcuts.filter((s) => + !queryLower || (s.name + ' ' + s.href).toLowerCase().includes(queryLower), + ) + + const isFloat = float != null + const floatStyle = isFloat + ? { + position: 'fixed' as const, + top: float.top, + left: floatGeo?.left ?? 14, + ...(floatGeo?.width != null ? { width: floatGeo.width } : {}), + zIndex: 70, + } + : undefined + + useEffect(() => { bottomInsetRef.current = bottomInset }, [bottomInset]) + useEffect(() => { floatRef.current = float }, [float]) + + // ── 底部悬浮面板避让 ── + // 停靠态下检测「侧边栏列内、贴近底部、且与区块自然位置重叠」的 fixed 面板 + // (如 dsh-usage 的余额 dock),把区块整体抬到面板上方,双方互不遮挡、都可调整位置。 + const measureBottomOverlay = useCallback(() => { + if (isFloat) return + const root = rootRef.current + if (!root) return + const self = root.getBoundingClientRect() + if (self.width < 10) return + const naturalBottom = self.bottom + bottomInsetRef.current + const loX = self.left - 8 + const hiX = self.right + 8 + const regionTop = window.innerHeight - 300 + let needed = 0 + const nodes = document.querySelectorAll('body *') + for (const el of nodes) { + const r = el.getBoundingClientRect() + if (r.width < 40 || r.height < 16) continue + if (r.top < regionTop) continue + if (r.left > 80) continue + if (r.right < loX || r.left > hiX) continue + const cs = getComputedStyle(el) + if (cs.position !== 'fixed') continue + if (cs.visibility === 'hidden' || Number(cs.opacity) === 0) continue + if (r.top >= naturalBottom + 4) continue + const overlap = Math.min(r.bottom, naturalBottom + 400) - r.top + if (overlap > 8) { + needed = Math.max(needed, Math.min(naturalBottom - r.top + 8, 340)) + } + } + setBottomInset((prev) => (Math.abs(prev - needed) < 2 ? prev : needed)) + }, [isFloat]) + + // 停靠/折叠变化时立即重测;停靠期间每 2s 轮询(悬浮面板自身可移动) + useEffect(() => { + if (!isFloat) measureBottomOverlay() + }, [isFloat, wide, measureBottomOverlay]) + + useEffect(() => { + const timer = window.setInterval(() => { + if (!floatRef.current) measureBottomOverlay() + }, 2000) + return () => window.clearInterval(timer) + }, [measureBottomOverlay]) + + const dockedStyle = !isFloat && bottomInset > 0 ? { marginBottom: bottomInset } : undefined + + if (!wide) { + const projectIcons = registeredIds.map((id) => metas[id]?.icon ?? '📦') + const shortcutIcons = projects.shortcuts.map((s) => s.icon) + const icons = [...projectIcons, ...shortcutIcons] + const railNames = [ + ...registeredIds.map((id) => projects.nameOverrides[id] ?? metas[id]?.name ?? id), + ...projects.shortcuts.map((s) => s.name), + ] + // 浮动态:fixed 定位到拖前高度(左/宽取折叠列实测几何);停靠态:文档流原位 + const railStyle = isFloat && railRect + ? { position: 'fixed' as const, top: float.top, left: railRect.left, width: railRect.width, zIndex: 70 } + : bottomInset > 0 ? { marginBottom: bottomInset } : undefined + return ( +
+
+
+ {icons.length > 0 + ? icons.map((icon, i) => {icon}) + : ≡} +
+
+ ) + } + + return ( +
+
+
+ + {t('title')} +
+ + + +
+
+ + {view.searchOpen && ( +
+ persistView({ query: e.target.value })} + onKeyDown={onSearchKeyDown} + /> + +
+ )} + + {addOpen && ( +
+ {t('add.guideTitle')} +

{t('add.guideBody')}

+ + {t('add.market')} ↗ + + {t('add.shortcutTitle')} +

{t('add.shortcutDesc')}

+
+ { setScName(e.target.value); setScError(false) }} /> + setScIcon(e.target.value)} /> + { setScHref(e.target.value); setScError(false) }} /> + +
+ {scError &&

{t('add.shortcutInvalid')}

} +
+ )} + + {viewOptionsOpen && ( +
+ {t('sort.label')} + + +
+ +
+ )} + + {managing && ( +
+
+ {t('manage.title')} + +
+ {effectiveOrder.map((id) => { + const meta = metas[id] + const display = projects.nameOverrides[id] ?? meta?.name ?? id + const isHidden = projects.hidden.includes(id) + return ( +
{ dragIdRef.current = id; try { e.dataTransfer.effectAllowed = 'move' } catch {} }} + onDragOver={(e: any) => { + e.preventDefault() + const dragId = dragIdRef.current + if (dragId && dragId !== id) moveTo(dragId, id) + }} + onDrop={(e: any) => e.preventDefault()} + onDragEnd={() => { dragIdRef.current = null }} + > + ≡ + {meta?.icon ?? '📦'} + renameProject(id, e.target.value)} + /> + + + +
+ ) + })} + {projects.shortcuts.map((s) => ( +
+ 🔗 + {s.icon} + {s.name} + +
+ ))} + +
+ )} + +
+ {renderProjectSlot + ? renderProjectSlot('sidebar.worktable.project', ownerProps) + :
{t('empty')}
} +
+ + {visibleShortcuts.length > 0 && ( +
+ {visibleShortcuts.map((s) => ( + + {s.icon} + {s.name} + {t('shortcut.badge')} + + ))} +
+ )} +
+ ) +} + +export const inject = ['slots', 'locale'] + +export function apply(ctx: any) { + ctx.effect(() => { + const style = document.createElement('style') + style.setAttribute('data-dsh-plugin', 'dsh-worktable') + style.textContent = css + document.head.appendChild(style) + return () => { style.remove() } + }, 'dsh-worktable: styles') + + // locale 词典(宿主 locale 服务缺席时由 t 的回退分支兜底) + ctx.effect(() => { + if (ctx.locale?.register) return ctx.locale.register(NS, { zh, en }) + }, 'dsh-worktable: dictionaries') + + // 子座位注册 id 序列跟踪(供排序/编辑模式使用) + const syncIds = () => { + const entries = ctx.slots.entries('sidebar.worktable.project') + const ids: string[] = [] + for (const entry of entries) { + const id = entry?.options?.id + if (typeof id === 'string' && id && !ids.includes(id)) ids.push(id) + } + registryStore.ids = ids + for (const fn of registryStore.listeners) fn() + } + const disposeSubscribe = ctx.slots.subscribe('sidebar.worktable.project', syncIds) + syncIds() + ctx.effect(() => disposeSubscribe, 'dsh-worktable: project registry watch') + + ctx.slots.inject('sidebar.footer.action', () => ctx.slots.register({ + name: 'sidebar.footer.action', + id: 'dsh-worktable', + order: 20, + children: { + 'sidebar.worktable.project': { + kind: 'list', + scope: 'root', + }, + }, + }, WorktableSection), 'dsh-worktable: worktable section') +} + +export { WorktableSection } diff --git a/01_content/src/client/locales.ts b/01_content/src/client/locales.ts new file mode 100644 index 0000000..10c80e4 --- /dev/null +++ b/01_content/src/client/locales.ts @@ -0,0 +1,94 @@ +/** + * dsh-worktable locale namespace(NS 'worktable'): + * 侧边栏区块、视图菜单、管理项目、添加面板、快捷方式等全部文案。 + * zh 为键集唯一来源;en 与 zh 键集完全对齐。 + */ + +/** Simplified Chinese dictionary (the key-set source of truth). */ +export const zh = { + 'title': '工作台', + 'rail.title': '工作台(展开侧边栏查看)', + 'handle.title': '按住拖动位置 · 双击复位', + 'handle.aria': '拖动工作台位置', + 'menu.search': '搜索项目', + 'menu.viewOptions': '视图选项', + 'menu.add': '添加项目', + 'search.placeholder': '搜索项目…', + 'search.close': '关闭搜索', + 'sort.label': '排序方式', + 'sort.manual': '手动', + 'sort.recent': '最近', + 'menu.manage': '管理项目…', + 'add.guideTitle': '接入新项目', + 'add.guideBody': '工作台里的项目就是 DSH 插件——插件注册一张「工作台项目卡片」即自动入驻(协议见项目 PRD §5.3)。已安装但未入驻的插件需在插件代码里完成注册。', + 'add.market': '浏览插件市场', + 'add.shortcutTitle': '本地快捷方式', + 'add.shortcutDesc': '把任意网址收进工作台,点击新标签打开(仅存本机)。', + 'add.shortcutNamePh': '名称', + 'add.shortcutIcon': '图标(emoji,可选)', + 'add.shortcutHref': '链接(http/https)', + 'add.shortcutAdd': '添加', + 'add.shortcutInvalid': '请填写名称与合法的 http/https 链接', + 'manage.title': '管理项目', + 'manage.done': '完成', + 'manage.renamePh': '显示名', + 'manage.hide': '隐藏', + 'manage.show': '显示', + 'manage.up': '上移', + 'manage.down': '下移', + 'manage.deleteShortcut': '删除快捷方式', + 'manage.reset': '恢复默认', + 'shortcut.badge': '本地', + 'empty': '暂无项目', +} satisfies Record + +/** The worktable namespace key union. */ +export type WorktableKey = keyof typeof zh + +/** English dictionary, checked complete against the zh key set. */ +export const en = { + 'title': 'Worktable', + 'rail.title': 'Worktable (expand sidebar to view)', + 'handle.title': 'Drag to move · double-click to reset', + 'handle.aria': 'Drag worktable position', + 'menu.search': 'Search projects', + 'menu.viewOptions': 'View options', + 'menu.add': 'Add project', + 'search.placeholder': 'Search projects…', + 'search.close': 'Close search', + 'sort.label': 'Sort by', + 'sort.manual': 'Manual', + 'sort.recent': 'Recent', + 'menu.manage': 'Manage projects…', + 'add.guideTitle': 'Add a project', + 'add.guideBody': 'Projects in the worktable are DSH plugins — a plugin joins by registering a worktable project card (protocol in the project PRD §5.3). Installed plugins that have not registered need to do so in their code.', + 'add.market': 'Browse plugin marketplace', + 'add.shortcutTitle': 'Local shortcut', + 'add.shortcutDesc': 'Pin any URL into the worktable; opens in a new tab (stored locally only).', + 'add.shortcutNamePh': 'Name', + 'add.shortcutIcon': 'Icon (emoji, optional)', + 'add.shortcutHref': 'URL (http/https)', + 'add.shortcutAdd': 'Add', + 'add.shortcutInvalid': 'Enter a name and a valid http/https URL', + 'manage.title': 'Manage projects', + 'manage.done': 'Done', + 'manage.renamePh': 'Display name', + 'manage.hide': 'Hide', + 'manage.show': 'Show', + 'manage.up': 'Move up', + 'manage.down': 'Move down', + 'manage.deleteShortcut': 'Delete shortcut', + 'manage.reset': 'Reset defaults', + 'shortcut.badge': 'Local', + 'empty': 'No projects yet', +} satisfies Record + +/** Locale namespace id registered under ctx.locale. */ +export const NS = 'worktable' + +declare module '@deepseek-ai/dsh-client-ui-slots' { + interface LocaleNamespaceMap { + /** The worktable sidebar copy. */ + [NS]: WorktableKey + } +} diff --git a/01_content/src/client/styles.ts b/01_content/src/client/styles.ts new file mode 100644 index 0000000..9c3a2e8 --- /dev/null +++ b/01_content/src/client/styles.ts @@ -0,0 +1,73 @@ +export const css = [ + // 区块整体 + '.dsh-wt_section{box-sizing:border-box;display:flex;flex-direction:column;gap:6px;width:100%;padding:0}', + '.dsh-wt_float{min-width:176px;max-width:264px;background:var(--dsw-alias-bg-base,#0b0e14);border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:10px;padding:8px;box-shadow:var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4))}', + '.dsh-wt_rail{pointer-events:none}', + '.dsh-wt_railBox{display:flex;flex-wrap:wrap;gap:3px;justify-content:center;padding:5px 3px;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:8px;background:var(--dsw-alias-bg-base,#0b0e14)}', + '.dsh-wt_railIcon{font-size:17px;line-height:22px}', + // 分隔线 + '.dsh-wt_divider{flex:none;height:1px;background:var(--dsw-alias-border-l1,#262b36);margin:2px 0}', + // 头部 + '.dsh-wt_header{display:flex;align-items:center;gap:6px;padding:0 2px}', + '.dsh-wt_handle{display:flex;align-items:center;justify-content:center;width:22px;height:22px;padding:0;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:13px;line-height:1;cursor:grab;touch-action:none;user-select:none;-webkit-user-select:none}', + '.dsh-wt_handle:hover{color:var(--dsw-alias-label-primary,#e6e8eb);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}', + '.dsh-wt_handle:active{cursor:grabbing}', + '.dsh-wt_handle[data-float=true]{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}', + '.dsh-wt_title{font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--dsw-alias-label-secondary,#9aa4b2);cursor:grab;user-select:none;-webkit-user-select:none;touch-action:none}', + '.dsh-wt_actions{margin-left:auto;display:flex;align-items:center;gap:2px}', + '.dsh-wt_iconBtn{display:flex;align-items:center;justify-content:center;width:24px;height:22px;padding:0;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa4b2);font-size:12px;line-height:1;cursor:pointer}', + '.dsh-wt_iconBtn:hover{color:var(--dsw-alias-label-primary,#e6e8eb);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}', + // 搜索行 + '.dsh-wt_search{display:flex;align-items:center;gap:6px;padding:3px 6px 3px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03))}', + '.dsh-wt_search input{flex:1;min-width:0;background:transparent;border:none;outline:none;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px}', + '.dsh-wt_search input::placeholder{color:var(--dsw-alias-label-tertiary,#6e7683)}', + '.dsh-wt_searchClear{display:flex;align-items:center;justify-content:center;width:18px;height:18px;padding:0;border:none;border-radius:4px;background:transparent;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:11px;cursor:pointer}', + '.dsh-wt_searchClear:hover{color:var(--dsw-alias-label-primary,#e6e8eb);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06))}', + // 菜单与提示 + '.dsh-wt_menu{position:relative;z-index:12;display:flex;flex-direction:column;gap:1px;padding:5px;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:8px;background:var(--dsw-alias-bg-base,#0b0e14);box-shadow:var(--dsw-shadow-lv2,0 8px 24px rgba(0,0,0,.4))}', + '.dsh-wt_menuLabel{font-size:10px;letter-spacing:.05em;color:var(--dsw-alias-label-tertiary,#6e7683);padding:3px 7px 1px}', + '.dsh-wt_menuItem{display:flex;align-items:center;padding:5px 8px;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:16px;cursor:pointer;text-align:left}', + '.dsh-wt_menuItem:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}', + '.dsh-wt_menuItem[data-on=true]{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}', + // 菜单分隔线 + '.dsh-wt_menuSep{height:1px;background:var(--dsw-alias-border-l1,#262b36);margin:4px 2px}', + // 添加面板 + '.dsh-wt_add{max-height:340px;overflow:auto}', + '.dsh-wt_addText{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary,#9aa4b2);padding:2px 8px 4px;margin:0}', + '.dsh-wt_addLink{color:var(--dsw-alias-state-accent-primary,#4f8ef7);text-decoration:none}', + '.dsh-wt_addForm{display:flex;flex-direction:column;gap:5px;padding:4px 8px 8px}', + '.dsh-wt_addForm input{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03));border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:6px;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;padding:5px 8px;outline:none}', + '.dsh-wt_addForm input:focus{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7)}', + '.dsh-wt_addForm .dsh-wt_addIcon{width:64px}', + '.dsh-wt_addBtn{display:flex;align-items:center;justify-content:center;padding:5px 8px;border:none;border-radius:6px;background:var(--dsw-alias-state-accent-primary,#4f8ef7);color:#fff;font:inherit;font-size:12px;line-height:16px;cursor:pointer}', + '.dsh-wt_addBtn:hover{filter:brightness(1.08)}', + '.dsh-wt_addError{font-size:11px;color:var(--dsw-alias-state-danger,#e5484d);padding:0 8px 8px;margin:0}', + // 管理项目(编辑模式) + '.dsh-wt_manage{position:relative;z-index:12;display:flex;flex-direction:column;gap:4px;padding:6px;border:1px solid var(--dsw-alias-border-l2,#3a4150);border-radius:8px;background:var(--dsw-alias-bg-base,#0b0e14)}', + '.dsh-wt_manageHead{display:flex;align-items:center;justify-content:space-between;padding:1px 2px 4px}', + '.dsh-wt_manageTitle{font-size:11px;font-weight:600;letter-spacing:.04em;color:var(--dsw-alias-label-primary,#e6e8eb)}', + '.dsh-wt_manageDone{display:flex;align-items:center;padding:2px 8px;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-state-accent-primary,#4f8ef7);font:inherit;font-size:11px;line-height:16px;cursor:pointer}', + '.dsh-wt_manageDone:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}', + '.dsh-wt_manageRow{display:flex;align-items:center;gap:6px;padding:4px 6px;border:1px solid transparent;border-radius:6px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.03))}', + '.dsh-wt_manageRow:hover{border-color:var(--dsw-alias-border-l2,#3a4150)}', + '.dsh-wt_manageRowOff{opacity:.45}', + '.dsh-wt_manageGrip{flex:none;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:12px;cursor:grab;user-select:none;-webkit-user-select:none}', + '.dsh-wt_manageIcon{flex:none;width:18px;font-size:13px;text-align:center}', + '.dsh-wt_manageInput{flex:1;min-width:0;background:transparent;border:none;outline:none;color:var(--dsw-alias-label-primary,#e6e8eb);font:inherit;font-size:12px;line-height:18px;padding:2px 0}', + '.dsh-wt_manageBtn{flex:none;display:flex;align-items:center;justify-content:center;width:20px;height:20px;padding:0;border:none;border-radius:4px;background:transparent;color:var(--dsw-alias-label-secondary,#9aa4b2);font-size:11px;line-height:1;cursor:pointer}', + '.dsh-wt_manageBtn:hover{color:var(--dsw-alias-label-primary,#e6e8eb);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06))}', + '.dsh-wt_manageScName{flex:1;min-width:0;font-size:12px;color:var(--dsw-alias-label-secondary,#9aa4b2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', + '.dsh-wt_manageReset{align-self:flex-start;margin-top:2px;padding:3px 8px;border:none;border-radius:5px;background:transparent;color:var(--dsw-alias-label-tertiary,#6e7683);font:inherit;font-size:11px;line-height:16px;cursor:pointer}', + '.dsh-wt_manageReset:hover{color:var(--dsw-alias-state-danger,#e5484d);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}', + // 快捷方式 + '.dsh-wt_shortcuts{display:flex;flex-direction:column;gap:4px}', + '.dsh-wt_shortcut{display:flex;align-items:center;gap:7px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.02));color:var(--dsw-alias-label-primary,#e6e8eb);text-decoration:none;font-size:12px;line-height:18px}', + '.dsh-wt_shortcut:hover{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}', + '.dsh-wt_shortcutIcon{flex:none;font-size:13px}', + '.dsh-wt_shortcutName{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}', + '.dsh-wt_shortcutBadge{flex:none;font-size:9px;line-height:14px;padding:0 5px;border-radius:8px;background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));color:var(--dsw-alias-label-tertiary,#6e7683)}', + // 项目区 + '.dsh-wt_projects{display:flex;flex-direction:column;gap:4px}', + '.dsh-wt_projects[data-managing=true]{opacity:.55;pointer-events:none}', + '.dsh-wt_empty{padding:8px 10px;border:1px dashed var(--dsw-alias-border-l1,#262b36);border-radius:8px;color:var(--dsw-alias-label-tertiary,#6e7683);font-size:11px;text-align:center}', +].join('\n') diff --git a/01_content/src/index.ts b/01_content/src/index.ts new file mode 100644 index 0000000..20dd40b --- /dev/null +++ b/01_content/src/index.ts @@ -0,0 +1,24 @@ +import { Context } from '@deepseek-ai/cordis' + +export const name = 'dsh-worktable' +/** 依赖 webServer 服务(web profile 由 dsh-web-app 挂载) */ +export const inject = ['webServer'] + +export const HEALTH_PATH = '/api/worktable/health' + +export function apply(ctx: Context) { + const webServer = (ctx as any).webServer + if (!webServer) { + ctx.logger?.warn('[dsh-worktable] ctx.webServer 不可用(headless profile?),跳过健康路由') + return + } + webServer.register({ + kind: 'exact', + path: HEALTH_PATH, + handler: (_req: any, res: any) => { + const body = JSON.stringify({ plugin: 'dsh-worktable', version: '0.1.0', ok: true }) + res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' }) + res.end(body) + }, + }) +} diff --git a/02_process/PRD.md b/02_process/PRD.md new file mode 100644 index 0000000..fc2216d --- /dev/null +++ b/02_process/PRD.md @@ -0,0 +1,232 @@ +# dsh-worktable(工作台)PRD + +> 版本:v0.2 草案 · 日期:2026-08-16 · 状态:v1 原型已上线;§10 待设计内容已定案并实现(v2,代码完成、待重启后 GUI 验收) +> 关联项目:dsh-travelatlas(第一个入驻项目)、上游参考 dsh-reminder(文件夹结构) + +## 1. 项目定位 + +**dsh-worktable 是 DeepSeek Harness Web GUI 的一个「工作台」容器插件**:在左侧侧边栏的「工作区」 +(官方会话/工作区浏览区)下方划出一条分隔线,其下开辟「工作台」区块,用于收纳**不同于 DSH 默认模式的 +agent 级项目**(如旅行图鉴 TravelAtlas),让用户可以像管理应用抽屉一样管理自己安装的项目。 + +- 一句话:**侧边栏里的「应用抽屉」,agent 级项目的家。** +- 与官方关系:纯增量插件,不替换、不禁用任何官方插件(与 dsh-plugin-ya-workspace-sidebar 的替换路线相反)。 + +## 2. 背景与问题 + +- DSH 的侧边栏只有「工作区」(会话浏览)与「设置」两层,没有承载用户自装项目的位置; +- 现有项目(如 dsh-travelatlas)只能挤在 `sidebar.footer.action` 底部,各自为政、无统一入口与元信息; +- 用户希望有一个与「工作区」对等的「工作台」区域,统一收纳、搜索、整理自己的 agent 级项目。 + +## 3. 目标 / 非目标 + +### 目标(v1 原型,本窗口已完成) + +- [x] 侧边栏底部(会话列表下方、设置行上方)渲染「工作台」区块:分隔线 + 标题 + 三按钮 + 项目列表; +- [x] 标题左侧 ≡ 拖动手柄:按住上下拖动整个区块,松手停靠(浮动位置持久化); +- [x] 三按钮照抄官方「工作区」头部:搜索(展开输入框过滤项目)、视图选项(分组/排序)、添加(占位符); +- [x] 项目注册协议:子座位 `sidebar.worktable.project`,任何插件注册即可入驻; +- [x] dsh-travelatlas 迁入成为第一个项目(含工作台缺席时的降级回退)。 + +### 目标(v2,§10 定案,本窗口已完成) + +- [x] 视图选项简化:取消分组方式,只留排序(手动/最近),旧 groupBy 状态忽略; +- [x] 卡片规范 v2 渐进上报协议(owner props 扩展,全部可选,v1 卡片零改动兼容); +- [x] 「管理项目…」编辑模式:显示名改名 / 隐藏 / 手动排序(拖拽 + ↑↓),持久化; +- [x] 添加(+) 真实逻辑:接入指引面板 + 本地快捷方式条目(新标签打开); +- [x] 使用埋点:卡片点击上报,「最近」排序生效; +- [x] 完整 zh/en 词典接入 dsh-client-locale(NS worktable); +- [x] dsh-travelatlas 卡片升级协议 v2(报到/埋点/排序/隐藏/改名)。 + +### 非目标(明确不做) + +- 不替换 ui-sidebar / ui-workspace / 官方任何组件; +- 不做「工作台自身的独立路由主页」——管理能力并入区块内编辑模式(见 §5.5); +- 不做项目市场/安装器(生态里已有 dsh-plugin-hub / dshfind;工作台仅提供外链)。 + +## 4. 用户故事 + +- 作为用户,我想在侧边栏一个固定的地方看到我装的 agent 级项目,而不是散落各处; +- 作为用户,我想拖动工作台区块到侧边栏里更顺手的高度,并且下次打开还记得; +- 作为用户,我想像搜索会话一样搜索我的项目; +- 作为插件作者,我想用几行代码让我的项目入驻工作台(拿到卡片 + 打开逻辑)。 + +## 5. 功能规格 + +### 5.1 区块结构(自上而下) + +``` +══════════ 分隔线 ══════════ +[≡] 工作台 [🔍][视图选项][+] +┌───────────────────────────┐ +│ 项目卡片(0..n,来自子座位) │ +└───────────────────────────┘ +``` + +- 标题文案:`工作台`(locale 键 `worktable.title`,en: `Worktable`); +- 底部悬浮面板避让:停靠态下检测侧边栏列内贴底的 fixed 面板(如 dsh-usage 余额 dock), + 与区块重叠时以 margin-bottom 整体让位到面板上方,双方互不遮挡、各自可拖动;停靠期间 2s 轮询跟随面板移动。 +- ≡ 手柄:`pointerdown` 捕获,垂直拖动 >6px 进入浮动模式(position:fixed 跟随指针,限制在侧边栏列宽内、 + 顶部不低于品牌行下沿、底部不超出设置行上沿);松手:与默认停靠位(footer 原位)距离 <32px 则回弹停靠, + 否则保持浮动位置;持久化键 `dsh.worktable.view.v1`(字段 `query/searchOpen/orderBy/dock/floatTop`,旧版 groupBy 字段忽略); + 双击 ≡ 复位到默认停靠;标题与 ≡ 均可作为拖拽手柄;浮动上限按区块实际高度计算(停靠位紧邻其下); + 松手时按指针落点判定——越出有效落点区(底部/顶部/侧边余量 24/24/80px)即回归拖前位置; +- 悬浮窗几何与 sidebar 联动(只宽度/水平定位,高度由拖拽决定):向上遍历父链识别 sidebar + (className 含 SidebarRoot/sidebar,或 aside/nav,到 body 为止);ResizeObserver 实时跟随; + dockWidth = sidebar 宽 − paddingLeft − paddingRight − 40px(每边内缩 20px); + left = sidebar 左边缘视口坐标 + paddingLeft + 20px;找不到 sidebar 或宽度 ≤0 时降级 + left 固定 14px、宽度不设内联(交 CSS min-width:176px / max-width:264px); + 侧边栏折叠/展开保持原停靠位置——折叠态以「项目图标框」(收纳所有项目 emoji)显示在拖前高度, + 展开即复原;图标框在折叠动画结束后(320/750ms 双次重测)按收敛后的折叠列几何水平居中; + 仅窗口尺寸变化时回弹 footer 停靠。 + +### 5.2 三按钮(照抄官方工作区头部,逻辑作用于项目列表) + +| 按钮 | 图标(primitives) | 行为 | +| --- | --- | --- | +| 搜索 | 🔍 | 点击展开输入行(Esc / 点 ✕ 收起);输入即过滤项目卡片与快捷方式(query 经座位 owner props 传给每个卡片,卡片自行判断是否隐藏) | +| 视图选项 | ☰ | 排序方式(手动/最近),选择即持久化到 `dsh.worktable.view.v1`;另含「管理项目…」入口(§5.5)。分组方式已按用户定案取消 | +| 添加 | + | 展开「添加项目」面板:接入指引(注册即入驻说明 + 插件市场外链)+ 本地快捷方式表单(§5.6) | + +> 官方工作区头部三按钮 = 搜索 / 视图选项(ViewOptionsMenu) / 添加工作区(+),已逆向确认。 + +### 5.3 项目注册协议(子座位,v2 卡片规范) + +工作台组件在注册 `sidebar.footer.action` 时声明子座位: + +```ts +ctx.slots.register({ + name: 'sidebar.footer.action', + id: 'dsh-worktable', + order: 20, + children: { 'sidebar.worktable.project': { kind: 'list', scope: 'root', owner: ProjectOwnerProps } }, +}, WorktableSection) +``` + +卡片注册约定:`id` 为项目唯一 id(如 `travelatlas`),`order` 为默认排序(注册序)。 + +**owner props v2(渐进上报协议,全部可选)**: + +| 字段 | 类型 | 含义与卡片行为 | +| --- | --- | --- | +| `query` | string | 当前搜索词;卡片自行判断是否返回 null | +| `wide` | boolean | 侧边栏是否展开 | +| `order` | string[] | 当前排序下的 id 序列;卡片用 `style={{ order: indexOf(自身id) + 1000 }}` 参与排序(+1000 偏移保证未上报的 v1 卡片在前) | +| `hidden` | string[] | 被隐藏的 id 集;包含自身 id 时返回 null | +| `nameOverrides` | Record | 显示名覆盖表;卡片优先显示覆盖名(编辑模式改名) | +| `managing` | boolean | 编辑模式标记(卡片可据此减弱交互) | +| `reportMeta(meta)` | 回调 | mount 时上报 `{ id, name, icon }`,供管理条渲染;回调引用稳定 | +| `reportUsed(id)` | 回调 | 点击时上报使用时间戳(「最近」排序埋点) | + +兼容性:未上报元信息的 v1 卡片零改动照常显示(按注册序排在最前),只是不参与排序/隐藏/改名。 +机制依据:列表座位渲染器输出 `display:contents` 锚点且错误边界不产生 DOM 包裹,卡片根节点即 +`.dsh-wt_projects` 的直接 flex 子项,CSS order 生效(已核实 web-react 渲染器实现)。 +参考实现:dsh-travelatlas `src/client/index.tsx` 的 `WorktableCard`。 + +### 5.4 降级回退协议(对项目插件) + +项目插件应实现:先 `ctx.slots.inject('sidebar.worktable.project', ...)` 注册工作台卡片;若工作台插件未安装 +(座位永不出现),超时(~2.5s)后回退到 `sidebar.footer.action` 注册独立入口。参考 dsh-travelatlas +`src/client/index.tsx` 的实现。 + +### 5.5 管理项目(编辑模式) + +- 入口:视图选项菜单「管理项目…」;「完成」退出(编辑状态不持久化)。 +- 管理条逐项目列出(含未上报元信息的卡片,名称回退为注册 id):≡ 拖拽排序(HTML5 drag)+ ↑↓ 按钮 + + 改名输入框 + 隐藏/显示切换(🙈/👁,隐藏后卡片区消失、管理条内可恢复)。 +- 快捷方式条目在管理条中显示并可删除(✕);「恢复默认」清空排序/隐藏/改名覆盖(保留 lastUsed 与快捷方式)。 +- 编辑模式下项目卡片区整体弱化(opacity + pointer-events:none)。 +- 所有变更写入 `dsh.worktable.projects.v1`(见 §6)。 + +### 5.6 本地快捷方式(添加面板) + +- 「+」展开添加面板:接入指引(项目=插件、注册即入驻,协议见 §5.3)+ 插件市场外链 + (https://github.com/hikariming/dshfind,已核实可访问;dshfind.com 未验证、不链死链)。 +- 快捷方式表单:名称 + 图标(emoji,可选,默认 🔗)+ 链接(校验 http/https);提交后立即出现在 + 项目卡片区下方,标「本地」角标;点击新标签打开(noopener)。 +- 快捷方式参与搜索过滤;只存 localStorage,无任何网络请求。 + +### 5.7 国际化 + +- 词典:`01_content/src/client/locales.ts`,NS `worktable`,zh 为键集唯一来源,en 全量对齐。 +- 接入方式(照 dsh-reminder):client inject `['slots','locale']`,apply 中 `ctx.locale.register(NS, { zh, en })`; + package.json `dsh.client.inject` 声明 `@deepseek-ai/dsh-client-locale`(peerDependencies 可选)。 +- 宿主 locale 服务缺席时 t 回退 zh 词典,保证工作台独立可用。 + +## 6. 架构与技术方案 + +- 插件包:`01_content/`(dsh.plugin.json + cordis.patch.yml + build.mjs,参照 dsh-travelatlas / dsh-usage); +- 服务端:最小 cordis 插件,注册 `GET /api/worktable/health`(`inject: ['webServer']`),无业务路由; +- 客户端:单文件 CJS(`window.__ModuleLoader__.load` 握手),external react / @deepseek-ai/*; + - 注入座位:`sidebar.footer.action`(order 20,位于 dsh-usage 之后); + - 服务注入:`['slots','locale']`(locale 词典见 §5.7;宿主缺席时回退 zh 词典); + - 图标为 emoji 字符(🔍/☰/+/≡); + - 排序机制:owner props 下发 order 序列,卡片以 CSS order 参与排序(渲染器 display:contents 锚点已核实,见 §5.3); + - 子座位注册跟踪:apply 中 `ctx.slots.subscribe` + `entries()` 维护模块级 id 序列; +- 持久化: + - `dsh.worktable.view.v1`:query/searchOpen/orderBy/dock/floatTop(旧 groupBy 忽略); + - `dsh.worktable.projects.v1`:order/lastUsed/hidden/nameOverrides/shortcuts; + - 卡片上报的 meta 注册表仅存内存,不持久化; +- 样式:暗色优先,跟随 `--dsw-alias-*` 设计变量(与 dsh-usage / dsh-travelatlas 一致)。 + +## 7. 与 dsh-travelatlas 的关系 + +- travelatlas 是「第一个入驻项目」,不是工作台的一部分; +- travelatlas 客户端(2026-08-16 重写):图鉴视图 = 官方 `conversation.view` 会话头标签页(iframe 到 + /travelatlas/site/),工作台卡片与降级入口点击时程序化切到该标签页;工作台缺席时回退 `sidebar.footer.action` 独立入口; +- 卡片协议 v2 已接入(2026-08-16 与并行重写冲突后被覆盖,已重新应用并构建); +- 项目图标 🌏(地球·亚洲)为 travelatlas 官方 emoji(2026-08-16 定案);工作台折叠态图标框 + 按各项目上报的自身 emoji 展示(协议 §5.3 reportMeta.icon)。 + +## 8. 隐私与安全 + +- 无个人数据采集;所有状态仅存 localStorage; +- 搜索仅在本机过滤项目名; +- 不读写任何工作区文件、不请求任何网络资源(除插件自身静态资源)。 + +## 9. 验收清单(v1) + +- [ ] 侧边栏底部出现分隔线 + 「工作台」标题 + 三按钮 + 项目卡片(🌍 旅行 Atlas); +- [ ] ≡ 拖动区块上下移动,松手停靠,刷新后位置保持;双击 ≡ 复位; +- [ ] 搜索框展开/收起正常,输入能过滤项目卡片,Esc / ✕ 可关; +- [ ] 视图选项下拉可切换分组/排序并持久化; +- [ ] 添加(+) 点击显示「待定」占位提示; +- [ ] 卸载 dsh-worktable 后,dsh-travelatlas 自动回退为底部独立入口(不白屏); +- [ ] 与 dsh-usage、dsh-reminder、官方侧边栏折叠态共存无异常。 + +### 验收清单(v2,§10 定案,待重启后 GUI 验证) + +- [ ] 视图选项菜单只含排序(手动/最近)+ 管理项目入口,选择持久化; +- [ ] 切「最近」后点击 travelatlas 卡片,卡片置顶; +- [ ] 管理项目:改名/隐藏/拖拽或 ↑↓ 排序生效,刷新保持;「恢复默认」清空排序/隐藏/改名; +- [ ] 隐藏后卡片从卡片区消失,管理条中可恢复显示; +- [ ] 「+」面板:接入指引 + 市场外链打开正常;快捷方式校验生效,添加后新标签打开、搜索可过滤、编辑模式可删除; +- [ ] 语言切 en 时工作台文案跟随(标题/菜单/面板); +- [ ] v1 兼容:未上报卡片仍显示且排在已上报卡片之前; +- [ ] travelatlas 降级回退不受影响(卸载工作台后回退底部入口)。 + +## 10. 定案记录(v2,本窗口与用户讨论后定案) + +> 以下条目原为「待设计内容」,已于 2026-08-16 与用户逐项讨论定案并实现,规格并入 §5/§6。 + +| # | 议题 | 定案 | +| --- | --- | --- | +| 1 | 工作台自身内容 | 不做独立路由页;☰ 菜单「管理项目…」→ 区块内编辑模式(改名/隐藏/排序),见 §5.5 | +| 2 | 添加(+) 逻辑 | 接入指引面板 + 本地快捷方式条目,见 §5.6 | +| 3 | 卡片规范 v2 | 渐进上报协议(owner props 扩展,全部可选),见 §5.3 | +| 4 | 分类 | **取消分类**(用户定案:每项目占一行、自成工作台,无需分组);视图菜单只留排序 | +| 5 | 国际化 | zh/en 词典接入 dsh-client-locale,见 §5.7 | + +已知边界:未上报元信息的 v1 卡片按注册序排在最前、不参与排序/隐藏/改名(协议兼容取舍); +市场外链用 GitHub 仓库 https://github.com/hikariming/dshfind(已核实可访问;dshfind.com 未验证、不链死链)。 + +## 11. 已实现 vs 待实现(接手分界线) + +| 部分 | 状态 | 位置 | +| --- | --- | --- | +| 侧边栏区块 + 三按钮 + ≡ 拖动 | ✅ v1 已实现 | `01_content/src/client/` | +| 项目子座位协议 + travelatlas 入驻 | ✅ v1 已实现 | 同上 + dsh-travelatlas/src/client | +| 服务端健康路由 | ✅ 已实现 | `01_content/src/index.ts` | +| 视图菜单去分组、编辑模式、添加面板、快捷方式、埋点、i18n | ✅ 本窗口(v2)已实现并构建,待重启后 GUI 验收 | `01_content/src/client/`(§5.3–§5.7) | +| travelatlas 卡片协议 v2 | ✅ 本窗口已实现并构建(并行重写覆盖后已重新应用) | `dsh-travelatlas/src/client/index.tsx` | + diff --git a/02_process/drafts/.gitkeep b/02_process/drafts/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/02_process/drafts/.gitkeep @@ -0,0 +1 @@ + diff --git a/02_process/research/.gitkeep b/02_process/research/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/02_process/research/.gitkeep @@ -0,0 +1 @@ + diff --git a/02_process/worklogs/.gitkeep b/02_process/worklogs/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/02_process/worklogs/.gitkeep @@ -0,0 +1 @@ + diff --git a/02_process/worklogs/2026-08-16_worktable-v2.md b/02_process/worklogs/2026-08-16_worktable-v2.md new file mode 100644 index 0000000..6a5446a --- /dev/null +++ b/02_process/worklogs/2026-08-16_worktable-v2.md @@ -0,0 +1,156 @@ +# 2026-08-16 dsh-worktable v2:PRD §10 定案并实现 + +> 日期 / 主题 / 状态:2026-08-16 · 工作台 v2(管理/添加/卡片规范 v2/排序埋点/i18n) · 代码完成,待重启后 GUI 验收 + +## 任务目标 + +按 PRD §10 与用户逐项讨论定案后实现:①管理视图(编辑模式)②添加(+) 真实逻辑 ③卡片规范 v2 +(渐进上报协议)④分类(取消)⑤i18n 接入 dsh-client-locale;并升级 dsh-travelatlas 卡片协议。 + +## 定案记录(与用户问答确认) + +1. **管理功能**:☰ 菜单「管理项目…」→ 区块内编辑模式(改名/隐藏/拖拽+↑↓排序),全存 localStorage;不做独立路由页;不做真正插件启停(宿主职责)。 +2. **添加(+)**:接入指引面板(注册即入驻说明 + 市场外链)+ 本地快捷方式条目(名称/图标/链接,点击新标签打开,标「本地」角标)。 +3. **卡片规范 v2**:owner props 扩展(query/wide/order/hidden/nameOverrides/managing/reportMeta/reportUsed),全部可选;v1 卡片零改动兼容(按注册序排最前)。 +4. **分类**:取消(用户定案:每项目占一行、自成工作台,无需分组);视图菜单只留「排序:手动/最近」;旧 groupBy 状态忽略。 +5. **i18n**:NS worktable zh/en 词典,ctx.locale.register + package.json 声明 dsh-client-locale;宿主缺席回退 zh。 +6. **市场外链**:https://github.com/hikariming/dshfind(已核实可访问;dshfind.com 未验证,不链死链)。 + +## 实际改动 + +- 工作台 `01_content/src/client/locales.ts`(新增):zh/en 词典(31 键)+ NS + 类型声明。 +- 工作台 `01_content/src/client/index.tsx`(重构): + - 视图菜单去分组,只留排序(手动/最近)+「管理项目…」; + - 管理面板:改名/隐藏/拖拽(HTML5 drag)/↑↓ 排序/恢复默认;编辑态弱化卡片区; + - 添加面板:接入指引 + 市场外链 + 快捷方式表单(http/https 校验); + - 排序:effectiveOrder(手动=持久化序+注册序补齐;最近=lastUsed 降序+其余手动序); + - 协议回调 reportMeta/reportUsed(useCallback 稳定引用)+ 模块级注册 id 跟踪 + (ctx.slots.subscribe + entries); + - 持久化 loadView/loadProjects 显式挑字段(旧 groupBy 忽略)。 +- 工作台 `01_content/src/client/styles.ts`:新增菜单分隔/添加面板/管理面板/快捷方式/编辑弱化样式;移除旧 tip 规则。 +- 工作台 `01_content/package.json`:dsh.client.inject + peerDependencies 增加 @deepseek-ai/dsh-client-locale。 +- travelatlas `src/client/index.tsx`:WorktableCard 升级协议 v2(WORKTABLE_CARD_ID、reportMeta、 + reportUsed、hidden/order/nameOverrides 消费,约 20 行);浮层/降级回退/站点未动。 +- 构建:两插件 npm run build + node --check 全部通过。 +- 健康检查:/api/worktable/health 200(旧进程)。 + +## 完成状态 + +代码实现完成;GUI 验收待重启后进行(bundle 层启动时组合)。 + +## 验证方式与结果 + +- 工作台、travelatlas 均 npm run build + node --check 通过; +- /api/worktable/health 200; +- 机制预核实(读宿主源码):列表座位渲染器 display:contents 锚点 + 错误边界无 DOM 包裹 + → 卡片根节点为 .dsh-wt_projects 直接 flex 子项,CSS order 排序可行; +- 待验证:重启后 GUI 验收清单(PRD §9 v2 项)。 + +## 尚未验证 / 待续事项 + +- 重启后的 GUI 验收(排序/编辑模式/添加面板/i18n/隐藏); +- 未上报 v1 卡片的兼容展示(当前 profile 无此类卡片,需造测试插件验证); +- 管理面板 HTML5 拖拽在侧边栏内的交互体验; +- 宿主无插件清单 API(/api/plugins 等已探测 404),「自动发现已装项目」本版不可行——PRD §10 已知边界。 + +## 后续待办 + +- 重启 dsh web(schedule-restart.ps1 已安排)后按 PRD §9 v2 清单验收; +- 观察 travelatlas 降级回退是否正常(卸载工作台场景)。 + +## 涉及文件清单 + +| 文件 | 操作 | +| --- | --- | +| dsh-worktable/01_content/src/client/locales.ts | 新增 | +| dsh-worktable/01_content/src/client/index.tsx | 重构 | +| dsh-worktable/01_content/src/client/styles.ts | 增删样式 | +| dsh-worktable/01_content/package.json | 声明 locale 依赖 | +| dsh-worktable/02_process/PRD.md | §3/§5/§6/§9/§10/§11 同步 | +| dsh-travelatlas/src/client/index.tsx | 卡片协议 v2(最小 diff) | +| 本文件 | 新增工作记录 | + +## 补记(22:30 左右,重启验收阶段) + +- 22:22 计划任务杀掉旧 dsh web 后,用户手动重启(22:22:59 起的新服务);工作台 v2 bundle + 经字节级验证已在服务器生效,用户硬刷新后确认 GUI 正常(☰ 菜单/添加面板均为 v2)。 +- **travelatlas 并行重写冲突**:22:10:44 其 src/client/index.tsx 被重写为 conversation.view + 会话标签页架构(旧全屏浮层废弃),覆盖了本窗口早前应用在旧卡片上的 v2 协议改动。 +- 处理:经用户确认后,把 v2 协议重新应用到新卡片(WorktableCard 约 20 行,不动 conversation.view + 逻辑),重新构建并通过 node --check;验证服务器实时下发新 bundle(served len=9933、 + 含 WORKTABLE_CARD_ID/reportMeta),无需再次重启。 +- **偶发排版错乱(已自愈)**:用户反馈界面排版乱、随后自行恢复。排查确认宿主资源未变、 + travelatlas 样式为局部类、3080 仅一台服务器(npx 与 bin.js 为父子进程)。推断为页面刷新时 + 恰逢 travelatlas bundle 重建落盘(22:33:17)的写入竞争或重启后页面半载状态;无需代码修复。 + 教训:对外交付前应避免「用户可刷新的时间窗」内重打包插件。 + +## 补记(拖动与折叠体验修正) + +- 需求(用户反馈):① 侧边栏折叠再展开、以及快速拖动越界松手时,不应重置到 footer, + 而应回归上一次位置;② 折叠态不应显示「≡」,应显示一个框框收纳所有项目 emoji, + 框框位置与展开前一致。 +- 实现:① 移除 wide 变化时的 dock 重置(折叠/展开保持原停靠;仅窗口 resize 仍回弹 footer); + 拖动新增越界判定(超出底部/顶部/侧边 24/24/80px 即「无有效落点」),松手回归拖前位置且不持久化; + ② 折叠态渲染 .dsh-wt_railBox:项目 emoji(无报到卡片回退 📦)+ 快捷方式图标; + 浮动态先以文档流实测折叠列几何(useLayoutEffect),再 fixed 定位到拖前高度;无项目时显示「≡」。 +- 验证:构建 + node --check 通过;服务端实时下发(served len=38441,含 railBox/overshoot 标记), + 无需重启,用户刷新后实测。 + +## 补记(与 dsh-usage 余额 dock 的共存修正) + +- 问题(用户反馈):侧边栏新出现 dsh-usage 的余额悬浮面板(.u_dock:position:fixed、 + bottom:72px、z-index:30,覆盖在侧边栏底部),遮挡了工作台停靠区与 ≡ 手柄,导致无法拖动。 +- 实现:① 停靠态自动避让——扫描侧边栏列内贴底(距底 <300px、left<80px)的 fixed 面板, + 与区块自然位置重叠时以 margin-bottom 把区块抬到面板上方(上限 340px,含多重面板取最大值); + 停靠期间每 2s 轮询跟随面板自身移动;浮动态不避让(z-index 70 天然在上)。 + ② 标题文字也作为拖拽手柄(≡ 与标题均可拖、双击复位),加大抓取面积。 + ③ 检测为通用算法(不硬编码 .u_dock 类名),不依赖 dsh-usage 内部结构。 +- 验证:构建 + node --check 通过;服务端实时下发(served len=40909,含 bottomInset/ + measureBottomOverlay 标记),无需重启,用户刷新后实测。 + +## 补记(拖动无法落位的根因修复) + +- 问题(用户反馈):能拖动,但松手必回原位。 +- 根因:避让让位后停靠位置低于「固定 380px 估算的浮动上限 maxTop」,旧越界闩锁把起始位置 + 本身判定为越界 → 每次松手都触发「回归拖前位置」。 +- 修复:① 浮动上限改为按区块实际高度计算(window.innerHeight - startRect.height - 12), + 停靠位紧邻浮动范围下沿;② 取消拖动过程越界闩锁,改为「松手瞬间按指针位置判定落点」—— + 指针在有效落点区内则落位,越出(底/顶 24px、侧边 80px)才回归拖前位置。 +- 验证:构建 + node --check 通过;服务端实时下发(served len=40969),无需重启。 + +## 补记(折叠态图标框水平偏移修复) + +- 问题(用户反馈):侧边栏收起后 emoji 图标框偏右,不在窄栏中间。 +- 根因:折叠瞬间在过渡帧上测得「展开态」的列宽/left,fixed 定位用旧几何 → 图标按旧宽度居中而偏位。 +- 修复:改为折叠动画结束后(320ms + 750ms 双次重测取收敛值)再测量并定位;测量带收敛守卫 + (差值 <1px 不重复 setState)。 +- 验证:构建 + node --check 通过;服务端实时下发(served len=41341,含 measureRailRect),无需重启。 + +## 补记(悬浮窗宽度与 sidebar 联动) + +- 需求(用户规格):悬浮窗只做宽度与水平定位的联动(高度仍由拖拽决定): + dockWidth = sidebar 宽 − paddingLeft − paddingRight − 40px(每边内缩 20px); + left = sidebar 左边缘视口坐标 + paddingLeft + 20px; + 用 ResizeObserver 监听 sidebar(拖宽/折叠/窗口缩放实时跟随); + sidebar 识别 = 从挂载点向上遍历父链,className 含 SidebarRoot/sidebar 或标签 aside/nav,到 body 为止; + 降级:找不到 sidebar 或宽度 ≤0 → left 固定 14px,宽度不设内联(CSS min-width:176px/max-width:264px)。 +- 实现:FloatRect 精简为 { top }(left/width 全部由派生几何提供);新增 findSidebar 帮助函数、 + floatGeo 状态与 measureFloatGeo(getBoundingClientRect + getComputedStyle 取 padding,取整); + useLayoutEffect 挂载首测 + ResizeObserver 订阅 + 卸载 disconnect,无轮询; + 拖动仅更新 top,落点/回弹逻辑不变;.dsh-wt_float 增加 min/max 宽度兜底。 +- 验证:构建 + node --check 通过;服务端实时下发(served len=42718,含 findSidebar/ResizeObserver)。 + +## 补记(折叠图标放大 + 项目 emoji 定案 🌏) + +- 需求(用户反馈):① 折叠态图标框里的 emoji 太小 → 放大(12px → 17px,行高 16 → 22px); + ② 旅行 Atlas 的 emoji 不是用户给的 → 经询问定案为 🌏(地球·亚洲,U+1F30F); + ③ 后续所有项目按各自上报的 emoji 展示(协议 reportMeta.icon 已支持,无需改工作台)。 +- 改动:travelatlas src/client/index.tsx 全部图标统一 🌏(卡片/报到/降级入口/分栏标题), + 重建并通过检查;worktable styles .dsh-wt_railIcon 放大。 +- 备注 1:23:21:58 dsh web 被重启过一次(非本窗口发起,疑并行窗口操作),打断了我的一步验证, + 服务恢复后健康检查正常。 +- 备注 2:travelatlas 客户端在本窗口期间被并行重写两次(会话标签页版 → 分栏模式版), + 每次重写都会覆盖 v2 协议与 emoji;本窗口已两次重新应用。 +- 备注 3(验证教训):esbuild 默认 ascii charset,非 ASCII 字符在 bundle 里以 \u{...} 转义 + 输出,且 pwsh Get-Content/IWR 按 ANSI 解码 UTF-8——校验 emoji 需用 read 工具读 UTF-8 + 或直接查转义序列(如 1F30F)。 diff --git a/04_test/.gitkeep b/04_test/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/04_test/.gitkeep @@ -0,0 +1 @@ + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..95ec454 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,28 @@ +# dsh-worktable 项目规则 + +> 工作台容器插件:侧边栏里收纳 agent 级项目的「应用抽屉」。纯增量,不替换官方插件。 + +## 边界 + +- 插件包根目录 = `01_content/`;本仓库其余目录是项目文档与本地工具。 +- **不替换、不禁用任何官方插件**(ui-sidebar / ui-workspace / ui-layout)。 +- 所有状态只存 localStorage(键 `dsh.worktable.view.v1`),不读写工作区文件。 +- dsh-travelatlas 是入驻项目而非本仓库的一部分;协议见 `02_process/PRD.md` §5.3/5.4。 + +## 构建与验证 + +```powershell +cd 01_content +npm install +npm run build # lib/index.js + lib/client.js +node --check lib/index.js +``` + +- 客户端 bundle 必须保持 `window.__ModuleLoader__.load` 握手与 external react/@deepseek-ai/*。 +- 变更视图状态结构时同步更新 PRD 的持久化说明。 + +## 安装 / 重启 + +- 注册:`dsh plugin --profile web add "link:/01_content"`(写 ~/.dsh,需用户授权)。 +- bundle 层只在启动时组合:改动后必须重启 dsh web 并刷新 GUI。 + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a664f1e --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2026 dsh-worktable contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c6a8a0 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# dsh-worktable · 工作台 + +> DeepSeek Harness 侧边栏「工作台」容器插件:在「工作区」下方开辟一个收纳 agent 级项目(如 TravelAtlas 旅行图鉴)的应用抽屉。 +> A DeepSeek Harness sidebar "worktable" container: a project drawer below the official workspace section, hosting agent-level projects (e.g. TravelAtlas). + +## 目录结构(仿 dsh-reminder) + +``` +dsh-worktable/ +├── 00_index/ # 项目导航 +├── 01_content/ # 插件包(dsh bundle:cordis 服务端 + 客户端 CJS) +├── 02_process/ # PRD.md / drafts / research / worklogs +├── 03_local/ # 本机脚本与日志(不入库) +├── 04_test/ # 测试与验证产物 +├── README.md # 本文件 +└── AGENTS.md # 项目规则 +``` + +## 当前状态 + +- v2 稳定版:侧边栏区块(拖动/停靠/折叠态项目图标框)+ 排序(手动/最近)+ 管理项目(改名/隐藏/拖拽排序)+ 添加面板(接入指引 + 本地快捷方式)+ zh/en 词典 + 使用埋点 ✅ +- 项目协议 v2:`sidebar.worktable.project` 子座位 + 渐进上报协议(reportMeta/reportUsed/order/hidden/nameOverrides,全部可选,v1 卡片兼容)✅ +- 悬浮窗几何与 sidebar 联动(ResizeObserver)、底部悬浮面板避让 ✅ +- 首个入驻项目:dsh-travelatlas(图标 🌏,协议 v2 参考实现)✅ + +## 构建与安装 + +```powershell +cd 01_content +npm install # 安装 esbuild 构建工具 +npm run build # esbuild 打包 lib/ +# 注册进本机 web profile(写入 ~/.dsh/profiles/web,需用户授权) +dsh plugin --profile web add "link:E:/AI_Workspace/DeepseekHarness/Projects/dsh-worktable/01_content" +# 重启 dsh web 后刷新 GUI 查看侧边栏底部「工作台」 +``` + +## 详见 + +- 需求与设计:`02_process/PRD.md` +- 项目规则:`AGENTS.md` +- 导航:`00_index/README.md` +