dsh-collab-workbench 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dsh-collab-workbench might be problematic. Click here for more details.
- package/README.md +75 -87
- package/client.js +42 -22
- package/package.json +2 -2
- package/src/client/update-ui.js +42 -22
- package/src/host/api.js +1 -2
- package/src/host/update.js +10 -32
package/README.md
CHANGED
|
@@ -1,133 +1,121 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 协作工作台 · Collaboration Workbench
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DeepSeek Harness 的多 Agent 项目协作插件。通过自定义角色、项目群聊和任务调度,让不同职责的 Agent 在同一项目中协作,并保留执行记录与交付依据。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
npm 包名:`dsh-collab-workbench`。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 核心功能
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **人员与角色**:创建自定义人员,设置职责描述和模型,为项目组织不同分工。
|
|
10
|
+
- **项目群聊**:在群内沟通,通过 @ 指定成员;群组关联项目工作目录。
|
|
11
|
+
- **任务管理与自动调度**:指派成员、设置优先级及前置依赖,系统按可用执行额度安排任务。
|
|
12
|
+
- **执行记录与恢复**:保存输入、执行状态、结果摘要和证据;支持显式重试、停止及有界的中断恢复。
|
|
13
|
+
- **群内权限审批**:在可明确关联的请求上,展示宿主原生审批入口;无法安全关联时回到原会话处理。
|
|
14
|
+
- **交付与记忆**:支持管理员最终交付,以及个人和团队记忆。
|
|
15
|
+
- **版本更新**:关于页手动检查更新,发现新版时显示侧栏云朵提示;安装前需要用户确认。
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
## 安装
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
本包是 **DeepSeek Harness 插件**,不是可单独运行的应用。需要已安装并可正常运行的 Harness,以及可用的模型配置。
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
在 Harness 的插件管理功能中,以 npm 包名安装并启用:
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
- 升级时历史已完成项目只标记“待复核”,不自动重跑或批量唤醒旧项目,也不假定历史已经交付。以后新的完成转换会自动交接。
|
|
21
|
-
- 已有且唯一匹配项目目录的 SOPAI 工作项从工作台派生状态与证据;保留人工决策/调用历史和首页人工正文,不补造过去的记录。无项目工作项时不创建;匹配歧义/读写冲突明确记录错误。JSON 与首页分别原子更新,非跨文件事务,失败通过重试收敛。
|
|
22
|
-
- 审批与普通群消息按时间稳定穿插;审批结果原位更新,不再按待审批/已处理重排。原生授权能力、卡片样式和明暗主题令牌不变。
|
|
23
|
-
- execution.status 返回项目交付、工作项同步状态及交接队列,便于检查未交付和同步异常。
|
|
23
|
+
```text
|
|
24
|
+
dsh-collab-workbench
|
|
25
|
+
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
也可让 Harness 中的助手协助安装,示例:
|
|
26
28
|
|
|
29
|
+
> 请从 npm registry 安装并启用 dsh-collab-workbench 插件。
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
安装结果若提示重启,请先重启 Harness,再刷新页面。普通的 `npm install` 只下载 npm 包,不等于已在 Harness 中启用插件。
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
若遇到下载失败,检查本机及 Harness profile 的 registry、作用域 registry 和网络设置;指定公共 registry 不一定覆盖本机已有的作用域配置。
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
- 按当前 Session、callId、工具和原因关联;无法明确关联、同一调用存在多个未结请求、已取消或过期时禁用群内授权,保留“打开原会话”入口。审计 UUID 与 Client 本地交互 key 不混用。
|
|
34
|
-
- 宿主的 `approval/asked`、`approval/decided` 日志通过增量会话投影提供显示数据。已允许、已拒绝、取消及过期结果会同步到群内卡片;最近记录可查看,较早记录仍以原会话审计为准。
|
|
35
|
-
- 执行实例进入 `approval-wait`,任务仍属进行中但执行汇总显示“等待权限审批”;占用成员和并发额度,不触发无活动提醒、继续提示或自动重试。普通消息不会解除审批等待。
|
|
36
|
-
- 在原会话回答同一请求后,群内入口失效;在群内回答后,原请求也随即结算。终止任务会取消其请求,迟到结果不能复活任务。
|
|
37
|
-
- 刷新或重连后只复用宿主仍在等待的原始请求,不根据历史记录制造新的批准能力;宿主进程重启按原有有界恢复处理,旧卡片不可授权。
|
|
38
|
-
- 打开群时保留相关活动会话的 Client 引用以接收原生审批。切换群组或页签不会撤销尚未回答的原生请求;引用由插件后台维护,任务结束、群删除或插件卸载时释放。本版没有跨群全局审批收件箱。
|
|
39
|
-
- 页面刷新后使用新版界面。安装结果为 applied 不等于已经验证当前用户页面的视觉;明暗主题、批准、拒绝和取消已在隔离的真实 Harness + Chrome 中验证,未操作用户业务审批。
|
|
35
|
+
## 快速开始
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
1. 打开侧栏中的“协作工作台”。
|
|
38
|
+
2. 创建人员,为每人填写名称、职责及模型配置。
|
|
39
|
+
3. 创建项目群,选择成员、管理员和项目工作目录。
|
|
40
|
+
4. 在群中描述目标,通过 @ 指定人员沟通;创建任务并设置指派和依赖。
|
|
41
|
+
5. 查看任务状态与成员结果,处理必要的决策或权限审批,由管理员核验并完成最终交付。
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
任务执行需要使用者提供适用的模型和工具权限。系统记录的“已完成”基于执行成员汇报和程序条件,不替代对代码、测试及成果的独立验收。
|
|
44
44
|
|
|
45
|
-
##
|
|
45
|
+
## 五种预设角色类型
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
2. 按高/中/低优先级、前置依赖和可用执行额度启动;默认全局最多 3 个执行,同一个角色最多 1 个。
|
|
49
|
-
3. 不同任务共享同一项目目录时顺序运行。同一个任务的多个指派成员可以协作并行;本版没有为每个成员自动建立 Git worktree。
|
|
50
|
-
4. 执行者用 `dwb_report_execution` 汇报。完成必须有摘要、依据,并处理完已经收到的新输入;一轮聊天结束不代表任务完成。
|
|
51
|
-
5. 全部当前指派成员完成后,任务自动变为“已完成”。等待、失败、暂停等详细状态独立保存在执行记录里,兼容原有任务界面的三种状态。
|
|
52
|
-
6. 失败、阻塞、长时间没有活动时,系统通过持久通知队列在群内提醒,不会仅在后台日志里留下错误。
|
|
47
|
+
创建人员时可选择下列模板,也可以使用自定义类型和职责:
|
|
53
48
|
|
|
54
|
-
|
|
49
|
+
| 类型 | 默认职责 |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| 研发负责人 | 接收需求、分解任务、协调进度、验收交付 |
|
|
52
|
+
| 架构师 | 技术方案设计、架构评审 |
|
|
53
|
+
| 高级工程师 | 代码实现、联调、自测 |
|
|
54
|
+
| 测试工程师 | 测试用例设计、质量验证 |
|
|
55
|
+
| 产品经理 | 需求澄清、验收标准定义、价值把关 |
|
|
55
56
|
|
|
56
|
-
|
|
57
|
+
这些是**可选模板**,安装不会自动创建五名人员。每位用户的人员、职责、模型、群组和任务由其自行配置;发布者本机的角色及工作数据不随包分发。
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
- 普通用户消息只含一个明确的 `task_N` 标识时,也可自动识别归属。
|
|
60
|
-
- 执行成员发言自动关联当前任务;有明确 @ 的任务消息送到相关指派成员的独立执行 Session。
|
|
61
|
-
- @ 未指派的管理员或评审成员时,保留原有群会话协调通道。
|
|
62
|
-
- 普通群消息仍按 @ 路由;无人被 @ 时先送管理员,无管理员时送全体。
|
|
63
|
-
- 不再通过相似任务标题猜测身份,跨群任务不能误关联。
|
|
64
|
-
- 群内进展汇报不会反过来唤醒自己,任务通知也不会另行唤醒一套群会话造成重复执行。
|
|
59
|
+
## 更新方式
|
|
65
60
|
|
|
66
|
-
|
|
61
|
+
在工作台侧栏底部打开“关于”,点击“检查更新”。
|
|
67
62
|
|
|
68
|
-
|
|
63
|
+
- npm registry 来源的安装可查询新版本;有更新时出现侧栏云朵和“关于”页入口。
|
|
64
|
+
- **插件不会自动安装更新**,也不会卸载自身。当前 Harness 拒绝用同名包覆盖运行中的插件,因此“关于”页只显示新版本号和升级步骤。
|
|
65
|
+
- 升级步骤:在 Harness 的插件页面卸载本插件,再安装“关于”页给出的版本(例如 `dsh-collab-workbench@0.1.3`),然后重启 Harness 并刷新页面。
|
|
66
|
+
- 卸载后若安装失败,工作台会暂时不可用;请先备份数据并确认网络与 registry 可用。
|
|
67
|
+
- 升级只替换程序,`$DSH_HOME/dsh-workbench` 中的角色、群组和任务数据会保留。
|
|
68
|
+
- 本地 tgz 或目录安装不提供 npm 在线检查,也不会擅自切换来源。
|
|
69
|
+
- 网络或 registry 异常只影响检查;不会因此删除工作台数据。
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
验证升级需先从 npm 安装较早版本,例如 `dsh-collab-workbench@0.1.2`,再在发布更高版本后检查更新。同版本安装不会显示“发现新版”。
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
- 重启时检查真实 Session 日志,区分已入箱、已读、已取消与未提交输入;接着原 Session 继续工作,完成任务不会自动重放。
|
|
74
|
-
- 如果进程在无法确认消息接收的时刻崩溃,采用稳定输入 ID 的至少一次投递语义。不能宣称任意文件操作、命令或外部服务恰好执行一次,恢复提示要求先检查已有成果。
|
|
75
|
-
- 停止成员或停用角色会持久暂停其相关执行;重新启用角色本身不会偷偷重新开工。管理员可以显式 retry。
|
|
76
|
-
- `dwb_manage_execution(action=stop)` 是终止操作,保留已结束记录;需要再次执行时重新派发任务,或先修改任务范围建立新版本。
|
|
77
|
-
- 删群先持久标记 deleting 并取消任务,再按精确 Session ID 清理;自定义项目目录与收养的工作区保留。
|
|
78
|
-
- 存在可恢复的任务 Session 时,不移动默认群目录;请先完成或停止任务。自定义目录只改群名。
|
|
73
|
+
## 数据与权限边界
|
|
79
74
|
|
|
80
|
-
|
|
75
|
+
工作台台账默认位于 `$DSH_HOME/dsh-workbench/state.json`,保存人员、群组、任务及执行状态;记忆保存在同一数据目录下。工作数据不上传到 npm。Agent 使用模型和工具时的数据处理仍由使用者的 Harness、模型服务及工具配置决定。
|
|
81
76
|
|
|
82
|
-
|
|
77
|
+
- 安装或更新插件不会把发布者的人员、任务或会话复制给使用者。
|
|
78
|
+
- 升级保留原数据目录;重要项目建议先备份工作台数据和项目文件。
|
|
79
|
+
- 调度不会提高 Agent 权限,工具调用仍受 Harness 的授权与文件访问规则约束。
|
|
80
|
+
- 恢复任务时应先核对已有成果;不保证任意外部操作或文件修改恰好执行一次。
|
|
81
|
+
- 同一任务的多个成员可能同时工作;插件不会自动为每人建立 Git worktree,需合理划分文件范围。
|
|
83
82
|
|
|
84
|
-
|
|
83
|
+
### 与 SOPAI 的关系
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
- 自由文本答案使用 `决策回复:具体答案`,或者 `decisions.resolve` RPC 带当前 seq 明确回复。
|
|
88
|
-
- 普通补充消息不会自动解除决策等待。
|
|
89
|
-
- 决策答案、解除等待及恢复输入在同一个台账提交里保存,避免重启丢失答案。
|
|
85
|
+
**不需要安装 SOPAI Skill,工作台可独立使用。**
|
|
90
86
|
|
|
91
|
-
|
|
87
|
+
若群组项目目录中已经存在唯一匹配的 `.sopai/runs/<工作项>/work_item.json`,插件可将工作台状态、执行摘要和证据同步到该工作项及复核首页。没有匹配工作项时跳过,不自动创建 SOPAI 项目,不要求以 SOPAI 流程启动任务。
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|---|---|
|
|
95
|
-
| `dwb_task_execution` | 分页查询执行状态、结果依据和输入;用 inputId/inputOffset 续读长输入 |
|
|
96
|
-
| `dwb_report_execution` | 当前执行会话汇报自己的 running/waiting/blocked/completed/failed |
|
|
97
|
-
| `dwb_manage_execution` | 管理员显式 retry 或 stop |
|
|
89
|
+
## 版本记录
|
|
98
90
|
|
|
99
|
-
|
|
91
|
+
### 0.1.3
|
|
100
92
|
|
|
101
|
-
|
|
93
|
+
- 修正更新入口:当前 Harness 不支持覆盖运行中的插件,因此移除一键安装按钮与安装接口,改为显示新版本号和手动升级步骤。
|
|
94
|
+
- 检查更新仍为只读;有新版时保留侧栏云朵提示。
|
|
95
|
+
- 增加升级风险说明与数据保留说明;不更改任务调度逻辑。
|
|
102
96
|
|
|
103
|
-
|
|
97
|
+
### 0.1.2
|
|
104
98
|
|
|
105
|
-
|
|
99
|
+
- 重写面向使用者的说明:补充安装、快速开始、角色模板、更新方式和数据权限边界。
|
|
100
|
+
- 修正公开版本说明,移除混杂在首页中的内部开发版本记录。
|
|
101
|
+
- 更新 npm 简介;本次不更改任务调度或界面运行逻辑。
|
|
106
102
|
|
|
107
|
-
|
|
103
|
+
### 0.1.1
|
|
108
104
|
|
|
109
|
-
|
|
105
|
+
首次正式公开发布,包含协作工作台及关于页版本检查、用户确认更新功能。
|
|
110
106
|
|
|
111
|
-
##
|
|
107
|
+
## 开发
|
|
112
108
|
|
|
113
|
-
|
|
109
|
+
以下命令用于开发工作目录;npm 安装包不包含测试目录:
|
|
114
110
|
|
|
115
111
|
```bash
|
|
112
|
+
npm run build
|
|
116
113
|
npm test
|
|
117
114
|
npm run check
|
|
118
|
-
npm run test:native
|
|
119
115
|
```
|
|
120
116
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
`test:native` 使用临时 DSH_HOME、本地模拟模型和独立测试进程,验证真实桌面 Harness 的加载、稳定 Session、收件箱、工具所有权、多人完成及进程被终止后的原会话恢复。它不使用用户 profile、不调用外部模型,也不会留下替代用户 GUI 的服务器。脚本的桌面运行时路径需要与本机安装一致。
|
|
124
|
-
|
|
125
|
-
Host 依赖来自安装此包的 Harness,不为测试下载另一版本的内核。隔离业务测试使用遵守消息不可变性与事件形状的桩。
|
|
126
|
-
|
|
127
|
-
## 数据和边界
|
|
128
|
-
|
|
129
|
-
台账默认位于 `$DSH_HOME/dsh-workbench/state.json`,仍使用单文件原子替换,新增版本化保存排空机制;不能读取或损坏的台账会阻止写入,不会静默覆盖成空数据。记忆仍在同目录下的个人/团队 Markdown 中。
|
|
117
|
+
Host 依赖由 Harness 提供。隔离测试使用宿主替身;测试通过不等于已完成使用者环境的真实模型、权限审批、明暗主题或冷启动验收。
|
|
130
118
|
|
|
131
|
-
|
|
119
|
+
## 许可证
|
|
132
120
|
|
|
133
|
-
|
|
121
|
+
Apache-2.0。原创 SVG 图标随包以同一许可证提供。
|
package/client.js
CHANGED
|
@@ -153,20 +153,38 @@ function createApprovalUI(React, ctx, call) {
|
|
|
153
153
|
// END WORKBENCH APPROVAL UI
|
|
154
154
|
|
|
155
155
|
// BEGIN WORKBENCH UPDATE UI
|
|
156
|
-
// Update
|
|
157
|
-
//
|
|
156
|
+
// Update check and manual-upgrade guidance for the existing Workbench panel;
|
|
157
|
+
// embedded into client.js at build time. Styles are owned by these components.
|
|
158
158
|
function createUpdateUI(React, ctx, call) {
|
|
159
159
|
const h = React.createElement
|
|
160
160
|
const NS = 'dwbUpdates'
|
|
161
161
|
const zh = {
|
|
162
|
-
about:'关于', cloud:'有新版本', title:'协作工作台', current:'当前版本', latest:'最新版本',
|
|
162
|
+
about:'关于', cloud:'有新版本', title:'协作工作台', current:'当前版本', latest:'最新版本',
|
|
163
|
+
check:'检查更新', checking:'正在检查…', update:'可升级',
|
|
164
|
+
ready:'发现新版本,可按下方步骤手动升级。', currentState:'已是最新版本',
|
|
165
|
+
local:'当前从本地文件安装,请从原安装来源更新。', other:'当前安装来源不支持在线检查更新。',
|
|
166
|
+
pending:'尚未检查更新', failed:'检查更新失败', error:'无法检查更新,请检查网络后重试。',
|
|
167
|
+
guideTitle:'升级步骤', note:'当前 Harness 不允许覆盖运行中的插件,因此不会自动安装。',
|
|
168
|
+
step1:'打开 Harness 的插件(管理)页面。', step2:'卸载「协作工作台」。',
|
|
169
|
+
step3:'安装下面这个版本,然后重启 Harness 并刷新页面。', spec:'安装版本',
|
|
170
|
+
warning:'卸载后若安装失败,工作台会暂时不可用;请先备份工作台数据和项目文件,并确认网络与 registry 可用。',
|
|
171
|
+
dataSafe:'升级只替换插件程序;$DSH_HOME/dsh-workbench 中的角色、群组和任务数据会保留。',
|
|
163
172
|
}
|
|
164
173
|
const en = {
|
|
165
|
-
about:'About', cloud:'Update available', title:'Collaboration Workbench', current:'Installed version', latest:'Latest version',
|
|
174
|
+
about:'About', cloud:'Update available', title:'Collaboration Workbench', current:'Installed version', latest:'Latest version',
|
|
175
|
+
check:'Check for updates', checking:'Checking…', update:'Upgrade',
|
|
176
|
+
ready:'A new version is available. Follow the steps below to upgrade manually.', currentState:'Up to date',
|
|
177
|
+
local:'Installed from a local file; update it from the original source.', other:'This install source cannot check for updates.',
|
|
178
|
+
pending:'Not checked yet', failed:'Update check failed', error:'Could not check for updates. Check your network and try again.',
|
|
179
|
+
guideTitle:'Upgrade steps', note:'This Harness does not allow replacing a running plugin, so nothing is installed automatically.',
|
|
180
|
+
step1:'Open the Harness plugins page.', step2:'Uninstall Collaboration Workbench.',
|
|
181
|
+
step3:'Install the version below, then restart Harness and refresh the page.', spec:'Install version',
|
|
182
|
+
warning:'If the install fails after uninstalling, the workbench stays unavailable until it succeeds. Back up workbench data and project files, and make sure the network and registry work first.',
|
|
183
|
+
dataSafe:'Upgrading only replaces the plugin program; roles, groups and tasks in $DSH_HOME/dsh-workbench are preserved.',
|
|
166
184
|
}
|
|
167
185
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'collaboration workbench: update locales')
|
|
168
186
|
const t = ctx.locale.bind(NS)
|
|
169
|
-
const css = `.dwb-update-cloud{width:52px;min-height:42px;padding:5px 0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-brand-primary);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:2px}.dwb-update-cloud:hover{background:var(--dsw-alias-bg-layer-2)}.dwb-update-cloud:focus-visible,.dwb-update-about button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.dwb-update-cloud span{font-size:10px}.dwb-update-about{flex:1;min-width:0;overflow:auto;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);padding:56px 24px 24px}.dwb-update-card{max-width:640px;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1)}.dwb-update-about h2{font-size:18px;margin:0 0 18px}.dwb-update-about dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:10px 20px;margin:0 0 18px}.dwb-update-about dt{color:var(--dsw-alias-label-secondary)}.dwb-update-about dd{margin:0;overflow-wrap:anywhere}.dwb-update-state{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-secondary);margin:0 0 16px}.dwb-update-error{color:var(--dsw-alias-state-error-primary)}.dwb-update-actions{display:flex;gap:10px;flex-wrap:wrap}.dwb-update-about button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:7px 12px;min-height:34px;cursor:pointer}.dwb-update-about button:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}.dwb-update-about button:disabled{opacity:.5;cursor:default}.dwb-update-
|
|
187
|
+
const css = `.dwb-update-cloud{width:52px;min-height:42px;padding:5px 0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-brand-primary);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:2px}.dwb-update-cloud:hover{background:var(--dsw-alias-bg-layer-2)}.dwb-update-cloud:focus-visible,.dwb-update-about button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.dwb-update-cloud span{font-size:10px}.dwb-update-about{flex:1;min-width:0;overflow:auto;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);padding:56px 24px 24px}.dwb-update-card{max-width:640px;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1)}.dwb-update-about h2{font-size:18px;margin:0 0 18px}.dwb-update-about h3{font-size:14px;margin:18px 0 8px}.dwb-update-about dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:10px 20px;margin:0 0 18px}.dwb-update-about dt{color:var(--dsw-alias-label-secondary)}.dwb-update-about dd{margin:0;overflow-wrap:anywhere}.dwb-update-state{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-secondary);margin:0 0 16px}.dwb-update-error{color:var(--dsw-alias-state-error-primary)}.dwb-update-actions{display:flex;gap:10px;flex-wrap:wrap}.dwb-update-about button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:7px 12px;min-height:34px;cursor:pointer}.dwb-update-about button:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}.dwb-update-about button:disabled{opacity:.5;cursor:default}.dwb-update-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.9}.dwb-update-spec{display:inline-block;margin-top:8px;padding:6px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;user-select:all;overflow-wrap:anywhere}.dwb-update-note,.dwb-update-safe{font-size:12px;line-height:1.7;color:var(--dsw-alias-label-secondary);margin:10px 0 0}.dwb-update-safe{color:var(--dsw-alias-state-success-primary)}`
|
|
170
188
|
function useUpdates() {
|
|
171
189
|
const [status, setStatus] = React.useState({ phase:'idle', source:'unknown', current:null, latest:null, available:false })
|
|
172
190
|
const [error, setError] = React.useState(null)
|
|
@@ -190,18 +208,10 @@ function createUpdateUI(React, ctx, call) {
|
|
|
190
208
|
catch (failure) { if (active) setError(String(failure?.message ?? failure)) }
|
|
191
209
|
}
|
|
192
210
|
void check()
|
|
193
|
-
const timer = setInterval(check,
|
|
211
|
+
const timer = setInterval(check, 60_000)
|
|
194
212
|
return () => { active = false; clearInterval(timer) }
|
|
195
|
-
}, [
|
|
196
|
-
|
|
197
|
-
if (!status.available || busy || !window.confirm(t('confirm').replace('{version}', status.latest))) return
|
|
198
|
-
setBusy(true)
|
|
199
|
-
setError(null)
|
|
200
|
-
try { setStatus(await call('updates.install')) }
|
|
201
|
-
catch (failure) { setError(String(failure?.message ?? failure)) }
|
|
202
|
-
finally { setBusy(false) }
|
|
203
|
-
}, [status.available, status.latest, busy])
|
|
204
|
-
return {status, error, busy, refresh, install}
|
|
213
|
+
}, [])
|
|
214
|
+
return {status, error, busy, refresh}
|
|
205
215
|
}
|
|
206
216
|
function UpdateCloud({ onClick }) {
|
|
207
217
|
return h(React.Fragment, null, h('style', null, css),
|
|
@@ -211,18 +221,28 @@ function createUpdateUI(React, ctx, call) {
|
|
|
211
221
|
h('path',{d:'m12 15 0-6m-2.5 2.5L12 9l2.5 2.5'})),
|
|
212
222
|
h('span',null,t('update'))))
|
|
213
223
|
}
|
|
214
|
-
function UpdateAbout({ status, error, busy, refresh
|
|
215
|
-
const
|
|
216
|
-
const
|
|
224
|
+
function UpdateAbout({ status, error, busy, refresh }) {
|
|
225
|
+
const failed = Boolean(error) || status.phase === 'failed'
|
|
226
|
+
const state = failed ? t('failed') : status.source === 'local' ? t('local') : status.source !== 'registry' ? t('other')
|
|
227
|
+
: status.available ? t('ready') : status.checkedAt ? status.latest ? t('currentState') : t('error') : t('pending')
|
|
228
|
+
const spec = status.latest ? `dsh-collab-workbench@${status.latest}` : null
|
|
217
229
|
return h('section', {className:'dwb-update-about', 'aria-label':t('title')},
|
|
218
230
|
h('style',null,css),
|
|
219
231
|
h('div',{className:'dwb-update-card'},
|
|
220
232
|
h('h2',null,t('title')),
|
|
221
233
|
h('dl',null,h('dt',null,t('current')),h('dd',null,status.current ?? '—'),h('dt',null,t('latest')),h('dd',null,status.latest ?? '—')),
|
|
222
|
-
h('p',{className:`dwb-update-state ${
|
|
234
|
+
h('p',{className:`dwb-update-state ${failed ? 'dwb-update-error' : ''}`,role:failed ? 'alert':undefined},
|
|
235
|
+
state, failed ? `: ${error ?? status.error ?? t('error')}` : ''),
|
|
223
236
|
h('div',{className:'dwb-update-actions'},
|
|
224
|
-
h('button',{type:'button',onClick:()=>refresh(true),disabled:busy
|
|
225
|
-
|
|
237
|
+
h('button',{type:'button',onClick:()=>refresh(true),disabled:busy},busy?t('checking'):t('check'))),
|
|
238
|
+
status.available ? h(React.Fragment, null,
|
|
239
|
+
h('h3',null,t('guideTitle')),
|
|
240
|
+
h('ol',{className:'dwb-update-steps'},
|
|
241
|
+
h('li',null,t('step1')), h('li',null,t('step2')),
|
|
242
|
+
h('li',null,t('step3'), spec ? h('div',null,h('code',{className:'dwb-update-spec'},spec)) : null)),
|
|
243
|
+
h('p',{className:'dwb-update-note'},t('note')),
|
|
244
|
+
h('p',{className:'dwb-update-note'},t('warning')),
|
|
245
|
+
h('p',{className:'dwb-update-safe'},t('dataSafe'))) : null))
|
|
226
246
|
}
|
|
227
247
|
return {useUpdates, UpdateCloud, UpdateAbout, t}
|
|
228
248
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-collab-workbench",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "DeepSeek Harness 多 Agent 协作工作台:自定义角色、项目群聊、任务自动调度、群内权限审批与执行恢复",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
7
7
|
"deepseek-harness",
|
package/src/client/update-ui.js
CHANGED
|
@@ -1,17 +1,35 @@
|
|
|
1
|
-
// Update
|
|
2
|
-
//
|
|
1
|
+
// Update check and manual-upgrade guidance for the existing Workbench panel;
|
|
2
|
+
// embedded into client.js at build time. Styles are owned by these components.
|
|
3
3
|
export function createUpdateUI(React, ctx, call) {
|
|
4
4
|
const h = React.createElement
|
|
5
5
|
const NS = 'dwbUpdates'
|
|
6
6
|
const zh = {
|
|
7
|
-
about:'关于', cloud:'有新版本', title:'协作工作台', current:'当前版本', latest:'最新版本',
|
|
7
|
+
about:'关于', cloud:'有新版本', title:'协作工作台', current:'当前版本', latest:'最新版本',
|
|
8
|
+
check:'检查更新', checking:'正在检查…', update:'可升级',
|
|
9
|
+
ready:'发现新版本,可按下方步骤手动升级。', currentState:'已是最新版本',
|
|
10
|
+
local:'当前从本地文件安装,请从原安装来源更新。', other:'当前安装来源不支持在线检查更新。',
|
|
11
|
+
pending:'尚未检查更新', failed:'检查更新失败', error:'无法检查更新,请检查网络后重试。',
|
|
12
|
+
guideTitle:'升级步骤', note:'当前 Harness 不允许覆盖运行中的插件,因此不会自动安装。',
|
|
13
|
+
step1:'打开 Harness 的插件(管理)页面。', step2:'卸载「协作工作台」。',
|
|
14
|
+
step3:'安装下面这个版本,然后重启 Harness 并刷新页面。', spec:'安装版本',
|
|
15
|
+
warning:'卸载后若安装失败,工作台会暂时不可用;请先备份工作台数据和项目文件,并确认网络与 registry 可用。',
|
|
16
|
+
dataSafe:'升级只替换插件程序;$DSH_HOME/dsh-workbench 中的角色、群组和任务数据会保留。',
|
|
8
17
|
}
|
|
9
18
|
const en = {
|
|
10
|
-
about:'About', cloud:'Update available', title:'Collaboration Workbench', current:'Installed version', latest:'Latest version',
|
|
19
|
+
about:'About', cloud:'Update available', title:'Collaboration Workbench', current:'Installed version', latest:'Latest version',
|
|
20
|
+
check:'Check for updates', checking:'Checking…', update:'Upgrade',
|
|
21
|
+
ready:'A new version is available. Follow the steps below to upgrade manually.', currentState:'Up to date',
|
|
22
|
+
local:'Installed from a local file; update it from the original source.', other:'This install source cannot check for updates.',
|
|
23
|
+
pending:'Not checked yet', failed:'Update check failed', error:'Could not check for updates. Check your network and try again.',
|
|
24
|
+
guideTitle:'Upgrade steps', note:'This Harness does not allow replacing a running plugin, so nothing is installed automatically.',
|
|
25
|
+
step1:'Open the Harness plugins page.', step2:'Uninstall Collaboration Workbench.',
|
|
26
|
+
step3:'Install the version below, then restart Harness and refresh the page.', spec:'Install version',
|
|
27
|
+
warning:'If the install fails after uninstalling, the workbench stays unavailable until it succeeds. Back up workbench data and project files, and make sure the network and registry work first.',
|
|
28
|
+
dataSafe:'Upgrading only replaces the plugin program; roles, groups and tasks in $DSH_HOME/dsh-workbench are preserved.',
|
|
11
29
|
}
|
|
12
30
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'collaboration workbench: update locales')
|
|
13
31
|
const t = ctx.locale.bind(NS)
|
|
14
|
-
const css = `.dwb-update-cloud{width:52px;min-height:42px;padding:5px 0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-brand-primary);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:2px}.dwb-update-cloud:hover{background:var(--dsw-alias-bg-layer-2)}.dwb-update-cloud:focus-visible,.dwb-update-about button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.dwb-update-cloud span{font-size:10px}.dwb-update-about{flex:1;min-width:0;overflow:auto;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);padding:56px 24px 24px}.dwb-update-card{max-width:640px;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1)}.dwb-update-about h2{font-size:18px;margin:0 0 18px}.dwb-update-about dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:10px 20px;margin:0 0 18px}.dwb-update-about dt{color:var(--dsw-alias-label-secondary)}.dwb-update-about dd{margin:0;overflow-wrap:anywhere}.dwb-update-state{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-secondary);margin:0 0 16px}.dwb-update-error{color:var(--dsw-alias-state-error-primary)}.dwb-update-actions{display:flex;gap:10px;flex-wrap:wrap}.dwb-update-about button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:7px 12px;min-height:34px;cursor:pointer}.dwb-update-about button:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}.dwb-update-about button:disabled{opacity:.5;cursor:default}.dwb-update-
|
|
32
|
+
const css = `.dwb-update-cloud{width:52px;min-height:42px;padding:5px 0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-brand-primary);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:2px}.dwb-update-cloud:hover{background:var(--dsw-alias-bg-layer-2)}.dwb-update-cloud:focus-visible,.dwb-update-about button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.dwb-update-cloud span{font-size:10px}.dwb-update-about{flex:1;min-width:0;overflow:auto;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);padding:56px 24px 24px}.dwb-update-card{max-width:640px;padding:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1)}.dwb-update-about h2{font-size:18px;margin:0 0 18px}.dwb-update-about h3{font-size:14px;margin:18px 0 8px}.dwb-update-about dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:10px 20px;margin:0 0 18px}.dwb-update-about dt{color:var(--dsw-alias-label-secondary)}.dwb-update-about dd{margin:0;overflow-wrap:anywhere}.dwb-update-state{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-secondary);margin:0 0 16px}.dwb-update-error{color:var(--dsw-alias-state-error-primary)}.dwb-update-actions{display:flex;gap:10px;flex-wrap:wrap}.dwb-update-about button{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:7px 12px;min-height:34px;cursor:pointer}.dwb-update-about button:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}.dwb-update-about button:disabled{opacity:.5;cursor:default}.dwb-update-steps{margin:0;padding-left:20px;font-size:13px;line-height:1.9}.dwb-update-spec{display:inline-block;margin-top:8px;padding:6px 10px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;user-select:all;overflow-wrap:anywhere}.dwb-update-note,.dwb-update-safe{font-size:12px;line-height:1.7;color:var(--dsw-alias-label-secondary);margin:10px 0 0}.dwb-update-safe{color:var(--dsw-alias-state-success-primary)}`
|
|
15
33
|
function useUpdates() {
|
|
16
34
|
const [status, setStatus] = React.useState({ phase:'idle', source:'unknown', current:null, latest:null, available:false })
|
|
17
35
|
const [error, setError] = React.useState(null)
|
|
@@ -35,18 +53,10 @@ export function createUpdateUI(React, ctx, call) {
|
|
|
35
53
|
catch (failure) { if (active) setError(String(failure?.message ?? failure)) }
|
|
36
54
|
}
|
|
37
55
|
void check()
|
|
38
|
-
const timer = setInterval(check,
|
|
56
|
+
const timer = setInterval(check, 60_000)
|
|
39
57
|
return () => { active = false; clearInterval(timer) }
|
|
40
|
-
}, [
|
|
41
|
-
|
|
42
|
-
if (!status.available || busy || !window.confirm(t('confirm').replace('{version}', status.latest))) return
|
|
43
|
-
setBusy(true)
|
|
44
|
-
setError(null)
|
|
45
|
-
try { setStatus(await call('updates.install')) }
|
|
46
|
-
catch (failure) { setError(String(failure?.message ?? failure)) }
|
|
47
|
-
finally { setBusy(false) }
|
|
48
|
-
}, [status.available, status.latest, busy])
|
|
49
|
-
return {status, error, busy, refresh, install}
|
|
58
|
+
}, [])
|
|
59
|
+
return {status, error, busy, refresh}
|
|
50
60
|
}
|
|
51
61
|
function UpdateCloud({ onClick }) {
|
|
52
62
|
return h(React.Fragment, null, h('style', null, css),
|
|
@@ -56,18 +66,28 @@ export function createUpdateUI(React, ctx, call) {
|
|
|
56
66
|
h('path',{d:'m12 15 0-6m-2.5 2.5L12 9l2.5 2.5'})),
|
|
57
67
|
h('span',null,t('update'))))
|
|
58
68
|
}
|
|
59
|
-
function UpdateAbout({ status, error, busy, refresh
|
|
60
|
-
const
|
|
61
|
-
const
|
|
69
|
+
function UpdateAbout({ status, error, busy, refresh }) {
|
|
70
|
+
const failed = Boolean(error) || status.phase === 'failed'
|
|
71
|
+
const state = failed ? t('failed') : status.source === 'local' ? t('local') : status.source !== 'registry' ? t('other')
|
|
72
|
+
: status.available ? t('ready') : status.checkedAt ? status.latest ? t('currentState') : t('error') : t('pending')
|
|
73
|
+
const spec = status.latest ? `dsh-collab-workbench@${status.latest}` : null
|
|
62
74
|
return h('section', {className:'dwb-update-about', 'aria-label':t('title')},
|
|
63
75
|
h('style',null,css),
|
|
64
76
|
h('div',{className:'dwb-update-card'},
|
|
65
77
|
h('h2',null,t('title')),
|
|
66
78
|
h('dl',null,h('dt',null,t('current')),h('dd',null,status.current ?? '—'),h('dt',null,t('latest')),h('dd',null,status.latest ?? '—')),
|
|
67
|
-
h('p',{className:`dwb-update-state ${
|
|
79
|
+
h('p',{className:`dwb-update-state ${failed ? 'dwb-update-error' : ''}`,role:failed ? 'alert':undefined},
|
|
80
|
+
state, failed ? `: ${error ?? status.error ?? t('error')}` : ''),
|
|
68
81
|
h('div',{className:'dwb-update-actions'},
|
|
69
|
-
h('button',{type:'button',onClick:()=>refresh(true),disabled:busy
|
|
70
|
-
|
|
82
|
+
h('button',{type:'button',onClick:()=>refresh(true),disabled:busy},busy?t('checking'):t('check'))),
|
|
83
|
+
status.available ? h(React.Fragment, null,
|
|
84
|
+
h('h3',null,t('guideTitle')),
|
|
85
|
+
h('ol',{className:'dwb-update-steps'},
|
|
86
|
+
h('li',null,t('step1')), h('li',null,t('step2')),
|
|
87
|
+
h('li',null,t('step3'), spec ? h('div',null,h('code',{className:'dwb-update-spec'},spec)) : null)),
|
|
88
|
+
h('p',{className:'dwb-update-note'},t('note')),
|
|
89
|
+
h('p',{className:'dwb-update-note'},t('warning')),
|
|
90
|
+
h('p',{className:'dwb-update-safe'},t('dataSafe'))) : null))
|
|
71
91
|
}
|
|
72
92
|
return {useUpdates, UpdateCloud, UpdateAbout, t}
|
|
73
93
|
}
|
package/src/host/api.js
CHANGED
|
@@ -192,9 +192,8 @@ export function install(rt) {
|
|
|
192
192
|
'execution.stop': async (payload) => ({runs:await rt.stopTaskExecution(payload.taskId)}),
|
|
193
193
|
'decisions.resolve': async (payload) => ({decision:await rt.resolveDecision(payload.groupId,payload.answer,payload.seq)}),
|
|
194
194
|
|
|
195
|
-
// ----
|
|
195
|
+
// ---- 协作工作台版本检查(只读;不注册 Agent 工具,也没有安装接口) ----
|
|
196
196
|
'updates.check': async (payload) => rt.updateCheck({ fresh: payload.fresh === true }),
|
|
197
|
-
'updates.install': async () => rt.updateStart(),
|
|
198
197
|
|
|
199
198
|
// ---- 群消息 ----
|
|
200
199
|
'messages.post': async (payload) => ({
|
package/src/host/update.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Read-only update checks: newest published version versus this installed one.
|
|
2
|
+
// Installation is never performed here (this Harness refuses in-place replacement).
|
|
2
3
|
// Keep npm identity separate from the historical workbench data and RPC paths.
|
|
3
4
|
import { readFileSync } from 'node:fs'
|
|
4
5
|
|
|
@@ -47,17 +48,16 @@ async function registries(manager) {
|
|
|
47
48
|
return publicRegistry ? [...new Set([own, ...fallback])] : [own]
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
+
/** Read-only update check installed by index.js; no installer and no agent tool are registered. */
|
|
51
52
|
export function install(rt, { fetchImpl = globalThis.fetch, now = Date.now } = {}) {
|
|
52
53
|
const manager = () => rt.ctx.get?.('pluginManager')
|
|
53
54
|
const status = {
|
|
54
55
|
name: PKG.name, current: PKG.version, latest: null, source: 'unknown',
|
|
55
|
-
checkedAt: 0, phase: 'idle',
|
|
56
|
+
checkedAt: 0, phase: 'idle', error: null,
|
|
56
57
|
}
|
|
57
58
|
let target = null
|
|
58
59
|
let checking = null
|
|
59
|
-
const view = () => ({ ...status, available: target !== null && newer(status.latest, status.current)
|
|
60
|
-
&& status.phase !== 'installing' && status.phase !== 'installed' })
|
|
60
|
+
const view = () => ({ ...status, available: target !== null && newer(status.latest, status.current) })
|
|
61
61
|
|
|
62
62
|
async function runCheck() {
|
|
63
63
|
const service = manager()
|
|
@@ -92,7 +92,6 @@ export function install(rt, { fetchImpl = globalThis.fetch, now = Date.now } = {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function check({ fresh = false } = {}) {
|
|
95
|
-
if (status.phase === 'installing' || status.phase === 'installed') return Promise.resolve(view())
|
|
96
95
|
if (checking) return checking
|
|
97
96
|
if (!fresh && status.checkedAt && now() - status.checkedAt < CHECK_TTL_MS) return Promise.resolve(view())
|
|
98
97
|
status.phase = 'checking'
|
|
@@ -106,30 +105,9 @@ export function install(rt, { fetchImpl = globalThis.fetch, now = Date.now } = {
|
|
|
106
105
|
return checking
|
|
107
106
|
}
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const chosen = target
|
|
115
|
-
status.phase = 'installing'
|
|
116
|
-
status.error = null
|
|
117
|
-
// The plugin can unload while installing itself; never let that reject a detached promise.
|
|
118
|
-
void Promise.resolve().then(() => service.installBundle(chosen.spec, { registry: chosen.registry }))
|
|
119
|
-
.then(result => {
|
|
120
|
-
if (result?.error || !['applied', 'restart-required'].includes(result?.application)
|
|
121
|
-
|| (result.bundle && result.bundle !== PKG.name)
|
|
122
|
-
|| (result.version && result.version !== chosen.version)) {
|
|
123
|
-
throw new Error(result?.error?.diagnostic ?? result?.error?.code ?? `Install ${result?.application ?? 'failed'}`)
|
|
124
|
-
}
|
|
125
|
-
status.phase = 'installed'
|
|
126
|
-
status.installed = chosen.version
|
|
127
|
-
})
|
|
128
|
-
.catch(error => {
|
|
129
|
-
status.phase = 'failed'
|
|
130
|
-
status.error = String(error?.message ?? error).slice(0, 300)
|
|
131
|
-
})
|
|
132
|
-
return view()
|
|
133
|
-
}
|
|
134
|
-
Object.assign(rt, { updateView: view, updateCheck: check, updateStart: start })
|
|
108
|
+
// Checking is read-only. Installing is deliberately not exposed: this Harness
|
|
109
|
+
// refuses to replace a running plugin in place, and removing the bundle first
|
|
110
|
+
// would unload this code before the reinstall could run. The About page shows
|
|
111
|
+
// the manual upgrade steps instead.
|
|
112
|
+
Object.assign(rt, { updateView: view, updateCheck: check })
|
|
135
113
|
}
|