release: v0.2.2 - cross-platform paths, health version, update-check hardening, clean packaging

This commit is contained in:
Aisland
2026-08-25 02:14:02 +08:00
parent d71ac54bb9
commit b6a96405e8
17 changed files with 369 additions and 305 deletions
+2 -1
View File
@@ -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:` 只接受本地路径,不要带空格):
+7 -2
View File
@@ -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',
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "dsh-worktable",
"description": "工作台:DSH 侧边栏的 agent 级项目容器,收纳如 TravelAtlas 等自装项目",
"version": "0.2.1",
"version": "0.2.2",
"entry": {
"name": "dsh-worktable",
"inject": []
-194
View File
@@ -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 */
+63 -27
View File
@@ -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,19 +19210,32 @@ 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" });
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 (!updateAliveRef.current) return;
if (!d) {
setUpdateStatus("failed");
return;
@@ -19218,6 +19252,9 @@ function WorktableSection(props) {
}
setUpdateInfo({ latest: tag, notes: (d.body ?? "").slice(0, 800), url: d.html_url ?? "" });
setUpdateStatus("uptodate");
} finally {
updateCheckingRef.current = false;
}
}, []);
(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
+3 -3
View File
@@ -88,6 +88,7 @@ body {
var dshell_default2 = '<!doctype html>\n<!-- dsh-worktable \u539F\u751F\u76AE\u80A4\u6A21\u677F\uFF1A\u65B0\u9875\u9762\u4EE5\u6B64\u4E3A\u57FA\u7840\uFF0C\u66FF\u6362\u4E0B\u9762\u793A\u4F8B\u5185\u5BB9\u5373\u53EF\u3002\n \u6837\u5F0F\u8868\u7531\u63D2\u4EF6\u63D0\u4F9B\uFF08\u968F\u4E3B\u9898\u81EA\u52A8\u9002\u914D\uFF09\uFF0C\u4E0D\u8981\u590D\u5236\u6216\u6539\u5199\u5B83\u3002 -->\n<html lang="zh-CN">\n<head>\n <meta charset="utf-8" />\n <meta name="viewport" content="width=device-width, initial-scale=1" />\n <title>\u6211\u7684\u7A97\u53E3</title>\n <link rel="stylesheet" href="/api/worktable/template/dshell.css" />\n</head>\n<body>\n <div class="dshell">\n <!-- \u6807\u9898\u533A -->\n <h1 class="dshell-title">\u7A97\u53E3\u6807\u9898</h1>\n <p class="dshell-sub">\u4E00\u53E5\u8BDD\u8BF4\u660E\u8FD9\u4E2A\u7A97\u53E3\u505A\u4EC0\u4E48\u3002</p>\n\n <!-- \u72B6\u6001\u5FBD\u6807\uFF1A\u5DF2\u5B8C\u6210 dshell-badgeDone / \u8FDB\u884C\u4E2D dshell-badgeWait / \u9ED8\u8BA4 -->\n <div>\n <span class="dshell-badge dshell-badgeDone">\u5DF2\u5B8C\u6210</span>\n <span class="dshell-badge dshell-badgeWait">\u8FDB\u884C\u4E2D</span>\n <span class="dshell-badge">\u672A\u5F00\u59CB</span>\n </div>\n\n <!-- \u6807\u7B7E\u9875 -->\n <div class="dshell-tabs">\n <button class="dshell-tab dshell-tabOn">\u6982\u89C8</button>\n <button class="dshell-tab">\u8BE6\u60C5</button>\n <button class="dshell-tab">\u8BBE\u7F6E</button>\n </div>\n\n <!-- \u7EDF\u8BA1\u5361\u7247\u7F51\u683C -->\n <div class="dshell-grid">\n <div class="dshell-stat">\n <div class="dshell-statLabel">\u603B\u6570</div>\n <div class="dshell-statValue">128</div>\n <div class="dshell-statDelta">+12.4%</div>\n </div>\n <div class="dshell-stat">\n <div class="dshell-statLabel">\u8FDB\u884C\u4E2D</div>\n <div class="dshell-statValue">7</div>\n </div>\n <div class="dshell-stat">\n <div class="dshell-statLabel">\u5DF2\u5B8C\u6210</div>\n <div class="dshell-statValue">121</div>\n </div>\n </div>\n\n <!-- \u5217\u8868 -->\n <div class="dshell-list">\n <div class="dshell-listItem">\n <span class="dshell-listItemTitle">\u6761\u76EE\u4E00\uFF1A\u793A\u4F8B\u5185\u5BB9\u6807\u9898</span>\n <span class="dshell-listItemMeta">\u6628\u5929</span>\n </div>\n <div class="dshell-listItem">\n <span class="dshell-listItemTitle">\u6761\u76EE\u4E8C\uFF1A\u793A\u4F8B\u5185\u5BB9\u6807\u9898</span>\n <span class="dshell-badge dshell-badgeDone">\u5DF2\u53D1\u5E03</span>\n </div>\n </div>\n\n <!-- \u5361\u7247 + \u952E\u503C\u5BF9 -->\n <div class="dshell-card">\n <h2 class="dshell-sub" style="margin:0 0 8px">\u8BE6\u60C5</h2>\n <div class="dshell-kv">\n <div class="dshell-kvRow"><span class="dshell-kvKey">\u5B57\u6BB5 A</span><span class="dshell-kvValue">\u503C A</span></div>\n <div class="dshell-kvRow"><span class="dshell-kvKey">\u5B57\u6BB5 B</span><span class="dshell-kvValue">\u503C B</span></div>\n </div>\n <div class="dshell-divider"></div>\n <div class="dshell-progress"><div class="dshell-progressBar" style="width:72%"></div></div>\n </div>\n\n <!-- \u64CD\u4F5C\u533A -->\n <div style="display:flex;gap:8px">\n <button class="dshell-btn">\u4E3B\u8981\u64CD\u4F5C</button>\n <button class="dshell-btn dshell-btnGhost">\u6B21\u8981\u64CD\u4F5C</button>\n </div>\n </div>\n</body>\n</html>\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
+3 -2
View File
@@ -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",
+30 -16
View File
@@ -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,18 +1053,28 @@ function WorktableSection(props: any) {
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 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' })
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 { /* 网络抖动:下一轮重试 */ }
} 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/, '')
@@ -1071,6 +1082,9 @@ function WorktableSection(props: any) {
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
}
}, [])
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') : ''}
</span>
<span className="dsh-wt_versionActions">
<button type="button" className="dsh-wt_updateBtn" onClick={() => void checkUpdates(true)}>
<button type="button" className="dsh-wt_updateBtn" disabled={updateStatus === 'checking'} onClick={() => void checkUpdates(true)}>
{updateStatus === 'checking' ? t('update.checking') : t('update.checkNow')}
</button>
<span className="dsh-wt_updateToggle" onClick={toggleUpdateCheck}>
+39
View File
@@ -0,0 +1,39 @@
/**
* 客户端路径小工具(纯字符串,不引第三方库):
* 保留输入路径原有的分隔符风格,只做跨平台安全的拼接/取父/取名。
* 覆盖:Windows 盘符根、UNC、POSIX 绝对路径与根目录、相对路径。
*/
/** 绝对路径判定:盘符 C:\ 或 C:/、UNC \\server\share、POSIX / 开头 */
export function isAbs(p: string): boolean {
if (/^[A-Za-z]:[\\/]/.test(p)) return true
if (p.startsWith('\\\\')) return true
if (p.startsWith('/')) return true
return false
}
/** 按 base 的分隔符风格拼接(base 含 \ 用 \,否则用 /;剥掉两端重复分隔符;盘符根与根目录不吞分隔符) */
export function joinPath(base: string, rel: string): string {
const relTrim = rel.replace(/^[\\/]+/, '').replace(/[\\/]+$/, '')
const sep = base.includes('\\') ? '\\' : '/'
if (base === '/' || base === '\\') return base + relTrim
if (/^[A-Za-z]:[\\/]$/.test(base)) return base + relTrim
const baseTrim = base.replace(/[\\/]+$/, '')
if (!baseTrim) return relTrim || sep
return baseTrim + sep + relTrim
}
/** 取父目录:'/foo'→'/';'C:\foo'→'C:\';'C:\'→'C:\';'/'→'/';'\\server\share\x'→'\\server\share' */
export function parentPathOf(p: string): string {
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)
}
/** 取文件名:'a/b/c.html'→'c.html' */
export function basenameOf(p: string): string {
const idx = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'))
return idx < 0 ? p : p.slice(idx + 1)
}
+1 -8
View File
@@ -1,4 +1,5 @@
import { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { parentPathOf } from './pathutil'
import { Terminal } from 'xterm'
import MarkdownIt from 'markdown-it'
@@ -253,14 +254,6 @@ async function postJson(url: string, body: unknown): Promise<any> {
return res.json()
}
function parentPathOf(p: string): string {
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
}
/** 拖拽换位暂存 */
let dragPane: { row: PaneRow; index: number } | null = null
/** 标签拖拽暂存(跨窗移动) */
+5 -2
View File
@@ -16,6 +16,9 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
* node-pty 与 ws,缺失时该路由不注册、终端窗降级提示)
*/
declare const __WT_VERSION__: string
const PLUGIN_VERSION = typeof __WT_VERSION__ === 'undefined' ? 'dev' : __WT_VERSION__
export const name = 'dsh-worktable'
export const inject = ['webServer', 'sessions']
@@ -209,7 +212,7 @@ export function apply(ctx: Context) {
kind: 'exact',
path: HEALTH_PATH,
handler: (_req: any, res: any) => {
json(res, 200, { plugin: 'dsh-worktable', version: '0.2.0', ok: true })
json(res, 200, { plugin: 'dsh-worktable', version: PLUGIN_VERSION, ok: true })
},
})
@@ -362,7 +365,7 @@ export function apply(ctx: Context) {
if (!p) { json(res, 400, { error: 'missing path' }); return }
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 { json(res, 400, { error: 'parent not found' }); return }
await fsx.mkdir(abs)
json(res, 200, { ok: true, path: abs })
+6 -17
View File
@@ -474,8 +474,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
var out={};
try{
var st=window.__dshWorktable.splitStore;
var probe=await fetch('/api/worktable/write',{method:'POST',headers:{'content-type':'application/json'},body:'{}'}).catch(function(){return null;});
out.writeRouteReady=!!(probe&&probe.status===400);
var probe=await fetch('/api/worktable/write',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({path:'C:/Users/SJL/AppData/Local/Temp/wt-edit-test.md',content:'# ORIG'})}).catch(function(){return null;});
out.writeRouteReady=!!(probe&&probe.status===200);
if(out.writeRouteReady){
st.open({id:'t-mdedit',title:'md',top:null,main:[{id:'m1',title:'m',min:200,content:null}],chatWidth:{default:320,min:240,max:600}});
await new Promise(function(r){setTimeout(r,400)});
@@ -708,28 +708,17 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// 还原临时夹具
try { fs.writeFileSync(TEMP_MD, '# ORIG\n', 'utf8') } catch {}
const errors = events.filter((e) => {
const ent = e.method === 'Log.entryAdded' ? e.params.entry : null;
const txt = ent ? (ent.text || '') + '|' + (ent.url || '') : '';
if (txt.indexOf('/api/worktable/write') >= 0) return false;
return true;
}).filter((e) => {
// 服务端新路由(site/write/workspaces):运行中的服务器未重启前其 404 属预期(重启后移除本过滤)
const ent = e.method === 'Log.entryAdded' ? e.params.entry : null;
const txt = ent ? (ent.text || '') + '|' + (ent.url || '') : '';
if (txt.indexOf('/api/worktable/site') >= 0) return false;
if (txt.indexOf('/api/worktable/workspaces') >= 0) return false;
return true;
}).filter((e) =>
// 严格门禁:不再掩盖任何插件路由错误(site/write/workspaces 曾因旧服务器未重启被过滤,已移除)
const errors = events.filter((e) =>
e.method === 'Runtime.exceptionThrown' ||
(e.method === 'Log.entryAdded' && e.params.entry.level === 'error') ||
(e.method === 'Runtime.consoleAPICalled' && e.params.type === 'error'),
).slice(0, 10).map((e) => e.method === 'Runtime.exceptionThrown'
? 'EXCEPTION: ' + ((e.params.exceptionDetails.exception || {}).description || e.params.exceptionDetails.text)
: 'LOG: ' + (e.params.entry ? e.params.entry.text : '').slice(0, 200));
: 'LOG: ' + (e.params.entry ? e.params.entry.text : '').slice(0, 200) + (e.params.entry && e.params.entry.url ? ' @ ' + e.params.entry.url : ''));
console.log('ERRORS_COUNT:', errors.length);
errors.forEach((x) => console.log(x));
ws.close();
proc.kill();
process.exit(0);
process.exit(errors.length === 0 ? 0 : 1); // 严格门禁:发现错误即非零退出
})().catch((e) => { console.log('SCRIPT_FAIL:', e); proc.kill(); process.exit(1); });
+55
View File
@@ -0,0 +1,55 @@
// pathutil-matrix.cjs — pathutil 路径矩阵定向验证(用 esbuild 把 TS 编译成 CJS 后实测)
const path = require('path');
const os = require('os');
const fs = require('fs');
const { buildSync } = require('../01_content/node_modules/esbuild');
const out = path.join(os.tmpdir(), 'wt-pathutil-' + process.pid + '.cjs');
buildSync({
entryPoints: [path.join(__dirname, '..', '01_content', 'src', 'client', 'pathutil.ts')],
outfile: out,
bundle: true,
platform: 'node',
format: 'cjs',
logLevel: 'silent',
});
const u = require(out);
fs.unlinkSync(out);
let fails = 0;
const eq = (label, got, want) => {
if (got === want) { console.log('PASS ' + label + ' = ' + JSON.stringify(got)); }
else { fails++; console.log('FAIL ' + label + ': got ' + JSON.stringify(got) + ' want ' + JSON.stringify(want)); }
};
// joinPath:Windows 反斜杠风格 / Windows 正斜杠风格 / POSIX / 根目录 / 盘符根 / UNC / 相对
eq("join C:\\base+rel", u.joinPath('C:\\base', 'rel'), 'C:\\base\\rel');
eq("join C:/base+rel", u.joinPath('C:/base', 'rel'), 'C:/base/rel');
eq("join /Users+project", u.joinPath('/Users/name', 'project'), '/Users/name/project');
eq("join /+project", u.joinPath('/', 'project'), '/project');
eq("join C:\\+project", u.joinPath('C:\\', 'project'), 'C:\\project');
eq("join UNC+x", u.joinPath('\\\\server\\share', 'x'), '\\\\server\\share\\x');
eq("join rel+rel", u.joinPath('a/b', 'c'), 'a/b/c');
eq("join trim seps", u.joinPath('C:\\base\\', '\\rel\\'), 'C:\\base\\rel');
// isAbs
eq("isAbs C:\\x", u.isAbs('C:\\x'), true);
eq("isAbs C:/x", u.isAbs('C:/x'), true);
eq("isAbs /x", u.isAbs('/x'), true);
eq("isAbs UNC", u.isAbs('\\\\server\\share'), true);
eq("isAbs rel", u.isAbs('rel/x'), false);
// parentPathOf:POSIX 根 / 盘符根 / 根自身 / UNC / 相对
eq("parent /foo", u.parentPathOf('/foo'), '/');
eq("parent C:\\foo", u.parentPathOf('C:\\foo'), 'C:\\');
eq("parent C:\\", u.parentPathOf('C:\\'), 'C:\\');
eq("parent /", u.parentPathOf('/'), '/');
eq("parent UNC/x", u.parentPathOf('\\\\server\\share\\x'), '\\\\server\\share');
eq("parent a/b", u.parentPathOf('a/b'), 'a');
// basenameOf
eq("base a/b/c.html", u.basenameOf('a/b/c.html'), 'c.html');
eq("base C:\\x\\y.txt", u.basenameOf('C:\\x\\y.txt'), 'y.txt');
console.log(fails === 0 ? 'MATRIX ALL PASS' : 'MATRIX FAILS: ' + fails);
process.exit(fails === 0 ? 0 : 1);
+117
View File
@@ -0,0 +1,117 @@
// probe-update-scenarios.cjs — 更新检查五场景定向验证(CDP Fetch 拦截;fulfill 带 CORS 头)
const http = require('http');
const { spawn } = require('child_process');
const WebSocket = require('ws');
const PORT = 9398;
const proc = spawn('C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', [
'--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check',
'--window-size=1200,800', '--force-device-scale-factor=1',
'--remote-debugging-port=' + PORT,
'--user-data-dir=C:\\Users\\SJL\\AppData\\Local\\Temp\\wt-chrome-updscn2',
'about:blank',
], { stdio: 'ignore' });
const getJSON = (p) => new Promise((res, rej) => {
http.get({ host: '127.0.0.1', port: PORT, path: p }, (r) => { let d = ''; r.on('data', c => d += c); r.on('end', () => res(JSON.parse(d))); }).on('error', rej);
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const OK_BODY = Buffer.from(JSON.stringify({ tag_name: 'v0.1.0', body: 'scenario test', html_url: 'https://example.com/x' })).toString('base64');
const FAIL_BODY = Buffer.from('{"message":"boom"}').toString('base64');
const HI_BODY = Buffer.from(JSON.stringify({ tag_name: 'v0.2.3', body: 'v0.2.3 release notes', html_url: 'https://example.com/hi' })).toString('base64');
const HDRS = [{ name: 'Content-Type', value: 'application/json' }, { name: 'Access-Control-Allow-Origin', value: '*' }];
let mode = 'success';
let reqCount = 0;
let fulfillCount = 0;
(async () => {
let list = null;
for (let i = 0; i < 40; i++) { try { list = await getJSON('/json/list'); if (list && list.length) break; } catch {} await sleep(500); }
const target = list.find((t) => t.type === 'page') || list[0];
const ws = new WebSocket(target.webSocketDebuggerUrl);
let id = 0; const pending = new Map();
const send = (method, params) => new Promise((res) => { const mid = ++id; pending.set(mid, res); ws.send(JSON.stringify({ id: mid, method, params })); });
ws.on('message', (raw) => {
const m = JSON.parse(String(raw));
if (m.id && pending.has(m.id)) { pending.get(m.id)(m); pending.delete(m.id); }
else if (m.method === 'Fetch.requestPaused') {
reqCount++;
const rid = m.params.requestId;
if (mode === 'pending') {
setTimeout(() => { try { ws.send(JSON.stringify({ id: ++id, method: 'Fetch.continueRequest', params: { requestId: rid } })); } catch {} }, 9000);
} else if (mode === 'fail500') {
ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 500, responseHeaders: HDRS, body: FAIL_BODY } }));
} else if (mode === 'twothenok') {
if (fulfillCount < 2) { fulfillCount++; ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 500, responseHeaders: HDRS, body: FAIL_BODY } })); }
else { ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 200, responseHeaders: HDRS, body: OK_BODY } })); }
} else if (mode === 'slowok') {
setTimeout(() => { try { ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 200, responseHeaders: HDRS, body: OK_BODY } })); } catch {} }, 2500);
} else if (mode === 'hiok') {
ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 200, responseHeaders: HDRS, body: HI_BODY } }));
} else {
ws.send(JSON.stringify({ id: ++id, method: 'Fetch.fulfillRequest', params: { requestId: rid, responseCode: 200, responseHeaders: HDRS, body: OK_BODY } }));
}
}
});
await new Promise((r) => ws.on('open', r));
await send('Runtime.enable');
await send('Page.enable');
await send('Fetch.enable', { patterns: [{ urlPattern: '*api.github.com*' }] });
const evaluate = async (expression) => {
const r = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
if (r.result && r.result.exceptionDetails) return { __error: String(r.result.exceptionDetails.text) };
return r.result && r.result.result ? r.result.result.value : undefined;
};
const rowSnap = () => evaluate("(function(){ var r=document.querySelector('.dsh-wt_versionRow'); if(!r) return null; var btn=Array.from(r.querySelectorAll('button')).find(function(b){ return b.textContent.indexOf('检查')>=0 || b.textContent.indexOf('Check')>=0; }); return { text: r.innerText.replace(/\\s+/g,' '), disabled: btn ? btn.disabled : null, label: btn ? btn.textContent.trim() : null }; })()");
const openSettings = async () => {
await evaluate("(function(){ var b=document.querySelector('button[title=\"设置\"]')||document.querySelector('button[title=\"Settings\"]'); if(b) b.click(); })()");
await sleep(500);
};
const clickCheck = async () => {
await evaluate("(function(){ var r=document.querySelector('.dsh-wt_versionRow'); if(!r) return; var btn=Array.from(r.querySelectorAll('button')).find(function(b){ return b.textContent.indexOf('检查')>=0 || b.textContent.indexOf('Check')>=0; }); if(btn) btn.click(); })()");
};
async function scenario(name, opts) {
mode = opts.mode; fulfillCount = 0;
// 预热加载:让首屏自动检查在计数前跑完
await send('Page.navigate', { url: 'http://127.0.0.1:3080/' });
await sleep(6000);
const rail = await evaluate("!!document.querySelector('.dsh-wt_rail')");
if (rail) { await evaluate("(function(){ var b=document.querySelector('button.hHd-Xa_toggle'); if(b) b.click(); })()"); await sleep(1500); }
await evaluate("localStorage.setItem('dsh.worktable.lastUpdateCheck.v1', String(Date.now())); localStorage.removeItem('dsh.worktable.skipVersion.v1');");
reqCount = 0;
await send('Page.navigate', { url: 'http://127.0.0.1:3080/' });
await sleep(6000);
const rail2 = await evaluate("!!document.querySelector('.dsh-wt_rail')");
if (rail2) { await evaluate("(function(){ var b=document.querySelector('button.hHd-Xa_toggle'); if(b) b.click(); })()"); await sleep(1500); }
await openSettings();
const before = await rowSnap();
await clickCheck();
if (opts.reentry) { for (let i = 0; i < 4; i++) { await clickCheck(); } }
await sleep(Math.min(900, opts.waitMs));
const mid = await rowSnap();
await sleep(opts.waitMs);
const end = await rowSnap();
console.log('SCENARIO[' + name + '] requests=' + reqCount);
console.log(' mid:', JSON.stringify(mid));
console.log(' end:', JSON.stringify(end));
return { before, mid, end, requests: reqCount };
}
const results = [];
results.push(['1-success', await scenario('1-success', { mode: 'success', waitMs: 4000 })]);
results.push(['2-pending-timeout', await scenario('2-pending-timeout', { mode: 'pending', waitMs: 26000 })]);
results.push(['3-twothenok', await scenario('3-twothenok', { mode: 'twothenok', waitMs: 5000 })]);
results.push(['4-fail-all', await scenario('4-fail-all', { mode: 'fail500', waitMs: 4000 })]);
results.push(['5-reentry', await scenario('5-reentry', { mode: 'slowok', waitMs: 4000, reentry: true })]);
results.push(['6-higher-version', await scenario('6-higher-version', { mode: 'hiok', waitMs: 4000 })]);
const hiState = await evaluate("(function(){ var b=document.querySelector('.dsh-wt_updateBadge'); if(!b) return {badge:false}; b.click(); return {badge:true}; })()");
await sleep(700);
const hiCard = await evaluate("(function(){ var c=document.querySelector('.dsh-wt_updateCard'); if(!c) return {card:false}; return {card:true, text:c.innerText.replace(/\\s+/g,' ')}; })()");
console.log('HI_BADGE:', JSON.stringify(hiState), 'HI_CARD:', JSON.stringify(hiCard));
let pass = true;
const ok = (cond, label) => { if (!cond) { pass = false; console.log(' ❌ ' + label); } else { console.log(' ✓ ' + label); } };
const [, s1] = results[0]; ok(s1.requests >= 1 && /已是最新版本|Up to date/.test(s1.end.text || ''), '成功 → 已是最新版本');
const [, s2] = results[1]; ok(s2.requests === 3 && /上次检查未成功|Last check failed/.test(s2.end.text || ''), 'pending×3 超时 → 失败提示'); ok(s2.mid && s2.mid.disabled === true && /检查中|Checking/.test(s2.mid.label || ''), '检查中按钮禁用+文案');
const [, s3] = results[2]; ok(s3.requests === 3 && /已是最新版本|Up to date/.test(s3.end.text || ''), '两败一成 → 已是最新版本');
const [, s4] = results[3]; ok(s4.requests === 3 && /上次检查未成功|Last check failed/.test(s4.end.text || ''), '三败 → 失败提示');
const [, s5] = results[4]; ok(s5.requests === 1, '慢响应期间连点 5 次仅 1 个 in-flight(防重入)'); ok(s5.mid && s5.mid.disabled === true, '请求进行中按钮保持禁用');
const [, s6] = results[5]; ok(s6.requests >= 1 && hiState && hiState.badge === true && hiCard && hiCard.card === true && /v0\.2\.3/.test(hiCard.text || ''), '更高版本 → 徽标亮起 + 更新卡显示 v0.2.3');
console.log(pass ? 'ALL SCENARIOS PASS' : 'SCENARIO FAILURES PRESENT');
ws.close(); proc.kill(); process.exit(pass ? 0 : 1);
})().catch((e) => { console.error('PROBE FAIL', e); try { proc.kill(); } catch {} process.exit(1); });
+5 -2
View File
@@ -17,6 +17,8 @@
- **不替换、不禁用任何官方插件**(ui-sidebar / ui-workspace / ui-layout)。
- 所有状态只存 localStorage(键 `dsh.worktable.view.v1`),不读写工作区文件。
- dsh-travelatlas 是入驻项目而非本仓库的一部分;协议见 `02_process/PRD.md` §5.3/5.4。
- **平台边界**:Windows 是当前完整验证平台;macOS 为实验性支持(核心文件路径代码已做跨平台适配,
尚未真机端到端验证)。路径拼接必须走 `pathutil.ts` helper 或 Node `path` API,不手写分隔符。
## 构建与验证
@@ -138,8 +140,9 @@ node --check lib/index.js
previewCache;sweepPreviews 在打开控制室时 + 控制室开着且会话快照变化防抖 6s 触发;
失败静默回退内存路径 lastTextOf。拉取是带宽成本不是 Token 成本。
- 状态指示:卡片右上角小圆点已删(整卡光效表达状态);状态计算不变。
- **更新检查(v0.2.1)**:客户端直连 GitHub Releases API 比版本(只读 GET;自动每天最多
一次,手动「立即检查」绕过节流;失败重试 3 次后状态行显示「上次检查未成功」)。
- **更新检查(v0.2.2)**:客户端直连 GitHub Releases API 比版本(只读 GET;自动每天最多
一次,手动「立即检查」绕过节流;单次 8s 超时(AbortController)+ 最多 3 次重试,
in-flight 防重入、检查中按钮禁用、组件卸载后停止重试与状态更新;失败后状态行显示「上次检查未成功」)。
状态四态 idle/checking/uptodate/failed;徽标 = 「工作台」标题右侧琥珀呼吸小圆(SVG 同步
图标、不显示版本号),仅发现更新时出现;更新卡在设置面板顶部(复制 AI 提示词 + 忽略此
版本 + 命令框供终端用户手抄),版本号与自动检查开关在面板底部;设置弹窗右上角 ✕ 关闭、
+2 -1
View File
@@ -98,12 +98,13 @@ node --check lib/index.js
- **Build must run inside `01_content`** — building from the repo root writes `lib/` to the wrong place while the host keeps loading the old bundle
- The client bundle keeps the `window.__ModuleLoader__.load` handshake; `react` and `@deepseek-ai/*` stay external
- Regression: `04_test/functional-diag.cjs` (20 steps) plus targeted probes (control room, bind panel, collapsed rail, model inheritance)
- Regression: `04_test/functional-diag.cjs` (20 steps, strict gate) plus targeted probes (control room, bind panel, collapsed rail, model inheritance), the path matrix (`04_test/pathutil-matrix.cjs`) and update-check scenarios (`04_test/probe-update-scenarios.cjs`)
---
## Known limits
- **Platform**: Windows is the fully tested platform. macOS support is experimental: the core file-path code has been adapted for cross-platform use, but no end-to-end test has been completed on macOS hardware.
- State lives in the browser (`localStorage`) — projects, bindings and views do not sync across machines
- The terminal pane is a plain PowerShell host on Windows (no PTY feature parity with the native terminal app)
- Auto-mount requires the agent to actually write `widget-result.json` in the project folder
+1
View File
@@ -104,6 +104,7 @@ node --check lib/index.js
## 已知限制
- **平台**:Windows 是当前完整验证平台。macOS 为实验性支持:核心文件路径代码已做跨平台适配,但尚未在 macOS 真机完成端到端验证。
- 状态在浏览器本地(localStorage)——项目、绑定与视图不跨设备同步
- 终端窗格在 Windows 上是朴素的 PowerShell 宿主(与原生终端应用无 PTY 对等)
- 自动挂载要求 agent 确实在项目文件夹写出 `widget-result.json`