diff --git a/01_content/README.md b/01_content/README.md
index bfcc201..8a0af8d 100644
--- a/01_content/README.md
+++ b/01_content/README.md
@@ -8,6 +8,7 @@
- **分栏工作区引擎(自研)**:声明式布局预设(左栏/顶行/主行 + 右侧对话窗),窗格可拖拽分割、标签页模型;内置 资源管理器 / 终端 / 浏览器 / 动画 / 自定义窗口。
- **控制室**:默认自带项目(固定首位、不可删除),项目卡片网格实时监控所有项目的状态(工作中/待你决定/已完成),零轮询零 Token。
- **自动挂载握手**:项目内 agent 完成窗口任务后写 widget-result.json,产物自动挂进对应窗口。
+- **平台**:Windows 是当前完整验证平台;macOS 为实验性支持(核心文件路径代码已做跨平台适配,尚未真机端到端验证)。
## 技术底座
@@ -20,7 +21,7 @@
方式 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"
+ dsh plugin --profile web add "https://github.com/Aisland-SJL/dsh-worktable/releases/latest/download/dsh-worktable.tgz"
方式 B(想改源码用)——克隆仓库后用本地路径注册(`link:` 只接受本地路径,不要带空格):
diff --git a/01_content/build.mjs b/01_content/build.mjs
index e789ce4..c19356f 100644
--- a/01_content/build.mjs
+++ b/01_content/build.mjs
@@ -14,6 +14,10 @@ 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 pluginManifest = JSON.parse(readFileSync(join(here, 'dsh.plugin.json'), 'utf8'))
+if (pluginManifest.version !== pkg.version) {
+ throw new Error('dsh-worktable build: package.json version (' + pkg.version + ') != dsh.plugin.json version (' + pluginManifest.version + ') — 发版前先统一两处版本号')
+}
const clientBanner = {
js: "window.__ModuleLoader__.load({ id: 'dsh-worktable', factory: (require) => { var module = { exports: {} }; var exports = module.exports;",
@@ -24,9 +28,10 @@ await build({
entryPoints: [join(here, 'src/index.ts')],
outfile: 'lib/index.js',
bundle: true,
- sourcemap: true,
+ sourcemap: 'external',
logLevel: 'info',
platform: 'node',
+ define: { __WT_VERSION__: JSON.stringify(pkg.version) },
format: 'esm',
target: ['node22'],
external: ['@deepseek-ai/*', 'node:*', 'ws', 'node-pty'],
@@ -37,7 +42,7 @@ await build({
entryPoints: [join(here, 'src/client/index.tsx')],
outfile: 'lib/client.js',
bundle: true,
- sourcemap: true,
+ sourcemap: 'external',
logLevel: 'info',
platform: 'browser',
format: 'cjs',
diff --git a/01_content/dsh.plugin.json b/01_content/dsh.plugin.json
index ae00f5f..d7c5415 100644
--- a/01_content/dsh.plugin.json
+++ b/01_content/dsh.plugin.json
@@ -1,7 +1,7 @@
{
"name": "dsh-worktable",
"description": "工作台:DSH 侧边栏的 agent 级项目容器,收纳如 TravelAtlas 等自装项目",
- "version": "0.2.1",
+ "version": "0.2.2",
"entry": {
"name": "dsh-worktable",
"inject": []
diff --git a/01_content/lib/client.css b/01_content/lib/client.css
deleted file mode 100644
index 9d35497..0000000
--- a/01_content/lib/client.css
+++ /dev/null
@@ -1,194 +0,0 @@
-/* node_modules/xterm/css/xterm.css */
-.xterm {
- cursor: text;
- position: relative;
- user-select: none;
- -ms-user-select: none;
- -webkit-user-select: none;
-}
-.xterm.focus,
-.xterm:focus {
- outline: none;
-}
-.xterm .xterm-helpers {
- position: absolute;
- top: 0;
- z-index: 5;
-}
-.xterm .xterm-helper-textarea {
- padding: 0;
- border: 0;
- margin: 0;
- position: absolute;
- opacity: 0;
- left: -9999em;
- top: 0;
- width: 0;
- height: 0;
- z-index: -5;
- white-space: nowrap;
- overflow: hidden;
- resize: none;
-}
-.xterm .composition-view {
- background: #000;
- color: #FFF;
- display: none;
- position: absolute;
- white-space: nowrap;
- z-index: 1;
-}
-.xterm .composition-view.active {
- display: block;
-}
-.xterm .xterm-viewport {
- background-color: #000;
- overflow-y: scroll;
- cursor: default;
- position: absolute;
- right: 0;
- left: 0;
- top: 0;
- bottom: 0;
-}
-.xterm .xterm-screen {
- position: relative;
-}
-.xterm .xterm-screen canvas {
- position: absolute;
- left: 0;
- top: 0;
-}
-.xterm .xterm-scroll-area {
- visibility: hidden;
-}
-.xterm-char-measure-element {
- display: inline-block;
- visibility: hidden;
- position: absolute;
- top: 0;
- left: -9999em;
- line-height: normal;
-}
-.xterm.enable-mouse-events {
- cursor: default;
-}
-.xterm.xterm-cursor-pointer,
-.xterm .xterm-cursor-pointer {
- cursor: pointer;
-}
-.xterm.column-select.focus {
- cursor: crosshair;
-}
-.xterm .xterm-accessibility,
-.xterm .xterm-message {
- position: absolute;
- left: 0;
- top: 0;
- bottom: 0;
- right: 0;
- z-index: 10;
- color: transparent;
- pointer-events: none;
-}
-.xterm .live-region {
- position: absolute;
- left: -9999px;
- width: 1px;
- height: 1px;
- overflow: hidden;
-}
-.xterm-dim {
- opacity: 1 !important;
-}
-.xterm-underline-1 {
- text-decoration: underline;
-}
-.xterm-underline-2 {
- text-decoration: double underline;
-}
-.xterm-underline-3 {
- text-decoration: wavy underline;
-}
-.xterm-underline-4 {
- text-decoration: dotted underline;
-}
-.xterm-underline-5 {
- text-decoration: dashed underline;
-}
-.xterm-overline {
- text-decoration: overline;
-}
-.xterm-overline.xterm-underline-1 {
- text-decoration: overline underline;
-}
-.xterm-overline.xterm-underline-2 {
- text-decoration: overline double underline;
-}
-.xterm-overline.xterm-underline-3 {
- text-decoration: overline wavy underline;
-}
-.xterm-overline.xterm-underline-4 {
- text-decoration: overline dotted underline;
-}
-.xterm-overline.xterm-underline-5 {
- text-decoration: overline dashed underline;
-}
-.xterm-strikethrough {
- text-decoration: line-through;
-}
-.xterm-screen .xterm-decoration-container .xterm-decoration {
- z-index: 6;
- position: absolute;
-}
-.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
- z-index: 7;
-}
-.xterm-decoration-overview-ruler {
- z-index: 8;
- position: absolute;
- top: 0;
- right: 0;
- pointer-events: none;
-}
-.xterm-decoration-top {
- z-index: 2;
- position: relative;
-}
-/*! Bundled license information:
-
-xterm/css/xterm.css:
- (**
- * Copyright (c) 2014 The xterm.js authors. All rights reserved.
- * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
- * https://github.com/chjj/term.js
- * @license MIT
- *
- * 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.
- *
- * Originally forked from (with the author's permission):
- * Fabrice Bellard's javascript vt100 for jslinux:
- * http://bellard.org/jslinux/
- * Copyright (c) 2011 Fabrice Bellard
- * The original design remains. The terminal itself
- * has been extended to include xterm CSI codes, among
- * other features.
- *)
-*/
-/*# sourceMappingURL=client.css.map */
diff --git a/01_content/lib/client.js b/01_content/lib/client.js
index 736f565..4583690 100644
--- a/01_content/lib/client.js
+++ b/01_content/lib/client.js
@@ -8580,6 +8580,34 @@ var en = {
};
var NS = "worktable";
+// src/client/pathutil.ts
+function isAbs(p) {
+ if (/^[A-Za-z]:[\\/]/.test(p)) return true;
+ if (p.startsWith("\\\\")) return true;
+ if (p.startsWith("/")) return true;
+ return false;
+}
+function joinPath(base2, rel) {
+ const relTrim = rel.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
+ const sep = base2.includes("\\") ? "\\" : "/";
+ if (base2 === "/" || base2 === "\\") return base2 + relTrim;
+ if (/^[A-Za-z]:[\\/]$/.test(base2)) return base2 + relTrim;
+ const baseTrim = base2.replace(/[\\/]+$/, "");
+ if (!baseTrim) return relTrim || sep;
+ return baseTrim + sep + relTrim;
+}
+function parentPathOf(p) {
+ const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
+ if (idx < 0) return p;
+ if (idx === 0) return p[0];
+ if (idx === 2 && /^[A-Za-z]:[\\/]/.test(p)) return p.slice(0, 3);
+ return p.slice(0, idx);
+}
+function basenameOf(p) {
+ const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
+ return idx < 0 ? p : p.slice(idx + 1);
+}
+
// src/client/split.tsx
var import_react = require("react");
var import_xterm2 = __toESM(require_xterm(), 1);
@@ -16386,7 +16414,7 @@ var BUILTIN_LABEL_KEYS = {
};
function tabTitleOf(content) {
if (content.kind === "builtin") return T(BUILTIN_LABEL_KEYS[content.type]);
- if (content.kind === "file") return basenameOf(content.path);
+ if (content.kind === "file") return basenameOf2(content.path);
if (content.kind === "iframe" && content.title) return content.title;
try {
const u = new URL(content.url);
@@ -16395,7 +16423,7 @@ function tabTitleOf(content) {
return content.url;
}
}
-function basenameOf(p) {
+function basenameOf2(p) {
const parts = String(p).replace(/[\\/]+$/, "").split(/[\\/]/);
return parts[parts.length - 1] || String(p);
}
@@ -16423,13 +16451,6 @@ async function postJson(url, body) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
}
-function parentPathOf(p) {
- const idx = Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
- if (idx <= 0) return p;
- let parent = p.slice(0, idx);
- if (/^[A-Za-z]:$/.test(parent)) parent += "\\";
- return parent;
-}
var dragPane = null;
var dragTab = null;
var dropTarget = null;
@@ -17555,10 +17576,10 @@ function FileViewer(props) {
const ext = (props.path.split(".").pop() || "").toLowerCase();
const fileUrl = "/api/worktable/file?path=" + encodeURIComponent(props.path);
if (ext === "pdf") {
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("iframe", { className: "dsh-wt_paneFrame", src: fileUrl, title: basenameOf(props.path) });
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("iframe", { className: "dsh-wt_paneFrame", src: fileUrl, title: basenameOf2(props.path) });
}
if (IMAGE_EXTS.test("." + ext)) {
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-wt_imgView", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: fileUrl, alt: basenameOf(props.path) }) });
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-wt_imgView", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { src: fileUrl, alt: basenameOf2(props.path) }) });
}
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextViewer, { path: props.path, fileUrl, isMd: MD_EXTS.test("." + ext) });
}
@@ -18266,7 +18287,7 @@ try {
// src/client/index.tsx
var import_jsx_runtime2 = require("react/jsx-runtime");
-var LOCAL_VERSION = false ? "0.2.0" : "0.2.1";
+var LOCAL_VERSION = false ? "dev" : "0.2.2";
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";
@@ -18889,7 +18910,7 @@ async function promptIntoSession(sessionId, text2) {
}
var KNOWLEDGE_PACK = [
"\u3010\u63D2\u4EF6\u77E5\u8BC6\u5305\xB7\u8BF7\u76F4\u63A5\u91C7\u7528\uFF0C\u4E0D\u8981\u91CD\u65B0\u4FA6\u5BDF\u63D2\u4EF6\u6E90\u7801\u3011",
- "- dsh-worktable \u662F\u672C\u673A DeepSeek Harness \u7684\u81EA\u5EFA\u5BB9\u5668\u63D2\u4EF6\uFF08\u4ED3\u5E93 E:\\AI_Workspace\\DeepseekHarness\\Projects\\dsh-worktable\uFF0C\u63D2\u4EF6\u5305\u6839\u76EE\u5F55 01_content/\uFF09\u3002",
+ "- dsh-worktable \u662F DeepSeek Harness \u7684\u81EA\u5EFA\u5BB9\u5668\u63D2\u4EF6\uFF08\u5DE5\u4F5C\u53F0\uFF1A\u4FA7\u8FB9\u680F\u91CC\u7684\u9879\u76EE\u5E94\u7528\u62BD\u5C49\uFF1B\u4E0D\u4E86\u89E3\u7684\u7EC6\u8282\u53EF\u76F4\u63A5\u5411\u7528\u6237\u63D0\u95EE\uFF09\u3002",
"- \u7A97\u53E3\uFF08\u5185\u5BB9\u7A97\uFF09\u6A21\u578B\uFF1A\u6BCF\u4E2A\u7A97 = \u4E00\u4E2A\u6807\u7B7E\u9875\uFF1B\u672A\u6307\u6D3E\u65F6\u663E\u793A\u9009\u62E9\u5668\uFF08\u6D4F\u89C8\u5668/\u52A8\u753B/\u8D44\u6E90\u7BA1\u7406\u5668/\u7EC8\u7AEF/\u2728\u81EA\u5B9A\u4E49\uFF09\uFF1B",
" \u7A97\u5185\u5BB9\u8FD8\u53EF\u653E iframe \u7F51\u9875\u4E0E\u6587\u4EF6\u9884\u89C8\uFF08.md/.txt/.tsx/.css/.html \u7B49\uFF09\u3002",
"- \u7A97\u53E3\u53EF\u88C5\u8F7D\u7684\u5F62\u5F0F\uFF1AHTML \u5355\u6587\u4EF6\uFF08\u4EA4\u4E92 UI\uFF0C\u653E\u9879\u76EE\u6587\u4EF6\u5939\u540E\u5728\u8D44\u6E90\u7BA1\u7406\u5668\u70B9\u51FB\u6E32\u67D3\uFF09\u3001\u7F51\u9875 URL\u3001",
@@ -18936,7 +18957,7 @@ async function createCustomSession(projectId, projectName, requirement, group, w
const parent = g.parent.replace(/[\\/]+$/, "");
const name = g.name.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
if (!parent || !name) throw new Error("invalid group path");
- const full = parent + "\\" + name;
+ const full = joinPath(parent, name);
try {
await ws.createDirectory?.(parent, name);
} catch {
@@ -19057,10 +19078,10 @@ function buildMountContent(folder, d) {
const p = String(d?.path ?? "").trim();
if (!p) return null;
if (d?.kind === "url") return { kind: "iframe", url: p, title: p };
- const full = p.indexOf(":") >= 0 ? p : folder + "\\" + p.replace(/^[\\/]+/, "");
+ const full = isAbs(p) ? p : joinPath(folder, p);
if (d?.kind === "file") return { kind: "file", path: full };
- const dir = full.replace(/[\\/][^\\/]*$/, "");
- const name = full.split(/[\\/]/).pop() ?? full;
+ const dir = parentPathOf(full);
+ const name = basenameOf(full);
return { kind: "iframe", url: "/api/worktable/site/" + encodeURIComponent(dir) + "/" + encodeURIComponent(name), title: name };
}
var notifyStateSeenRef = { current: {} };
@@ -19189,35 +19210,51 @@ function WorktableSection(props) {
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 updateCheckingRef = (0, import_react2.useRef)(false);
+ const updateAliveRef = (0, import_react2.useRef)(true);
+ (0, import_react2.useEffect)(() => () => {
+ updateAliveRef.current = false;
+ }, []);
const checkUpdates = (0, import_react2.useCallback)(async (force = false) => {
+ if (updateCheckingRef.current) return;
const last = Number(localStorage.getItem("dsh.worktable.lastUpdateCheck.v1") ?? "0");
if (!force && Date.now() - last < 24 * 3600 * 1e3) return;
+ updateCheckingRef.current = true;
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 {
+ try {
+ let d = null;
+ for (let attempt = 0; attempt < 3 && !d && updateAliveRef.current; attempt++) {
+ const ctrl = new AbortController();
+ const timer = setTimeout(() => ctrl.abort(), 8e3);
+ try {
+ const r = await fetch("https://api.github.com/repos/" + UPDATE_REPO + "/releases/latest", { headers: { Accept: "application/vnd.github+json" }, cache: "no-store", signal: ctrl.signal });
+ if (r.ok) d = await r.json();
+ else if (r.status === 403 || r.status === 404) break;
+ } catch {
+ } finally {
+ clearTimeout(timer);
+ }
}
- }
- 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) {
+ if (!updateAliveRef.current) return;
+ 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");
- return;
+ } finally {
+ updateCheckingRef.current = false;
}
- 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();
@@ -19674,7 +19711,7 @@ function WorktableSection(props) {
return [
"\u3010\u4E3A dsh-worktable\uFF08\u5DE5\u4F5C\u53F0\uFF09\u63D2\u4EF6\u589E\u52A0\u4E00\u4E2A\u65B0\u7684\u5E03\u5C40\u9884\u8BBE\u3011",
"",
- "\u80CC\u666F\uFF1Adsh-worktable \u662F DeepSeek Harness \u7684\u81EA\u5EFA\u5BB9\u5668\u63D2\u4EF6\uFF08\u672C\u673A\u4ED3\u5E93 dsh-worktable\uFF0C\u63D2\u4EF6\u5305\u6839\u76EE\u5F55 01_content/\uFF09\u3002",
+ "\u80CC\u666F\uFF1Adsh-worktable \u662F DeepSeek Harness \u7684\u81EA\u5EFA\u5BB9\u5668\u63D2\u4EF6\uFF08\u672C\u673A\u4ED3\u5E93 dsh-worktable\uFF1B\u4E0D\u4E86\u89E3\u7684\u7EC6\u8282\u53EF\u76F4\u63A5\u5411\u7528\u6237\u63D0\u95EE\uFF09\u3002",
"\u4FA7\u8FB9\u680F\u300C\u5DE5\u4F5C\u53F0\u300D\u533A\u5757\u7BA1\u7406\u9879\u76EE\uFF0C\u6BCF\u4E2A\u9879\u76EE\u6253\u5F00\u540E\u662F\u4E00\u4E2A\u5E73\u94FA\u5DE5\u4F5C\u533A\uFF08\u82E5\u5E72\u5185\u5BB9\u7A97 + \u53F3\u4FA7\u5BF9\u8BDD\u7A97\uFF09\u3002",
"\u5E03\u5C40\u9884\u8BBE\u5B9A\u4E49\u5728 01_content/src/client/index.tsx \u7684 PRESET_DEFS \u6570\u7EC4\uFF0C\u9009\u62E9\u5668\u7F29\u7565\u56FE\u5728 presetThumb() \u51FD\u6570\u3002",
"",
@@ -19804,13 +19841,13 @@ function WorktableSection(props) {
const ws = b.workspaces;
const parent = consoleParent.replace(/[\\/]+$/, "");
const name = consoleName.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
- const full = parent + "\\" + name;
+ const full = joinPath(parent, name);
try {
await ws?.createDirectory?.(parent, name);
} catch {
}
try {
- const r = await fetch("/api/worktable/mkdir", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ parent, name }) });
+ const r = await fetch("/api/worktable/mkdir", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ path: full }) });
const d = await r.json();
if (!r.ok || !d?.ok) throw new Error(d?.error ?? "mkdir failed");
} catch {
@@ -19926,7 +19963,7 @@ function WorktableSection(props) {
try {
let raw = rawManifest;
if (raw == null) {
- const r = await fetch("/api/worktable/file?path=" + encodeURIComponent(folder + "\\widget-result.json"), { cache: "no-store" });
+ const r = await fetch("/api/worktable/file?path=" + encodeURIComponent(joinPath(folder, "widget-result.json")), { cache: "no-store" });
if (!r.ok) return;
raw = (await r.text()).trim();
}
@@ -19997,7 +20034,7 @@ function WorktableSection(props) {
const folder = folders[pid];
if (!folder) continue;
try {
- const r = await fetch("/api/worktable/file?path=" + encodeURIComponent(folder + "\\widget-result.json"), { cache: "no-store" });
+ const r = await fetch("/api/worktable/file?path=" + encodeURIComponent(joinPath(folder, "widget-result.json")), { cache: "no-store" });
if (!r.ok) continue;
const raw = (await r.text()).trim();
if (!raw) continue;
@@ -20918,7 +20955,7 @@ function WorktableSection(props) {
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.jsx)("button", { type: "button", className: "dsh-wt_updateBtn", disabled: updateStatus === "checking", 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" })
@@ -21343,4 +21380,3 @@ markdown-it/dist/markdown-it.mjs:
(*! markdown-it 15.0.0 https://github.com/markdown-it/markdown-it @license MIT *)
*/
return module.exports; } });
-//# sourceMappingURL=client.js.map
diff --git a/01_content/lib/index.js b/01_content/lib/index.js
index 940dad2..c798639 100644
--- a/01_content/lib/index.js
+++ b/01_content/lib/index.js
@@ -88,6 +88,7 @@ body {
var dshell_default2 = '\n\n\n
\n \n \n \u6211\u7684\u7A97\u53E3\n \n\n\n \n \n
\u7A97\u53E3\u6807\u9898
\n
\u4E00\u53E5\u8BDD\u8BF4\u660E\u8FD9\u4E2A\u7A97\u53E3\u505A\u4EC0\u4E48\u3002
\n\n \n
\n \u5DF2\u5B8C\u6210\n \u8FDB\u884C\u4E2D\n \u672A\u5F00\u59CB\n
\n\n \n
\n \n \n \n
\n\n \n
\n
\n
\u603B\u6570
\n
128
\n
+12.4%
\n
\n
\n
\u8FDB\u884C\u4E2D
\n
7
\n
\n
\n
\u5DF2\u5B8C\u6210
\n
121
\n
\n
\n\n \n
\n
\n \u6761\u76EE\u4E00\uFF1A\u793A\u4F8B\u5185\u5BB9\u6807\u9898\n \u6628\u5929\n
\n
\n \u6761\u76EE\u4E8C\uFF1A\u793A\u4F8B\u5185\u5BB9\u6807\u9898\n \u5DF2\u53D1\u5E03\n
\n
\n\n \n
\n
\u8BE6\u60C5
\n
\n
\u5B57\u6BB5 A\u503C A
\n
\u5B57\u6BB5 B\u503C B
\n
\n
\n
\n
\n\n \n
\n \n \n
\n
\n\n\n';
// src/index.ts
+var PLUGIN_VERSION = false ? "dev" : "0.2.2";
var name = "dsh-worktable";
var inject = ["webServer", "sessions"];
var HEALTH_PATH = "/api/worktable/health";
@@ -300,7 +301,7 @@ function apply(ctx) {
kind: "exact",
path: HEALTH_PATH,
handler: (_req, res) => {
- json(res, 200, { plugin: "dsh-worktable", version: "0.2.0", ok: true });
+ json(res, 200, { plugin: "dsh-worktable", version: PLUGIN_VERSION, ok: true });
}
});
webServer.register({
@@ -501,7 +502,7 @@ function apply(ctx) {
}
const abs = pathResolve(p);
const fsx = await import("node:fs/promises");
- const parent = abs.split(/[\\/]/).slice(0, -1).join("\\") || "\\";
+ const parent = dirname(abs);
try {
await fsx.access(parent);
} catch {
@@ -532,4 +533,3 @@ export {
inject,
name
};
-//# sourceMappingURL=index.js.map
diff --git a/01_content/package.json b/01_content/package.json
index 157fb6d..5f6a50e 100644
--- a/01_content/package.json
+++ b/01_content/package.json
@@ -1,7 +1,7 @@
{
"name": "dsh-worktable",
"description": "工作台 Worktable:DeepSeek Harness 侧边栏的 agent 级项目容器(应用抽屉)",
- "version": "0.2.1",
+ "version": "0.2.2",
"license": "MIT",
"type": "module",
"main": "lib/index.js",
@@ -11,7 +11,8 @@
"./package.json": "./package.json"
},
"files": [
- "lib",
+ "lib/index.js",
+ "lib/client.js",
"cordis.patch.yml",
"dsh.plugin.json",
"README.md",
diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx
index a2beab7..5d0a2e5 100644
--- a/01_content/src/client/index.tsx
+++ b/01_content/src/client/index.tsx
@@ -1,6 +1,7 @@
import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import { css } from './styles'
import { NS, zh, en, type WorktableKey } from './locales'
+import { isAbs, joinPath, parentPathOf, basenameOf } from './pathutil'
import { splitStore, SplitWorkspace, setSplitT, setSplitEnv, type LayoutSpec, type SplitPane, type ConsoleCardData } from './split'
/**
@@ -21,7 +22,7 @@ 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 LOCAL_VERSION = typeof __WT_VERSION__ === 'undefined' ? 'dev' : __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 并刷新页面'
@@ -729,7 +730,7 @@ export type NewSessionGroup = { kind: 'none' } | { kind: 'existing'; workspaceId
/** 插件知识包:附在窗口任务提示词里,让接收方跳过对插件源码的重新侦察,直接干活 */
const KNOWLEDGE_PACK = [
'【插件知识包·请直接采用,不要重新侦察插件源码】',
- '- dsh-worktable 是本机 DeepSeek Harness 的自建容器插件(仓库 E:\\AI_Workspace\\DeepseekHarness\\Projects\\dsh-worktable,插件包根目录 01_content/)。',
+ '- dsh-worktable 是 DeepSeek Harness 的自建容器插件(工作台:侧边栏里的项目应用抽屉;不了解的细节可直接向用户提问)。',
'- 窗口(内容窗)模型:每个窗 = 一个标签页;未指派时显示选择器(浏览器/动画/资源管理器/终端/✨自定义);',
' 窗内容还可放 iframe 网页与文件预览(.md/.txt/.tsx/.css/.html 等)。',
'- 窗口可装载的形式:HTML 单文件(交互 UI,放项目文件夹后在资源管理器点击渲染)、网页 URL、',
@@ -783,7 +784,7 @@ export async function createCustomSession(projectId: string, projectName: string
const parent = g.parent.replace(/[\\/]+$/, '')
const name = g.name.replace(/^[\\/]+/, '').replace(/[\\/]+$/, '')
if (!parent || !name) throw new Error('invalid group path')
- const full = parent + '\\' + name
+ const full = joinPath(parent, name)
try { await ws.createDirectory?.(parent, name) } catch { /* 宿主 browse 能力本机为 native 时不可用 */ }
try {
// 兜底:插件服务端 mkdir(父目录必须已存在,避免误建深层目录)
@@ -908,10 +909,10 @@ function buildMountContent(folder: string, d: any): any {
const p = String(d?.path ?? '').trim()
if (!p) return null
if (d?.kind === 'url') return { kind: 'iframe', url: p, title: p }
- const full = p.indexOf(':') >= 0 ? p : (folder + '\\' + p.replace(/^[\\/]+/, ''))
+ const full = isAbs(p) ? p : joinPath(folder, p)
if (d?.kind === 'file') return { kind: 'file', path: full }
- const dir = full.replace(/[\\/][^\\/]*$/, '')
- const name = full.split(/[\\/]/).pop() ?? full
+ const dir = parentPathOf(full)
+ const name = basenameOf(full)
return { kind: 'iframe', url: '/api/worktable/site/' + encodeURIComponent(dir) + '/' + encodeURIComponent(name), title: name }
}
@@ -1052,25 +1053,38 @@ function WorktableSection(props: any) {
const [updateCheckOn, setUpdateCheckOn] = useState(() => localStorage.getItem('dsh.worktable.updateCheck.v1') !== '0')
const [updateCopied, setUpdateCopied] = useState(false)
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'uptodate' | 'failed'>('idle')
+ const updateCheckingRef = useRef(false)
+ const updateAliveRef = useRef(true)
+ useEffect(() => () => { updateAliveRef.current = false }, [])
const checkUpdates = useCallback(async (force = false) => {
+ if (updateCheckingRef.current) return // 防重入:并发点击只保留一个 in-flight
const last = Number(localStorage.getItem('dsh.worktable.lastUpdateCheck.v1') ?? '0')
if (!force && Date.now() - last < 24 * 3600 * 1000) return // 自动检查节流;手动「立即检查」绕过节流
+ updateCheckingRef.current = true
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 { /* 网络抖动:下一轮重试 */ }
+ try {
+ let d: { tag_name?: string; body?: string; html_url?: string } | null = null
+ for (let attempt = 0; attempt < 3 && !d && updateAliveRef.current; attempt++) {
+ const ctrl = new AbortController()
+ const timer = setTimeout(() => ctrl.abort(), 8000) // 单次 8s 超时,计入下一轮重试
+ try {
+ const r = await fetch('https://api.github.com/repos/' + UPDATE_REPO + '/releases/latest', { headers: { Accept: 'application/vnd.github+json' }, cache: 'no-store', signal: ctrl.signal })
+ 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 { /* 网络抖动/超时:下一轮重试 */ }
+ finally { clearTimeout(timer) }
+ }
+ if (!updateAliveRef.current) return // 组件已卸载:不再更新状态
+ 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')
+ } finally {
+ updateCheckingRef.current = false
}
- 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 () => {
@@ -1533,7 +1547,7 @@ function buildCustomLayoutPrompt(req: string): string {
return [
'【为 dsh-worktable(工作台)插件增加一个新的布局预设】',
'',
- '背景:dsh-worktable 是 DeepSeek Harness 的自建容器插件(本机仓库 dsh-worktable,插件包根目录 01_content/)。',
+ '背景:dsh-worktable 是 DeepSeek Harness 的自建容器插件(本机仓库 dsh-worktable;不了解的细节可直接向用户提问)。',
'侧边栏「工作台」区块管理项目,每个项目打开后是一个平铺工作区(若干内容窗 + 右侧对话窗)。',
'布局预设定义在 01_content/src/client/index.tsx 的 PRESET_DEFS 数组,选择器缩略图在 presetThumb() 函数。',
'',
@@ -1646,10 +1660,10 @@ function buildCustomLayoutPrompt(req: string): string {
const ws = b.workspaces as any
const parent = consoleParent.replace(/[\\/]+$/, '')
const name = consoleName.replace(/^[\\/]+/, '').replace(/[\\/]+$/, '')
- const full = parent + '\\' + name
+ const full = joinPath(parent, name)
try { await ws?.createDirectory?.(parent, name) } catch {}
try {
- const r = await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ parent, name }) })
+ const r = await fetch('/api/worktable/mkdir', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ path: full }) })
const d = await r.json()
if (!r.ok || !d?.ok) throw new Error(d?.error ?? 'mkdir failed')
} catch { /* 目录建不出的错误最终由 workspace.create 暴露 */ }
@@ -1764,7 +1778,7 @@ function buildCustomLayoutPrompt(req: string): string {
try {
let raw = rawManifest
if (raw == null) {
- const r = await fetch('/api/worktable/file?path=' + encodeURIComponent(folder + '\\widget-result.json'), { cache: 'no-store' })
+ const r = await fetch('/api/worktable/file?path=' + encodeURIComponent(joinPath(folder, 'widget-result.json')), { cache: 'no-store' })
if (!r.ok) return
raw = (await r.text()).trim()
}
@@ -1839,7 +1853,7 @@ function buildCustomLayoutPrompt(req: string): string {
const folder = folders[pid]
if (!folder) continue
try {
- const r = await fetch('/api/worktable/file?path=' + encodeURIComponent(folder + '\\widget-result.json'), { cache: 'no-store' })
+ const r = await fetch('/api/worktable/file?path=' + encodeURIComponent(joinPath(folder, 'widget-result.json')), { cache: 'no-store' })
if (!r.ok) continue
const raw = (await r.text()).trim()
if (!raw) continue
@@ -2713,7 +2727,7 @@ function buildCustomLayoutPrompt(req: string): string {
{updateStatus === 'failed' && !updateInfo ? ' · ' + t('update.checkFail') : ''}
-