release: prepare v0.2.1 - update checker, mount notification, docs sync

This commit is contained in:
Aisland
2026-08-24 23:47:26 +08:00
parent 31b14df9c4
commit 58baa65441
12 changed files with 508 additions and 22 deletions
+16 -5
View File
@@ -18,13 +18,24 @@
## 安装
方式 A(推荐,无需 Git)——直接安装 GitHub Release 的安装包:
dsh plugin --profile web add "https://github.com/Aisland-SJL/dsh-worktable/releases/download/v0.2.0/dsh-worktable-0.2.0.tgz"
方式 B(想改源码用)——克隆仓库后用本地路径注册(`link:` 只接受本地路径,不要带空格):
git clone https://github.com/Aisland-SJL/dsh-worktable.git
dsh plugin --profile web add "link:<克隆出来的 dsh-worktable 仓库目录的绝对路径>/01_content"
两种方式 `add` 都会把 `dsh-worktable` 注册进 profile 的 bundle 列表(写入 `~/.dsh`),装完重启 dsh web、刷新界面生效。
## 从源码构建
cd 01_content
npm install
npm run build # lib/index.js + lib/client.js
node --check lib/index.js
注册到 DSH:`dsh plugin --profile web add "link:<repo>/01_content"`
## 构建注意事项
- **必须在 01_content 目录下构建**:误在仓库根跑会把 lib 写到仓库根,宿主仍加载 01_content/lib 旧 bundle(「改完不生效」假象)。
@@ -32,9 +43,9 @@
## 相关文档
- 项目规则:仓库根 AGENTS.md
- 需求与协议:02_process/PRD.md
- 工作日志:02_process/worklogs/
- 项目规则:https://github.com/Aisland-SJL/dsh-worktable/blob/main/AGENTS.md
- 需求与协议:https://github.com/Aisland-SJL/dsh-worktable/blob/main/02_process/PRD.md
- 工作日志:https://github.com/Aisland-SJL/dsh-worktable/tree/main/02_process/worklogs
## License
+3
View File
@@ -13,6 +13,7 @@ import { fileURLToPath } from 'node:url'
const here = dirname(fileURLToPath(import.meta.url))
mkdirSync(join(here, 'lib'), { recursive: true })
const pkg = JSON.parse(readFileSync(join(here, 'package.json'), 'utf8'))
const clientBanner = {
js: "window.__ModuleLoader__.load({ id: 'dsh-worktable', factory: (require) => { var module = { exports: {} }; var exports = module.exports;",
@@ -46,6 +47,8 @@ await build({
loader: { '.css': 'text' }, // xterm.css 以文本内联进样式串(宿主不加载独立 css 文件)
banner: clientBanner,
footer: clientFooter,
// 客户端自报版本(随 package.json version 走,发版即自动一致)
define: { __WT_VERSION__: JSON.stringify(pkg.version) },
})
console.log('[dsh-worktable build] done: lib/index.js, lib/client.js')
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "dsh-worktable",
"description": "工作台:DSH 侧边栏的 agent 级项目容器,收纳如 TravelAtlas 等自装项目",
"version": "0.2.0",
"version": "0.2.1",
"entry": {
"name": "dsh-worktable",
"inject": []
+228 -9
View File
@@ -7807,6 +7807,8 @@ var css = xterm_default + "\n" + [
".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_settingsClose{position:absolute;top:6px;right:6px;width:20px;height:20px;padding:0;display:flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary,#7d8aa5);font-size:13px;line-height:1;cursor:pointer}",
".dsh-wt_settingsClose:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));color:var(--dsw-alias-label-primary,#dbe4f3)}",
".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))}",
// 排序方式:一左一右两个选择按钮(手动 / 最近)
@@ -8206,7 +8208,31 @@ var css = xterm_default + "\n" + [
".dsh-wt_consoleErr{margin:0;font-size:11px;line-height:16px;color:var(--dsw-alias-state-danger,#f85149)}",
".dsh-wt_consoleCreateBtn{margin-top:2px;padding:7px 10px;border:1px solid var(--dsw-alias-state-accent-primary,#4f8ef7);border-radius:8px;background:transparent;color:var(--dsw-alias-state-accent-primary,#4f8ef7);font:inherit;font-size:12px;line-height:18px;cursor:pointer}",
".dsh-wt_consoleCreateBtn:hover:not(:disabled){background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}",
".dsh-wt_consoleCreateBtn:disabled{opacity:.55;cursor:default}"
".dsh-wt_consoleCreateBtn:disabled{opacity:.55;cursor:default}",
// ── 更新提示:侧栏标题旁徽标(仅图标 + 琥珀呼吸光)与设置面板更新卡 / 版本行 ──
".dsh-wt_updateBadge{flex:none;display:flex;align-items:center;justify-content:center;width:18px;height:18px;margin-left:3px;padding:0;border:none;border-radius:50%;background:rgba(245,185,66,.16);color:#f7d488;font-size:11px;line-height:1;cursor:pointer;position:relative}",
'.dsh-wt_updateBadge::after{content:"";position:absolute;inset:-1px;border-radius:50%;border:1px solid rgba(245,185,66,.55);animation:dsh-wt_updatePulse 1.8s ease-out infinite}',
"@keyframes dsh-wt_updatePulse{0%{opacity:.9;box-shadow:0 0 0 0 rgba(245,185,66,.5)}70%{opacity:.35;box-shadow:0 0 0 6px rgba(245,185,66,0)}100%{opacity:.9;box-shadow:0 0 0 0 rgba(245,185,66,0)}}",
".dsh-wt_updateCard{margin:0 0 10px;padding:10px;border:1px solid rgba(245,185,66,.45);border-radius:10px;background:rgba(245,185,66,.08);display:flex;flex-direction:column;gap:6px}",
".dsh-wt_updateHead{display:flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;color:#f7d488}",
".dsh-wt_updateDot{width:7px;height:7px;border-radius:50%;background:#f5b942;animation:dsh-wt_updatePulse 1.8s ease-out infinite}",
".dsh-wt_updateVers{font-size:11.5px;color:var(--dsw-alias-label-tertiary,#7d8aa5)}",
".dsh-wt_updateNotes{max-height:96px;overflow:auto;font-size:11px;line-height:1.6;color:var(--dsw-alias-label-tertiary,#7d8aa5);white-space:pre-wrap;word-break:break-all}",
".dsh-wt_updateCmd{margin:2px 0;padding:7px 8px;background:var(--dsw-alias-bg-base,#0b0e14);border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;font-family:Consolas,monospace;font-size:10.5px;line-height:1.5;color:#9cc6ff;word-break:break-all}",
".dsh-wt_updateBtns{display:flex;gap:6px;flex-wrap:wrap}",
".dsh-wt_updateBtn{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:7px;border:1px solid var(--dsw-alias-border-l1,#262b36);background:transparent;color:var(--dsw-alias-label-secondary,#aab6cd);font-size:11px;cursor:pointer}",
".dsh-wt_updateBtn svg{width:12px;height:12px;flex:none}",
".dsh-wt_updateBadge svg{width:12px;height:12px;flex:none}",
".dsh-wt_updateBtn:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06))}",
".dsh-wt_updateBtnCopy{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:rgba(79,142,247,.18);color:#cfe0ff}",
".dsh-wt_updateHint{font-size:10.5px;color:var(--dsw-alias-label-tertiary,#7d8aa5)}",
".dsh-wt_versionRow{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l1,#262b36);display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--dsw-alias-label-tertiary,#7d8aa5)}",
".dsh-wt_updateToggle{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none}",
".dsh-wt_versionActions{display:flex;align-items:center;gap:8px}",
".dsh-wt_updateSwitch{position:relative;width:26px;height:14px;border-radius:999px;background:var(--dsw-alias-state-accent-primary,#4f8ef7);cursor:pointer}",
'.dsh-wt_updateSwitch::after{content:"";position:absolute;top:2px;right:2px;width:10px;height:10px;border-radius:50%;background:#fff;transition:right .15s}',
".dsh-wt_updateSwitch[data-off=true]{background:var(--dsw-alias-border-l1,#333a48)}",
".dsh-wt_updateSwitch[data-off=true]::after{right:14px}"
].join("\n");
// src/client/locales.ts
@@ -8365,7 +8391,21 @@ var zh = {
"split.dragSwap": "\u62D6\u52A8\u6807\u9898\u680F\u4E0E\u5176\u4ED6\u7A97\u4EA4\u6362\u4F4D\u7F6E",
"icons.title": "\u9009\u62E9\u56FE\u6807",
"icons.change": "\u66F4\u6362\u56FE\u6807",
"empty": "\u6682\u65E0\u9879\u76EE"
"empty": "\u6682\u65E0\u9879\u76EE",
"update.badgeTitle": "\u6709\u65B0\u7248\u672C\u53EF\u7528",
"update.available": "\u65B0\u7248\u672C\u53EF\u7528",
"update.current": "\u5F53\u524D\u7248\u672C",
"update.copyAi": "\u590D\u5236 AI \u63D0\u793A\u8BCD",
"update.skip": "\u5FFD\u7565\u6B64\u7248\u672C",
"update.autoCheck": "\u81EA\u52A8\u68C0\u67E5\u66F4\u65B0",
"update.checkNow": "\u7ACB\u5373\u68C0\u67E5",
"update.checkFail": "\u4E0A\u6B21\u68C0\u67E5\u672A\u6210\u529F",
"update.checking": "\u68C0\u67E5\u4E2D\u2026",
"update.upToDate": "\u5DF2\u662F\u6700\u65B0\u7248\u672C",
"update.copied": "\u5DF2\u590D\u5236",
"update.copyFail": "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u624B\u52A8\u9009\u62E9\u6587\u672C",
"update.skipDone": "\u5DF2\u5FFD\u7565\u6B64\u7248\u672C\uFF08\u4E0B\u4E00\u7248\u53D1\u5E03\u65F6\u518D\u63D0\u9192\uFF09",
"update.upgradeHint": "\u6267\u884C\u547D\u4EE4\u540E\u9700\u91CD\u542F dsh web \u5E76\u5237\u65B0\u9875\u9762"
};
var en = {
"title": "Worktable",
@@ -8522,7 +8562,21 @@ var en = {
"split.dragSwap": "Drag the title bar to swap panes",
"icons.title": "Pick an icon",
"icons.change": "Change icon",
"empty": "No projects yet"
"empty": "No projects yet",
"update.badgeTitle": "New version available",
"update.available": "New version available",
"update.current": "Current version",
"update.copyAi": "Copy AI prompt",
"update.skip": "Skip this version",
"update.autoCheck": "Auto-check updates",
"update.checkNow": "Check now",
"update.checkFail": "Last check failed",
"update.checking": "Checking\u2026",
"update.upToDate": "Up to date",
"update.copied": "Copied",
"update.copyFail": "Copy failed \u2014 select the text manually",
"update.skipDone": "Skipped (will remind on the next release)",
"update.upgradeHint": "Run the command, then restart dsh web and refresh"
};
var NS = "worktable";
@@ -18212,6 +18266,40 @@ try {
// src/client/index.tsx
var import_jsx_runtime2 = require("react/jsx-runtime");
var LOCAL_VERSION = false ? "0.2.0" : "0.2.1";
var UPDATE_REPO = "Aisland-SJL/dsh-worktable";
var UPGRADE_CMD = 'dsh plugin --profile web add "https://github.com/Aisland-SJL/dsh-worktable/releases/latest/download/dsh-worktable.tgz"';
var UPGRADE_AI = "\u5E2E\u6211\u5347\u7EA7 dsh-worktable\uFF1A\u6267\u884C " + UPGRADE_CMD + "\uFF0C\u5B8C\u6210\u540E\u63D0\u9192\u6211\u91CD\u542F dsh web \u5E76\u5237\u65B0\u9875\u9762";
var ICON_SYNC = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { viewBox: "0 0 16 16", "aria-hidden": true, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", d: "M1.5 8a6.5 6.5 0 0 1 11.1-4.6L14.5 5" }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", d: "M14.5 1.5V5h-3.5" }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", d: "M14.5 8a6.5 6.5 0 0 1-11.1 4.6L1.5 11" }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", d: "M1.5 14.5V11h3.5" })
] });
var ICON_SPARK = /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("svg", { viewBox: "0 0 16 16", "aria-hidden": true, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M8 1.6l1.5 3.9 3.9 1.5-3.9 1.5L8 12.4 6.5 8.5 2.6 7l3.9-1.5z", fill: "none", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M12.6 11.2l.5 1.3 1.3.5-1.3.5-.5 1.3-.5-1.3-1.3-.5 1.3-.5z", fill: "currentColor" })
] });
function cmpVer(a, b) {
const pa = a.split(".").map(Number);
const pb = b.split(".").map(Number);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const x = pa[i] ?? 0;
const y = pb[i] ?? 0;
if (x !== y) return x - y;
}
return 0;
}
async function copyText(text2) {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text2);
return true;
}
} catch {
}
return false;
}
var PERSIST_KEY2 = "dsh.worktable.view.v1";
var PROJECTS_KEY = "dsh.worktable.projects.v1";
var MIN_TOP = 56;
@@ -19097,6 +19185,62 @@ function WorktableSection(props) {
const [registeredIds, setRegisteredIds] = (0, import_react2.useState)(() => [...registryStore.ids]);
const [addOpen, setAddOpen] = (0, import_react2.useState)(false);
const [viewOptionsOpen, setViewOptionsOpen] = (0, import_react2.useState)(false);
const [updateInfo, setUpdateInfo] = (0, import_react2.useState)(null);
const [updateCheckOn, setUpdateCheckOn] = (0, import_react2.useState)(() => localStorage.getItem("dsh.worktable.updateCheck.v1") !== "0");
const [updateCopied, setUpdateCopied] = (0, import_react2.useState)(false);
const [updateStatus, setUpdateStatus] = (0, import_react2.useState)("idle");
const checkUpdates = (0, import_react2.useCallback)(async (force = false) => {
const last = Number(localStorage.getItem("dsh.worktable.lastUpdateCheck.v1") ?? "0");
if (!force && Date.now() - last < 24 * 3600 * 1e3) return;
setUpdateStatus("checking");
let d = null;
for (let attempt = 0; attempt < 3 && !d; attempt++) {
try {
const r = await fetch("https://api.github.com/repos/" + UPDATE_REPO + "/releases/latest", { headers: { Accept: "application/vnd.github+json" }, cache: "no-store" });
if (r.ok) d = await r.json();
else if (r.status === 403 || r.status === 404) break;
} catch {
}
}
if (!d) {
setUpdateStatus("failed");
return;
}
localStorage.setItem("dsh.worktable.lastUpdateCheck.v1", String(Date.now()));
const tag = (d.tag_name ?? "").replace(/^v/, "");
if (!tag || cmpVer(tag, LOCAL_VERSION) <= 0) {
setUpdateStatus("uptodate");
return;
}
if ((localStorage.getItem("dsh.worktable.skipVersion.v1") ?? "") === tag) {
setUpdateStatus("uptodate");
return;
}
setUpdateInfo({ latest: tag, notes: (d.body ?? "").slice(0, 800), url: d.html_url ?? "" });
setUpdateStatus("uptodate");
}, []);
(0, import_react2.useEffect)(() => {
if (updateCheckOn) void checkUpdates();
}, [updateCheckOn, checkUpdates]);
const copyUpgradeAi = async () => {
const ok = await copyText(UPGRADE_AI);
if (ok) {
setUpdateCopied(true);
setTimeout(() => setUpdateCopied(false), 2200);
}
};
const skipUpdate = () => {
if (updateInfo) {
localStorage.setItem("dsh.worktable.skipVersion.v1", updateInfo.latest);
setUpdateInfo(null);
}
};
const toggleUpdateCheck = () => {
setUpdateCheckOn((v) => {
localStorage.setItem("dsh.worktable.updateCheck.v1", v ? "0" : "1");
return !v;
});
};
const [wsPreset, setWsPreset] = (0, import_react2.useState)("2h");
const [wsName, setWsName] = (0, import_react2.useState)("");
const [wsError, setWsError] = (0, import_react2.useState)(false);
@@ -20349,6 +20493,21 @@ function WorktableSection(props) {
const sectionTop = rootRef.current?.getBoundingClientRect().top ?? 100;
const popLeft = sidebarRight != null ? Math.min(sidebarRight + 8, Math.max(16, window.innerWidth - 344)) : 16;
const popTop = clamp2(sectionTop, MIN_TOP, Math.max(MIN_TOP, window.innerHeight - 540));
const POP_BOTTOM_MARGIN = 12;
const settingsRef = (0, import_react2.useRef)(null);
const [settingsTop, setSettingsTop] = (0, import_react2.useState)(null);
(0, import_react2.useLayoutEffect)(() => {
const recalc = () => {
if (!viewOptionsOpen || !settingsRef.current) return;
const h = settingsRef.current.offsetHeight;
const rawTop = Math.max(MIN_TOP, sectionTop);
const maxTop = Math.max(MIN_TOP, window.innerHeight - h - POP_BOTTOM_MARGIN);
setSettingsTop(Math.min(rawTop, maxTop));
};
recalc();
window.addEventListener("resize", recalc);
return () => window.removeEventListener("resize", recalc);
}, [viewOptionsOpen, updateInfo, sectionTop]);
const openRailProject = (id) => {
const pr = projectsRef.current.projects;
const layout = pr.layouts.find((l) => l.id === id);
@@ -20426,6 +20585,17 @@ function WorktableSection(props) {
children: t("title")
}
),
updateInfo && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"button",
{
type: "button",
className: "dsh-wt_updateBadge",
title: t("update.badgeTitle") + " \xB7 v" + updateInfo.latest,
"aria-label": t("update.badgeTitle"),
onClick: () => setViewOptionsOpen(true),
children: ICON_SYNC
}
),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_actions", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"button",
@@ -20585,7 +20755,44 @@ function WorktableSection(props) {
] })
] }),
viewOptionsOpen && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-wt_popBackdrop", onClick: () => setViewOptionsOpen(false) }),
viewOptionsOpen && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_manage dsh-wt_pop dsh-wt_settings", style: { position: "fixed", left: popLeft, top: popTop, width: 280, zIndex: 80 }, children: [
viewOptionsOpen && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { ref: settingsRef, className: "dsh-wt_manage dsh-wt_pop dsh-wt_settings", style: { position: "fixed", left: popLeft, top: settingsTop ?? popTop, width: 280, zIndex: 80 }, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
"button",
{
type: "button",
className: "dsh-wt_settingsClose",
"aria-label": t("manage.done"),
title: t("manage.done"),
onClick: () => setViewOptionsOpen(false),
children: "\u2715"
}
),
updateInfo && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_updateCard", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_updateHead", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-wt_updateDot" }),
t("update.available"),
" \xB7 v",
updateInfo.latest
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_updateVers", children: [
t("update.current"),
" v",
LOCAL_VERSION,
" \u2192 v",
updateInfo.latest
] }),
updateInfo.notes && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-wt_updateNotes", children: updateInfo.notes }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-wt_updateCmd", children: UPGRADE_CMD }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_updateBtns", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "dsh-wt_updateBtn dsh-wt_updateBtnCopy", onClick: () => void copyUpgradeAi(), children: updateCopied ? "\u2713 " + t("update.copied") : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
ICON_SPARK,
" ",
t("update.copyAi")
] }) }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "dsh-wt_updateBtn", onClick: skipUpdate, children: t("update.skip") })
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-wt_updateHint", children: t("update.upgradeHint") })
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-wt_manageHead", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-wt_manageTitle", children: t("sort.label") }) }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_sortRow", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -20610,10 +20817,7 @@ function WorktableSection(props) {
)
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-wt_menuSep" }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_manageHead", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-wt_manageTitle", children: t("manage.title") }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "dsh-wt_manageDone", onClick: () => setViewOptionsOpen(false), children: t("manage.done") })
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-wt_manageHead", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-wt_manageTitle", children: t("manage.title") }) }),
effectiveOrder.map((id) => {
const meta = metas[id];
const layout = projects.layouts.find((l) => l.id === id);
@@ -20705,7 +20909,22 @@ function WorktableSection(props) {
),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-wt_manageScName", children: s.name }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "dsh-wt_manageBtn", title: t("manage.deleteShortcut"), onClick: () => askDelete("shortcut", s.id, s.name), children: "\u2715" })
] }, s.id))
] }, s.id)),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_versionRow", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
"v",
LOCAL_VERSION,
updateStatus === "uptodate" && !updateInfo ? " \xB7 " + t("update.upToDate") : "",
updateStatus === "failed" && !updateInfo ? " \xB7 " + t("update.checkFail") : ""
] }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsh-wt_versionActions", children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { type: "button", className: "dsh-wt_updateBtn", onClick: () => void checkUpdates(true), children: updateStatus === "checking" ? t("update.checking") : t("update.checkNow") }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsh-wt_updateToggle", onClick: toggleUpdateCheck, children: [
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: t("update.autoCheck") }),
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-wt_updateSwitch", "data-off": updateCheckOn ? void 0 : "true" })
] })
] })
] })
] }),
viewPickFor && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-wt_popBackdrop", style: { zIndex: 81 }, onClick: () => setViewPickFor(null) }),
viewPickFor && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-wt_menu dsh-wt_pop", style: { position: "fixed", left: popLeft, top: popTop, width: 320, zIndex: 82 }, children: [
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "dsh-worktable",
"description": "工作台 Worktable:DeepSeek Harness 侧边栏的 agent 级项目容器(应用抽屉)",
"version": "0.2.0",
"version": "0.2.1",
"license": "MIT",
"type": "module",
"main": "lib/index.js",
+146 -2
View File
@@ -19,6 +19,45 @@ import { splitStore, SplitWorkspace, setSplitT, setSplitEnv, type LayoutSpec, ty
type OrderBy = 'manual' | 'recent'
type DockMode = 'footer' | 'float'
// ── 更新检查(客户端直连 GitHub Releases API,只读 GET;失败静默)──
declare const __WT_VERSION__: string
const LOCAL_VERSION = typeof __WT_VERSION__ === 'undefined' ? '0.2.0' : __WT_VERSION__
const UPDATE_REPO = 'Aisland-SJL/dsh-worktable'
const UPGRADE_CMD = 'dsh plugin --profile web add "https://github.com/Aisland-SJL/dsh-worktable/releases/latest/download/dsh-worktable.tgz"'
const UPGRADE_AI = '帮我升级 dsh-worktable:执行 ' + UPGRADE_CMD + ',完成后提醒我重启 dsh web 并刷新页面'
// 更新提示图标(手绘 SVG,避免 emoji 跨平台渲染差异)
const ICON_SYNC = (
<svg viewBox="0 0 16 16" aria-hidden>
<path fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" d="M1.5 8a6.5 6.5 0 0 1 11.1-4.6L14.5 5" />
<path fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" d="M14.5 1.5V5h-3.5" />
<path fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" d="M14.5 8a6.5 6.5 0 0 1-11.1 4.6L1.5 11" />
<path fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" d="M1.5 14.5V11h3.5" />
</svg>
)
const ICON_SPARK = (
<svg viewBox="0 0 16 16" aria-hidden>
<path d="M8 1.6l1.5 3.9 3.9 1.5-3.9 1.5L8 12.4 6.5 8.5 2.6 7l3.9-1.5z" fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinejoin="round" />
<path d="M12.6 11.2l.5 1.3 1.3.5-1.3.5-.5 1.3-.5-1.3-1.3-.5 1.3-.5z" fill="currentColor" />
</svg>
)
type UpdateInfo = { latest: string; notes: string; url: string }
function cmpVer(a: string, b: string): number {
const pa = a.split('.').map(Number)
const pb = b.split('.').map(Number)
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const x = pa[i] ?? 0
const y = pb[i] ?? 0
if (x !== y) return x - y
}
return 0
}
async function copyText(text: string): Promise<boolean> {
try {
if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true }
} catch { /* fallthrough */ }
return false
}
type ViewState = {
query: string
searchOpen: boolean
@@ -1008,6 +1047,48 @@ function WorktableSection(props: any) {
const [registeredIds, setRegisteredIds] = useState<string[]>(() => [...registryStore.ids])
const [addOpen, setAddOpen] = useState(false)
const [viewOptionsOpen, setViewOptionsOpen] = useState(false)
// 更新检查:徽标 / 更新卡 / 版本行共用;节流一天一次,忽略按版本号存 localStorage
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
const [updateCheckOn, setUpdateCheckOn] = useState<boolean>(() => localStorage.getItem('dsh.worktable.updateCheck.v1') !== '0')
const [updateCopied, setUpdateCopied] = useState(false)
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'uptodate' | 'failed'>('idle')
const checkUpdates = useCallback(async (force = false) => {
const last = Number(localStorage.getItem('dsh.worktable.lastUpdateCheck.v1') ?? '0')
if (!force && Date.now() - last < 24 * 3600 * 1000) return // 自动检查节流;手动「立即检查」绕过节流
setUpdateStatus('checking')
let d: { tag_name?: string; body?: string; html_url?: string } | null = null
for (let attempt = 0; attempt < 3 && !d; attempt++) {
try {
const r = await fetch('https://api.github.com/repos/' + UPDATE_REPO + '/releases/latest', { headers: { Accept: 'application/vnd.github+json' }, cache: 'no-store' })
if (r.ok) d = await r.json() as { tag_name?: string; body?: string; html_url?: string }
else if (r.status === 403 || r.status === 404) break // 限流/不存在:不再重试
} catch { /* 网络抖动:下一轮重试 */ }
}
if (!d) { setUpdateStatus('failed'); return }
localStorage.setItem('dsh.worktable.lastUpdateCheck.v1', String(Date.now()))
const tag = (d.tag_name ?? '').replace(/^v/, '')
if (!tag || cmpVer(tag, LOCAL_VERSION) <= 0) { setUpdateStatus('uptodate'); return }
if ((localStorage.getItem('dsh.worktable.skipVersion.v1') ?? '') === tag) { setUpdateStatus('uptodate'); return }
setUpdateInfo({ latest: tag, notes: (d.body ?? '').slice(0, 800), url: d.html_url ?? '' })
setUpdateStatus('uptodate')
}, [])
useEffect(() => { if (updateCheckOn) void checkUpdates() }, [updateCheckOn, checkUpdates])
const copyUpgradeAi = async () => {
const ok = await copyText(UPGRADE_AI)
if (ok) { setUpdateCopied(true); setTimeout(() => setUpdateCopied(false), 2200) }
}
const skipUpdate = () => {
if (updateInfo) {
localStorage.setItem('dsh.worktable.skipVersion.v1', updateInfo.latest)
setUpdateInfo(null)
}
}
const toggleUpdateCheck = () => {
setUpdateCheckOn((v) => {
localStorage.setItem('dsh.worktable.updateCheck.v1', v ? '0' : '1')
return !v
})
}
const [wsPreset, setWsPreset] = useState<string>('2h')
const [wsName, setWsName] = useState('')
const [wsError, setWsError] = useState(false)
@@ -2303,6 +2384,23 @@ function buildCustomLayoutPrompt(req: string): string {
: 16
const popTop = clamp(sectionTop, MIN_TOP, Math.max(MIN_TOP, window.innerHeight - 540))
// 设置弹窗底部防溢出:弹窗底与视口底保持 POP_BOTTOM_MARGIN;触底后以底为基准向上生长
const POP_BOTTOM_MARGIN = 12
const settingsRef = useRef<HTMLDivElement | null>(null)
const [settingsTop, setSettingsTop] = useState<number | null>(null)
useLayoutEffect(() => {
const recalc = () => {
if (!viewOptionsOpen || !settingsRef.current) return
const h = settingsRef.current.offsetHeight
const rawTop = Math.max(MIN_TOP, sectionTop)
const maxTop = Math.max(MIN_TOP, window.innerHeight - h - POP_BOTTOM_MARGIN)
setSettingsTop(Math.min(rawTop, maxTop))
}
recalc()
window.addEventListener('resize', recalc)
return () => window.removeEventListener('resize', recalc)
}, [viewOptionsOpen, updateInfo, sectionTop])
/** 收起态点击项目图标:视图覆盖/布局 → openSplit;入驻无视图 → 切绑定对话 */
const openRailProject = (id: string) => {
const pr = projectsRef.current.projects
@@ -2380,6 +2478,15 @@ function buildCustomLayoutPrompt(req: string): string {
onPointerCancel={onHandlePointerUp}
onDoubleClick={resetDock}
>{t('title')}</span>
{updateInfo && (
<button
type="button"
className="dsh-wt_updateBadge"
title={t('update.badgeTitle') + ' · v' + updateInfo.latest}
aria-label={t('update.badgeTitle')}
onClick={() => setViewOptionsOpen(true)}
>{ICON_SYNC}</button>
)}
<div className="dsh-wt_actions">
<button
type="button"
@@ -2500,7 +2607,29 @@ function buildCustomLayoutPrompt(req: string): string {
{viewOptionsOpen && <div className="dsh-wt_popBackdrop" onClick={() => setViewOptionsOpen(false)} />}
{viewOptionsOpen && (
<div className="dsh-wt_manage dsh-wt_pop dsh-wt_settings" style={{ position: 'fixed', left: popLeft, top: popTop, width: 280, zIndex: 80 }}>
<div ref={settingsRef} className="dsh-wt_manage dsh-wt_pop dsh-wt_settings" style={{ position: 'fixed', left: popLeft, top: settingsTop ?? popTop, width: 280, zIndex: 80 }}>
<button
type="button"
className="dsh-wt_settingsClose"
aria-label={t('manage.done')}
title={t('manage.done')}
onClick={() => setViewOptionsOpen(false)}
>✕</button>
{updateInfo && (
<div className="dsh-wt_updateCard">
<div className="dsh-wt_updateHead"><span className="dsh-wt_updateDot" />{t('update.available')} · v{updateInfo.latest}</div>
<div className="dsh-wt_updateVers">{t('update.current')} v{LOCAL_VERSION} → v{updateInfo.latest}</div>
{updateInfo.notes && <div className="dsh-wt_updateNotes">{updateInfo.notes}</div>}
<div className="dsh-wt_updateCmd">{UPGRADE_CMD}</div>
<div className="dsh-wt_updateBtns">
<button type="button" className="dsh-wt_updateBtn dsh-wt_updateBtnCopy" onClick={() => void copyUpgradeAi()}>
{updateCopied ? '✓ ' + t('update.copied') : <>{ICON_SPARK} {t('update.copyAi')}</>}
</button>
<button type="button" className="dsh-wt_updateBtn" onClick={skipUpdate}>{t('update.skip')}</button>
</div>
<div className="dsh-wt_updateHint">{t('update.upgradeHint')}</div>
</div>
)}
<div className="dsh-wt_manageHead">
<span className="dsh-wt_manageTitle">{t('sort.label')}</span>
</div>
@@ -2513,7 +2642,6 @@ function buildCustomLayoutPrompt(req: string): string {
<div className="dsh-wt_menuSep" />
<div className="dsh-wt_manageHead">
<span className="dsh-wt_manageTitle">{t('manage.title')}</span>
<button type="button" className="dsh-wt_manageDone" onClick={() => setViewOptionsOpen(false)}>{t('manage.done')}</button>
</div>
{effectiveOrder.map((id) => {
const meta = metas[id]
@@ -2578,6 +2706,22 @@ function buildCustomLayoutPrompt(req: string): string {
<button type="button" className="dsh-wt_manageBtn" title={t('manage.deleteShortcut')} onClick={() => askDelete('shortcut', s.id, s.name)}>✕</button>
</div>
))}
<div className="dsh-wt_versionRow">
<span>
v{LOCAL_VERSION}
{updateStatus === 'uptodate' && !updateInfo ? ' · ' + t('update.upToDate') : ''}
{updateStatus === 'failed' && !updateInfo ? ' · ' + t('update.checkFail') : ''}
</span>
<span className="dsh-wt_versionActions">
<button type="button" className="dsh-wt_updateBtn" onClick={() => void checkUpdates(true)}>
{updateStatus === 'checking' ? t('update.checking') : t('update.checkNow')}
</button>
<span className="dsh-wt_updateToggle" onClick={toggleUpdateCheck}>
<span>{t('update.autoCheck')}</span>
<span className="dsh-wt_updateSwitch" data-off={updateCheckOn ? undefined : 'true'} />
</span>
</span>
</div>
</div>
)}
+28
View File
@@ -161,6 +161,20 @@ export const zh = {
'icons.title': '选择图标',
'icons.change': '更换图标',
'empty': '暂无项目',
'update.badgeTitle': '有新版本可用',
'update.available': '新版本可用',
'update.current': '当前版本',
'update.copyAi': '复制 AI 提示词',
'update.skip': '忽略此版本',
'update.autoCheck': '自动检查更新',
'update.checkNow': '立即检查',
'update.checkFail': '上次检查未成功',
'update.checking': '检查中…',
'update.upToDate': '已是最新版本',
'update.copied': '已复制',
'update.copyFail': '复制失败,请手动选择文本',
'update.skipDone': '已忽略此版本(下一版发布时再提醒)',
'update.upgradeHint': '执行命令后需重启 dsh web 并刷新页面',
} satisfies Record<string, string>
/** The worktable namespace key union. */
@@ -323,6 +337,20 @@ export const en = {
'icons.title': 'Pick an icon',
'icons.change': 'Change icon',
'empty': 'No projects yet',
'update.badgeTitle': 'New version available',
'update.available': 'New version available',
'update.current': 'Current version',
'update.copyAi': 'Copy AI prompt',
'update.skip': 'Skip this version',
'update.autoCheck': 'Auto-check updates',
'update.checkNow': 'Check now',
'update.checkFail': 'Last check failed',
'update.checking': 'Checking…',
'update.upToDate': 'Up to date',
'update.copied': 'Copied',
'update.copyFail': 'Copy failed — select the text manually',
'update.skipDone': 'Skipped (will remind on the next release)',
'update.upgradeHint': 'Run the command, then restart dsh web and refresh',
} satisfies Record<WorktableKey, string>
/** Locale namespace id registered under ctx.locale. */
+26
View File
@@ -53,6 +53,8 @@ export const css = xtermCss + '\n' + [
'.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_settingsClose{position:absolute;top:6px;right:6px;width:20px;height:20px;padding:0;display:flex;align-items:center;justify-content:center;border:none;border-radius:6px;background:transparent;color:var(--dsw-alias-label-tertiary,#7d8aa5);font-size:13px;line-height:1;cursor:pointer}',
'.dsh-wt_settingsClose:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06));color:var(--dsw-alias-label-primary,#dbe4f3)}',
'.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))}',
// 排序方式:一左一右两个选择按钮(手动 / 最近)
@@ -455,4 +457,28 @@ export const css = xtermCss + '\n' + [
'.dsh-wt_consoleCreateBtn{margin-top:2px;padding:7px 10px;border:1px solid var(--dsw-alias-state-accent-primary,#4f8ef7);border-radius:8px;background:transparent;color:var(--dsw-alias-state-accent-primary,#4f8ef7);font:inherit;font-size:12px;line-height:18px;cursor:pointer}',
'.dsh-wt_consoleCreateBtn:hover:not(:disabled){background:var(--dsw-alias-fill-l1,rgba(255,255,255,.05))}',
'.dsh-wt_consoleCreateBtn:disabled{opacity:.55;cursor:default}',
// ── 更新提示:侧栏标题旁徽标(仅图标 + 琥珀呼吸光)与设置面板更新卡 / 版本行 ──
'.dsh-wt_updateBadge{flex:none;display:flex;align-items:center;justify-content:center;width:18px;height:18px;margin-left:3px;padding:0;border:none;border-radius:50%;background:rgba(245,185,66,.16);color:#f7d488;font-size:11px;line-height:1;cursor:pointer;position:relative}',
'.dsh-wt_updateBadge::after{content:"";position:absolute;inset:-1px;border-radius:50%;border:1px solid rgba(245,185,66,.55);animation:dsh-wt_updatePulse 1.8s ease-out infinite}',
'@keyframes dsh-wt_updatePulse{0%{opacity:.9;box-shadow:0 0 0 0 rgba(245,185,66,.5)}70%{opacity:.35;box-shadow:0 0 0 6px rgba(245,185,66,0)}100%{opacity:.9;box-shadow:0 0 0 0 rgba(245,185,66,0)}}',
'.dsh-wt_updateCard{margin:0 0 10px;padding:10px;border:1px solid rgba(245,185,66,.45);border-radius:10px;background:rgba(245,185,66,.08);display:flex;flex-direction:column;gap:6px}',
'.dsh-wt_updateHead{display:flex;align-items:center;gap:6px;font-size:12.5px;font-weight:600;color:#f7d488}',
'.dsh-wt_updateDot{width:7px;height:7px;border-radius:50%;background:#f5b942;animation:dsh-wt_updatePulse 1.8s ease-out infinite}',
'.dsh-wt_updateVers{font-size:11.5px;color:var(--dsw-alias-label-tertiary,#7d8aa5)}',
'.dsh-wt_updateNotes{max-height:96px;overflow:auto;font-size:11px;line-height:1.6;color:var(--dsw-alias-label-tertiary,#7d8aa5);white-space:pre-wrap;word-break:break-all}',
'.dsh-wt_updateCmd{margin:2px 0;padding:7px 8px;background:var(--dsw-alias-bg-base,#0b0e14);border:1px solid var(--dsw-alias-border-l1,#262b36);border-radius:8px;font-family:Consolas,monospace;font-size:10.5px;line-height:1.5;color:#9cc6ff;word-break:break-all}',
'.dsh-wt_updateBtns{display:flex;gap:6px;flex-wrap:wrap}',
'.dsh-wt_updateBtn{display:inline-flex;align-items:center;gap:5px;padding:5px 8px;border-radius:7px;border:1px solid var(--dsw-alias-border-l1,#262b36);background:transparent;color:var(--dsw-alias-label-secondary,#aab6cd);font-size:11px;cursor:pointer}',
'.dsh-wt_updateBtn svg{width:12px;height:12px;flex:none}',
'.dsh-wt_updateBadge svg{width:12px;height:12px;flex:none}',
'.dsh-wt_updateBtn:hover{background:var(--dsw-alias-fill-l1,rgba(255,255,255,.06))}',
'.dsh-wt_updateBtnCopy{border-color:var(--dsw-alias-state-accent-primary,#4f8ef7);background:rgba(79,142,247,.18);color:#cfe0ff}',
'.dsh-wt_updateHint{font-size:10.5px;color:var(--dsw-alias-label-tertiary,#7d8aa5)}',
'.dsh-wt_versionRow{margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l1,#262b36);display:flex;align-items:center;justify-content:space-between;font-size:11px;color:var(--dsw-alias-label-tertiary,#7d8aa5)}',
'.dsh-wt_updateToggle{display:flex;align-items:center;gap:6px;cursor:pointer;user-select:none}',
'.dsh-wt_versionActions{display:flex;align-items:center;gap:8px}',
'.dsh-wt_updateSwitch{position:relative;width:26px;height:14px;border-radius:999px;background:var(--dsw-alias-state-accent-primary,#4f8ef7);cursor:pointer}',
'.dsh-wt_updateSwitch::after{content:"";position:absolute;top:2px;right:2px;width:10px;height:10px;border-radius:50%;background:#fff;transition:right .15s}',
'.dsh-wt_updateSwitch[data-off=true]{background:var(--dsw-alias-border-l1,#333a48)}',
'.dsh-wt_updateSwitch[data-off=true]::after{right:14px}',
].join('\n')
+3
View File
@@ -172,6 +172,8 @@ ctx.slots.register({
- bindings:项目 → 绑定会话(含 wt-console 管理对话);
- folders:项目 → 项目文件夹(含 wt-console);
- 卡片上报的 meta 注册表仅存内存,不持久化;
- 更新检查:`dsh.worktable.lastUpdateCheck.v1`(上次成功检查时间戳,节流一天一次)、
`dsh.worktable.skipVersion.v1`(忽略的版本号)、`dsh.worktable.updateCheck.v1`(自动检查开关,'0'=关);
- 样式:暗色优先,跟随 `--dsw-alias-*` 设计变量(与 dsh-usage / dsh-travelatlas 一致)。
## 7. 与 dsh-travelatlas 的关系
@@ -188,6 +190,7 @@ ctx.slots.register({
- 无个人数据采集;所有状态仅存 localStorage;
- 搜索仅在本机过滤项目名;
- 不读写任何工作区文件、不请求任何网络资源(除插件自身静态资源)。
- 更新检查为可关闭的只读 GET(GitHub Releases API,自动每天最多一次 + 手动「立即检查」),不上传任何数据。
## 9. 验收清单(v1)
+20
View File
@@ -129,6 +129,17 @@ node --check lib/index.js
previewCache;sweepPreviews 在打开控制室时 + 控制室开着且会话快照变化防抖 6s 触发;
失败静默回退内存路径 lastTextOf。拉取是带宽成本不是 Token 成本。
- 状态指示:卡片右上角小圆点已删(整卡光效表达状态);状态计算不变。
- **更新检查(v0.2.1)**:客户端直连 GitHub Releases API 比版本(只读 GET;自动每天最多
一次,手动「立即检查」绕过节流;失败重试 3 次后状态行显示「上次检查未成功」)。
状态四态 idle/checking/uptodate/failed;徽标 = 「工作台」标题右侧琥珀呼吸小圆(SVG 同步
图标、不显示版本号),仅发现更新时出现;更新卡在设置面板顶部(复制 AI 提示词 + 忽略此
版本 + 命令框供终端用户手抄),版本号与自动检查开关在面板底部;设置弹窗右上角 ✕ 关闭、
底部防溢出钳制(POP_BOTTOM_MARGIN=12,贴底后向上生长;按钮与开关必须平级防冒泡)。
localStorage 键 lastUpdateCheck.v1 / skipVersion.v1 / updateCheck.v1。
**发布纪律**:改动≠发布,只有 tag+Release 才触发提醒;新版本 = 新 tag + 新 Release,每个
Release 必须同时附固定名资产 `dsh-worktable.tgz`(供 releases/latest/download 永久链接)
与版本化资产。版本注入:build.mjs 把 package.json version 打进 __WT_VERSION__,发版前
改 version 再构建;升级动作(执行 add + 重启)永远留给用户或其 Agent,插件不自更新。
## 宿主升级事故(0.1.1-rc.2 link 插件回归)
@@ -146,8 +157,17 @@ node --check lib/index.js
- 恢复后验收:functional-diag 全 20 STEP ERROR_COUNT: 0;/api/worktable/health ok、
/workspaces 200。
## 宿主 bug 跟踪(UTF-16 路径截断)
- 官方 `dsh-host-directory-picker-native` 的 readUtf16 只查 UTF-16LE 码元低字节,含 U+XX00
字符(开/一/言/Ā/🀀 等)的文件夹路径在选目录时被截断 → 创建工作区异常。非我们插件问题。
- 修复与回归测试以 patch 存档:`02_process/upstream/utf16-picker-fix.patch`;官方 Discussions
#580 已接单(tianyicui「我们修复一下」),我们的补证评论已发(Aisland-SJL)。
- 待办:官方修 master 后核验对应 npm 版本是否含修复,随后可清理 patch 存档。
## 安装 / 重启
- 注册:`dsh plugin --profile web add "link:<repo>/01_content"`(写 ~/.dsh,需用户授权)。
- 发布版安装(给用户):`dsh plugin --profile web add "https://github.com/Aisland-SJL/dsh-worktable/releases/latest/download/dsh-worktable.tgz"`(依赖每个 Release 的固定名资产)。
- bundle 层只在启动时组合:改动后必须重启 dsh web 并刷新 GUI。
+18 -2
View File
@@ -53,7 +53,23 @@
## Quick start
1. **Install**: `dsh plugin --profile web add "link:<repo>/01_content"` and register `dsh-worktable` in the profile bundle list
1. **Install** (pick one):
**A · one-liner (recommended)** — straight from the GitHub Release tarball, no Git needed:
```bash
dsh plugin --profile web add "https://github.com/Aisland-SJL/dsh-worktable/releases/latest/download/dsh-worktable.tgz"
```
**B · local clone (for hacking on the source)** — `link:` accepts a local absolute path only (no spaces in the path):
```bash
git clone https://github.com/Aisland-SJL/dsh-worktable.git
dsh plugin --profile web add "link:<absolute path of the cloned dsh-worktable directory>/01_content"
# e.g. cloned into D:\tools → dsh plugin --profile web add "link:D:/tools/dsh-worktable/01_content"
```
Either way the `add` command registers `dsh-worktable` in the profile bundle list (writes to `~/.dsh`, may ask for authorization). If the `dsh` command is missing, use `npx @deepseek-ai/dsh` instead.
2. **Restart** the DSH web process, refresh the GUI
3. **Open the control room**: click the pinned 🖥️ control-room card → bind one conversation (join existing or create new) → you get the live card grid
4. **Create projects**: sidebar + → pick a layout preset, set a project folder
@@ -97,7 +113,7 @@ node --check lib/index.js
## Privacy
No telemetry, no network calls beyond the host APIs and the plugin routes. All user state stays in localStorage.
No telemetry, no network calls beyond the host APIs and the plugin routes. All user state stays in localStorage. Optional update check: a read-only GET to the GitHub Releases API (automatic at most once a day, plus a manual "Check now" button); nothing is uploaded, and it can be disabled in Settings.
---
+18 -2
View File
@@ -53,7 +53,23 @@
## 快速开始
1. **安装**:`dsh plugin --profile web add "link:<repo>/01_content"`,并把 `dsh-worktable` 注册进 profile 的 bundle 列表
1. **安装**(二选一):
**A · 一行命令(推荐)** —— 直接安装 GitHub Release 的安装包,无需 Git:
```bash
dsh plugin --profile web add "https://github.com/Aisland-SJL/dsh-worktable/releases/latest/download/dsh-worktable.tgz"
```
**B · 本地克隆(想改源码用)** —— `link:` 只接受本地绝对路径(路径不要带空格):
```bash
git clone https://github.com/Aisland-SJL/dsh-worktable.git
dsh plugin --profile web add "link:<克隆出来的 dsh-worktable 仓库目录的绝对路径>/01_content"
# 例:克隆到 D:\tools 后 → dsh plugin --profile web add "link:D:/tools/dsh-worktable/01_content"
```
两种方式 `add` 都会把 `dsh-worktable` 注册进 profile 的 bundle 列表(写入 `~/.dsh`,可能需要授权确认);若提示找不到 `dsh` 命令,用 `npx @deepseek-ai/dsh` 代替。
2. **重启** DSH web 进程并刷新界面
3. **打开控制室**:点击固定首位的 🖥️ 控制室卡片 → 绑定一条对话(加入现有或新建)→ 得到实时卡片网格
4. **创建项目**:侧边栏 + → 选布局预设、填项目文件夹
@@ -97,7 +113,7 @@ node --check lib/index.js
## 隐私
无遥测;除宿主 API 与插件自身路由外无任何网络请求。用户状态全部留在 localStorage。
无遥测;除宿主 API 与插件自身路由外无任何网络请求。用户状态全部留在 localStorage。可选更新检查:对 GitHub Releases API 做只读 GET(自动每天最多一次,另有手动「立即检查」),不上传任何数据,可在设置中关闭。
---