fix(desktop): 状态镜像到宿主侧,修复随机端口导致重启丢数据
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dsh-worktable",
|
||||
"description": "工作台:DSH 侧边栏的 agent 级项目容器,收纳如 TravelAtlas 等自装项目",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2-local.1",
|
||||
"entry": {
|
||||
"name": "dsh-worktable",
|
||||
"inject": []
|
||||
|
||||
+140
-23
File diff suppressed because one or more lines are too long
+123
-76
@@ -118,84 +118,84 @@ function createPreviewFiles(now = () => Date.now()) {
|
||||
}
|
||||
|
||||
// template/dshell.css
|
||||
var dshell_default = `/* dsh-worktable \u539F\u751F\u76AE\u80A4 \xB7 DSH \u8BBE\u8BA1\u7CFB\u7EDF\u7EC4\u4EF6\u5E93
|
||||
\u7528\u6CD5\uFF1A<link rel="stylesheet" href="/api/worktable/template/dshell.css">
|
||||
\u6240\u6709\u989C\u8272\u8D70 DSH \u4E3B\u9898\u53D8\u91CF\uFF08--dsw-alias-*\uFF09\uFF0C\u81EA\u52A8\u9002\u914D\u660E\u6697\u4E3B\u9898\u3002 */
|
||||
:root { color-scheme: dark; }
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--dsw-alias-bg-base, #0b0e14);
|
||||
color: var(--dsw-alias-label-primary, #e6e8eb);
|
||||
font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.dshell { display: flex; flex-direction: column; gap: 12px; padding: 14px 16px; min-height: 100%; }
|
||||
/* \u6587\u5B57\u5C42\u7EA7 */
|
||||
.dshell-title { margin: 0; font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, #e6e8eb); }
|
||||
.dshell-sub { margin: 0; font-size: 12px; color: var(--dsw-alias-label-secondary, #9aa4b2); }
|
||||
.dshell-muted { color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 11.5px; }
|
||||
/* \u5361\u7247 */
|
||||
.dshell-card { border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 10px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.02)); padding: 12px 14px; }
|
||||
.dshell-card + .dshell-card { margin-top: 10px; }
|
||||
/* \u6309\u94AE\uFF08\u7EFF\u8272\u4E3B\u6309\u94AE / \u5E7D\u7075\u6309\u94AE / \u5371\u9669\uFF09 */
|
||||
.dshell-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 999px; border: 1px solid transparent; background: #3fb950; color: #0b0e14; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; }
|
||||
.dshell-btn:hover { filter: brightness(1.08); }
|
||||
.dshell-btnGhost { background: transparent; border-color: var(--dsw-alias-border-l1, #262b36); color: var(--dsw-alias-label-secondary, #9aa4b2); }
|
||||
.dshell-btnGhost:hover { color: var(--dsw-alias-label-primary, #e6e8eb); border-color: var(--dsw-alias-border-l2, #3a4150); }
|
||||
.dshell-btnDanger { background: transparent; border-color: #f85149; color: #f85149; }
|
||||
/* \u72B6\u6001\u5FBD\u6807\uFF08\u5706\u70B9 + \u6587\u5B57\uFF1B\u7EFF=\u5DF2\u5B8C\u6210 \u9EC4=\u5F85\u529E/\u5F85\u53D1\u5E03 \u7070=\u672A\u5F00\u59CB\uFF09 */
|
||||
.dshell-badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1, #262b36); font-size: 11.5px; color: var(--dsw-alias-label-secondary, #9aa4b2); background: var(--dsw-alias-fill-l1, rgba(255,255,255,.03)); }
|
||||
.dshell-badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #6b7280); }
|
||||
.dshell-badgeDone { color: #3fb950; border-color: rgba(63,185,80,.4); }
|
||||
.dshell-badgeDone::before { background: #3fb950; box-shadow: 0 0 5px #3fb950; }
|
||||
.dshell-badgeWait { color: #d29922; border-color: rgba(210,153,34,.4); }
|
||||
.dshell-badgeWait::before { background: #d29922; box-shadow: 0 0 5px #d29922; }
|
||||
.dshell-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #6b7280); }
|
||||
.dshell-dotDone { background: #3fb950; box-shadow: 0 0 5px #3fb950; }
|
||||
.dshell-dotWait { background: #d29922; box-shadow: 0 0 5px #d29922; }
|
||||
/* \u6807\u7B7E\u9875 */
|
||||
.dshell-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l1, #262b36); }
|
||||
.dshell-tab { padding: 7px 12px; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #9aa4b2); cursor: pointer; border: none; background: none; font: inherit; border-bottom: 2px solid transparent; margin-bottom: -1px; }
|
||||
.dshell-tabOn { color: var(--dsw-alias-label-primary, #e6e8eb); border-bottom-color: var(--dsw-alias-state-accent-primary, #4f8ef7); }
|
||||
/* \u5217\u8868 */
|
||||
.dshell-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.dshell-listItem { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 12px; border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 8px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.02)); cursor: pointer; }
|
||||
.dshell-listItem:hover { border-color: var(--dsw-alias-border-l2, #3a4150); background: var(--dsw-alias-fill-l1, rgba(255,255,255,.05)); }
|
||||
.dshell-listItemTitle { font-size: 12.5px; color: var(--dsw-alias-label-primary, #e6e8eb); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.dshell-listItemMeta { flex: none; font-size: 11px; color: var(--dsw-alias-label-tertiary, #6b7280); }
|
||||
/* \u7F51\u683C / \u7EDF\u8BA1 */
|
||||
.dshell-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
|
||||
.dshell-stat { padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 10px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.02)); }
|
||||
.dshell-statLabel { font-size: 11px; color: var(--dsw-alias-label-secondary, #9aa4b2); }
|
||||
.dshell-statValue { font-size: 20px; font-weight: 600; color: var(--dsw-alias-label-primary, #e6e8eb); }
|
||||
.dshell-statDelta { font-size: 11px; color: #3fb950; }
|
||||
/* \u8FDB\u5EA6\u6761 */
|
||||
.dshell-progress { height: 6px; border-radius: 3px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.06)); overflow: hidden; }
|
||||
.dshell-progressBar { height: 100%; border-radius: 3px; background: #3fb950; }
|
||||
/* \u8F93\u5165 */
|
||||
.dshell-input, .dshell-textarea { width: 100%; padding: 7px 10px; border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 8px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.03)); color: var(--dsw-alias-label-primary, #e6e8eb); font: inherit; font-size: 12.5px; outline: none; }
|
||||
.dshell-input:focus, .dshell-textarea:focus { border-color: var(--dsw-alias-state-accent-primary, #4f8ef7); }
|
||||
/* \u8868\u683C */
|
||||
.dshell-table { width: 100%; border-collapse: collapse; font-size: 12px; }
|
||||
.dshell-table th, .dshell-table td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1, #262b36); }
|
||||
.dshell-table th { color: var(--dsw-alias-label-secondary, #9aa4b2); font-weight: 500; }
|
||||
/* \u952E\u503C\u5BF9 */
|
||||
.dshell-kv { display: flex; flex-direction: column; gap: 6px; }
|
||||
.dshell-kvRow { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; }
|
||||
.dshell-kvKey { color: var(--dsw-alias-label-secondary, #9aa4b2); }
|
||||
.dshell-kvValue { color: var(--dsw-alias-label-primary, #e6e8eb); text-align: right; }
|
||||
/* \u5206\u5272\u7EBF */
|
||||
.dshell-divider { height: 1px; background: var(--dsw-alias-border-l1, #262b36); margin: 6px 0; }
|
||||
/* \u6EDA\u52A8\u6761 */
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-thumb { background: rgba(255,255,255,.14); border-radius: 5px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
var dshell_default = `/* dsh-worktable \u539F\u751F\u76AE\u80A4 \xB7 DSH \u8BBE\u8BA1\u7CFB\u7EDF\u7EC4\u4EF6\u5E93\r
|
||||
\u7528\u6CD5\uFF1A<link rel="stylesheet" href="/api/worktable/template/dshell.css">\r
|
||||
\u6240\u6709\u989C\u8272\u8D70 DSH \u4E3B\u9898\u53D8\u91CF\uFF08--dsw-alias-*\uFF09\uFF0C\u81EA\u52A8\u9002\u914D\u660E\u6697\u4E3B\u9898\u3002 */\r
|
||||
:root { color-scheme: dark; }\r
|
||||
* { box-sizing: border-box; }\r
|
||||
html, body { margin: 0; padding: 0; }\r
|
||||
body {\r
|
||||
background: var(--dsw-alias-bg-base, #0b0e14);\r
|
||||
color: var(--dsw-alias-label-primary, #e6e8eb);\r
|
||||
font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif;\r
|
||||
font-size: 13px;\r
|
||||
line-height: 1.6;\r
|
||||
}\r
|
||||
.dshell { display: flex; flex-direction: column; gap: 12px; padding: 14px 16px; min-height: 100%; }\r
|
||||
/* \u6587\u5B57\u5C42\u7EA7 */\r
|
||||
.dshell-title { margin: 0; font-size: 16px; font-weight: 600; color: var(--dsw-alias-label-primary, #e6e8eb); }\r
|
||||
.dshell-sub { margin: 0; font-size: 12px; color: var(--dsw-alias-label-secondary, #9aa4b2); }\r
|
||||
.dshell-muted { color: var(--dsw-alias-label-tertiary, #6b7280); font-size: 11.5px; }\r
|
||||
/* \u5361\u7247 */\r
|
||||
.dshell-card { border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 10px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.02)); padding: 12px 14px; }\r
|
||||
.dshell-card + .dshell-card { margin-top: 10px; }\r
|
||||
/* \u6309\u94AE\uFF08\u7EFF\u8272\u4E3B\u6309\u94AE / \u5E7D\u7075\u6309\u94AE / \u5371\u9669\uFF09 */\r
|
||||
.dshell-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; border-radius: 999px; border: 1px solid transparent; background: #3fb950; color: #0b0e14; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; }\r
|
||||
.dshell-btn:hover { filter: brightness(1.08); }\r
|
||||
.dshell-btnGhost { background: transparent; border-color: var(--dsw-alias-border-l1, #262b36); color: var(--dsw-alias-label-secondary, #9aa4b2); }\r
|
||||
.dshell-btnGhost:hover { color: var(--dsw-alias-label-primary, #e6e8eb); border-color: var(--dsw-alias-border-l2, #3a4150); }\r
|
||||
.dshell-btnDanger { background: transparent; border-color: #f85149; color: #f85149; }\r
|
||||
/* \u72B6\u6001\u5FBD\u6807\uFF08\u5706\u70B9 + \u6587\u5B57\uFF1B\u7EFF=\u5DF2\u5B8C\u6210 \u9EC4=\u5F85\u529E/\u5F85\u53D1\u5E03 \u7070=\u672A\u5F00\u59CB\uFF09 */\r
|
||||
.dshell-badge { display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px; border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1, #262b36); font-size: 11.5px; color: var(--dsw-alias-label-secondary, #9aa4b2); background: var(--dsw-alias-fill-l1, rgba(255,255,255,.03)); }\r
|
||||
.dshell-badge::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #6b7280); }\r
|
||||
.dshell-badgeDone { color: #3fb950; border-color: rgba(63,185,80,.4); }\r
|
||||
.dshell-badgeDone::before { background: #3fb950; box-shadow: 0 0 5px #3fb950; }\r
|
||||
.dshell-badgeWait { color: #d29922; border-color: rgba(210,153,34,.4); }\r
|
||||
.dshell-badgeWait::before { background: #d29922; box-shadow: 0 0 5px #d29922; }\r
|
||||
.dshell-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #6b7280); }\r
|
||||
.dshell-dotDone { background: #3fb950; box-shadow: 0 0 5px #3fb950; }\r
|
||||
.dshell-dotWait { background: #d29922; box-shadow: 0 0 5px #d29922; }\r
|
||||
/* \u6807\u7B7E\u9875 */\r
|
||||
.dshell-tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l1, #262b36); }\r
|
||||
.dshell-tab { padding: 7px 12px; font-size: 12.5px; color: var(--dsw-alias-label-secondary, #9aa4b2); cursor: pointer; border: none; background: none; font: inherit; border-bottom: 2px solid transparent; margin-bottom: -1px; }\r
|
||||
.dshell-tabOn { color: var(--dsw-alias-label-primary, #e6e8eb); border-bottom-color: var(--dsw-alias-state-accent-primary, #4f8ef7); }\r
|
||||
/* \u5217\u8868 */\r
|
||||
.dshell-list { display: flex; flex-direction: column; gap: 6px; }\r
|
||||
.dshell-listItem { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 12px; border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 8px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.02)); cursor: pointer; }\r
|
||||
.dshell-listItem:hover { border-color: var(--dsw-alias-border-l2, #3a4150); background: var(--dsw-alias-fill-l1, rgba(255,255,255,.05)); }\r
|
||||
.dshell-listItemTitle { font-size: 12.5px; color: var(--dsw-alias-label-primary, #e6e8eb); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\r
|
||||
.dshell-listItemMeta { flex: none; font-size: 11px; color: var(--dsw-alias-label-tertiary, #6b7280); }\r
|
||||
/* \u7F51\u683C / \u7EDF\u8BA1 */\r
|
||||
.dshell-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }\r
|
||||
.dshell-stat { padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 10px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.02)); }\r
|
||||
.dshell-statLabel { font-size: 11px; color: var(--dsw-alias-label-secondary, #9aa4b2); }\r
|
||||
.dshell-statValue { font-size: 20px; font-weight: 600; color: var(--dsw-alias-label-primary, #e6e8eb); }\r
|
||||
.dshell-statDelta { font-size: 11px; color: #3fb950; }\r
|
||||
/* \u8FDB\u5EA6\u6761 */\r
|
||||
.dshell-progress { height: 6px; border-radius: 3px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.06)); overflow: hidden; }\r
|
||||
.dshell-progressBar { height: 100%; border-radius: 3px; background: #3fb950; }\r
|
||||
/* \u8F93\u5165 */\r
|
||||
.dshell-input, .dshell-textarea { width: 100%; padding: 7px 10px; border: 1px solid var(--dsw-alias-border-l1, #262b36); border-radius: 8px; background: var(--dsw-alias-fill-l1, rgba(255,255,255,.03)); color: var(--dsw-alias-label-primary, #e6e8eb); font: inherit; font-size: 12.5px; outline: none; }\r
|
||||
.dshell-input:focus, .dshell-textarea:focus { border-color: var(--dsw-alias-state-accent-primary, #4f8ef7); }\r
|
||||
/* \u8868\u683C */\r
|
||||
.dshell-table { width: 100%; border-collapse: collapse; font-size: 12px; }\r
|
||||
.dshell-table th, .dshell-table td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--dsw-alias-border-l1, #262b36); }\r
|
||||
.dshell-table th { color: var(--dsw-alias-label-secondary, #9aa4b2); font-weight: 500; }\r
|
||||
/* \u952E\u503C\u5BF9 */\r
|
||||
.dshell-kv { display: flex; flex-direction: column; gap: 6px; }\r
|
||||
.dshell-kvRow { display: flex; justify-content: space-between; gap: 10px; font-size: 12px; }\r
|
||||
.dshell-kvKey { color: var(--dsw-alias-label-secondary, #9aa4b2); }\r
|
||||
.dshell-kvValue { color: var(--dsw-alias-label-primary, #e6e8eb); text-align: right; }\r
|
||||
/* \u5206\u5272\u7EBF */\r
|
||||
.dshell-divider { height: 1px; background: var(--dsw-alias-border-l1, #262b36); margin: 6px 0; }\r
|
||||
/* \u6EDA\u52A8\u6761 */\r
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }\r
|
||||
::-webkit-scrollbar-thumb { background: rgba(255,255,255,.14); border-radius: 5px; }\r
|
||||
::-webkit-scrollbar-track { background: transparent; }\r
|
||||
`;
|
||||
|
||||
// template/dshell.html
|
||||
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';
|
||||
var dshell_default2 = '<!doctype html>\r\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\r\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 -->\r\n<html lang="zh-CN">\r\n<head>\r\n <meta charset="utf-8" />\r\n <meta name="viewport" content="width=device-width, initial-scale=1" />\r\n <title>\u6211\u7684\u7A97\u53E3</title>\r\n <link rel="stylesheet" href="/api/worktable/template/dshell.css" />\r\n</head>\r\n<body>\r\n <div class="dshell">\r\n <!-- \u6807\u9898\u533A -->\r\n <h1 class="dshell-title">\u7A97\u53E3\u6807\u9898</h1>\r\n <p class="dshell-sub">\u4E00\u53E5\u8BDD\u8BF4\u660E\u8FD9\u4E2A\u7A97\u53E3\u505A\u4EC0\u4E48\u3002</p>\r\n\r\n <!-- \u72B6\u6001\u5FBD\u6807\uFF1A\u5DF2\u5B8C\u6210 dshell-badgeDone / \u8FDB\u884C\u4E2D dshell-badgeWait / \u9ED8\u8BA4 -->\r\n <div>\r\n <span class="dshell-badge dshell-badgeDone">\u5DF2\u5B8C\u6210</span>\r\n <span class="dshell-badge dshell-badgeWait">\u8FDB\u884C\u4E2D</span>\r\n <span class="dshell-badge">\u672A\u5F00\u59CB</span>\r\n </div>\r\n\r\n <!-- \u6807\u7B7E\u9875 -->\r\n <div class="dshell-tabs">\r\n <button class="dshell-tab dshell-tabOn">\u6982\u89C8</button>\r\n <button class="dshell-tab">\u8BE6\u60C5</button>\r\n <button class="dshell-tab">\u8BBE\u7F6E</button>\r\n </div>\r\n\r\n <!-- \u7EDF\u8BA1\u5361\u7247\u7F51\u683C -->\r\n <div class="dshell-grid">\r\n <div class="dshell-stat">\r\n <div class="dshell-statLabel">\u603B\u6570</div>\r\n <div class="dshell-statValue">128</div>\r\n <div class="dshell-statDelta">+12.4%</div>\r\n </div>\r\n <div class="dshell-stat">\r\n <div class="dshell-statLabel">\u8FDB\u884C\u4E2D</div>\r\n <div class="dshell-statValue">7</div>\r\n </div>\r\n <div class="dshell-stat">\r\n <div class="dshell-statLabel">\u5DF2\u5B8C\u6210</div>\r\n <div class="dshell-statValue">121</div>\r\n </div>\r\n </div>\r\n\r\n <!-- \u5217\u8868 -->\r\n <div class="dshell-list">\r\n <div class="dshell-listItem">\r\n <span class="dshell-listItemTitle">\u6761\u76EE\u4E00\uFF1A\u793A\u4F8B\u5185\u5BB9\u6807\u9898</span>\r\n <span class="dshell-listItemMeta">\u6628\u5929</span>\r\n </div>\r\n <div class="dshell-listItem">\r\n <span class="dshell-listItemTitle">\u6761\u76EE\u4E8C\uFF1A\u793A\u4F8B\u5185\u5BB9\u6807\u9898</span>\r\n <span class="dshell-badge dshell-badgeDone">\u5DF2\u53D1\u5E03</span>\r\n </div>\r\n </div>\r\n\r\n <!-- \u5361\u7247 + \u952E\u503C\u5BF9 -->\r\n <div class="dshell-card">\r\n <h2 class="dshell-sub" style="margin:0 0 8px">\u8BE6\u60C5</h2>\r\n <div class="dshell-kv">\r\n <div class="dshell-kvRow"><span class="dshell-kvKey">\u5B57\u6BB5 A</span><span class="dshell-kvValue">\u503C A</span></div>\r\n <div class="dshell-kvRow"><span class="dshell-kvKey">\u5B57\u6BB5 B</span><span class="dshell-kvValue">\u503C B</span></div>\r\n </div>\r\n <div class="dshell-divider"></div>\r\n <div class="dshell-progress"><div class="dshell-progressBar" style="width:72%"></div></div>\r\n </div>\r\n\r\n <!-- \u64CD\u4F5C\u533A -->\r\n <div style="display:flex;gap:8px">\r\n <button class="dshell-btn">\u4E3B\u8981\u64CD\u4F5C</button>\r\n <button class="dshell-btn dshell-btnGhost">\u6B21\u8981\u64CD\u4F5C</button>\r\n </div>\r\n </div>\r\n</body>\r\n</html>\r\n';
|
||||
|
||||
// src/index.ts
|
||||
function baseDshHome() {
|
||||
@@ -225,7 +225,7 @@ function resolveDshHomeSafe() {
|
||||
cachedDshHome = baseDshHome();
|
||||
return cachedDshHome;
|
||||
}
|
||||
var PLUGIN_VERSION = false ? "dev" : "0.5.1";
|
||||
var PLUGIN_VERSION = false ? "dev" : "0.5.2-local.1";
|
||||
var name = "dsh-worktable";
|
||||
var inject = ["webServer", "sessions"];
|
||||
var HEALTH_PATH = "/api/worktable/health";
|
||||
@@ -831,6 +831,53 @@ function apply(ctx) {
|
||||
}
|
||||
}
|
||||
});
|
||||
const statePath = () => pathResolve(resolveDshHomeSafe(), "worktable-state.json");
|
||||
register({
|
||||
kind: "exact",
|
||||
path: "/api/worktable/state",
|
||||
handler: async (req, res) => {
|
||||
const file = statePath();
|
||||
if (req.method === "GET" || req.method === "HEAD") {
|
||||
try {
|
||||
const raw = await readFile(file, "utf8");
|
||||
const text = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
||||
json(res, 200, { ok: true, path: file, state: JSON.parse(text) });
|
||||
} catch (err) {
|
||||
if (err?.code === "ENOENT") {
|
||||
json(res, 200, { ok: true, path: file, state: null });
|
||||
return;
|
||||
}
|
||||
json(res, 500, { error: String(err) });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (req.method !== "POST") {
|
||||
json(res, 405, { error: "method not allowed" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const body = await readJsonBody(req);
|
||||
const state = body && typeof body.state === "object" && body.state !== null ? body.state : null;
|
||||
if (!state) {
|
||||
json(res, 400, { error: "missing state" });
|
||||
return;
|
||||
}
|
||||
const text = JSON.stringify(state);
|
||||
if (text.length > 20 * 1024 * 1024) {
|
||||
json(res, 413, { error: "state too large" });
|
||||
return;
|
||||
}
|
||||
const fsx = await import("node:fs/promises");
|
||||
await fsx.mkdir(dirname(file), { recursive: true });
|
||||
const tmp = file + ".tmp-" + process.pid + "-" + Date.now();
|
||||
await fsx.writeFile(tmp, text, "utf8");
|
||||
await fsx.rename(tmp, file);
|
||||
json(res, 200, { ok: true, path: file });
|
||||
} catch (err) {
|
||||
json(res, err?.status ?? 500, { error: String(err) });
|
||||
}
|
||||
}
|
||||
});
|
||||
setupTerminal(webServer, ctx);
|
||||
}
|
||||
export {
|
||||
|
||||
Generated
+4
-4
@@ -8,16 +8,16 @@
|
||||
"name": "dsh-worktable",
|
||||
"version": "0.5.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"dsh": "0.2.0-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"highlight.js": "^11.12.0",
|
||||
"markdown-it": "^15.0.0",
|
||||
"xterm": "^5.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"dsh": "0.2.0-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.5",
|
||||
"node": ">=22"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/cordis": "*",
|
||||
"@deepseek-ai/dsh-client-locale": "*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dsh-worktable",
|
||||
"description": "工作台 Worktable:DeepSeek Harness 侧边栏的 agent 级项目容器(应用抽屉)",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2-local.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Aisland-SJL/dsh-worktable.git",
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* 宿主侧状态镜像(本地补丁)。
|
||||
*
|
||||
* 背景:桌面端宿主每次启动都带 `--port 0`(随机端口),页面来源随之变化
|
||||
* (如 127.0.0.1:2080 → 127.0.0.1:8494)。浏览器按来源隔离 localStorage /
|
||||
* IndexedDB,换端口就等于换一个空库,项目/布局/外观重启后全丢。
|
||||
*
|
||||
* 做法:把「项目 + 视图」这份 JSON 镜像到宿主侧文件(/api/worktable/state),
|
||||
* 启动时若宿主侧比本地新,就用宿主侧恢复。localStorage 仍是第一落点(同步、快),
|
||||
* 宿主文件是跨重启的兜底。
|
||||
*/
|
||||
|
||||
const ENDPOINT = '/api/worktable/state'
|
||||
|
||||
export type HostStateParts = { projects?: unknown; view?: unknown }
|
||||
|
||||
/** 宿主镜像里可能带的写入时间戳(毫秒) */
|
||||
export type HostState = HostStateParts & { updatedAt?: number }
|
||||
|
||||
/** 拉取宿主侧镜像;没有/不可用都返回 null(不抛错,不影响工作台本身) */
|
||||
export async function loadHostState(): Promise<HostState | null> {
|
||||
try {
|
||||
const r = await fetch(ENDPOINT, { cache: 'no-store', headers: { accept: 'application/json' } })
|
||||
if (!r.ok) return null
|
||||
const j: any = await r.json()
|
||||
return j && typeof j === 'object' && j.state && typeof j.state === 'object' ? (j.state as HostState) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** 累积中的镜像内容:POST 只带最新值,避免每次编辑都发整包 */
|
||||
let parts: HostStateParts = {}
|
||||
let timer: any = null
|
||||
let inflight = false
|
||||
let dirty = false
|
||||
/** 恢复完成前不写宿主,否则首屏的默认值会把宿主里的旧数据覆盖掉 */
|
||||
let hydrated = false
|
||||
|
||||
function schedule() {
|
||||
if (timer) return
|
||||
timer = setTimeout(() => {
|
||||
timer = null
|
||||
void flush()
|
||||
}, 400)
|
||||
}
|
||||
|
||||
async function flush(): Promise<void> {
|
||||
if (!hydrated || inflight) {
|
||||
if (inflight) dirty = true
|
||||
return
|
||||
}
|
||||
if (!parts.projects && !parts.view) return
|
||||
inflight = true
|
||||
try {
|
||||
await fetch(ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ state: { ...parts, updatedAt: Date.now() } }),
|
||||
keepalive: true,
|
||||
})
|
||||
} catch {
|
||||
// 宿主不可用时静默:localStorage 仍是本次会话的真值
|
||||
} finally {
|
||||
inflight = false
|
||||
if (dirty) {
|
||||
dirty = false
|
||||
schedule()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 记下要镜像的部分;400ms 内多次调用合并成一次 POST */
|
||||
export function queueHostSave(patch: HostStateParts): void {
|
||||
parts = { ...parts, ...patch }
|
||||
if (!hydrated) return
|
||||
schedule()
|
||||
}
|
||||
|
||||
/** 首次恢复判定结束:之后再放行宿主写入 */
|
||||
export function markHydrated(): void {
|
||||
hydrated = true
|
||||
}
|
||||
|
||||
/** 立即冲刷(页面隐藏/关闭前调用,避免丢掉最后一个 400ms 内的改动) */
|
||||
export function flushHostSave(): void {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
void flush()
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import { previewFromEvents, previewFromNodes, readSessionPreview, presetApiOf, m
|
||||
import { photoStore, kindOf } from './photoStore'
|
||||
import { DEFAULT_BG_SVG, b64ToBlob } from './defaultBg'
|
||||
import { WAVE_BG_B64 } from './waveBg'
|
||||
import { loadHostState, queueHostSave, markHydrated, flushHostSave } from './hostState'
|
||||
|
||||
/**
|
||||
* dsh-worktable 客户端(v2):侧边栏底部「工作台」区块。
|
||||
@@ -124,6 +125,8 @@ type ProjectsState = {
|
||||
|
||||
const PERSIST_KEY = 'dsh.worktable.view.v1'
|
||||
const PROJECTS_KEY = 'dsh.worktable.projects.v1'
|
||||
/** 本地最后写入时间(毫秒):与宿主镜像的 updatedAt 比较,决定启动时以哪边为准 */
|
||||
const SAVED_AT_KEY = 'dsh.worktable.savedAt.v1'
|
||||
const MIN_TOP = 56
|
||||
const SNAP_PX = 32
|
||||
/** 插件市场外链(GitHub 仓库,已核实可访问;PRD 提及的 dshfind.com 未验证,不用死链)。 */
|
||||
@@ -308,11 +311,11 @@ function normHsl(v: any): { h: number; s: number; l: number } | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
function loadView(): ViewState {
|
||||
/** 视图状态归一化(本地存档与宿主镜像共用同一套字段校验) */
|
||||
function normalizeView(raw: any): ViewState {
|
||||
try {
|
||||
const raw = localStorage.getItem(PERSIST_KEY)
|
||||
if (!raw) return { ...DEFAULT_VIEW }
|
||||
const p = JSON.parse(raw)
|
||||
if (!raw || typeof raw !== 'object') return { ...DEFAULT_VIEW }
|
||||
const p = raw
|
||||
// 显式挑字段:旧版遗留的 groupBy 等未知字段直接忽略。
|
||||
// 一次性迁移:v2 起默认「手动」排序(最近排序点击即置顶、体验差),旧存「最近」回落为「手动」;
|
||||
// 之后用户手动选择「最近」会写入 sortMigratedV2 标记并被尊重。
|
||||
@@ -356,11 +359,21 @@ function loadView(): ViewState {
|
||||
}
|
||||
}
|
||||
|
||||
function loadProjects(): ProjectsState {
|
||||
function loadView(): ViewState {
|
||||
try {
|
||||
const raw = localStorage.getItem(PROJECTS_KEY)
|
||||
if (!raw) return { ...DEFAULT_PROJECTS }
|
||||
const p = JSON.parse(raw)
|
||||
const raw = localStorage.getItem(PERSIST_KEY)
|
||||
if (!raw) return { ...DEFAULT_VIEW }
|
||||
return normalizeView(JSON.parse(raw))
|
||||
} catch {
|
||||
return { ...DEFAULT_VIEW }
|
||||
}
|
||||
}
|
||||
|
||||
/** 项目状态归一化(本地存档与宿主镜像共用同一套字段校验) */
|
||||
function normalizeProjects(raw: any): ProjectsState {
|
||||
try {
|
||||
if (!raw || typeof raw !== 'object') return { ...DEFAULT_PROJECTS }
|
||||
const p = raw
|
||||
return {
|
||||
order: Array.isArray(p.order) ? p.order.filter((x: unknown): x is string => typeof x === 'string') : [],
|
||||
lastUsed: p.lastUsed && typeof p.lastUsed === 'object' ? p.lastUsed : {},
|
||||
@@ -383,6 +396,21 @@ function loadProjects(): ProjectsState {
|
||||
}
|
||||
}
|
||||
|
||||
function loadProjects(): ProjectsState {
|
||||
try {
|
||||
const raw = localStorage.getItem(PROJECTS_KEY)
|
||||
if (!raw) return { ...DEFAULT_PROJECTS }
|
||||
return normalizeProjects(JSON.parse(raw))
|
||||
} catch {
|
||||
return { ...DEFAULT_PROJECTS }
|
||||
}
|
||||
}
|
||||
|
||||
/** 记下本地写入时间,供启动时与宿主镜像比较新鲜度 */
|
||||
function markSaved(): void {
|
||||
try { localStorage.setItem(SAVED_AT_KEY, String(Date.now())) } catch {}
|
||||
}
|
||||
|
||||
const clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(v, lo), hi)
|
||||
|
||||
type FloatRect = { top: number }
|
||||
@@ -1380,6 +1408,8 @@ function WorktableSection(props: any) {
|
||||
setView((prev) => {
|
||||
const next = { ...prev, ...patch }
|
||||
try { localStorage.setItem(PERSIST_KEY, JSON.stringify({ ...next, sortMigratedV2: true })) } catch {}
|
||||
markSaved()
|
||||
queueHostSave({ view: next })
|
||||
return next
|
||||
})
|
||||
}
|
||||
@@ -1388,10 +1418,44 @@ function WorktableSection(props: any) {
|
||||
setProjects((prev) => {
|
||||
const next = typeof patch === 'function' ? patch(prev) : { ...prev, ...patch }
|
||||
try { localStorage.setItem(PROJECTS_KEY, JSON.stringify(next)) } catch {}
|
||||
markSaved()
|
||||
queueHostSave({ projects: next })
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 宿主侧状态恢复(本地补丁):桌面端每次启动端口随机,localStorage 按来源隔离,
|
||||
* 重启后本地必然是空的。这里比对「本地最后写入时间」与「宿主镜像 updatedAt」,
|
||||
* 只在宿主更新时回填,避免用旧镜像盖掉当前页面刚改的内容。
|
||||
* 恢复判定结束前不写宿主(markHydrated),防止首屏默认值先把镜像冲掉。
|
||||
*/
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
const host = await loadHostState()
|
||||
if (cancelled) return
|
||||
let localAt = 0
|
||||
try { localAt = Number(localStorage.getItem(SAVED_AT_KEY) ?? '0') || 0 } catch {}
|
||||
const hostAt = typeof host?.updatedAt === 'number' ? host.updatedAt : 0
|
||||
if (host && hostAt > localAt) {
|
||||
if (host.projects) persistProjects(normalizeProjects(host.projects))
|
||||
if (host.view) persistView(normalizeView(host.view))
|
||||
}
|
||||
markHydrated()
|
||||
if (hostAt <= localAt) flushHostSave()
|
||||
})()
|
||||
const flush = () => flushHostSave()
|
||||
window.addEventListener('pagehide', flush)
|
||||
window.addEventListener('beforeunload', flush)
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.removeEventListener('pagehide', flush)
|
||||
window.removeEventListener('beforeunload', flush)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// 自愈:启动时若控制室存档是坏布局(旧版关掉控制室标签造成窗格退化成选择器),立即重建默认面板
|
||||
useEffect(() => {
|
||||
const saved = projects.views[CONSOLE_ID]
|
||||
|
||||
@@ -544,5 +544,48 @@ export function apply(ctx: Context) {
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* 工作台状态宿主侧镜像(本地补丁)。
|
||||
*
|
||||
* 桌面端宿主以 `--port 0` 启动,端口每次随机,页面来源跟着变;而 localStorage 按来源隔离,
|
||||
* 于是「新建的项目重启就没了」。这里把项目/视图这份 JSON 落到 DSH_HOME 下的固定文件:
|
||||
* 跨重启、跨端口稳定。客户端启动时比对时间戳决定是否回填(见 client/hostState.ts)。
|
||||
*/
|
||||
const statePath = () => pathResolve(resolveDshHomeSafe(), 'worktable-state.json')
|
||||
register({
|
||||
kind: 'exact',
|
||||
path: '/api/worktable/state',
|
||||
handler: async (req: any, res: any) => {
|
||||
const file = statePath()
|
||||
if (req.method === 'GET' || req.method === 'HEAD') {
|
||||
try {
|
||||
const raw = await readFile(file, 'utf8')
|
||||
const text = raw.charCodeAt(0) === 0xFEFF ? raw.slice(1) : raw
|
||||
json(res, 200, { ok: true, path: file, state: JSON.parse(text) })
|
||||
} catch (err: any) {
|
||||
if (err?.code === 'ENOENT') { json(res, 200, { ok: true, path: file, state: null }); return }
|
||||
json(res, 500, { error: String(err) })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (req.method !== 'POST') { json(res, 405, { error: 'method not allowed' }); return }
|
||||
try {
|
||||
const body = await readJsonBody(req)
|
||||
const state = body && typeof body.state === 'object' && body.state !== null ? body.state : null
|
||||
if (!state) { json(res, 400, { error: 'missing state' }); return }
|
||||
const text = JSON.stringify(state)
|
||||
if (text.length > 20 * 1024 * 1024) { json(res, 413, { error: 'state too large' }); return }
|
||||
const fsx = await import('node:fs/promises')
|
||||
await fsx.mkdir(dirname(file), { recursive: true })
|
||||
const tmp = file + '.tmp-' + process.pid + '-' + Date.now()
|
||||
await fsx.writeFile(tmp, text, 'utf8')
|
||||
await fsx.rename(tmp, file)
|
||||
json(res, 200, { ok: true, path: file })
|
||||
} catch (err: any) {
|
||||
json(res, err?.status ?? 500, { error: String(err) })
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
setupTerminal(webServer, ctx)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# 本地补丁:工作台状态镜像到宿主侧
|
||||
|
||||
基于上游 `Aisland-SJL/dsh-worktable` **v0.5.1**,本地版本号改成 `0.5.2-local.1`。
|
||||
|
||||
## 为什么打这个补丁
|
||||
|
||||
桌面端宿主启动参数写死 `--port 0`(`@deepseek-ai/dsh-slark-desktop-host` 的
|
||||
`windows-startup.js`:`--profile web --no-open --host 127.0.0.1 --port 0`),每次启动端口随机。
|
||||
而工作台把项目/布局/绑定存在 localStorage(`dsh.worktable.projects.v1`、`dsh.worktable.view.v1`),
|
||||
媒体存 IndexedDB —— 浏览器按「来源」隔离存储,端口一变就是另一个空库。
|
||||
现象:新建的工作台,重启后消失。
|
||||
|
||||
## 改了什么
|
||||
|
||||
1. `01_content/src/index.ts`:新增路由 `GET/POST /api/worktable/state`
|
||||
- 文件位置:`<DSH_HOME>/worktable-state.json`(走)`resolveDshHomeSafe()`)
|
||||
- GET 读回,POST 原子写入(先写 `.tmp-<pid>-<ts>` 再 rename)
|
||||
- 走宿主既有鉴权(`requestRejection`),和 `/api/worktable/file` 等私有路由一致
|
||||
2. `01_content/src/client/hostState.ts`(新增):宿主镜像的读/写与 400ms 防抖合并
|
||||
3. `01_content/src/client/index.tsx`:
|
||||
- `loadView` / `loadProjects` 抽出 `normalizeView` / `normalizeProjects`,本地存档与宿主镜像共用校验
|
||||
- `persistView` / `persistProjects` 额外写 `dsh.worktable.savedAt.v1` 并把状态排进宿主镜像
|
||||
- 挂载时拉宿主镜像,只有 `updatedAt > 本地 savedAt` 才回填,避免旧镜像盖掉刚改的内容
|
||||
- `markHydrated()` 之前的写入一律不发,防止首屏默认值把镜像冲掉
|
||||
- `pagehide` / `beforeunload` 立即冲刷
|
||||
|
||||
## 覆盖范围
|
||||
|
||||
- 覆盖:项目、布局、绑定、文件夹、视图/外观设置
|
||||
- **不覆盖**:图库媒体(照片)仍在 IndexedDB,换端口照样丢
|
||||
|
||||
## 构建 / 安装
|
||||
|
||||
```powershell
|
||||
cd 01_content
|
||||
npm install
|
||||
npm run build # lib/index.js + lib/client.js
|
||||
npm pack # 产出 dsh-worktable-0.5.2-local.1.tgz(7 文件,无 .map)
|
||||
dsh plugin --profile web add "file:./dsh-plugins/dsh-worktable-0.5.2-local.1.tgz"
|
||||
```
|
||||
|
||||
装完必须**完全退出宿主再重开**。
|
||||
|
||||
## 升级官方版时的注意
|
||||
|
||||
上游出 0.5.2 及以上时,本地补丁会被覆盖,需要把上面的三处改动重新合并到新版本再打包。
|
||||
如果上游自己解决了「桌面端随机端口 + localStorage」,这个补丁就可以直接丢掉。
|
||||
Reference in New Issue
Block a user