fix(v0.5.1): 适配rc.5并修正控制室与更新提醒
This commit is contained in:
+15
-3
@@ -17,13 +17,25 @@
|
||||
- 状态监控为宿主会话运行时快照的事件订阅镜像;视图/项目/绑定状态存 localStorage。
|
||||
- 客户端:TypeScript + React(host externals)+ 原生 CSS;服务端:Node。
|
||||
|
||||
## 0.5.0:兼容与安全说明
|
||||
## 0.5.1:rc.5兼容、控制室与更新提醒修复
|
||||
|
||||
公开状态以 GitHub Release 为准。
|
||||
|
||||
- 兼容范围仅列 **DSH 0.2.0-rc.2 / 0.2.1-rc.3 / 0.2.1-rc.5**。新增 rc.5 的精确兼容声明,修复宿主因原版本声明不匹配而跳过加载;不通配放开未测宿主。旧 0.1.x 不在本版支持范围,macOS 尚未真机验证。
|
||||
- 会话停止统一显示中性的“本轮已结束”,不把失败或停止当作成功;正在运行优先于旧结束标记。卡片摘要只取可读助手答复,过滤注入上下文及模型替换副本,无答复时留空。
|
||||
- 设置与控制室共用版本排序,只有确定高于本机且未忽略的版本才显示升级卡;正式版高于同号测试版,构建元数据不参与排序。缓存读取重验版本,检查失败或无更新清掉旧提示,无效远端版本报告检查失败,不引导降级。
|
||||
- rc.5 已有使用 Desktop 自带运行时的隔离网页/浏览器检查;候选已安装官方 Windows Desktop,用户确认面板恢复、更新误报消失、看起来正常。正式包另做独立安装与自动回归,不能将这些证据称为正式包双端全功能验收。更早两版沿用此前页面证据与当前代码回归。
|
||||
- 沿用 0.5.0 的权限、网页安全隔离、传输与项目存储边界,不迁移、不清空用户数据。本版不包含宿主旧会话 HTTP 400 的修复,也不改变 Session Log 上传设置。
|
||||
|
||||
已知限制:rc.5 输入框中已有确认的 /命令标签时,标注追加可能退回剪贴板。新旧宿主的命令坐标表示不同,本版保留原安全降级,不以未验证的能力猜测影响旧宿主。
|
||||
|
||||
## 0.5.0:安全基础与历史验收说明
|
||||
|
||||
公开发布状态以 GitHub Release 为准。
|
||||
|
||||
- HTTP 文件/目录/写入/Git/工作区与终端握手先通过宿主身份验证,仅放行 owner(0.2.0 使用单 operator 身份);缺少可确认身份的服务时返回 503,不降级开放。health 仍可公开读取状态。以前直接用无登录 curl 调用这些接口的方式将被拒绝;不提供跳过鉴权开关。
|
||||
- 候选 dev.1 已在 Windows 隔离数据目录验证 **DSH 0.2.0-rc.2 / 0.2.1-rc.3 网页端**的冷启动、加载、分栏、文件与网页资源;0.2.1 另测创建/写入、控制室绑定和终端,未发送模型请求。用户已在官方 Windows Desktop 0.2.1-rc.3 验证 dev.2 的项目打开、分栏缩放与侧栏设置弹窗正常;前轮新项目也已点测。正式包另做安装与回归检查,这些证据不等于双端全功能验收。
|
||||
- 本版的 `engines.dsh` 与兼容矩阵只列上述两个 0.2 版本。旧 0.1.x 不在本版支持范围;下方 v0.4.0 的记录是历史范围,不代表 0.5.0 继续支持旧宿主。
|
||||
- 0.5.0 当时的 `engines.dsh` 与兼容矩阵只列上述两个 0.2 版本;0.5.1 的当前范围以上节为准。旧 0.1.x 不在本版支持范围;下方 v0.4.0 的记录是历史范围,不代表 0.5.0 继续支持旧宿主。
|
||||
- 折叠侧栏与展开侧栏共用一份弹窗层,使控制室新建项目、绑定、图标与删除弹窗不因侧栏收起而消失,避免重复渲染。
|
||||
- 修复分栏 margin 的小数值被浏览器规范化后误判为外部改写,以及会话根更换后观察器仍盯着旧元素的问题。
|
||||
|
||||
@@ -84,7 +96,7 @@
|
||||
|
||||
先保存任务,从应用菜单/托盘完整退出(只关窗口不够);将占位路径替换为实际桌面端安装目录,在 PowerShell 执行,再手动重新打开应用:
|
||||
|
||||
& "<桌面端安装目录>\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add "https://github.com/Aisland-SJL/dsh-worktable/releases/download/v0.5.0/dsh-worktable-0.5.0.tgz"
|
||||
& "<桌面端安装目录>\resources\runtime\cli\bin\dsh.cmd" plugin --profile desktop add "https://github.com/Aisland-SJL/dsh-worktable/releases/download/v0.5.1/dsh-worktable-0.5.1.tgz"
|
||||
|
||||
不要以不固定版本的 npx 或网页端 CLI 代替桌面端自带 CLI;保留实际 DSH_HOME,勿把测试数据目录覆盖正式数据。新建控制室管理对话时,DSH 0.2 用户请选择分组,或直接绑定可用的现有对话。
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dsh-worktable",
|
||||
"description": "工作台:DSH 侧边栏的 agent 级项目容器,收纳如 TravelAtlas 等自装项目",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"entry": {
|
||||
"name": "dsh-worktable",
|
||||
"inject": []
|
||||
|
||||
+136
-83
@@ -7951,9 +7951,9 @@ var css = xterm_default + "\n" + [
|
||||
".dsh-wt_bindBtn{position:absolute;right:26px;top:50%;transform:translateY(-50%);width:22px;height:22px;margin:0;display:block;border-radius:6px;color:var(--dsw-alias-label-secondary,#9aa4b2);cursor:pointer;user-select:none}",
|
||||
".dsh-wt_bindBtn:hover{background:var(--dsw-alias-fill-l2,rgba(255,255,255,.09));color:var(--dsw-alias-label-primary,#e6e8eb)}",
|
||||
".dsh-wt_bindBtn[data-bound=true]{color:var(--dsw-alias-label-primary,#e6e8eb)}",
|
||||
// 任务完成提醒镜像:绿色发光 / 待决提醒:黄色发光(点击项目即 ack 恢复常态实心);
|
||||
// 本轮结束提醒:中性发光 / 待决提醒:黄色发光(点击项目即 ack 恢复常态实心);
|
||||
// 工作中:蓝色发光 + 两圆交替亮灭(对应 DSH 对话的转圈标记,无需 ack,running 变 false 自动停)
|
||||
".dsh-wt_bindBtn[data-bound=done]{color:#3fb950}",
|
||||
".dsh-wt_bindBtn[data-bound=done]{color:#94a3b8}",
|
||||
".dsh-wt_bindBtn[data-bound=need]{color:#d29922}",
|
||||
".dsh-wt_bindBtn[data-bound=busy]{color:#4f8ef7}",
|
||||
"@keyframes dsh-wt-busyA{0%,44%{opacity:1}50%,94%{opacity:.12}100%{opacity:1}}",
|
||||
@@ -8332,12 +8332,12 @@ var css = xterm_default + "\n" + [
|
||||
".dsh-wt_consoleStatus{flex:none;font-size:22px;font-weight:700;line-height:30px;color:var(--wt-text2)}",
|
||||
".dsh-wt_consoleStatus-busy{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}",
|
||||
".dsh-wt_consoleStatus-need{color:var(--dsw-alias-state-warning,#d29922)}",
|
||||
".dsh-wt_consoleStatus-done{color:var(--dsw-alias-state-success,#3fb950)}",
|
||||
".dsh-wt_consoleStatus-done{color:var(--wt-text2)}",
|
||||
".dsh-wt_consoleRuntime{flex:none;font-size:17px;font-weight:600;line-height:24px;color:var(--wt-text2);font-variant-numeric:tabular-nums}",
|
||||
".dsh-wt_consolePreview{display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;font-size:8.5px;line-height:13px;color:var(--wt-text2);word-break:break-word}",
|
||||
".dsh-wt_consolePreviewNone{color:var(--wt-text3);font-style:italic;-webkit-line-clamp:1}",
|
||||
// 三色光效:整卡霓虹描边光晕(像侧栏双圆点的发光质感)——完成=绿、待决=黄;工作=蓝色描边顺时针旋转
|
||||
".dsh-wt_consoleCard-glowDone{border-color:rgba(63,185,80,.9);box-shadow:0 0 8px rgba(63,185,80,.5),0 0 26px rgba(63,185,80,.3),inset 0 0 16px rgba(63,185,80,.07)}",
|
||||
// 结束只用中性提醒光,不作为成功证明;待决=黄,工作=蓝色描边顺时针旋转
|
||||
".dsh-wt_consoleCard-glowDone{border-color:rgba(148,163,184,.8);box-shadow:0 0 8px rgba(148,163,184,.35),0 0 26px rgba(148,163,184,.2),inset 0 0 16px rgba(148,163,184,.05)}",
|
||||
".dsh-wt_consoleCard-glowNeed{border-color:rgba(210,153,34,.95);box-shadow:0 0 8px rgba(210,153,34,.5),0 0 26px rgba(210,153,34,.3),inset 0 0 16px rgba(210,153,34,.08)}",
|
||||
'@property --consoleAngle{syntax:"<angle>";initial-value:0deg;inherits:false}',
|
||||
".dsh-wt_consoleCard-busy{box-shadow:0 0 10px rgba(94,160,255,.55),0 0 34px rgba(79,142,247,.4)}",
|
||||
@@ -8503,7 +8503,7 @@ var zh = {
|
||||
"bind.titleUnbound": "\u7ED1\u5B9A\u5BF9\u8BDD\uFF08\u672A\u7ED1\u5B9A \u25CB\u25CB\uFF09\uFF1A\u70B9\u51FB\u9009\u62E9\u8981\u8DDF\u968F\u7684\u5BF9\u8BDD",
|
||||
"bind.titleBound": "\u5DF2\u7ED1\u5B9A\u5BF9\u8BDD\uFF08\u25CF\u25CF\uFF09\uFF1A\u70B9\u51FB\u66F4\u6362\u6216\u89E3\u7ED1",
|
||||
"bind.tipUnbound": "\u672A\u7ED1\u5B9A\u5BF9\u8BDD\uFF1A\u70B9\u51FB\u9009\u62E9\u8981\u8DDF\u968F\u7684\u5BF9\u8BDD",
|
||||
"bind.tipDone": "\uFF08\u4EFB\u52A1\u5DF2\u5B8C\u6210\uFF0C\u70B9\u51FB\u9879\u76EE\u67E5\u770B\uFF09",
|
||||
"bind.tipDone": "\uFF08\u672C\u8F6E\u5DF2\u7ED3\u675F\uFF0C\u70B9\u51FB\u9879\u76EE\u67E5\u770B\uFF09",
|
||||
"bind.tipBusy": "\uFF08\u5BF9\u8BDD\u6B63\u5728\u5904\u7406\u4EFB\u52A1\u2026\uFF09",
|
||||
"bind.tipNeed": "\uFF08\u9700\u8981\u4F60\u7684\u51B3\u5B9A\uFF0C\u70B9\u51FB\u9879\u76EE\u5904\u7406\uFF09",
|
||||
"bind.folder": "\u9879\u76EE\u6587\u4EF6\u5939",
|
||||
@@ -8559,9 +8559,9 @@ var zh = {
|
||||
"console.idle": "\u7A7A\u95F2",
|
||||
"console.busy": "\u5DE5\u4F5C\u4E2D",
|
||||
"console.need": "\u5F85\u4F60\u51B3\u5B9A",
|
||||
"console.done": "\u5DF2\u5B8C\u6210",
|
||||
"console.done": "\u672C\u8F6E\u5DF2\u7ED3\u675F",
|
||||
"console.kids": "\u5B50\u4EE3\u7406",
|
||||
"console.noPreview": "\u6682\u65E0\u6D88\u606F",
|
||||
"console.noPreview": "\u6682\u65E0\u52A9\u624B\u56DE\u590D",
|
||||
"console.unboundShort": "\u672A\u7ED1\u5B9A\u5BF9\u8BDD",
|
||||
"console.jump": "\u8DF3\u8F6C\u5230\u7ED1\u5B9A\u5BF9\u8BDD",
|
||||
"console.empty": "\u8FD8\u6CA1\u6709\u9879\u76EE",
|
||||
@@ -8736,7 +8736,7 @@ var en = {
|
||||
"bind.titleUnbound": "Bind conversation (unbound \u25CB\u25CB): pick the conversation to follow",
|
||||
"bind.titleBound": "Conversation bound (\u25CF\u25CF): click to change or unbind",
|
||||
"bind.tipUnbound": "No conversation bound \u2014 click to pick one",
|
||||
"bind.tipDone": " (task completed \u2014 click the project to view)",
|
||||
"bind.tipDone": " (run ended \u2014 click the project to view)",
|
||||
"bind.tipBusy": " (conversation is working on the task\u2026)",
|
||||
"bind.tipNeed": " (needs your decision \u2014 click the project to handle)",
|
||||
"bind.folder": "Project folder",
|
||||
@@ -8792,9 +8792,9 @@ var en = {
|
||||
"console.idle": "Idle",
|
||||
"console.busy": "Working",
|
||||
"console.need": "Needs you",
|
||||
"console.done": "Done",
|
||||
"console.done": "Run ended",
|
||||
"console.kids": "subagents",
|
||||
"console.noPreview": "No messages yet",
|
||||
"console.noPreview": "No assistant reply yet",
|
||||
"console.unboundShort": "No conversation bound",
|
||||
"console.jump": "Jump to bound conversation",
|
||||
"console.empty": "No projects yet",
|
||||
@@ -9020,7 +9020,23 @@ function SandboxedFrame(props) {
|
||||
}
|
||||
|
||||
// src/client/changelog.ts
|
||||
var CHANGELOG_V030 = `\u66F4\u65B0\u516C\u544A \xB7 v0.5.0
|
||||
var CHANGELOG_V030 = `\u66F4\u65B0\u516C\u544A \xB7 v0.5.1
|
||||
|
||||
\u{1F527} DSH 0.2.1-rc.5 \u517C\u5BB9\u4E0E\u66F4\u65B0\u63D0\u9192\u4FEE\u590D
|
||||
|
||||
\u3010\u52A0\u8F7D\u4FEE\u590D\u3011\u5C06 rc.5 \u517C\u5BB9\u76EE\u6807\u52A0\u5165\u7CBE\u786E\u7248\u672C\u58F0\u660E\uFF0C\u5904\u7406\u5BBF\u4E3B\u56E0\u7248\u672C\u58F0\u660E\u4E0D\u5339\u914D\u800C\u8DF3\u8FC7\u5DE5\u4F5C\u53F0\u52A0\u8F7D\u7684\u95EE\u9898\uFF1B\u4E0D\u901A\u914D\u653E\u5F00\u672A\u6765\u7248\u672C\u3002
|
||||
|
||||
\u3010\u66F4\u65B0\u63D0\u9192\u3011\u4FEE\u590D\u6D4B\u8BD5\u7248\u7248\u672C\u53F7\u6BD4\u8F83\u51FA\u9519\u3001\u628A\u65E7\u6B63\u5F0F\u7248\u5F53\u6210\u65B0\u7248\u672C\u7684\u95EE\u9898\u3002\u8BBE\u7F6E\u4E0E\u63A7\u5236\u5BA4\u5171\u7528\u7248\u672C\u6392\u5E8F\u89C4\u5219\uFF0C\u8BFB\u53D6\u7F13\u5B58\u548C\u663E\u793A\u5361\u7247\u65F6\u91CD\u65B0\u786E\u8BA4\u76EE\u6807\u66F4\u9AD8\u4E14\u672A\u5FFD\u7565\uFF1B\u68C0\u67E5\u5931\u8D25\u6216\u65E0\u66F4\u65B0\u65F6\u6E05\u6389\u65E7\u63D0\u793A\uFF0C\u4E0D\u5F15\u5BFC\u964D\u7EA7\u3002
|
||||
|
||||
\u3010\u63A7\u5236\u5BA4\u663E\u793A\u3011\u4F1A\u8BDD\u505C\u6B62\u7EDF\u4E00\u663E\u793A\u4E2D\u6027\u7684\u201C\u672C\u8F6E\u5DF2\u7ED3\u675F\u201D\uFF0C\u4E0D\u518D\u628A\u5931\u8D25\u6216\u505C\u6B62\u5F53\u4F5C\u6210\u529F\uFF1B\u6B63\u5728\u8FD0\u884C\u4F18\u5148\u4E8E\u65E7\u7ED3\u675F\u6807\u8BB0\u3002\u5361\u7247\u6458\u8981\u53EA\u663E\u793A\u53EF\u8BFB\u7684\u52A9\u624B\u7B54\u590D\uFF0C\u8FC7\u6EE4\u6CE8\u5165\u4E0A\u4E0B\u6587\u4E0E\u4EC5\u4F9B\u6A21\u578B\u7684\u66FF\u6362\u526F\u672C\uFF1B\u65E0\u7B54\u590D\u65F6\u7559\u7A7A\u3002
|
||||
|
||||
\u3010\u517C\u5BB9\u4E0E\u9A8C\u6536\u3011\u4EC5\u5217\u5DF2\u6D4B DSH 0.2.0-rc.2 / 0.2.1-rc.3 / 0.2.1-rc.5\u3002rc.5 \u5DF2\u6709\u9694\u79BB\u7F51\u9875\u68C0\u67E5\uFF1B\u7528\u6237\u5728\u5B98\u65B9 Windows Desktop \u5B89\u88C5\u5019\u9009\u540E\u786E\u8BA4\u5DE5\u4F5C\u53F0\u6062\u590D\u3001\u66F4\u65B0\u8BEF\u62A5\u6D88\u5931\u3002\u6B63\u5F0F\u5305\u53E6\u505A\u72EC\u7ACB\u5B89\u88C5\u4E0E\u81EA\u52A8\u56DE\u5F52\uFF0C\u4E0D\u5C06\u5019\u9009\u9875\u9762\u8BC1\u636E\u6269\u5927\u4E3A\u6B63\u5F0F\u5305\u5168\u529F\u80FD\u9A8C\u6536\uFF1B\u65E7 0.1.x\u3001\u672A\u6D4B\u5BBF\u4E3B\u53CA macOS \u4E0D\u4F5C\u517C\u5BB9\u4FDD\u8BC1\u3002
|
||||
|
||||
\u3010\u4FDD\u7559\u8FB9\u754C\u3011\u6CBF\u7528 0.5.0 \u7684\u7F51\u9875\u5B89\u5168\u9694\u79BB\u3001\u63A5\u53E3\u8EAB\u4EFD\u6821\u9A8C\u4E0E\u9879\u76EE\u5B58\u50A8\u65B9\u5F0F\u3002\u65B0\u7248\u8F93\u5165\u6846\u5DF2\u6709 /\u547D\u4EE4\u6807\u7B7E\u65F6\uFF0C\u6807\u6CE8\u8FFD\u52A0\u53EF\u80FD\u9000\u56DE\u526A\u8D34\u677F\uFF1B\u6B64\u9879\u8DE8\u7248\u672C\u5750\u6807\u9002\u914D\u5F85\u540E\u7EED\u5904\u7406\u3002\u672C\u7248\u4E0D\u5305\u542B\u65E7\u4F1A\u8BDD\u6A21\u578B\u8BF7\u6C42 HTTP 400 \u7684\u4FEE\u590D\u3002
|
||||
|
||||
\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
||||
|
||||
\u6B64\u524D\u7248\u672C\u8BF4\u660E\uFF08v0.5.0\uFF09
|
||||
|
||||
\u{1F6E1}\uFE0F \u65B0\u7248\u5BBF\u4E3B\u517C\u5BB9\u3001\u5B89\u5168\u9694\u79BB\u4E0E\u4FA7\u680F\u4FEE\u590D
|
||||
|
||||
@@ -9144,21 +9160,49 @@ var CHANGELOG_V030 = `\u66F4\u65B0\u516C\u544A \xB7 v0.5.0
|
||||
\u30103.1 \u6587\u5B57\u4E0E\u7EC6\u8282\u6253\u78E8\u3011\u5168\u5C40\u5B57\u4F53\u4E0E\u5B57\u53F7\u7EDF\u4E00\u4F18\u5316\u3001\u4E0B\u62C9\u9762\u677F\u73BB\u7483\u5316\u4E0E\u5BF9\u9F50\u3001\u83DC\u5355\u70B9\u9009\u540E\u4FDD\u6301\u6253\u5F00\u4FBF\u4E8E\u8FDE\u7EED\u9884\u89C8\u3001\u6309\u94AE\u63CF\u8FB9\u4E0E\u60AC\u505C\u53CD\u9988\u7B49\u4EA4\u4E92\u7EC6\u8282\uFF1B\u540C\u65F6\u4FEE\u590D\u4E86\u591A\u9879\u4F53\u9A8C\u95EE\u9898\uFF08\u7167\u7247\u4E0A\u4F20\u6E05\u6670\u5EA6\u3001\u80CC\u666F\u7F51\u683C\u7EBF\u5728\u7167\u7247\u6A21\u5F0F\u4E0B\u4E0D\u751F\u6548\u3001\u6D45\u8272\u4E3B\u9898\u4E0B\u5DE5\u4F5C\u72B6\u6001\u5149\u6548\u4E0D\u53EF\u89C1\u7B49\uFF09\u3002`;
|
||||
|
||||
// src/client/updateCheck.ts
|
||||
var LOCAL_VERSION = false ? "dev" : "0.5.0";
|
||||
var LOCAL_VERSION = false ? "dev" : "0.5.1";
|
||||
var UPDATE_REPO = "Aisland-SJL/dsh-worktable";
|
||||
var K_UPDATE_CHECK = "dsh.worktable.updateCheck.v1";
|
||||
var K_LAST_CHECK = "dsh.worktable.lastUpdateCheck.v1";
|
||||
var K_SKIP = "dsh.worktable.skipVersion.v1";
|
||||
var K_CACHE = "dsh.worktable.updateCache.v1";
|
||||
function versionParts(value) {
|
||||
if (typeof value !== "string") return null;
|
||||
const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value.trim());
|
||||
if (!match) return null;
|
||||
const pre = match[4]?.split(".") ?? [];
|
||||
if (pre.some((part) => /^\d+$/.test(part) && part.length > 1 && part[0] === "0")) return null;
|
||||
return { core: match.slice(1, 4), pre };
|
||||
}
|
||||
function numericOrder(a, b) {
|
||||
if (a.length !== b.length) return a.length > b.length ? 1 : -1;
|
||||
return a === b ? 0 : a > b ? 1 : -1;
|
||||
}
|
||||
function cmpVer(a, b) {
|
||||
const pa = String(a).split(".").map((x) => parseInt(x, 10) || 0);
|
||||
const pb = String(b).split(".").map((x) => parseInt(x, 10) || 0);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const d = (pa[i] || 0) - (pb[i] || 0);
|
||||
if (d !== 0) return d > 0 ? 1 : -1;
|
||||
const pa = versionParts(a), pb = versionParts(b);
|
||||
if (!pa || !pb) return null;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const order = numericOrder(pa.core[i], pb.core[i]);
|
||||
if (order !== 0) return order;
|
||||
}
|
||||
if (!pa.pre.length || !pb.pre.length) return pa.pre.length ? -1 : pb.pre.length ? 1 : 0;
|
||||
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
||||
const x = pa.pre[i], y = pb.pre[i];
|
||||
if (x === void 0 || y === void 0) return x === void 0 ? -1 : 1;
|
||||
if (x === y) continue;
|
||||
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y);
|
||||
if (nx && ny) return numericOrder(x, y);
|
||||
if (nx !== ny) return nx ? -1 : 1;
|
||||
return x > y ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
function newerUpdateInfo(value) {
|
||||
const info = value;
|
||||
if (!info || typeof info.latest !== "string" || typeof info.notes !== "string" || typeof info.url !== "string") return null;
|
||||
if (cmpVer(info.latest, LOCAL_VERSION) !== 1 || cmpVer(info.latest, getSkipVersion()) === 0) return null;
|
||||
return info;
|
||||
}
|
||||
function getSkipVersion() {
|
||||
try {
|
||||
return localStorage.getItem(K_SKIP) ?? "";
|
||||
@@ -9190,8 +9234,8 @@ function readCache() {
|
||||
const raw = localStorage.getItem(K_CACHE);
|
||||
if (raw) {
|
||||
const c = JSON.parse(raw);
|
||||
if (c && typeof c.status === "string") {
|
||||
return { status: c.status === "checking" ? "idle" : c.status, info: c.info ?? null };
|
||||
if (c?.status === "uptodate" || c?.status === "failed" || c?.status === "idle") {
|
||||
return { status: c.status, info: c.status === "uptodate" ? newerUpdateInfo(c.info) : null };
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -9200,13 +9244,18 @@ function readCache() {
|
||||
}
|
||||
function writeCache(status, info) {
|
||||
try {
|
||||
localStorage.setItem(K_CACHE, JSON.stringify({ status, info }));
|
||||
localStorage.setItem(K_CACHE, JSON.stringify({ status, info: status === "uptodate" ? newerUpdateInfo(info) : null }));
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
async function checkUpdate(force = false) {
|
||||
const last = Number(localStorage.getItem(K_LAST_CHECK) ?? "0");
|
||||
if (!force && Date.now() - last < 24 * 3600 * 1e3) return readCache();
|
||||
let last = 0;
|
||||
try {
|
||||
last = Number(localStorage.getItem(K_LAST_CHECK) ?? "0");
|
||||
} catch {
|
||||
}
|
||||
const age = Date.now() - last;
|
||||
if (!force && Number.isFinite(last) && last > 0 && age >= 0 && age < 24 * 3600 * 1e3) return readCache();
|
||||
try {
|
||||
localStorage.setItem(K_LAST_CHECK, String(Date.now()));
|
||||
} catch {
|
||||
@@ -9228,16 +9277,17 @@ async function checkUpdate(force = false) {
|
||||
writeCache("failed", null);
|
||||
return { status: "failed", info: null };
|
||||
}
|
||||
const tag = (d.tag_name ?? "").replace(/^v/, "");
|
||||
if (!tag || cmpVer(tag, LOCAL_VERSION) <= 0) {
|
||||
const tag = typeof d.tag_name === "string" ? d.tag_name.trim() : "";
|
||||
const order = cmpVer(tag, LOCAL_VERSION);
|
||||
if (order == null) {
|
||||
writeCache("failed", null);
|
||||
return { status: "failed", info: null };
|
||||
}
|
||||
if (order <= 0) {
|
||||
writeCache("uptodate", null);
|
||||
return { status: "uptodate", info: null };
|
||||
}
|
||||
if (getSkipVersion() === tag) {
|
||||
writeCache("uptodate", null);
|
||||
return { status: "uptodate", info: null };
|
||||
}
|
||||
const info = { latest: tag, notes: (d.body ?? "").slice(0, 800), url: d.html_url ?? "" };
|
||||
const info = newerUpdateInfo({ latest: tag.replace(/^v/, ""), notes: typeof d.body === "string" ? d.body.slice(0, 800) : "", url: typeof d.html_url === "string" ? d.html_url : "" });
|
||||
writeCache("uptodate", info);
|
||||
return { status: "uptodate", info };
|
||||
}
|
||||
@@ -18394,7 +18444,8 @@ function ConsolePane() {
|
||||
const [glowSpeed, setGlowSpeedState] = (0, import_react2.useState)(() => splitEnv?.console?.getGlowSpeed?.() ?? 50);
|
||||
const [annOpen, setAnnOpen] = (0, import_react2.useState)(false);
|
||||
const [updStatus, setUpdStatus] = (0, import_react2.useState)(() => readCache().status);
|
||||
const [updInfo, setUpdInfo] = (0, import_react2.useState)(() => readCache().info);
|
||||
const [rawUpdInfo, setUpdInfo] = (0, import_react2.useState)(() => readCache().info);
|
||||
const updInfo = newerUpdateInfo(rawUpdInfo);
|
||||
const [autoCheckOn, setAutoCheckOn] = (0, import_react2.useState)(() => getAutoCheck());
|
||||
const updBusyRef = (0, import_react2.useRef)(false);
|
||||
const photoUrlRef = (0, import_react2.useRef)("");
|
||||
@@ -20217,6 +20268,13 @@ function createSessionSnapshotAdapter() {
|
||||
return { ...snapshot, byId, subagentsByParent };
|
||||
};
|
||||
}
|
||||
function consoleStatusOf(entry, pending, legacyFace) {
|
||||
if (!entry) return "idle";
|
||||
if (pending != null) return "need";
|
||||
if (entry.running === true) return "busy";
|
||||
if (entry.completed === true || legacyFace?.completed === true) return "done";
|
||||
return "idle";
|
||||
}
|
||||
function childSessionIdsOf(snapshot, sid) {
|
||||
const children = /* @__PURE__ */ new Set();
|
||||
for (const [id, row] of Object.entries(snapshot?.byId ?? {})) {
|
||||
@@ -20441,13 +20499,27 @@ function previewFromEvents(entries) {
|
||||
if (!Array.isArray(entries)) return "";
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const ev = entries[i]?.event;
|
||||
if (ev?.type !== "user/message" && ev?.type !== "assistant/message") continue;
|
||||
const data = ev.type === "assistant/message" ? ev.data?.message ?? ev.data : ev.data;
|
||||
if (ev?.type !== "assistant/message") continue;
|
||||
if (ev.surfaceOp != null && ev.surfaceOp !== "append") continue;
|
||||
const data = ev.data?.message ?? ev.data;
|
||||
const blocks = data?.content ?? data?.blocks;
|
||||
if (!Array.isArray(blocks)) continue;
|
||||
const raw = blocks.filter((b) => b?.type === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
||||
const text2 = cleanPreviewText(raw);
|
||||
if (text2.length >= 8) return text2.slice(0, 220);
|
||||
if (text2) return text2.slice(0, 220);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function previewFromNodes(nodes) {
|
||||
if (!Array.isArray(nodes)) return "";
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
const node = nodes[i];
|
||||
if (node?.kind !== "assistant" || node.interrupted === true) continue;
|
||||
const blocks = node.blocks;
|
||||
if (!Array.isArray(blocks)) continue;
|
||||
const raw = blocks.filter((b) => b?.kind === "text" && typeof b.text === "string").map((b) => b.text).join("\n");
|
||||
const text2 = cleanPreviewText(raw);
|
||||
if (text2) return text2.slice(0, 220);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -20663,7 +20735,6 @@ var WAVE_BG_B64 = "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQUFBAYFBQUHBgYHCQ8KCQgIC
|
||||
|
||||
// src/client/index.tsx
|
||||
var import_jsx_runtime3 = require("react/jsx-runtime");
|
||||
var LOCAL_VERSION2 = false ? "dev" : "0.5.0";
|
||||
var UPDATE_REPO2 = "Aisland-SJL/dsh-worktable";
|
||||
var { command: UPGRADE_CMD, prompt: UPGRADE_AI2, desktop: DESKTOP_HOST } = worktableUpgrade();
|
||||
var ICON_SYNC = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { viewBox: "0 0 16 16", "aria-hidden": true, children: [
|
||||
@@ -20676,16 +20747,6 @@ var ICON_SPARK = /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("svg", { viewBox:
|
||||
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M8 1.6l1.5 3.9 3.9 1.5-3.9 1.5L8 12.4 6.5 8.5 2.6 7l3.9-1.5z", fill: "none", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }),
|
||||
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12.6 11.2l.5 1.3 1.3.5-1.3.5-.5 1.3-.5-1.3-1.3-.5 1.3-.5z", fill: "currentColor" })
|
||||
] });
|
||||
function cmpVer2(a, b) {
|
||||
const pa = a.split(".").map(Number);
|
||||
const pb = b.split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const x = pa[i] ?? 0;
|
||||
const y = pb[i] ?? 0;
|
||||
if (x !== y) return x - y;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
async function copyText(text2) {
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) {
|
||||
@@ -21146,24 +21207,7 @@ function lastTextOf(sid) {
|
||||
const binding = sessionBridge?.sessions?.binding?.(sid);
|
||||
if (binding?.eventSource?.getSnapshot) return previewFromEvents(binding.eventSource.getSnapshot().entries);
|
||||
const face = binding?.session?.getSnapshot?.();
|
||||
const nodes = face?.nodes ?? [];
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
const n = nodes[i];
|
||||
const blocks = Array.isArray(n?.blocks) ? n.blocks : Array.isArray(n?.content) ? n.content : [];
|
||||
let raw = "";
|
||||
for (const b of blocks) {
|
||||
const s = typeof b?.text === "string" ? b.text.trim() : "";
|
||||
if (!s) continue;
|
||||
if (b?.type === "text") {
|
||||
raw = s;
|
||||
break;
|
||||
}
|
||||
if (!raw) raw = s;
|
||||
}
|
||||
if (!raw) continue;
|
||||
const clean = cleanPreviewText(raw);
|
||||
if (clean.length >= 8) return clean.slice(0, 220);
|
||||
}
|
||||
return previewFromNodes(face?.nodes);
|
||||
} catch {
|
||||
}
|
||||
return "";
|
||||
@@ -21262,7 +21306,7 @@ function buildWindowTaskText(projectId, projectName, windowLabel, requirement, f
|
||||
const win = windowLabel || "\u4E00\u4E2A\u5185\u5BB9\u7A97";
|
||||
const folderLine = folder ? "\u9879\u76EE\u6587\u4EF6\u5939\uFF1A" + folder + "\uFF08\u672C\u9879\u76EE\u6240\u6709\u4EA7\u51FA\u6587\u4EF6\u4E00\u5F8B\u653E\u8FDB\u8FD9\u4E2A\u6587\u4EF6\u5939\uFF1B\u4E0D\u8981\u5199\u5230\u522B\u7684\u9ED8\u8BA4\u4F4D\u7F6E\uFF09\u3002" : "\u672C\u9879\u76EE\u6682\u672A\u8BBE\u7F6E\u4E13\u5C5E\u6587\u4EF6\u5939\uFF1A\u4EA7\u51FA\u6587\u4EF6\u5148\u5411\u7528\u6237\u786E\u8BA4\u5B58\u653E\u4F4D\u7F6E\uFF0C\u4E0D\u8981\u968F\u4FBF\u5199\u3002";
|
||||
const lines = [
|
||||
"\u3010\u5DE5\u4F5C\u53F0\u81EA\u5B9A\u4E49\u7A97\u53E3\u4EFB\u52A1\u3011",
|
||||
"\u3010\u5DE5\u4F5C\u53F0\u3011",
|
||||
"\u4EE5\u4E0B\u5185\u5BB9\u7531 dsh-worktable\uFF08\u5DE5\u4F5C\u53F0\uFF09\u63D2\u4EF6" + (mode === "new" ? "\u81EA\u52A8\u53D1\u9001" : "\u53D1\u9001") + "\uFF1A\u7528\u6237\u60F3\u628A\u300C" + projectName + "\u300D\u9879\u76EE\u4E2D\u7684\u300C" + win + "\u300D\u7A97\u53E3\u6253\u9020\u6210\u4ED6\u60F3\u8981\u7684\u5185\u5BB9\u3002",
|
||||
"1. dsh-worktable \u662F\u4FA7\u8FB9\u680F\u5E95\u90E8\u7684\u81EA\u5EFA\u300C\u5DE5\u4F5C\u53F0\u300D\u63D2\u4EF6\uFF08\u5B98\u65B9\u6587\u6863\u6CA1\u6709\u5B83\u7684\u8BF4\u660E\uFF0C\u4E0D\u4E86\u89E3\u4E4B\u5904\u53EF\u76F4\u63A5\u5411\u7528\u6237\u63D0\u95EE\uFF09\u3002",
|
||||
"2. \u7528\u6237\u9700\u6C42\uFF1A" + requirement,
|
||||
@@ -21442,6 +21486,7 @@ function clearNotifyAck(sid) {
|
||||
function sessionNotifyState(entry) {
|
||||
if (!entry) return null;
|
||||
if (entry.pendingInteraction != null) return "need";
|
||||
if (entry.running === true) return null;
|
||||
if (entry.completed === true) return "done";
|
||||
return null;
|
||||
}
|
||||
@@ -21561,7 +21606,8 @@ function WorktableSection(props) {
|
||||
const [registeredIds, setRegisteredIds] = (0, import_react3.useState)(() => [...registryStore.ids]);
|
||||
const [addOpen, setAddOpen] = (0, import_react3.useState)(false);
|
||||
const [viewOptionsOpen, setViewOptionsOpen] = (0, import_react3.useState)(false);
|
||||
const [updateInfo, setUpdateInfo] = (0, import_react3.useState)(null);
|
||||
const [rawUpdateInfo, setUpdateInfo] = (0, import_react3.useState)(null);
|
||||
const updateInfo = newerUpdateInfo(rawUpdateInfo);
|
||||
const [updateCheckOn, setUpdateCheckOn] = (0, import_react3.useState)(() => localStorage.getItem("dsh.worktable.updateCheck.v1") !== "0");
|
||||
const [updateCopied, setUpdateCopied] = (0, import_react3.useState)(false);
|
||||
const [updateStatus, setUpdateStatus] = (0, import_react3.useState)("idle");
|
||||
@@ -21572,9 +21618,15 @@ function WorktableSection(props) {
|
||||
}, []);
|
||||
const checkUpdates = (0, import_react3.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;
|
||||
let last = 0;
|
||||
try {
|
||||
last = Number(localStorage.getItem("dsh.worktable.lastUpdateCheck.v1") ?? "0");
|
||||
} catch {
|
||||
}
|
||||
const age = Date.now() - last;
|
||||
if (!force && Number.isFinite(last) && last > 0 && age >= 0 && age < 24 * 3600 * 1e3) return;
|
||||
updateCheckingRef.current = true;
|
||||
setUpdateInfo(null);
|
||||
setUpdateStatus("checking");
|
||||
try {
|
||||
let d = null;
|
||||
@@ -21595,17 +21647,21 @@ function WorktableSection(props) {
|
||||
setUpdateStatus("failed");
|
||||
return;
|
||||
}
|
||||
localStorage.setItem("dsh.worktable.lastUpdateCheck.v1", String(Date.now()));
|
||||
const tag = (d.tag_name ?? "").replace(/^v/, "");
|
||||
if (!tag || cmpVer2(tag, LOCAL_VERSION2) <= 0) {
|
||||
try {
|
||||
localStorage.setItem("dsh.worktable.lastUpdateCheck.v1", String(Date.now()));
|
||||
} catch {
|
||||
}
|
||||
const tag = typeof d.tag_name === "string" ? d.tag_name.trim() : "";
|
||||
const order = cmpVer(tag, LOCAL_VERSION);
|
||||
if (order == null) {
|
||||
setUpdateStatus("failed");
|
||||
return;
|
||||
}
|
||||
if (order <= 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 ?? "" });
|
||||
setUpdateInfo(newerUpdateInfo({ latest: tag.replace(/^v/, ""), notes: typeof d.body === "string" ? d.body.slice(0, 800) : "", url: typeof d.html_url === "string" ? d.html_url : "" }));
|
||||
setUpdateStatus("uptodate");
|
||||
} finally {
|
||||
updateCheckingRef.current = false;
|
||||
@@ -21698,15 +21754,12 @@ function WorktableSection(props) {
|
||||
if (!sid) return "idle";
|
||||
const e = byId[sid];
|
||||
if (!e) return "idle";
|
||||
if (pendingAckForSession(sid) != null) return "need";
|
||||
if (e.completed === true) return "done";
|
||||
let face;
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.();
|
||||
if (face && face.completed === true) return "done";
|
||||
face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.();
|
||||
} catch {
|
||||
}
|
||||
if (e.running === true) return "busy";
|
||||
return "idle";
|
||||
return consoleStatusOf(e, pendingAckForSession(sid), face);
|
||||
};
|
||||
const runtimeOf = (sid) => {
|
||||
if (!sid) return null;
|
||||
@@ -23163,7 +23216,7 @@ function WorktableSection(props) {
|
||||
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-wt_updateVers", children: [
|
||||
t("update.current"),
|
||||
" v",
|
||||
LOCAL_VERSION2,
|
||||
LOCAL_VERSION,
|
||||
" \u2192 v",
|
||||
updateInfo.latest
|
||||
] }),
|
||||
@@ -23299,7 +23352,7 @@ function WorktableSection(props) {
|
||||
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "dsh-wt_versionRow", children: [
|
||||
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { children: [
|
||||
"v",
|
||||
LOCAL_VERSION2,
|
||||
LOCAL_VERSION,
|
||||
updateStatus === "uptodate" && !updateInfo ? " \xB7 " + t("update.upToDate") : "",
|
||||
updateStatus === "failed" && !updateInfo ? " \xB7 " + t("update.checkFail") : ""
|
||||
] }),
|
||||
|
||||
@@ -225,7 +225,7 @@ function resolveDshHomeSafe() {
|
||||
cachedDshHome = baseDshHome();
|
||||
return cachedDshHome;
|
||||
}
|
||||
var PLUGIN_VERSION = false ? "dev" : "0.5.0";
|
||||
var PLUGIN_VERSION = false ? "dev" : "0.5.1";
|
||||
var name = "dsh-worktable";
|
||||
var inject = ["webServer", "sessions"];
|
||||
var HEALTH_PATH = "/api/worktable/health";
|
||||
|
||||
Generated
+3
-3
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "dsh-worktable",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dsh-worktable",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"dsh": "0.2.0-rc.2 || 0.2.1-rc.3"
|
||||
"dsh": "0.2.0-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dsh-worktable",
|
||||
"description": "工作台 Worktable:DeepSeek Harness 侧边栏的 agent 级项目容器(应用抽屉)",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Aisland-SJL/dsh-worktable.git",
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"dsh": "0.2.0-rc.2 || 0.2.1-rc.3"
|
||||
"dsh": "0.2.0-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.5"
|
||||
},
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
@@ -40,10 +40,11 @@
|
||||
]
|
||||
},
|
||||
"compatibility": {
|
||||
"dsh": "0.2.0-rc.2 || 0.2.1-rc.3",
|
||||
"dsh": "0.2.0-rc.2 || 0.2.1-rc.3 || 0.2.1-rc.5",
|
||||
"dshReleases": {
|
||||
"0.2.0-rc.2": "compatible",
|
||||
"0.2.1-rc.3": "compatible"
|
||||
"0.2.1-rc.3": "compatible",
|
||||
"0.2.1-rc.5": "compatible"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -56,6 +57,7 @@
|
||||
"test:input": "node ../04_test/host-input.test.mjs",
|
||||
"test:session": "node ../04_test/session-compat.test.mjs",
|
||||
"test:details": "node ../04_test/session-details.test.mjs",
|
||||
"test:update": "node ../04_test/update-check.test.mjs",
|
||||
"test:transport": "node ../04_test/host-transport.test.mjs",
|
||||
"test:widget": "node ../04_test/widget-mount.test.mjs",
|
||||
"test:security": "node --test ../04_test/request-auth.test.mjs ../04_test/preview-files.test.mjs ../04_test/sandboxed-frame.test.mjs",
|
||||
|
||||
@@ -83,7 +83,7 @@ if (!patchOk) process.exit(1)
|
||||
console.log('[release-prep] identity check passed (' + IDENTITY + ', cordis.patch.yml strict)')
|
||||
|
||||
// ---------- 2. 会话/输入兼容回归 + 构建 + 语法检查 ----------
|
||||
const compatTests = ['host-input.test.mjs', 'session-compat.test.mjs', 'session-details.test.mjs', 'host-transport.test.mjs', 'widget-mount.test.mjs', 'request-auth.test.mjs', 'preview-files.test.mjs', 'sandboxed-frame.test.mjs']
|
||||
const compatTests = ['host-input.test.mjs', 'session-compat.test.mjs', 'session-details.test.mjs', 'host-transport.test.mjs', 'widget-mount.test.mjs', 'request-auth.test.mjs', 'preview-files.test.mjs', 'sandboxed-frame.test.mjs', 'update-check.test.mjs']
|
||||
.map((file) => join(HERE, '..', '04_test', file))
|
||||
const compatRun = spawnSync(process.execPath, ['--test', ...compatTests], { cwd: HERE, stdio: 'inherit' })
|
||||
if (compatRun.status !== 0) { console.error('[release-prep] FAIL: session/input compatibility tests'); process.exit(1) }
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
/** 更新公告正文;公开发布状态以 GitHub Release 为准。 */
|
||||
export const CHANGELOG_VERSION = 'v0.5.0'
|
||||
export const CHANGELOG_V030 = `更新公告 · v0.5.0
|
||||
export const CHANGELOG_VERSION = 'v0.5.1'
|
||||
export const CHANGELOG_V030 = `更新公告 · v0.5.1
|
||||
|
||||
🔧 DSH 0.2.1-rc.5 兼容与更新提醒修复
|
||||
|
||||
【加载修复】将 rc.5 兼容目标加入精确版本声明,处理宿主因版本声明不匹配而跳过工作台加载的问题;不通配放开未来版本。
|
||||
|
||||
【更新提醒】修复测试版版本号比较出错、把旧正式版当成新版本的问题。设置与控制室共用版本排序规则,读取缓存和显示卡片时重新确认目标更高且未忽略;检查失败或无更新时清掉旧提示,不引导降级。
|
||||
|
||||
【控制室显示】会话停止统一显示中性的“本轮已结束”,不再把失败或停止当作成功;正在运行优先于旧结束标记。卡片摘要只显示可读的助手答复,过滤注入上下文与仅供模型的替换副本;无答复时留空。
|
||||
|
||||
【兼容与验收】仅列已测 DSH 0.2.0-rc.2 / 0.2.1-rc.3 / 0.2.1-rc.5。rc.5 已有隔离网页检查;用户在官方 Windows Desktop 安装候选后确认工作台恢复、更新误报消失。正式包另做独立安装与自动回归,不将候选页面证据扩大为正式包全功能验收;旧 0.1.x、未测宿主及 macOS 不作兼容保证。
|
||||
|
||||
【保留边界】沿用 0.5.0 的网页安全隔离、接口身份校验与项目存储方式。新版输入框已有 /命令标签时,标注追加可能退回剪贴板;此项跨版本坐标适配待后续处理。本版不包含旧会话模型请求 HTTP 400 的修复。
|
||||
|
||||
───────
|
||||
|
||||
此前版本说明(v0.5.0)
|
||||
|
||||
🛡️ 新版宿主兼容、安全隔离与侧栏修复
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import { isAbs, joinPath, parentPathOf, basenameOf } from './pathutil'
|
||||
import { splitStore, SplitWorkspace, setSplitT, setSplitEnv, type LayoutSpec, type SplitPane, type ConsoleCardData } from './split'
|
||||
import { appendHostInput } from './hostInput'
|
||||
import { worktableUpgrade } from './hostTransport'
|
||||
import { LOCAL_VERSION, cmpVer, newerUpdateInfo, type UpdateInfo } from './updateCheck'
|
||||
import { WidgetMountRegistry, newWidgetId, widgetManifestPath, type WidgetBinding } from './widgetMount'
|
||||
import { currentSessionOf, openHostSession, sendHostSession, createSessionSnapshotAdapter, childSessionIdsOf, pendingAckOf, sessionRuntimeMs } from './sessionCompat'
|
||||
import { cleanPreviewText, previewFromEvents, readSessionPreview, presetApiOf, modelApiOf, createHostSession, blankSessionNeedsWorkspace } from './sessionDetails'
|
||||
import { currentSessionOf, openHostSession, sendHostSession, createSessionSnapshotAdapter, childSessionIdsOf, pendingAckOf, sessionRuntimeMs, consoleStatusOf } from './sessionCompat'
|
||||
import { previewFromEvents, previewFromNodes, readSessionPreview, presetApiOf, modelApiOf, createHostSession, blankSessionNeedsWorkspace } from './sessionDetails'
|
||||
import { photoStore, kindOf } from './photoStore'
|
||||
import { DEFAULT_BG_SVG, b64ToBlob } from './defaultBg'
|
||||
import { WAVE_BG_B64 } from './waveBg'
|
||||
@@ -29,8 +30,6 @@ type OrderBy = 'manual' | 'recent'
|
||||
type DockMode = 'footer' | 'float'
|
||||
|
||||
// ── 更新检查(客户端直连 GitHub Releases API,只读 GET;失败静默)──
|
||||
declare const __WT_VERSION__: string
|
||||
const LOCAL_VERSION = typeof __WT_VERSION__ === 'undefined' ? 'dev' : __WT_VERSION__
|
||||
const UPDATE_REPO = 'Aisland-SJL/dsh-worktable'
|
||||
const { command: UPGRADE_CMD, prompt: UPGRADE_AI, desktop: DESKTOP_HOST } = worktableUpgrade()
|
||||
// 更新提示图标(手绘 SVG,避免 emoji 跨平台渲染差异)
|
||||
@@ -48,17 +47,6 @@ const ICON_SPARK = (
|
||||
<path d="M12.6 11.2l.5 1.3 1.3.5-1.3.5-.5 1.3-.5-1.3-1.3-.5 1.3-.5z" fill="currentColor" />
|
||||
</svg>
|
||||
)
|
||||
type UpdateInfo = { latest: string; notes: string; url: string }
|
||||
function cmpVer(a: string, b: string): number {
|
||||
const pa = a.split('.').map(Number)
|
||||
const pb = b.split('.').map(Number)
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const x = pa[i] ?? 0
|
||||
const y = pb[i] ?? 0
|
||||
if (x !== y) return x - y
|
||||
}
|
||||
return 0
|
||||
}
|
||||
async function copyText(text: string): Promise<boolean> {
|
||||
try {
|
||||
if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(text); return true }
|
||||
@@ -618,27 +606,13 @@ function schedulePreviewSweep() {
|
||||
previewTimer = window.setTimeout(() => { previewTimer = null; sweepPreviews() }, 6000)
|
||||
}
|
||||
|
||||
/** 从会话快照节点里提取最近一条文本(纯读内存镜像,零 Token;代码清洗后无则 '') */
|
||||
/** 从宿主内存镜像提取最近助手文本;无可读答复则留空,不回退注入/推理/工具内容。 */
|
||||
function lastTextOf(sid: string): string {
|
||||
try {
|
||||
const binding = sessionBridge?.sessions?.binding?.(sid)
|
||||
if (binding?.eventSource?.getSnapshot) return previewFromEvents(binding.eventSource.getSnapshot().entries)
|
||||
const face = binding?.session?.getSnapshot?.()
|
||||
const nodes: any[] = face?.nodes ?? []
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
const n = nodes[i]
|
||||
const blocks = Array.isArray(n?.blocks) ? n.blocks : (Array.isArray(n?.content) ? n.content : [])
|
||||
let raw = ''
|
||||
for (const b of blocks) {
|
||||
const s = typeof b?.text === 'string' ? b.text.trim() : ''
|
||||
if (!s) continue
|
||||
if (b?.type === 'text') { raw = s; break }
|
||||
if (!raw) raw = s
|
||||
}
|
||||
if (!raw) continue
|
||||
const clean = cleanPreviewText(raw)
|
||||
if (clean.length >= 8) return clean.slice(0, 220)
|
||||
}
|
||||
return previewFromNodes(face?.nodes)
|
||||
} catch {}
|
||||
return ''
|
||||
}
|
||||
@@ -759,7 +733,7 @@ function buildWindowTaskText(projectId: string, projectName: string, windowLabel
|
||||
? '项目文件夹:' + folder + '(本项目所有产出文件一律放进这个文件夹;不要写到别的默认位置)。'
|
||||
: '本项目暂未设置专属文件夹:产出文件先向用户确认存放位置,不要随便写。'
|
||||
const lines = [
|
||||
'【工作台自定义窗口任务】',
|
||||
'【工作台】',
|
||||
'以下内容由 dsh-worktable(工作台)插件' + (mode === 'new' ? '自动发送' : '发送') + ':用户想把「' + projectName + '」项目中的「' + win + '」窗口打造成他想要的内容。',
|
||||
'1. dsh-worktable 是侧边栏底部的自建「工作台」插件(官方文档没有它的说明,不了解之处可直接向用户提问)。',
|
||||
'2. 用户需求:' + requirement,
|
||||
@@ -921,7 +895,7 @@ function buildMountContent(folder: string, d: any): any {
|
||||
|
||||
/** 每会话最近观察到的待决身份;同为 need 的问题替换也必须重新点亮。 */
|
||||
const notifyStateSeenRef: { current: Record<string, string | null> } = { current: {} }
|
||||
/** 每会话最近观察到的「已完成」布尔值:新一次完成时清除旧 done ack(控制室绿光重新点亮) */
|
||||
/** 每会话最近观察到的结束边沿:新一轮结束清除旧 done ack;结束不表示成功。 */
|
||||
const doneSeenRef: { current: Record<string, boolean> } = { current: {} }
|
||||
/** 清除某会话的 ack 记录(状态转移时调用,保证新问题不会被旧确认压住) */
|
||||
function clearNotifyAck(sid: string) {
|
||||
@@ -934,11 +908,12 @@ function clearNotifyAck(sid: string) {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 会话状态 → 提醒类型:待决(黄) 优先于 完成(绿)——等待用户判断时 pendingInteraction 与
|
||||
/** 会话状态 → 提醒类型:待决(黄) 优先于 本轮结束(中性)——等待用户判断时 pendingInteraction 与
|
||||
* running 会同时为真,原生 UI 以黄点优先,镜像必须一致 */
|
||||
function sessionNotifyState(entry: any): 'done' | 'need' | null {
|
||||
if (!entry) return null
|
||||
if (entry.pendingInteraction != null) return 'need'
|
||||
if (entry.running === true) return null
|
||||
if (entry.completed === true) return 'done'
|
||||
return null
|
||||
}
|
||||
@@ -1062,7 +1037,8 @@ function WorktableSection(props: any) {
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
const [viewOptionsOpen, setViewOptionsOpen] = useState(false)
|
||||
// 更新检查:徽标 / 更新卡 / 版本行共用;节流一天一次,忽略按版本号存 localStorage
|
||||
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
|
||||
const [rawUpdateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
|
||||
const updateInfo = newerUpdateInfo(rawUpdateInfo)
|
||||
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')
|
||||
@@ -1071,9 +1047,12 @@ function WorktableSection(props: any) {
|
||||
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 // 自动检查节流;手动「立即检查」绕过节流
|
||||
let last = 0
|
||||
try { last = Number(localStorage.getItem('dsh.worktable.lastUpdateCheck.v1') ?? '0') } catch {}
|
||||
const age = Date.now() - last
|
||||
if (!force && Number.isFinite(last) && last > 0 && age >= 0 && age < 24 * 3600 * 1000) return // 自动检查节流;手动「立即检查」绕过节流
|
||||
updateCheckingRef.current = true
|
||||
setUpdateInfo(null) // Every new result replaces the old card, including failure/no update.
|
||||
setUpdateStatus('checking')
|
||||
try {
|
||||
let d: { tag_name?: string; body?: string; html_url?: string } | null = null
|
||||
@@ -1089,11 +1068,12 @@ function WorktableSection(props: any) {
|
||||
}
|
||||
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 ?? '' })
|
||||
try { localStorage.setItem('dsh.worktable.lastUpdateCheck.v1', String(Date.now())) } catch {}
|
||||
const tag = typeof d.tag_name === 'string' ? d.tag_name.trim() : ''
|
||||
const order = cmpVer(tag, LOCAL_VERSION)
|
||||
if (order == null) { setUpdateStatus('failed'); return }
|
||||
if (order <= 0) { setUpdateStatus('uptodate'); return }
|
||||
setUpdateInfo(newerUpdateInfo({ latest: tag.replace(/^v/, ''), notes: typeof d.body === 'string' ? d.body.slice(0, 800) : '', url: typeof d.html_url === 'string' ? d.html_url : '' }))
|
||||
setUpdateStatus('uptodate')
|
||||
} finally {
|
||||
updateCheckingRef.current = false
|
||||
@@ -1198,15 +1178,10 @@ function WorktableSection(props: any) {
|
||||
if (!sid) return 'idle'
|
||||
const e = byId[sid]
|
||||
if (!e) return 'idle'
|
||||
if (pendingAckForSession(sid) != null) return 'need'
|
||||
if (e.completed === true) return 'done'
|
||||
// 会话面兜底:宿主快照可能滞后或缺少完成字段
|
||||
try {
|
||||
const face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.()
|
||||
if (face && face.completed === true) return 'done'
|
||||
} catch {}
|
||||
if (e.running === true) return 'busy'
|
||||
return 'idle'
|
||||
// Read an existing binding only; never activate a cold session to classify status.
|
||||
let face: any
|
||||
try { face = sessionBridge?.sessions?.binding?.(sid)?.session?.getSnapshot?.() } catch {}
|
||||
return consoleStatusOf(e, pendingAckForSession(sid), face)
|
||||
}
|
||||
// 运行时长:任务 startedAt → 新版 chat legacy.turnTimings → 旧会话面;未知不虚构。
|
||||
const runtimeOf = (sid: string | undefined): number | null => {
|
||||
@@ -1217,7 +1192,7 @@ function WorktableSection(props: any) {
|
||||
const make = (id: string, name: string, icon: string, self: boolean): ConsoleCardData => {
|
||||
const sid = pr.projects.bindings[id]
|
||||
const status = statusOf(sid)
|
||||
// 发光 = 完成/待决且本轮未被确认;点卡片确认后熄灭
|
||||
// 发光 = 本轮结束/待决且未被确认;结束不承诺任务成功。
|
||||
const glow = !!sid && ((status === 'done' && ackMap[sid] !== 'done') || (status === 'need' && ackMap[sid] !== pendingAckForSession(sid)))
|
||||
return {
|
||||
id, name, icon,
|
||||
@@ -1976,7 +1951,7 @@ function buildCustomLayoutPrompt(req: string): string {
|
||||
return childSessionIdsOf(sessionsSnapshotStore.snapshot, sid)
|
||||
}, [])
|
||||
|
||||
/** 项目 → 提醒态:绑定会话(含其子代理)待决(黄) > 完成(绿) > 工作中(蓝);
|
||||
/** 项目 → 提醒态:绑定会话(含其子代理)待决(黄) > 本轮结束(中性) > 工作中(蓝);
|
||||
* 状态转移自动清除旧 ack——原生 UI 每个新问题都会重新亮黄,镜像不得被旧确认压住 */
|
||||
const bindNotifyMap: Record<string, 'done' | 'need' | 'busy'> = useMemo(() => {
|
||||
const map: Record<string, 'done' | 'need' | 'busy'> = {}
|
||||
|
||||
@@ -105,7 +105,7 @@ export const zh = {
|
||||
'bind.titleUnbound': '绑定对话(未绑定 ○○):点击选择要跟随的对话',
|
||||
'bind.titleBound': '已绑定对话(●●):点击更换或解绑',
|
||||
'bind.tipUnbound': '未绑定对话:点击选择要跟随的对话',
|
||||
'bind.tipDone': '(任务已完成,点击项目查看)',
|
||||
'bind.tipDone': '(本轮已结束,点击项目查看)',
|
||||
'bind.tipBusy': '(对话正在处理任务…)',
|
||||
'bind.tipNeed': '(需要你的决定,点击项目处理)',
|
||||
'bind.folder': '项目文件夹',
|
||||
@@ -161,9 +161,9 @@ export const zh = {
|
||||
'console.idle': '空闲',
|
||||
'console.busy': '工作中',
|
||||
'console.need': '待你决定',
|
||||
'console.done': '已完成',
|
||||
'console.done': '本轮已结束',
|
||||
'console.kids': '子代理',
|
||||
'console.noPreview': '暂无消息',
|
||||
'console.noPreview': '暂无助手回复',
|
||||
'console.unboundShort': '未绑定对话',
|
||||
'console.jump': '跳转到绑定对话',
|
||||
'console.empty': '还没有项目',
|
||||
@@ -343,7 +343,7 @@ export const en = {
|
||||
'bind.titleUnbound': 'Bind conversation (unbound ○○): pick the conversation to follow',
|
||||
'bind.titleBound': 'Conversation bound (●●): click to change or unbind',
|
||||
'bind.tipUnbound': 'No conversation bound — click to pick one',
|
||||
'bind.tipDone': ' (task completed — click the project to view)',
|
||||
'bind.tipDone': ' (run ended — click the project to view)',
|
||||
'bind.tipBusy': ' (conversation is working on the task…)',
|
||||
'bind.tipNeed': ' (needs your decision — click the project to handle)',
|
||||
'bind.folder': 'Project folder',
|
||||
@@ -399,9 +399,9 @@ export const en = {
|
||||
'console.idle': 'Idle',
|
||||
'console.busy': 'Working',
|
||||
'console.need': 'Needs you',
|
||||
'console.done': 'Done',
|
||||
'console.done': 'Run ended',
|
||||
'console.kids': 'subagents',
|
||||
'console.noPreview': 'No messages yet',
|
||||
'console.noPreview': 'No assistant reply yet',
|
||||
'console.unboundShort': 'No conversation bound',
|
||||
'console.jump': 'Jump to bound conversation',
|
||||
'console.empty': 'No projects yet',
|
||||
|
||||
@@ -86,6 +86,17 @@ export function createSessionSnapshotAdapter() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Display only: done means the run ended, never that the task succeeded.
|
||||
* Keep the adapter's completion edge unchanged for widget-result/ack handling.
|
||||
*/
|
||||
export function consoleStatusOf(entry: any, pending: string | null, legacyFace?: any): 'idle' | 'busy' | 'need' | 'done' {
|
||||
if (!entry) return 'idle'
|
||||
if (pending != null) return 'need'
|
||||
if (entry.running === true) return 'busy'
|
||||
if (entry.completed === true || legacyFace?.completed === true) return 'done'
|
||||
return 'idle'
|
||||
}
|
||||
|
||||
/** Parent links and the host's child catalog can each contain otherwise missing children. */
|
||||
export function childSessionIdsOf(snapshot: any, sid: string): Set<string> {
|
||||
const children = new Set<string>()
|
||||
|
||||
@@ -12,13 +12,31 @@ export function previewFromEvents(entries: any): string {
|
||||
if (!Array.isArray(entries)) return ''
|
||||
for (let i = entries.length - 1; i >= 0; i--) {
|
||||
const ev = entries[i]?.event
|
||||
if (ev?.type !== 'user/message' && ev?.type !== 'assistant/message') continue
|
||||
const data = ev.type === 'assistant/message' ? ev.data?.message ?? ev.data : ev.data
|
||||
// user/message also carries injected runtime context. Preview assistant replies only.
|
||||
if (ev?.type !== 'assistant/message') continue
|
||||
// New hosts keep model-only replacement copies out of the visible conversation.
|
||||
if (ev.surfaceOp != null && ev.surfaceOp !== 'append') continue
|
||||
const data = ev.data?.message ?? ev.data
|
||||
const blocks = data?.content ?? data?.blocks
|
||||
if (!Array.isArray(blocks)) continue
|
||||
const raw = blocks.filter((b: any) => b?.type === 'text' && typeof b.text === 'string').map((b: any) => b.text).join('\n')
|
||||
const text = cleanPreviewText(raw)
|
||||
if (text.length >= 8) return text.slice(0, 220)
|
||||
if (text) return text.slice(0, 220)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/** Legacy in-memory nodes have kind-discriminated blocks, not event content blocks. */
|
||||
export function previewFromNodes(nodes: any): string {
|
||||
if (!Array.isArray(nodes)) return ''
|
||||
for (let i = nodes.length - 1; i >= 0; i--) {
|
||||
const node = nodes[i]
|
||||
if (node?.kind !== 'assistant' || node.interrupted === true) continue
|
||||
const blocks = node.blocks
|
||||
if (!Array.isArray(blocks)) continue
|
||||
const raw = blocks.filter((b: any) => b?.kind === 'text' && typeof b.text === 'string').map((b: any) => b.text).join('\n')
|
||||
const text = cleanPreviewText(raw)
|
||||
if (text) return text.slice(0, 220)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { parentPathOf } from './pathutil'
|
||||
import { hostWebSocketUrl, worktableUpgrade } from './hostTransport'
|
||||
import { SandboxedFrame } from './SandboxedFrame'
|
||||
import { CHANGELOG_V030 } from './changelog'
|
||||
import { LOCAL_VERSION, checkUpdate, getAutoCheck, readCache, setAutoCheck as storeAutoCheck, setSkipVersion, type UpdateInfo, type UpdateStatus } from './updateCheck'
|
||||
import { LOCAL_VERSION, checkUpdate, getAutoCheck, readCache, newerUpdateInfo, setAutoCheck as storeAutoCheck, setSkipVersion, type UpdateInfo, type UpdateStatus } from './updateCheck'
|
||||
import { Terminal } from 'xterm'
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
@@ -207,19 +207,19 @@ export type ConsoleCardData = {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
/** 三态 + 空闲:need(待决黄) > done(完成绿) > busy(工作中蓝) > idle */
|
||||
/** 展示待决/运行/本轮结束/空闲;结束为中性提醒,不判定任务成功。 */
|
||||
status: 'idle' | 'busy' | 'need' | 'done'
|
||||
/** 正在运行时的本轮已用毫秒;非运行态 null */
|
||||
runtimeMs: number | null
|
||||
/** 子代理数量 */
|
||||
kids: number
|
||||
/** 最近一条消息预览(单行文本,可能为空) */
|
||||
/** 最近可读的助手答复(单行文本,可能为空) */
|
||||
preview: string
|
||||
/** 是否绑定对话 */
|
||||
bound: boolean
|
||||
/** 是否为「工作台」自己(卡片点击无操作) */
|
||||
self: boolean
|
||||
/** 完成/待决且未被确认:卡片发对应色光(点击确认后熄灭) */
|
||||
/** 本轮结束/待决且未被确认:卡片发提醒光(点击确认后熄灭) */
|
||||
glow: boolean
|
||||
}
|
||||
|
||||
@@ -1629,7 +1629,8 @@ function ConsolePane() {
|
||||
const [glowSpeed, setGlowSpeedState] = useState<number>(() => splitEnv?.console?.getGlowSpeed?.() ?? 50)
|
||||
const [annOpen, setAnnOpen] = useState(false)
|
||||
const [updStatus, setUpdStatus] = useState<UpdateStatus>(() => readCache().status)
|
||||
const [updInfo, setUpdInfo] = useState<UpdateInfo | null>(() => readCache().info)
|
||||
const [rawUpdInfo, setUpdInfo] = useState<UpdateInfo | null>(() => readCache().info)
|
||||
const updInfo = newerUpdateInfo(rawUpdInfo)
|
||||
const [autoCheckOn, setAutoCheckOn] = useState<boolean>(() => getAutoCheck())
|
||||
const updBusyRef = useRef(false)
|
||||
const photoUrlRef = useRef<string>('')
|
||||
|
||||
@@ -198,9 +198,9 @@ export const css = xtermCss + '\n' + [
|
||||
'.dsh-wt_bindBtn{position:absolute;right:26px;top:50%;transform:translateY(-50%);width:22px;height:22px;margin:0;display:block;border-radius:6px;color:var(--dsw-alias-label-secondary,#9aa4b2);cursor:pointer;user-select:none}',
|
||||
'.dsh-wt_bindBtn:hover{background:var(--dsw-alias-fill-l2,rgba(255,255,255,.09));color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
'.dsh-wt_bindBtn[data-bound=true]{color:var(--dsw-alias-label-primary,#e6e8eb)}',
|
||||
// 任务完成提醒镜像:绿色发光 / 待决提醒:黄色发光(点击项目即 ack 恢复常态实心);
|
||||
// 本轮结束提醒:中性发光 / 待决提醒:黄色发光(点击项目即 ack 恢复常态实心);
|
||||
// 工作中:蓝色发光 + 两圆交替亮灭(对应 DSH 对话的转圈标记,无需 ack,running 变 false 自动停)
|
||||
'.dsh-wt_bindBtn[data-bound=done]{color:#3fb950}',
|
||||
'.dsh-wt_bindBtn[data-bound=done]{color:#94a3b8}',
|
||||
'.dsh-wt_bindBtn[data-bound=need]{color:#d29922}',
|
||||
'.dsh-wt_bindBtn[data-bound=busy]{color:#4f8ef7}',
|
||||
'@keyframes dsh-wt-busyA{0%,44%{opacity:1}50%,94%{opacity:.12}100%{opacity:1}}',
|
||||
@@ -581,12 +581,12 @@ export const css = xtermCss + '\n' + [
|
||||
'.dsh-wt_consoleStatus{flex:none;font-size:22px;font-weight:700;line-height:30px;color:var(--wt-text2)}',
|
||||
'.dsh-wt_consoleStatus-busy{color:var(--dsw-alias-state-accent-primary,#4f8ef7)}',
|
||||
'.dsh-wt_consoleStatus-need{color:var(--dsw-alias-state-warning,#d29922)}',
|
||||
'.dsh-wt_consoleStatus-done{color:var(--dsw-alias-state-success,#3fb950)}',
|
||||
'.dsh-wt_consoleStatus-done{color:var(--wt-text2)}',
|
||||
'.dsh-wt_consoleRuntime{flex:none;font-size:17px;font-weight:600;line-height:24px;color:var(--wt-text2);font-variant-numeric:tabular-nums}',
|
||||
'.dsh-wt_consolePreview{display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;overflow:hidden;font-size:8.5px;line-height:13px;color:var(--wt-text2);word-break:break-word}',
|
||||
'.dsh-wt_consolePreviewNone{color:var(--wt-text3);font-style:italic;-webkit-line-clamp:1}',
|
||||
// 三色光效:整卡霓虹描边光晕(像侧栏双圆点的发光质感)——完成=绿、待决=黄;工作=蓝色描边顺时针旋转
|
||||
'.dsh-wt_consoleCard-glowDone{border-color:rgba(63,185,80,.9);box-shadow:0 0 8px rgba(63,185,80,.5),0 0 26px rgba(63,185,80,.3),inset 0 0 16px rgba(63,185,80,.07)}',
|
||||
// 结束只用中性提醒光,不作为成功证明;待决=黄,工作=蓝色描边顺时针旋转
|
||||
'.dsh-wt_consoleCard-glowDone{border-color:rgba(148,163,184,.8);box-shadow:0 0 8px rgba(148,163,184,.35),0 0 26px rgba(148,163,184,.2),inset 0 0 16px rgba(148,163,184,.05)}',
|
||||
'.dsh-wt_consoleCard-glowNeed{border-color:rgba(210,153,34,.95);box-shadow:0 0 8px rgba(210,153,34,.5),0 0 26px rgba(210,153,34,.3),inset 0 0 16px rgba(210,153,34,.08)}',
|
||||
'@property --consoleAngle{syntax:"<angle>";initial-value:0deg;inherits:false}',
|
||||
'.dsh-wt_consoleCard-busy{box-shadow:0 0 10px rgba(94,160,255,.55),0 0 34px rgba(79,142,247,.4)}',
|
||||
|
||||
@@ -11,14 +11,45 @@ const K_LAST_CHECK = 'dsh.worktable.lastUpdateCheck.v1'
|
||||
const K_SKIP = 'dsh.worktable.skipVersion.v1'
|
||||
const K_CACHE = 'dsh.worktable.updateCache.v1'
|
||||
|
||||
export function cmpVer(a: string, b: string): number {
|
||||
const pa = String(a).split('.').map((x) => parseInt(x, 10) || 0)
|
||||
const pb = String(b).split('.').map((x) => parseInt(x, 10) || 0)
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const d = (pa[i] || 0) - (pb[i] || 0)
|
||||
if (d !== 0) return d > 0 ? 1 : -1
|
||||
/** Strict version precedence; invalid input is incomparable, never a potential update. */
|
||||
function versionParts(value: string): { core: string[]; pre: string[] } | null {
|
||||
if (typeof value !== 'string') return null
|
||||
const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(value.trim())
|
||||
if (!match) return null
|
||||
const pre = match[4]?.split('.') ?? []
|
||||
if (pre.some((part) => /^\d+$/.test(part) && part.length > 1 && part[0] === '0')) return null
|
||||
return { core: match.slice(1, 4), pre }
|
||||
}
|
||||
function numericOrder(a: string, b: string): number {
|
||||
if (a.length !== b.length) return a.length > b.length ? 1 : -1
|
||||
return a === b ? 0 : a > b ? 1 : -1
|
||||
}
|
||||
export function cmpVer(a: string, b: string): number | null {
|
||||
const pa = versionParts(a), pb = versionParts(b)
|
||||
if (!pa || !pb) return null
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const order = numericOrder(pa.core[i], pb.core[i])
|
||||
if (order !== 0) return order
|
||||
}
|
||||
return 0
|
||||
if (!pa.pre.length || !pb.pre.length) return pa.pre.length ? -1 : pb.pre.length ? 1 : 0
|
||||
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
||||
const x = pa.pre[i], y = pb.pre[i]
|
||||
if (x === undefined || y === undefined) return x === undefined ? -1 : 1
|
||||
if (x === y) continue
|
||||
const nx = /^\d+$/.test(x), ny = /^\d+$/.test(y)
|
||||
if (nx && ny) return numericOrder(x, y)
|
||||
if (nx !== ny) return nx ? -1 : 1
|
||||
return x > y ? 1 : -1
|
||||
}
|
||||
return 0 // Build metadata has no effect on version precedence.
|
||||
}
|
||||
|
||||
/** Shared by cache reads and both UIs. Preserve identity to avoid render-effect churn. */
|
||||
export function newerUpdateInfo(value: unknown): UpdateInfo | null {
|
||||
const info = value as UpdateInfo | null
|
||||
if (!info || typeof info.latest !== 'string' || typeof info.notes !== 'string' || typeof info.url !== 'string') return null
|
||||
if (cmpVer(info.latest, LOCAL_VERSION) !== 1 || cmpVer(info.latest, getSkipVersion()) === 0) return null
|
||||
return info
|
||||
}
|
||||
|
||||
export function getSkipVersion(): string {
|
||||
@@ -38,21 +69,23 @@ export function readCache(): { status: UpdateStatus; info: UpdateInfo | null } {
|
||||
const raw = localStorage.getItem(K_CACHE)
|
||||
if (raw) {
|
||||
const c = JSON.parse(raw)
|
||||
if (c && typeof c.status === 'string') {
|
||||
return { status: c.status === 'checking' ? 'idle' : c.status, info: c.info ?? null }
|
||||
if (c?.status === 'uptodate' || c?.status === 'failed' || c?.status === 'idle') {
|
||||
return { status: c.status, info: c.status === 'uptodate' ? newerUpdateInfo(c.info) : null }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return { status: 'idle', info: null }
|
||||
}
|
||||
export function writeCache(status: UpdateStatus, info: UpdateInfo | null) {
|
||||
try { localStorage.setItem(K_CACHE, JSON.stringify({ status, info })) } catch {}
|
||||
try { localStorage.setItem(K_CACHE, JSON.stringify({ status, info: status === 'uptodate' ? newerUpdateInfo(info) : null })) } catch {}
|
||||
}
|
||||
|
||||
/** 检查更新:节流一天一次(force 绕过);8s 超时 × 3 重试;防重入由调用方保证 */
|
||||
export async function checkUpdate(force = false): Promise<{ status: UpdateStatus; info: UpdateInfo | null }> {
|
||||
const last = Number(localStorage.getItem(K_LAST_CHECK) ?? '0')
|
||||
if (!force && Date.now() - last < 24 * 3600 * 1000) return readCache()
|
||||
let last = 0
|
||||
try { last = Number(localStorage.getItem(K_LAST_CHECK) ?? '0') } catch {}
|
||||
const age = Date.now() - last
|
||||
if (!force && Number.isFinite(last) && last > 0 && age >= 0 && age < 24 * 3600 * 1000) return readCache()
|
||||
try { localStorage.setItem(K_LAST_CHECK, String(Date.now())) } catch {}
|
||||
let d: { tag_name?: string; body?: string; html_url?: string } | null = null
|
||||
for (let attempt = 0; attempt < 3 && !d; attempt++) {
|
||||
@@ -66,10 +99,11 @@ export async function checkUpdate(force = false): Promise<{ status: UpdateStatus
|
||||
finally { clearTimeout(timer) }
|
||||
}
|
||||
if (!d) { writeCache('failed', null); return { status: 'failed', info: null } }
|
||||
const tag = (d.tag_name ?? '').replace(/^v/, '')
|
||||
if (!tag || cmpVer(tag, LOCAL_VERSION) <= 0) { writeCache('uptodate', null); return { status: 'uptodate', info: null } }
|
||||
if (getSkipVersion() === tag) { writeCache('uptodate', null); return { status: 'uptodate', info: null } }
|
||||
const info: UpdateInfo = { latest: tag, notes: (d.body ?? '').slice(0, 800), url: d.html_url ?? '' }
|
||||
const tag = typeof d.tag_name === 'string' ? d.tag_name.trim() : ''
|
||||
const order = cmpVer(tag, LOCAL_VERSION)
|
||||
if (order == null) { writeCache('failed', null); return { status: 'failed', info: null } }
|
||||
if (order <= 0) { writeCache('uptodate', null); return { status: 'uptodate', info: null } }
|
||||
const info = newerUpdateInfo({ latest: tag.replace(/^v/, ''), notes: typeof d.body === 'string' ? d.body.slice(0, 800) : '', url: typeof d.html_url === 'string' ? d.html_url : '' })
|
||||
writeCache('uptodate', info)
|
||||
return { status: 'uptodate', info }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user