dsh-just-chat 0.1.0
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.
- package/LICENSE +201 -0
- package/README.md +97 -0
- package/RED.md +18 -0
- package/client.js +425 -0
- package/cordis.patch.yml +3 -0
- package/docs/development.md +124 -0
- package/docs/images/quick-chat.png +0 -0
- package/docs/images/settings.png +0 -0
- package/index.js +50 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# 快速对话 · Just Chat
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/dsh-just-chat)
|
|
4
|
+
[](https://github.com/exoticknight/dsh-just-chat/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/exoticknight/dsh-just-chat/blob/main/LICENSE)
|
|
6
|
+

|
|
7
|
+
<a href="https://github.com/exoticknight/red"><img src="https://img.shields.io/badge/maintained_with-RED-C1121F" alt="Maintained with RED"></a>
|
|
8
|
+
|
|
9
|
+
为 DeepSeek Harness 添加一键对话入口:点击即可进入独立工作目录中的原生会话,省去手动选择工作区。
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## 功能特点
|
|
14
|
+
|
|
15
|
+
- **一键开始**:首页“标准模式”旁、侧栏各有一个入口,可分别开启或关闭。
|
|
16
|
+
- **独立目录**:每次完成一次入口点击,都会新建独立工作区及对话。启动插件时不预建工作区。
|
|
17
|
+
- **原生对话体验**:复用 DSH 的会话、模型、权限与附件流程;Workspace 和 Session ID 由 DSH 生成。
|
|
18
|
+
- **自定义命名**:用占位符设置新工作区名称,编辑时预览,保存后生效。
|
|
19
|
+
- **融入现有界面**:使用 DSH 原生图标和按钮,窄窗口自动换行;中文显示“快速对话”,英文显示“Just Chat”。
|
|
20
|
+
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
### 安装插件
|
|
24
|
+
|
|
25
|
+
适用于 DSH `0.1.5-rc.2` 的 Web 界面。先确认你的 DSH 已能正常对话,并且终端中可以使用 `dsh`。
|
|
26
|
+
|
|
27
|
+
通过 DSH 插件管理命令从 npm 安装:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
dsh plugin --profile web add dsh-just-chat
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
如果你使用自定义 `DSH_HOME` 或其他 profile,请选择与平时启动 DSH 相同的环境。安装后重启该 DSH 并刷新页面。
|
|
34
|
+
|
|
35
|
+
### 开始对话
|
|
36
|
+
|
|
37
|
+
1. 打开你平时使用的 DSH 页面。
|
|
38
|
+
2. 点击首页“标准模式”旁或侧栏的 **快速对话**。
|
|
39
|
+
3. 在打开的对话中输入消息并发送。
|
|
40
|
+
|
|
41
|
+
插件会自动准备独立工作目录并打开原生对话,无需选择目录。模型与权限沿用 DSH 的配置。
|
|
42
|
+
|
|
43
|
+
## 配置入口与命名
|
|
44
|
+
|
|
45
|
+
打开 **设置 → 插件 → 插件配置 → 快速对话**。
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
“标准模式旁入口”和“侧栏入口”控制两个按钮的显示位置。修改开关或模板后,卡片会标记“未保存”;点击“保存”统一应用,或点击“放弃更改”恢复已保存的值。配置集中在这张卡片中。
|
|
50
|
+
|
|
51
|
+
### 工作区名称模板
|
|
52
|
+
|
|
53
|
+
默认模板:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
{label} · {MM}-{DD} {HH}:{mm}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
例如,中文界面创建的工作区显示为 `快速对话 · 09-14 18:01`;英文界面显示为 `Just Chat · 09-14 18:01`。
|
|
60
|
+
|
|
61
|
+
| 占位符 | 含义 | 示例 |
|
|
62
|
+
| --- | --- | --- |
|
|
63
|
+
| `{label}` | 创建时的本地化名称 | 快速对话 / Just Chat |
|
|
64
|
+
| `{YYYY}` | 四位年份 | 2026 |
|
|
65
|
+
| `{MM}` / `{DD}` | 两位月份 / 日期 | 09 / 14 |
|
|
66
|
+
| `{HH}` / `{mm}` / `{ss}` | 24 小时制的时 / 分 / 秒 | 18 / 01 / 05 |
|
|
67
|
+
|
|
68
|
+
也可以写成 `灵感 {YYYY}-{MM}-{DD}` 或 `{label} {HH}:{mm}:{ss}`,普通文字会原样保留。
|
|
69
|
+
|
|
70
|
+
- 时间取自浏览器本地时间,占位符区分大小写。
|
|
71
|
+
- 模板最多 200 个字符;展开并去除首尾空白后的名称必须为 1–100 个字符。
|
|
72
|
+
- 未知或不完整的占位符会显示错误并阻止保存。
|
|
73
|
+
- “恢复默认模板”只修改草稿,还需点击“保存”。
|
|
74
|
+
- 模板与语言变更只影响之后新建的工作区,不会重命名已有工作区,也不会改变磁盘目录。
|
|
75
|
+
|
|
76
|
+
## 工作区与对话的关系
|
|
77
|
+
|
|
78
|
+
每次通过快速对话入口创建的工作目录位于:
|
|
79
|
+
|
|
80
|
+
```text
|
|
81
|
+
<DSH_HOME>/just-chat/sessions/chat-<随机后缀>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`DSH_HOME` 是当前 DSH 的用户数据目录。显示名称与目录名分开管理;即使两个工作区显示名称相同,目录和原生 ID 仍然不同。
|
|
85
|
+
|
|
86
|
+
| 操作 | 结果 |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| 点击快速对话入口 | 新建独立目录、工作区和原生对话 |
|
|
89
|
+
| 在该工作区使用 DSH 原生“新建会话” | 仍使用该工作区的目录 |
|
|
90
|
+
| 归档会话 | 隐藏会话,保留记录和目录,可恢复 |
|
|
91
|
+
| 原生删除工作区 | 移除工作区登记,保留目录与会话记录,会话转入“未分组” |
|
|
92
|
+
|
|
93
|
+
快速对话创建的会话和工作区使用 DSH 原生管理操作。删除工作区后,磁盘上的工作目录仍会保留;快速对话插件不会自动清理这些目录。
|
|
94
|
+
|
|
95
|
+
## 开发与维护
|
|
96
|
+
|
|
97
|
+
本地环境初始化、调试、测试和实现说明见[开发指南](https://github.com/exoticknight/dsh-just-chat/blob/main/docs/development.md)。
|
package/RED.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# RED 工作约定
|
|
2
|
+
|
|
3
|
+
本项目使用 RED Protocol 1,把未解决的研究证据、进行中的工程变更和已接受的项目知识分开管理。
|
|
4
|
+
|
|
5
|
+
## 当前映射
|
|
6
|
+
|
|
7
|
+
- Document:`README.md` 与 `docs/**`。README 面向使用者说明安装、功能、配置与生命周期;`docs/development.md` 面向开发者说明初始化、调试、测试与实现结构;`docs/images/` 保存配图。
|
|
8
|
+
- Research:`.research/**`。这里保留既有纯净 DSH 调研、运行时证据和交接材料,不迁移旧文件,也不把其中的历史工作记录当作规范要求。
|
|
9
|
+
- Evolve:`.evolve/**`。这里记录当前 Just Chat 插件的实施范围、验收证据、未决替代方案和后续工作。
|
|
10
|
+
|
|
11
|
+
项目使用 Git。Research 和 Evolve 由 `.gitignore` 排除,仅作为本地工作记录保存;Document 和配置纳入版本管理。
|
|
12
|
+
|
|
13
|
+
## 维护规则
|
|
14
|
+
|
|
15
|
+
- 新 Research / Evolve 文件使用 UTF-8 Markdown 和 RED TOML front matter。
|
|
16
|
+
- Document 只写已接受、可供使用者和后续 agent 直接依赖的知识;验证日志、历史过程和开放问题留在 Research / Evolve。
|
|
17
|
+
- Evolve 的验收证据提交评审后,获得单独接受前不更新 Document。
|
|
18
|
+
- DSH 宿主和宿主 UI 的边界以实现证据为准;如规范文档与代码、配置或运行时结果冲突,先记录为 Research 冲突。
|
package/client.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: 'dsh-just-chat',
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
const React = require('react')
|
|
5
|
+
const { createPortal } = require('react-dom')
|
|
6
|
+
const { Button, Tag, IconChevronDownOutline14, IconNewChatOutline16 } = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
7
|
+
const module = { exports: {} }
|
|
8
|
+
const exports = module.exports
|
|
9
|
+
|
|
10
|
+
let runtimeCtx
|
|
11
|
+
let entrySettings
|
|
12
|
+
let sidebarRegistration
|
|
13
|
+
let sidebarInjectionLive = false
|
|
14
|
+
let openingChat
|
|
15
|
+
let pendingPath
|
|
16
|
+
|
|
17
|
+
const localeNamespace = 'dsh-just-chat'
|
|
18
|
+
const settingsNamespace = 'dsh-just-chat'
|
|
19
|
+
const defaultNameTemplate = '{label} · {MM}-{DD} {HH}:{mm}'
|
|
20
|
+
const defaultSettings = Object.freeze({ hero: true, sidebar: true, workspaceNameTemplate: defaultNameTemplate })
|
|
21
|
+
const fallbackSettingsSnapshot = Object.freeze({
|
|
22
|
+
status: 'loading',
|
|
23
|
+
value: defaultSettings,
|
|
24
|
+
writable: false,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const localeDictionaries = {
|
|
28
|
+
zh: {
|
|
29
|
+
entry: '快速对话',
|
|
30
|
+
opening: '打开中…',
|
|
31
|
+
'panel.loaded': '正在打开快速对话…',
|
|
32
|
+
'panel.failed': '打开失败:{error}',
|
|
33
|
+
'panel.retry': '重试',
|
|
34
|
+
'settings.description': '配置快速对话入口和新工作区的命名规则',
|
|
35
|
+
'settings.hero': '标准模式旁入口',
|
|
36
|
+
'settings.sidebar': '侧栏入口',
|
|
37
|
+
'settings.template': '工作区名称模板',
|
|
38
|
+
'settings.help': '可用占位符:{label} 本地化名称、{YYYY} 年、{MM} 月、{DD} 日、{HH} 时、{mm} 分、{ss} 秒。使用本地时间,仅影响新建工作区。',
|
|
39
|
+
'settings.preview': '预览',
|
|
40
|
+
'settings.save': '保存',
|
|
41
|
+
'settings.reset': '恢复默认模板',
|
|
42
|
+
'settings.invalid': '请使用已列出的占位符,且展开后的名称须为 1–100 个字符。',
|
|
43
|
+
'settings.discard': '放弃更改',
|
|
44
|
+
'settings.unsaved': '未保存',
|
|
45
|
+
'settings.saving': '保存中…',
|
|
46
|
+
'settings.expand': '展开设置',
|
|
47
|
+
'settings.collapse': '收起设置',
|
|
48
|
+
'settings.readOnly': '当前设置只读。',
|
|
49
|
+
},
|
|
50
|
+
en: {
|
|
51
|
+
entry: 'Just Chat',
|
|
52
|
+
opening: 'Opening…',
|
|
53
|
+
'panel.loaded': 'Opening Just Chat…',
|
|
54
|
+
'panel.failed': 'Could not open Just Chat: {error}',
|
|
55
|
+
'panel.retry': 'Retry',
|
|
56
|
+
'settings.description': 'Configure entry points and names for new workspaces',
|
|
57
|
+
'settings.hero': 'Beside Standard mode',
|
|
58
|
+
'settings.sidebar': 'In the sidebar',
|
|
59
|
+
'settings.template': 'Workspace name template',
|
|
60
|
+
'settings.help': 'Placeholders: {label} localized name, {YYYY} year, {MM} month, {DD} day, {HH} hour, {mm} minute, {ss} second. Uses local time; affects new workspaces only.',
|
|
61
|
+
'settings.preview': 'Preview',
|
|
62
|
+
'settings.save': 'Save',
|
|
63
|
+
'settings.reset': 'Restore default template',
|
|
64
|
+
'settings.invalid': 'Use the listed placeholders and a resulting name of 1–100 characters.',
|
|
65
|
+
'settings.discard': 'Discard changes',
|
|
66
|
+
'settings.unsaved': 'Unsaved',
|
|
67
|
+
'settings.saving': 'Saving…',
|
|
68
|
+
'settings.expand': 'Show settings',
|
|
69
|
+
'settings.collapse': 'Hide settings',
|
|
70
|
+
'settings.readOnly': 'These settings are read-only.',
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function PanelIcon({ size = 16 }) {
|
|
75
|
+
return React.createElement(IconNewChatOutline16, { size })
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Deliberately a small literal-token formatter, not an executable template language.
|
|
79
|
+
function formatWorkspaceName(template, label, now = new Date()) {
|
|
80
|
+
const pad = (value) => String(value).padStart(2, '0')
|
|
81
|
+
const values = { label, YYYY: String(now.getFullYear()), MM: pad(now.getMonth() + 1),
|
|
82
|
+
DD: pad(now.getDate()), HH: pad(now.getHours()), mm: pad(now.getMinutes()), ss: pad(now.getSeconds()) }
|
|
83
|
+
if (typeof template !== 'string' || template.length > 200) throw new Error('Invalid template')
|
|
84
|
+
const title = template.replace(/\{([^{}]*)\}/g, (token, key) => {
|
|
85
|
+
if (!Object.hasOwn(values, key)) throw new Error('Unknown placeholder')
|
|
86
|
+
return values[key]
|
|
87
|
+
}).trim()
|
|
88
|
+
if (/[{}]/.test(title) || !title || title.length > 100) throw new Error('Invalid workspace name')
|
|
89
|
+
return title
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function useJustChatT() {
|
|
93
|
+
const locale = runtimeCtx.get('locale')
|
|
94
|
+
const subscribe = React.useCallback((listener) => locale.subscribe(listener), [locale])
|
|
95
|
+
const getRevision = React.useCallback(() => locale.getSnapshot().revision, [locale])
|
|
96
|
+
React.useSyncExternalStore(subscribe, getRevision, getRevision)
|
|
97
|
+
return locale.bind(localeNamespace)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function useEntrySettings() {
|
|
101
|
+
const scope = entrySettings
|
|
102
|
+
const subscribe = React.useCallback(
|
|
103
|
+
(listener) => scope?.subscribe(listener) ?? (() => {}),
|
|
104
|
+
[scope],
|
|
105
|
+
)
|
|
106
|
+
const getSnapshot = React.useCallback(
|
|
107
|
+
() => scope?.getSnapshot?.() ?? fallbackSettingsSnapshot,
|
|
108
|
+
[scope],
|
|
109
|
+
)
|
|
110
|
+
return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function settingEnabled(snapshot, key) {
|
|
114
|
+
return snapshot?.value?.[key] !== false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function openJustChat() {
|
|
118
|
+
if (openingChat) return openingChat
|
|
119
|
+
openingChat = (async () => {
|
|
120
|
+
if (!pendingPath) {
|
|
121
|
+
let title
|
|
122
|
+
const t = runtimeCtx.get('locale').bind(localeNamespace)
|
|
123
|
+
try {
|
|
124
|
+
title = formatWorkspaceName(entrySettings.getSnapshot()?.value?.workspaceNameTemplate ?? defaultNameTemplate, t('entry'))
|
|
125
|
+
} catch {
|
|
126
|
+
throw new Error(t('settings.invalid'))
|
|
127
|
+
}
|
|
128
|
+
const response = await fetch('/api/just-chat/prepare', {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
131
|
+
body: JSON.stringify({ title }),
|
|
132
|
+
})
|
|
133
|
+
if (!response.ok) throw new Error(await response.text())
|
|
134
|
+
const result = await response.json()
|
|
135
|
+
pendingPath = result.path
|
|
136
|
+
}
|
|
137
|
+
// Native create adopts the registered path and synchronizes Client state.
|
|
138
|
+
// Keep it for retry if opening fails; both native operations are idempotent.
|
|
139
|
+
const workspace = await runtimeCtx.get('workspaces').create({ path: pendingPath })
|
|
140
|
+
await runtimeCtx.get('uiWorkspace').openWorkspace(workspace.workspaceId)
|
|
141
|
+
pendingPath = undefined
|
|
142
|
+
})().finally(() => { openingChat = undefined })
|
|
143
|
+
return openingChat
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function useHeroMount() {
|
|
147
|
+
const [mount, setMount] = React.useState(null)
|
|
148
|
+
React.useEffect(() => {
|
|
149
|
+
const node = document.createElement('span')
|
|
150
|
+
node.dataset.justChatMount = 'true'
|
|
151
|
+
const style = document.createElement('style')
|
|
152
|
+
style.textContent = 'div:has(> [data-just-chat-mount]) { flex-wrap: wrap; row-gap: 4px; }'
|
|
153
|
+
document.head.appendChild(style)
|
|
154
|
+
Object.assign(node.style, { display: 'inline-flex', minWidth: '0', flexShrink: '0', marginLeft: '6px' })
|
|
155
|
+
const sync = () => {
|
|
156
|
+
const slot = document.querySelector('[data-slot="conversation.hero.agentPreset"]')
|
|
157
|
+
if (!slot) {
|
|
158
|
+
node.remove()
|
|
159
|
+
setMount(null)
|
|
160
|
+
} else {
|
|
161
|
+
// Join the existing flex row, so long workspace labels reserve space for the action.
|
|
162
|
+
if (node.previousSibling !== slot) slot.after(node)
|
|
163
|
+
setMount(node)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const observer = new MutationObserver(sync)
|
|
167
|
+
observer.observe(document.getElementById('root') ?? document.body, { childList: true, subtree: true })
|
|
168
|
+
sync()
|
|
169
|
+
return () => { observer.disconnect(); node.remove(); style.remove() }
|
|
170
|
+
}, [])
|
|
171
|
+
return mount
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function JustChatOverlay() {
|
|
175
|
+
const t = useJustChatT()
|
|
176
|
+
const settings = useEntrySettings()
|
|
177
|
+
const mount = useHeroMount()
|
|
178
|
+
const [busy, setBusy] = React.useState(false)
|
|
179
|
+
const [error, setError] = React.useState(null)
|
|
180
|
+
if (!mount || !settingEnabled(settings, 'hero')) return null
|
|
181
|
+
const openChat = async () => {
|
|
182
|
+
if (busy) return
|
|
183
|
+
setBusy(true)
|
|
184
|
+
setError(null)
|
|
185
|
+
try { await openJustChat() } catch (error) { setError(error.message) }
|
|
186
|
+
finally { setBusy(false) }
|
|
187
|
+
}
|
|
188
|
+
return createPortal(React.createElement('span', { style: { position: 'relative' } },
|
|
189
|
+
React.createElement(Button, {
|
|
190
|
+
variant: 'ghost', size: 'sm', onClick: openChat, disabled: busy,
|
|
191
|
+
'aria-label': busy ? t('opening') : t('entry'), title: t('entry'),
|
|
192
|
+
style: { borderRadius: 16, height: 28, padding: '0 8px', gap: 4, whiteSpace: 'nowrap' },
|
|
193
|
+
}, React.createElement(IconNewChatOutline16, { size: 14 }), busy ? t('opening') : t('entry')),
|
|
194
|
+
error && React.createElement('span', { role: 'alert', style: {
|
|
195
|
+
position: 'absolute', top: '100%', right: 0, width: 280, zIndex: 10,
|
|
196
|
+
padding: 8, background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-error)',
|
|
197
|
+
} }, error),
|
|
198
|
+
), mount)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function ChatPanel() {
|
|
202
|
+
const t = useJustChatT()
|
|
203
|
+
const [attempt, setAttempt] = React.useState(0)
|
|
204
|
+
const [message, setMessage] = React.useState({ key: 'panel.loaded' })
|
|
205
|
+
|
|
206
|
+
React.useEffect(() => {
|
|
207
|
+
let active = true
|
|
208
|
+
setMessage({ key: 'panel.loaded' })
|
|
209
|
+
void openJustChat().catch((error) => {
|
|
210
|
+
if (active) {
|
|
211
|
+
setMessage({
|
|
212
|
+
key: 'panel.failed',
|
|
213
|
+
params: { error: error?.message ?? JSON.stringify(error) },
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
})
|
|
217
|
+
return () => {
|
|
218
|
+
active = false
|
|
219
|
+
}
|
|
220
|
+
}, [attempt])
|
|
221
|
+
|
|
222
|
+
return React.createElement(
|
|
223
|
+
'section',
|
|
224
|
+
{ style: { fontFamily: 'sans-serif', padding: '24px' } },
|
|
225
|
+
React.createElement('h1', null, t('entry')),
|
|
226
|
+
React.createElement('p', null, t(message.key, message.params)),
|
|
227
|
+
message.key === 'panel.failed'
|
|
228
|
+
? React.createElement(
|
|
229
|
+
Button,
|
|
230
|
+
{ variant: 'outline', size: 'sm', onClick: () => setAttempt((value) => value + 1), type: 'button' },
|
|
231
|
+
t('panel.retry'),
|
|
232
|
+
)
|
|
233
|
+
: null,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// The host's PluginCard is private. Match its layout with owned, scoped styles.
|
|
238
|
+
const settingsCss = `
|
|
239
|
+
.jc-card{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);border-radius:16px;list-style:none;transition:border-color .16s,background .16s}
|
|
240
|
+
.jc-card:hover{border-color:var(--dsw-alias-label-dimmed)}
|
|
241
|
+
.jc-card[data-open=true]{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
|
|
242
|
+
.jc-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:transparent;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}
|
|
243
|
+
.jc-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
|
244
|
+
.jc-card-heading{display:flex;flex:1;flex-direction:column;gap:4px;min-width:0}
|
|
245
|
+
.jc-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
|
|
246
|
+
.jc-card-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
247
|
+
.jc-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
|
|
248
|
+
.jc-card[data-open=true] .jc-card-chevron{transform:rotate(180deg)}
|
|
249
|
+
.jc-card-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
|
|
250
|
+
.jc-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}
|
|
251
|
+
.jc-field+.jc-field{border-top:.5px solid var(--dsw-alias-border-l2)}
|
|
252
|
+
.jc-field-head{display:flex;align-items:center;gap:8px}
|
|
253
|
+
.jc-field-label{flex:1;min-width:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}
|
|
254
|
+
.jc-field-toggle{accent-color:var(--dsw-alias-brand-primary);width:16px;height:16px;cursor:pointer}
|
|
255
|
+
.jc-field-input{box-sizing:border-box;width:100%;height:34px;border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;line-height:1.5}
|
|
256
|
+
.jc-field-input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
257
|
+
.jc-field-input[aria-invalid=true]{border-color:var(--dsw-alias-label-error)}
|
|
258
|
+
.jc-field-hint{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.5;overflow-wrap:anywhere}
|
|
259
|
+
.jc-field-reset{background:none;border:0;padding:0;font:inherit;font-size:12px;color:var(--dsw-alias-label-secondary);cursor:pointer}
|
|
260
|
+
.jc-card-footer{display:flex;justify-content:flex-end;align-items:center;flex-wrap:wrap;gap:8px;border-top:.5px solid var(--dsw-alias-border-l2);padding:12px 0 4px}
|
|
261
|
+
.jc-card-error{color:var(--dsw-alias-label-error);font-size:12px;line-height:1.5;overflow-wrap:anywhere}
|
|
262
|
+
`
|
|
263
|
+
|
|
264
|
+
function PluginSettingsCard() {
|
|
265
|
+
const t = useJustChatT()
|
|
266
|
+
const snapshot = useEntrySettings()
|
|
267
|
+
const current = { ...defaultSettings, ...(snapshot.value ?? {}) }
|
|
268
|
+
const [open, setOpen] = React.useState(false)
|
|
269
|
+
const [editor, setEditor] = React.useState(null)
|
|
270
|
+
const [saving, setSaving] = React.useState(false)
|
|
271
|
+
const [error, setError] = React.useState(null)
|
|
272
|
+
const draft = editor?.value ?? current
|
|
273
|
+
const dirty = Object.keys(defaultSettings).some((key) => draft[key] !== current[key])
|
|
274
|
+
const disabled = saving || snapshot.writable === false
|
|
275
|
+
let preview
|
|
276
|
+
try { preview = formatWorkspaceName(draft.workspaceNameTemplate, t('entry')) } catch {}
|
|
277
|
+
const edit = (key, value) => {
|
|
278
|
+
setEditor((previous) => ({ revision: previous?.revision ?? snapshot.revision,
|
|
279
|
+
value: { ...(previous?.value ?? current), [key]: value } }))
|
|
280
|
+
setError(null)
|
|
281
|
+
}
|
|
282
|
+
const save = async () => {
|
|
283
|
+
if (!dirty || !preview || disabled) return
|
|
284
|
+
setSaving(true); setError(null)
|
|
285
|
+
try {
|
|
286
|
+
const ops = Object.keys(defaultSettings).filter((key) => draft[key] !== current[key])
|
|
287
|
+
.map((key) => ({ op: 'set', path: [key], value: draft[key] }))
|
|
288
|
+
await entrySettings.mutate(ops, editor?.revision)
|
|
289
|
+
setEditor(null); setOpen(false)
|
|
290
|
+
} catch (error) { setError(error.message) }
|
|
291
|
+
finally { setSaving(false) }
|
|
292
|
+
}
|
|
293
|
+
if (snapshot.status === 'unavailable') return null
|
|
294
|
+
return React.createElement('li', { className: 'jc-card', 'data-open': open },
|
|
295
|
+
React.createElement('button', {
|
|
296
|
+
className: 'jc-card-header', type: 'button', 'aria-expanded': open,
|
|
297
|
+
'aria-controls': 'just-chat-settings-body',
|
|
298
|
+
'aria-label': t(open ? 'settings.collapse' : 'settings.expand') + ': ' + t('entry'),
|
|
299
|
+
onClick: () => setOpen(!open),
|
|
300
|
+
},
|
|
301
|
+
React.createElement('span', { className: 'jc-card-heading' },
|
|
302
|
+
React.createElement('span', { className: 'jc-card-name' }, t('entry')),
|
|
303
|
+
React.createElement('span', { className: 'jc-card-description' }, t('settings.description'))),
|
|
304
|
+
dirty && React.createElement(Tag, { tone: 'neutral' }, t('settings.unsaved')),
|
|
305
|
+
React.createElement(IconChevronDownOutline14, { className: 'jc-card-chevron' })),
|
|
306
|
+
open && React.createElement('div', { className: 'jc-card-body', id: 'just-chat-settings-body' },
|
|
307
|
+
snapshot.writable === false && React.createElement('p', { className: 'jc-field-hint', role: 'status' }, t('settings.readOnly')),
|
|
308
|
+
['hero', 'sidebar'].map((key) => React.createElement('div', { key, className: 'jc-field' },
|
|
309
|
+
React.createElement('label', { className: 'jc-field-head' },
|
|
310
|
+
React.createElement('span', { className: 'jc-field-label' }, t('settings.' + key)),
|
|
311
|
+
React.createElement('input', { className: 'jc-field-toggle', type: 'checkbox',
|
|
312
|
+
checked: draft[key], disabled, 'aria-label': t('settings.' + key),
|
|
313
|
+
onChange: (event) => edit(key, event.target.checked) })))),
|
|
314
|
+
React.createElement('div', { className: 'jc-field' },
|
|
315
|
+
React.createElement('div', { className: 'jc-field-head' },
|
|
316
|
+
React.createElement('label', { className: 'jc-field-label', htmlFor: 'just-chat-name-template' }, t('settings.template')),
|
|
317
|
+
React.createElement('button', { className: 'jc-field-reset', type: 'button', disabled,
|
|
318
|
+
onClick: () => edit('workspaceNameTemplate', defaultNameTemplate) }, t('settings.reset'))),
|
|
319
|
+
React.createElement('input', { className: 'jc-field-input', id: 'just-chat-name-template',
|
|
320
|
+
value: draft.workspaceNameTemplate, maxLength: 200, disabled, 'aria-invalid': !preview,
|
|
321
|
+
'aria-describedby': 'just-chat-template-help just-chat-template-preview',
|
|
322
|
+
onChange: (event) => edit('workspaceNameTemplate', event.target.value) }),
|
|
323
|
+
React.createElement('p', { id: 'just-chat-template-help', className: 'jc-field-hint' }, t('settings.help')),
|
|
324
|
+
React.createElement('p', { id: 'just-chat-template-preview', role: preview ? 'status' : 'alert',
|
|
325
|
+
className: preview ? 'jc-field-hint' : 'jc-card-error' },
|
|
326
|
+
preview ? t('settings.preview') + ': ' + preview : t('settings.invalid'))),
|
|
327
|
+
error && React.createElement('p', { role: 'alert', className: 'jc-card-error' }, error),
|
|
328
|
+
React.createElement('div', { className: 'jc-card-footer' },
|
|
329
|
+
React.createElement(Button, { variant: 'outline', size: 'sm', disabled: !dirty || saving,
|
|
330
|
+
onClick: () => { setEditor(null); setError(null) } }, t('settings.discard')),
|
|
331
|
+
React.createElement(Button, { size: 'sm', disabled: disabled || !dirty || !preview,
|
|
332
|
+
onClick: save }, t(saving ? 'settings.saving' : 'settings.save'))),
|
|
333
|
+
))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const inject = [
|
|
337
|
+
'slots',
|
|
338
|
+
'sessions',
|
|
339
|
+
'workspaces',
|
|
340
|
+
'uiWorkspace',
|
|
341
|
+
'layout',
|
|
342
|
+
'locale',
|
|
343
|
+
'settingsScope',
|
|
344
|
+
]
|
|
345
|
+
|
|
346
|
+
function apply(ctx) {
|
|
347
|
+
runtimeCtx = ctx
|
|
348
|
+
ctx.effect(() => {
|
|
349
|
+
const style = document.createElement('style')
|
|
350
|
+
style.dataset.pluginCss = 'dsh-just-chat/settings'
|
|
351
|
+
style.textContent = settingsCss
|
|
352
|
+
document.head.appendChild(style)
|
|
353
|
+
return () => style.remove()
|
|
354
|
+
}, 'dsh-just-chat: settings styles')
|
|
355
|
+
const locale = ctx.get('locale')
|
|
356
|
+
entrySettings = ctx.settingsScope.bind({ namespace: settingsNamespace })
|
|
357
|
+
|
|
358
|
+
ctx.effect(
|
|
359
|
+
() => locale.register(localeNamespace, localeDictionaries),
|
|
360
|
+
'dsh-just-chat: dictionaries',
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
let sidebarEnabled = settingEnabled(entrySettings.getSnapshot(), 'sidebar')
|
|
364
|
+
const registerSidebarEntry = () => ctx.slots.register(
|
|
365
|
+
{
|
|
366
|
+
name: 'sidebar.panellist',
|
|
367
|
+
id: 'dsh-just-chat',
|
|
368
|
+
order: 10,
|
|
369
|
+
label: () => locale.bind(localeNamespace)('entry'),
|
|
370
|
+
},
|
|
371
|
+
PanelIcon,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
ctx.slots.inject('sidebar.panellist', () => {
|
|
375
|
+
sidebarInjectionLive = true
|
|
376
|
+
sidebarRegistration = sidebarEnabled ? registerSidebarEntry() : undefined
|
|
377
|
+
return () => {
|
|
378
|
+
sidebarRegistration?.()
|
|
379
|
+
sidebarRegistration = undefined
|
|
380
|
+
sidebarInjectionLive = false
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
ctx.effect(() => {
|
|
385
|
+
const syncSidebar = () => {
|
|
386
|
+
const next = settingEnabled(entrySettings.getSnapshot(), 'sidebar')
|
|
387
|
+
if (next === sidebarEnabled) return
|
|
388
|
+
sidebarEnabled = next
|
|
389
|
+
if (!sidebarInjectionLive) return
|
|
390
|
+
sidebarRegistration?.()
|
|
391
|
+
sidebarRegistration = sidebarEnabled ? registerSidebarEntry() : undefined
|
|
392
|
+
}
|
|
393
|
+
const stop = entrySettings.subscribe(syncSidebar)
|
|
394
|
+
syncSidebar()
|
|
395
|
+
return stop
|
|
396
|
+
}, 'dsh-just-chat: sidebar entry setting')
|
|
397
|
+
|
|
398
|
+
ctx.slots.inject('shell.overlay', () => ctx.slots.register(
|
|
399
|
+
{
|
|
400
|
+
name: 'shell.overlay',
|
|
401
|
+
id: 'dsh-just-chat-hero',
|
|
402
|
+
order: 100,
|
|
403
|
+
},
|
|
404
|
+
JustChatOverlay,
|
|
405
|
+
))
|
|
406
|
+
|
|
407
|
+
ctx.slots.inject('main', () => ctx.slots.register(
|
|
408
|
+
{ name: 'main', key: 'dsh-just-chat' },
|
|
409
|
+
ChatPanel,
|
|
410
|
+
))
|
|
411
|
+
|
|
412
|
+
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
413
|
+
{ name: 'settings.plugin.item', key: settingsNamespace, locale: localeNamespace },
|
|
414
|
+
PluginSettingsCard,
|
|
415
|
+
))
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
exports.formatWorkspaceName = formatWorkspaceName
|
|
421
|
+
exports.apply = apply
|
|
422
|
+
exports.inject = inject
|
|
423
|
+
return module.exports
|
|
424
|
+
},
|
|
425
|
+
})
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# 开发指南
|
|
2
|
+
|
|
3
|
+
本页面向修改和调试插件的开发者。插件安装、功能与配置见 [README](../README.md)。
|
|
4
|
+
|
|
5
|
+
## 准备环境
|
|
6
|
+
|
|
7
|
+
开发脚本面向 Windows,要求系统已安装 Node.js(`^22.19.0 || >=24.0.0`)、npm、pnpm,并可使用 PowerShell。
|
|
8
|
+
|
|
9
|
+
从 GitHub 克隆或下载源码后,在仓库根目录运行:
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
npm run dev
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
脚本会依次:
|
|
16
|
+
|
|
17
|
+
1. 使用系统 pnpm 按锁文件安装依赖,包括项目内的 DSH `0.1.5-rc.2`。
|
|
18
|
+
2. 创建或复用仓库中的 `.dsh-dev`,作为调试用 `DSH_HOME`。
|
|
19
|
+
3. 将当前插件安装到该 Home 的 `web` profile,同时安装调试伴随插件 `@michengai/dsh-archive-manager@0.1.41`。
|
|
20
|
+
4. 使用项目内的 DSH 启动服务,在终端输出带登录 token 的本地访问链接。
|
|
21
|
+
|
|
22
|
+
打开终端链接,在 DSH 模型设置中配置凭据,即可点击“快速对话”调试。端口由系统分配,每次启动可能不同;不要公开带 token 的链接。结束调试时在启动终端按 `Ctrl+C`。
|
|
23
|
+
|
|
24
|
+
仅初始化、不启动服务:
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
npm run setup
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
重复运行会保留 `.dsh-dev` 中的对话、设置与凭据。脚本不依赖全局 DSH,不复制其他 Home 的凭据;Node/pnpm 使用系统原生环境,不重定位缓存。修改插件后先停止当前调试服务,再运行 `npm run dev`,打开新输出的链接。
|
|
31
|
+
|
|
32
|
+
### 安装本地源码到已有 DSH
|
|
33
|
+
|
|
34
|
+
若需要在指定的已有 DSH 环境中验证源码,在仓库根目录运行:
|
|
35
|
+
|
|
36
|
+
```powershell
|
|
37
|
+
pnpm install --frozen-lockfile
|
|
38
|
+
dsh plugin --profile web add .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
确认 `DSH_HOME` 和 profile 与目标环境一致,安装后重启目标 DSH 并刷新页面。本地安装引用当前目录,请保留该目录。日常开发优先使用上面的隔离调试环境。
|
|
42
|
+
|
|
43
|
+
## 业务验证
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
npm test
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
自动测试覆盖入口调用和命名规则。运行环境中还应检查:
|
|
50
|
+
|
|
51
|
+
- 从没有工作区的状态点击入口,能够进入对话并收到真实模型回复。
|
|
52
|
+
- 两个入口连续新建时使用不同目录;原生“新建会话”仍使用所在工作区的目录。
|
|
53
|
+
- 归档会话、删除工作区后,再次点击入口仍可对话;刷新后记录正常保留。
|
|
54
|
+
- 两个入口开关、模板预览与校验、保存/放弃、语言切换,以及刷新和重启后的配置保留。
|
|
55
|
+
- 窄窗口布局、附件、取消响应和断线恢复。
|
|
56
|
+
|
|
57
|
+
模型凭据与原生目录选择器由人工操作。清理测试数据前确认目标,保留非测试会话和文件。
|
|
58
|
+
|
|
59
|
+
### 归档管理伴随插件
|
|
60
|
+
|
|
61
|
+
开发初始化额外安装的归档管理插件在 **设置 → 归档会话** 中提供恢复和永久删除功能,用于验证快速对话与原生会话管理的兼容性。它不是快速对话插件的依赖。
|
|
62
|
+
|
|
63
|
+
**永久删除会话不可恢复**,但会保留工作目录。原生删除工作区也不删除磁盘目录;详细生命周期见 [README](../README.md#工作区与对话的关系)。
|
|
64
|
+
|
|
65
|
+
## 实现结构
|
|
66
|
+
|
|
67
|
+
仓库根目录就是可安装的插件 package root,无需单独构建。
|
|
68
|
+
|
|
69
|
+
| 文件 | 职责 |
|
|
70
|
+
| --- | --- |
|
|
71
|
+
| `index.js` | 注册设置、准备目录、调用原生 Workspace 服务 |
|
|
72
|
+
| `client.js` | 入口、原生 UI 组件、配置卡片和命名模板 |
|
|
73
|
+
| `cordis.patch.yml` | DSH bundle 的 Host 激活配置 |
|
|
74
|
+
| `scripts/install-dsh-dev.ps1` | 安装项目内 DSH 与调试插件 |
|
|
75
|
+
| `scripts/start-dsh-dev.ps1` | 初始化并启动隔离的调试环境 |
|
|
76
|
+
| `tests/` | 入口调用与命名规则测试 |
|
|
77
|
+
|
|
78
|
+
所有功能在插件中实现,不修改 DSH 宿主源码。首页入口使用 `conversation.hero.agentPreset` 的 DOM 锚点,侧栏和配置卡片使用官方插槽;升级 DSH 时需重新验证入口挂载和布局。
|
|
79
|
+
|
|
80
|
+
调试时生成的工作目录位于 `.dsh-dev/just-chat/sessions/`。Workspace 和 Session ID、归档和工作区删除均由 DSH 管理。
|
|
81
|
+
|
|
82
|
+
## 打包与版本控制
|
|
83
|
+
|
|
84
|
+
```powershell
|
|
85
|
+
npm pack --dry-run
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
检查发布清单包含插件运行文件、README、开发指南和配图,不含调试 Home。提交源码、锁文件和文档;`.gitignore` 排除调试 Home、依赖、临时输出及本地研究记录。不要提交 `.dsh-dev` 中的凭据与对话数据。
|
|
89
|
+
|
|
90
|
+
### GitHub 与 npm 发布准备
|
|
91
|
+
|
|
92
|
+
仓库提供以下自动化:
|
|
93
|
+
|
|
94
|
+
- `ci.yml`:main 推送和 PR 触发,在 Windows/Linux、Node 22.19/24 上按锁文件安装依赖,检查 JavaScript 语法、运行现有测试并预检发布包。
|
|
95
|
+
- `release.yml`:推送 `v*` tag 后先运行同一套 CI;tag 必须等于 `v` 加 package.json 的版本号。通过后发布 npm,随后创建带自动发布说明的 GitHub Release。含预发布后缀的版本使用 npm `next` 和 GitHub prerelease,其余使用 `latest`。
|
|
96
|
+
- Dependabot:每周检查 GitHub Actions 更新并创建更新 PR。
|
|
97
|
+
|
|
98
|
+
CI 不启动模型服务,真实对话和界面流程仍按上面的业务验证执行。
|
|
99
|
+
|
|
100
|
+
首次启用发布:
|
|
101
|
+
|
|
102
|
+
1. 将源码推送到实际 GitHub 仓库,完成下面的发布准备项。发布工作流会检查 repository.url 与当前仓库一致,并要求设置许可证。
|
|
103
|
+
2. 在 GitHub 创建 `npm` environment,限制仅发布 tag 可部署;按团队需要配置审批,并保护 main 和 `v*` tag。
|
|
104
|
+
3. 在 npm 包设置中添加 GitHub Actions Trusted Publisher,填写实际 owner/repository、工作流文件名 `release.yml`、environment `npm`,允许直接 `npm publish`。见 [npm Trusted Publishing 配置说明](https://docs.npmjs.com/trusted-publishers/)。如果新包尚不能配置 Trusted Publisher,先由维护者登录 npm 手动完成首次发布,再绑定后续自动发布。
|
|
105
|
+
4. 工作流使用 Node 24 和 OIDC,不需保存 NPM_TOKEN。npm CLI 需至少 11.5.1。
|
|
106
|
+
|
|
107
|
+
发布一个新版本(以下以 0.1.1 为例,工作区应先保持干净):
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
npm version 0.1.1
|
|
111
|
+
git push origin main
|
|
112
|
+
git push origin v0.1.1
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
`npm version` 会更新版本、提交并创建 tag;首次手动发布过的版本不要再次触发 npm 发布。npm 版本不可覆盖。若 npm 成功但 GitHub Release 失败,使用 Actions 的 **Re-run failed jobs**,只重试后续 Release job;不要重跑已经成功的 npm 发布 job。
|
|
116
|
+
|
|
117
|
+
GitHub 提供源码和开发文档,npm 提供可安装的 `dsh-just-chat` 插件包。发布前:
|
|
118
|
+
|
|
119
|
+
- 确认 npm 包名可用、版本号和发布权限,以及项目许可证。
|
|
120
|
+
- 在 `package.json` 中补齐实际 GitHub 地址对应的 `repository`、`homepage` 和 `bugs`。
|
|
121
|
+
- 将 README 配图和开发指南链接改为该 GitHub 仓库的绝对链接,并检查 GitHub 与 npm 页面都能正常展示;仅把图片打入 npm 包不能保证 npm 页面解析相对图片地址。
|
|
122
|
+
- 检查发布清单和业务流程,发布后在独立 DSH Home 中验证 README 的 npm 安装命令。
|
|
123
|
+
- 首次 npm 发布可用后,移除 README 中“发布后可用”的提示。
|
|
124
|
+
- npm 版本 badge 从注册表读取已发布版本,首次发布前可能显示无法获取;RED badge 使用 RED 项目提供的样式并链接到其 GitHub 仓库。
|
|
Binary file
|
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery'
|
|
2
|
+
import { mkdir, mkdtemp, rmdir } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
|
|
5
|
+
|
|
6
|
+
export const name = 'dsh-just-chat'
|
|
7
|
+
|
|
8
|
+
const SETTINGS_NAMESPACE = 'dsh-just-chat'
|
|
9
|
+
const SettingsSchema = z.object({
|
|
10
|
+
hero: z.boolean().default(true),
|
|
11
|
+
sidebar: z.boolean().default(true),
|
|
12
|
+
workspaceNameTemplate: z.string().min(1).max(200).default('{label} · {MM}-{DD} {HH}:{mm}'),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export function apply(ctx) {
|
|
16
|
+
ctx.inject(['settings'], (pluginCtx) => {
|
|
17
|
+
pluginCtx.settings.register(SETTINGS_NAMESPACE, SettingsSchema)
|
|
18
|
+
})
|
|
19
|
+
ctx.inject(['connection', 'workspaceRegistry'], (pluginCtx) => {
|
|
20
|
+
// The shared API channel owns authentication, origin checks, and route lifetime.
|
|
21
|
+
pluginCtx.connection.fetch.register({
|
|
22
|
+
path: '/api/just-chat/prepare',
|
|
23
|
+
methods: ['POST'],
|
|
24
|
+
requestBody: 'buffered',
|
|
25
|
+
async fetch(request) {
|
|
26
|
+
let path
|
|
27
|
+
try {
|
|
28
|
+
request.signal.throwIfAborted()
|
|
29
|
+
const { title } = await request.json()
|
|
30
|
+
if (typeof title !== 'string' || !title.trim() || title.length > 100) {
|
|
31
|
+
return Response.json({ error: 'Invalid workspace title' }, { status: 400 })
|
|
32
|
+
}
|
|
33
|
+
const root = dshHomePath('just-chat', 'sessions')
|
|
34
|
+
await mkdir(root, { recursive: true })
|
|
35
|
+
path = await mkdtemp(join(root, 'chat-'))
|
|
36
|
+
request.signal.throwIfAborted()
|
|
37
|
+
// Only the folder name is ours; DSH generates Workspace and Session IDs.
|
|
38
|
+
await pluginCtx.workspaceRegistry.create(path, title)
|
|
39
|
+
return Response.json({ path })
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// Undo only an empty directory that never acquired a Workspace.
|
|
42
|
+
if (path && !pluginCtx.workspaceRegistry.list().some((workspace) => workspace.path === path)) {
|
|
43
|
+
await rmdir(path).catch(() => {})
|
|
44
|
+
}
|
|
45
|
+
return Response.json({ error: error.message }, { status: 500 })
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-just-chat",
|
|
3
|
+
"description": "One-click native conversations with independent workspaces for DeepSeek Harness",
|
|
4
|
+
"homepage": "https://github.com/exoticknight/dsh-just-chat#readme",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/exoticknight/dsh-just-chat.git"
|
|
8
|
+
},
|
|
9
|
+
"bugs": {
|
|
10
|
+
"url": "https://github.com/exoticknight/dsh-just-chat/issues"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["dsh", "deepseek-harness", "dsh-plugin", "chat", "workspace"],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org/"
|
|
19
|
+
},
|
|
20
|
+
"version": "0.1.0",
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"author": "exoticknight",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"main": "./index.js",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./index.js",
|
|
27
|
+
"./client": "./client.js",
|
|
28
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"index.js",
|
|
33
|
+
"client.js",
|
|
34
|
+
"cordis.patch.yml",
|
|
35
|
+
"README.md",
|
|
36
|
+
"RED.md",
|
|
37
|
+
"docs/images",
|
|
38
|
+
"docs/development.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"setup": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\install-dsh-dev.ps1",
|
|
42
|
+
"test": "node --test \"tests/*.test.js\"",
|
|
43
|
+
"dev": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\start-dsh-dev.ps1",
|
|
44
|
+
"dev:install": "powershell -NoProfile -ExecutionPolicy Bypass -File .\\scripts\\install-dsh-dev.ps1"
|
|
45
|
+
},
|
|
46
|
+
"dsh": {
|
|
47
|
+
"bundle": {
|
|
48
|
+
"patch": "./cordis.patch.yml"
|
|
49
|
+
},
|
|
50
|
+
"client": {
|
|
51
|
+
"platform": "web",
|
|
52
|
+
"inject": [
|
|
53
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
54
|
+
"@deepseek-ai/dsh-api-workspace-controller",
|
|
55
|
+
"@deepseek-ai/dsh-client-locale",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
59
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
60
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"@deepseek-ai/dsh-home-paths": "0.1.5-rc.2",
|
|
67
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@deepseek-ai/dsh": "0.1.5-rc.2"
|
|
71
|
+
}
|
|
72
|
+
}
|