dsh-prompt-predict 0.0.0-stage → 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 +21 -0
- package/README.md +169 -2
- package/client/client.js +410 -0
- package/cordis.patch.yml +3 -0
- package/lib/config.mjs +28 -0
- package/lib/host-route.mjs +264 -0
- package/lib/index.js +16 -0
- package/lib/predict.mjs +130 -0
- package/package.json +51 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ferrisching
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,170 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-prompt-predict
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
|
+
<b>Codex 风格的输入框提示词联想</b><br />
|
|
5
|
+
助手回复结束后,把你的下一句话以灰色 ghost text 直接写进输入框 —— <b>Tab 采纳</b>、<b>Esc 忽略</b>、开始输入自动消失<br /><br />
|
|
6
|
+
<a href="https://github.com/ferrisching/dsh-prompt-predict/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/ferrisching/dsh-prompt-predict" /></a>
|
|
7
|
+
<img alt="version 0.1.0" src="https://img.shields.io/badge/version-0.1.0-blue" />
|
|
8
|
+
<img alt="已验证 DSH 0.2.0-rc.2" src="https://img.shields.io/badge/DSH-0.2.0--rc.2-4d6bfe" />
|
|
9
|
+
<img alt="零运行时依赖" src="https://img.shields.io/badge/%E4%BE%9D%E8%B5%96-%E9%9B%B6%E8%BF%90%E8%A1%8C%E6%97%B6%E4%BE%9D%E8%B5%96-41ba41" />
|
|
10
|
+
<img alt="客户端 + Host 双端插件" src="https://img.shields.io/badge/%E5%BD%A2%E6%80%81-Host%20%2B%20%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%8F%92%E4%BB%B6-8250df" />
|
|
11
|
+
<a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" /></a>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
`dsh-prompt-predict` 是一个 [DeepSeek Harness](https://www.deepseek.com/) 插件:每轮助手回复结束后,它根据当前会话预测你**下一句最可能输入什么**,并以灰色 ghost text 的形式浮在输入框里。整条建议不写进编辑器,只有按下 `Tab` 采纳时才真正插入(可撤销),因此误预测零成本。
|
|
15
|
+
|
|
16
|
+
## 📑 目录
|
|
17
|
+
|
|
18
|
+
- [✨ 功能一览](#-功能一览)
|
|
19
|
+
- [🚀 安装](#-安装)
|
|
20
|
+
- [⌨️ 使用](#-使用)
|
|
21
|
+
- [⚙️ 设置](#-设置)
|
|
22
|
+
- [🔌 HTTP 接口](#-http-接口)
|
|
23
|
+
- [🧩 工作原理](#-工作原理)
|
|
24
|
+
- [🛠️ 开发](#-开发)
|
|
25
|
+
- [🔐 安全与成本](#-安全与成本)
|
|
26
|
+
- [⚠️ 已知限制](#-已知限制)
|
|
27
|
+
- [📄 许可](#-许可)
|
|
28
|
+
|
|
29
|
+
## ✨ 功能一览
|
|
30
|
+
|
|
31
|
+
- **回复结束即出建议**:在输入框首字符处浮出灰色 ghost text,光标位置、字号、行高都对齐当前编辑器
|
|
32
|
+
- **三种交互**:`Tab` 采纳 · `Esc` 忽略 · 开始输入自动消失;采纳前建议不属于会话内容
|
|
33
|
+
- **轻量上下文**:只取最近若干条 user/assistant 的纯文本,工具结果、附件、系统提示一律不进辅助调用
|
|
34
|
+
- **默认跟随当前会话模型**:不写死任何提供商,联想调用沿用会话自身选用的模型;也可以在设置页固定到某个更便宜的模型
|
|
35
|
+
- **全部可调**:开关、提供商 / 模型、响应超时、上下文规模、思考深度,保存后立即生效,无需重启 DSH
|
|
36
|
+
- **零运行时依赖**:纯 ESM,Host 端与客户端都不引入第三方包
|
|
37
|
+
|
|
38
|
+
## 🚀 安装
|
|
39
|
+
|
|
40
|
+
### 从源码安装(推荐)
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/ferrisching/dsh-prompt-predict.git
|
|
44
|
+
cd dsh-prompt-predict
|
|
45
|
+
bash scripts/install.sh
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
脚本会现场打包本仓库、装进 profile 的 `node_modules`,并把 `dsh-prompt-predict` 接进 `dsh.profile.bundles`。默认目标是 `~/.dsh/profiles/desktop`,装别的 profile:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bash scripts/install.sh ~/.dsh/profiles/web
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**装完重启 DSH 生效**,然后在 **设置 → 提示词联想** 里能看到开关与各项参数。
|
|
55
|
+
|
|
56
|
+
### 手动安装
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
cd ~/.dsh/profiles/desktop
|
|
60
|
+
pnpm add /path/to/dsh-prompt-predict
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
再打开该 profile 的 `package.json`,把 `"dsh-prompt-predict"` 加进 `dsh.profile.bundles` 数组,重启 DSH。
|
|
64
|
+
|
|
65
|
+
> `pnpm add <仓库目录>` 装的是符号链接,指向你的工作区,适合边改边调;`scripts/install.sh` 装的是打包副本。
|
|
66
|
+
|
|
67
|
+
### 安装要求
|
|
68
|
+
|
|
69
|
+
- 已在 **DSH `0.2.0-rc.2`** 上验证(桌面端)
|
|
70
|
+
- `node` / `pnpm`:安装脚本优先使用 `~/.dsh/dsh-runtimes/*/dependencies` 下 DSH 自带的运行时,找不到才回退 `PATH`
|
|
71
|
+
- macOS / Linux;Windows 未验证
|
|
72
|
+
|
|
73
|
+
## ⌨️ 使用
|
|
74
|
+
|
|
75
|
+
1. 正常对话,等助手这一轮回复结束,且**输入框为空**
|
|
76
|
+
2. 约 350 ms 后,输入框里浮出一条灰色建议
|
|
77
|
+
3. 想用就按 `Tab`(建议此刻才插入输入框,可撤销);不想用按 `Esc`,或直接开始打字,建议自动消失
|
|
78
|
+
|
|
79
|
+
建议只在输入框为空、且当前会话不在运行中时出现;切换会话、中断回复都会清掉旧建议。
|
|
80
|
+
|
|
81
|
+
## ⚙️ 设置
|
|
82
|
+
|
|
83
|
+
**设置 → 提示词联想**:
|
|
84
|
+
|
|
85
|
+
| 设置项 | 说明 | 默认 |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| 启用联想 | 总开关 | 开 |
|
|
88
|
+
| 提供商 / 模型 | 联想调用使用的模型;选「跟随当前会话模型」则沿用会话自身模型 | 跟随当前会话模型 |
|
|
89
|
+
| 响应超时(高级) | 1–60 秒 | 12 秒 |
|
|
90
|
+
| 上下文规模(高级) | 少 3 条 / 标准 8 条 / 多 12 条 | 标准 8 条 |
|
|
91
|
+
| 思考深度(高级) | 选项**按所选模型动态生成**,模型不支持的档位不会出现 | 关闭(最快最省) |
|
|
92
|
+
|
|
93
|
+
- 保存后**立即生效**,无需重启 DSH
|
|
94
|
+
- 设置落盘在 `~/.dsh/dsh-prompt-predict/config.json`(权限 600)
|
|
95
|
+
- 优先级:**用户保存的设置 > cordis patch 部署默认 > 内置默认**
|
|
96
|
+
|
|
97
|
+
## 🔌 HTTP 接口
|
|
98
|
+
|
|
99
|
+
插件在 DSH 的 Web 服务上注册同源前缀路由 `/dsh-prompt-predict`,设置页与输入框联想都走它。所有接口都经 DSH 浏览器 Cookie 认证(`connection.requestRejection`),非本机浏览器调用会拿到 401/403。
|
|
100
|
+
|
|
101
|
+
| 方法 | 路径 | 说明 |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| GET | `/dsh-prompt-predict/status` | 当前会话模型、有效设置、已保存项、部署默认 |
|
|
104
|
+
| GET | `/dsh-prompt-predict/catalog` | 全部 provider 与模型(供下拉选择) |
|
|
105
|
+
| GET | `/dsh-prompt-predict/efforts?provider=&model=` | 该模型真实支持的思考档位 |
|
|
106
|
+
| POST | `/dsh-prompt-predict/settings` | 保存设置;`{ "reset": true }` 恢复默认 |
|
|
107
|
+
| POST | `/dsh-prompt-predict/suggest` | `{ sessionId }` → 一条建议 |
|
|
108
|
+
|
|
109
|
+
## 🧩 工作原理
|
|
110
|
+
|
|
111
|
+
**Host 端**(`lib/`)
|
|
112
|
+
|
|
113
|
+
1. `lib/index.js` 作为插件入口注册同源 HTTP 路由
|
|
114
|
+
2. 收到 `suggest` 后,`lib/predict.mjs` 从会话里裁出最近的 user/assistant 纯文本——按条数上限和字符预算从后往前取
|
|
115
|
+
3. 裁剪结果被**帧化成一条 request-only 的 user 输入**再发给辅助模型:DSH 的 Assistant Message 必须带完整 `stream` / `replayState`,不能拿裁剪片段伪装成助手消息(与 session-title 的做法一致)
|
|
116
|
+
4. 流式收回的文本经归一化(剥掉代码块包装、`建议:` 前缀、成对引号,合并换行)后返回给客户端
|
|
117
|
+
|
|
118
|
+
模型选择上,`provider` / `model` 为 `auto` 表示跟随会话;此时不向辅助调用写死路由,由 `predictNextPrompt` 继承会话自身选用的模型。
|
|
119
|
+
|
|
120
|
+
**客户端**(`client/`)
|
|
121
|
+
|
|
122
|
+
- 注入 `conversation.composer.dock`(输入框旁的生命周期挂载点)与 `settings.section`(设置页分区)两个 slot
|
|
123
|
+
- ghost text 是一层**覆盖层**,不写进 Lexical 编辑器;只有采纳时才通过 `inputActions.insertText` 真正插入
|
|
124
|
+
|
|
125
|
+
## 🛠️ 开发
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
git clone https://github.com/ferrisching/dsh-prompt-predict.git
|
|
129
|
+
cd dsh-prompt-predict
|
|
130
|
+
node test/predict.mjs # 31 项:文本裁剪、建议清洗、路由继承、设置校验、接口端到端
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
.
|
|
135
|
+
├── lib/ Host 端(纯 ESM,无构建步骤)
|
|
136
|
+
│ ├── index.js 插件入口,注册 HTTP 路由
|
|
137
|
+
│ ├── host-route.mjs 同源接口:状态 / 模型目录 / 思考档位 / 设置 / 建议
|
|
138
|
+
│ ├── predict.mjs 会话裁剪、调用辅助模型、建议归一化
|
|
139
|
+
│ └── config.mjs 用户设置持久化
|
|
140
|
+
├── client/
|
|
141
|
+
│ └── client.js 客户端:输入框 ghost text 覆盖层 + 设置页
|
|
142
|
+
├── test/
|
|
143
|
+
│ └── predict.mjs 31 项用例
|
|
144
|
+
├── scripts/
|
|
145
|
+
│ └── install.sh 一键安装 / 升级
|
|
146
|
+
├── cordis.patch.yml 插件挂载补丁
|
|
147
|
+
└── package.json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
测试不依赖 DSH 运行时:用假 ctx 与本地 HTTP server 覆盖路由注册、设置校验、路由继承与端到端调用。
|
|
151
|
+
|
|
152
|
+
## 🔐 安全与成本
|
|
153
|
+
|
|
154
|
+
- 每轮助手回复最多产生**一次**小额辅助调用,默认 `maxTokens: 64`
|
|
155
|
+
- 建议**不写入会话日志**;只有按 `Tab` 采纳并发送后才成为正式消息
|
|
156
|
+
- 只发送裁剪后的 user/assistant 纯文本,工具结果、附件、系统提示都不出本机
|
|
157
|
+
- 设置页与联想接口均走 DSH 自身的浏览器 Cookie 认证
|
|
158
|
+
|
|
159
|
+
## ⚠️ 已知限制
|
|
160
|
+
|
|
161
|
+
- 仅在 DSH `0.2.0-rc.2` 上验证过,其它版本未测
|
|
162
|
+
- 依赖宿主的 `conversation.composer.dock` 与 `settings.section` 两个 slot 名称,DSH 大版本调整后可能需要适配
|
|
163
|
+
- **尚未发布到 npm 官方源**,因此暂时无法通过 DSH 的「添加插件」向导安装,只能用上面的源码安装方式
|
|
164
|
+
- 联想会消耗所选模型的额度;固定到快速小模型比跟随推理模型更省
|
|
165
|
+
|
|
166
|
+
## 📄 许可
|
|
167
|
+
|
|
168
|
+
[MIT](LICENSE) © ferrisching
|
|
169
|
+
|
|
170
|
+
可以自由使用、修改、分发,包括闭源商业项目;只需保留版权声明与许可证文本。
|
package/client/client.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-prompt-predict",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
var React = require("react");
|
|
7
|
+
var h = React.createElement;
|
|
8
|
+
var useState = React.useState;
|
|
9
|
+
var useEffect = React.useEffect;
|
|
10
|
+
var useRef = React.useRef;
|
|
11
|
+
var useCallback = React.useCallback;
|
|
12
|
+
|
|
13
|
+
var name = "dsh-prompt-predict";
|
|
14
|
+
var inject = ["slots"];
|
|
15
|
+
|
|
16
|
+
var EFFORT_LABELS = {
|
|
17
|
+
off: "关闭", none: "关闭", minimal: "最小", low: "低", medium: "中", high: "高", max: "最大",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ---------- 设置页样式 ----------
|
|
21
|
+
var SS = {
|
|
22
|
+
card: { border: "1px solid var(--dsh-border, #e7e8ee)", borderRadius: 14, padding: "20px 22px", maxWidth: 560, background: "var(--dsh-bg, #fff)" },
|
|
23
|
+
title: { fontSize: 15, fontWeight: 600, color: "#111827", margin: "0 0 4px" },
|
|
24
|
+
muted: { color: "#6b7280", fontSize: 13, lineHeight: 1.7 },
|
|
25
|
+
row: { display: "flex", alignItems: "center", gap: 12, margin: "14px 0" },
|
|
26
|
+
label: { width: 110, fontSize: 13, color: "#374151", flex: "none" },
|
|
27
|
+
select: { flex: 1, padding: "7px 10px", border: "1px solid #d1d5db", borderRadius: 8, fontSize: 13, background: "#fff", maxWidth: 320 },
|
|
28
|
+
input: { width: 90, padding: "7px 10px", border: "1px solid #d1d5db", borderRadius: 8, fontSize: 13 },
|
|
29
|
+
btnPrimary: { padding: "8px 16px", border: 0, borderRadius: 8, background: "#4f6ef7", color: "#fff", cursor: "pointer", fontSize: 13 },
|
|
30
|
+
btnGhost: { padding: "8px 16px", border: "1px solid #e5e7eb", borderRadius: 8, background: "#fff", color: "#374151", cursor: "pointer", fontSize: 13 },
|
|
31
|
+
toggleTrack: function (on) { return { width: 38, height: 22, borderRadius: 22, border: 0, cursor: "pointer", padding: 2, background: on ? "#4f6ef7" : "#d1d5db", transition: "background .15s" }; },
|
|
32
|
+
toggleThumb: function (on) { return { display: "block", width: 18, height: 18, borderRadius: 18, background: "#fff", transform: on ? "translateX(16px)" : "translateX(0)", transition: "transform .15s" }; },
|
|
33
|
+
err: { color: "#dc2626", fontSize: 12, marginTop: 8 },
|
|
34
|
+
ok: { color: "#047857", fontSize: 12, marginTop: 8 },
|
|
35
|
+
divider: { borderTop: "1px solid #ececf1", margin: "16px 0" },
|
|
36
|
+
advBtn: { background: "none", border: 0, color: "#4f6ef7", fontSize: 13, cursor: "pointer", padding: 0 },
|
|
37
|
+
};
|
|
38
|
+
function dim(style, busy) { return busy ? Object.assign({}, style, { opacity: .55, cursor: "not-allowed" }) : style; }
|
|
39
|
+
|
|
40
|
+
function api(path, body) {
|
|
41
|
+
var controller = new AbortController();
|
|
42
|
+
var timer = setTimeout(function () { controller.abort(); }, 15000);
|
|
43
|
+
var opts = body === undefined ? {} : {
|
|
44
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body),
|
|
45
|
+
};
|
|
46
|
+
return fetch("./dsh-prompt-predict/" + path, Object.assign({}, opts, { signal: controller.signal }))
|
|
47
|
+
.then(function (r) { return r.json(); })
|
|
48
|
+
.finally(function () { clearTimeout(timer); });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function notifySettings(effective) {
|
|
52
|
+
window.dispatchEvent(new CustomEvent("dsh-prompt-predict:settings", { detail: effective }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findEditor(sessionId) {
|
|
56
|
+
var scoped = sessionId ? document.querySelector('[data-conversation-session="' + sessionId + '"] [data-composer-input="true"]') : null;
|
|
57
|
+
return scoped || document.querySelector('[data-composer-input="true"]');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 行内 ghost text 覆盖层:不写进 Lexical,只有 Tab/点击采纳时才 insertText。
|
|
61
|
+
function GhostOverlay(props) {
|
|
62
|
+
var suggestion = props.suggestion;
|
|
63
|
+
var sessionId = props.sessionId;
|
|
64
|
+
var onAccept = props.onAccept;
|
|
65
|
+
var _p = useState(null), pos = _p[0], setPos = _p[1];
|
|
66
|
+
|
|
67
|
+
var measure = useCallback(function () {
|
|
68
|
+
var ed = findEditor(sessionId);
|
|
69
|
+
if (!ed) { setPos(null); return; }
|
|
70
|
+
var r = ed.getBoundingClientRect();
|
|
71
|
+
var cs = getComputedStyle(ed);
|
|
72
|
+
setPos({
|
|
73
|
+
left: r.left + parseFloat(cs.paddingLeft || "0"),
|
|
74
|
+
top: r.top + parseFloat(cs.paddingTop || "0"),
|
|
75
|
+
fontFamily: cs.fontFamily,
|
|
76
|
+
fontSize: cs.fontSize,
|
|
77
|
+
lineHeight: cs.lineHeight,
|
|
78
|
+
fontWeight: cs.fontWeight,
|
|
79
|
+
});
|
|
80
|
+
}, [sessionId]);
|
|
81
|
+
|
|
82
|
+
useEffect(function () {
|
|
83
|
+
measure();
|
|
84
|
+
var ed = findEditor(sessionId);
|
|
85
|
+
var ro = null;
|
|
86
|
+
if (typeof ResizeObserver !== "undefined" && ed) { ro = new ResizeObserver(measure); ro.observe(ed); }
|
|
87
|
+
window.addEventListener("resize", measure, true);
|
|
88
|
+
window.addEventListener("scroll", measure, true);
|
|
89
|
+
return function () {
|
|
90
|
+
if (ro) ro.disconnect();
|
|
91
|
+
window.removeEventListener("resize", measure, true);
|
|
92
|
+
window.removeEventListener("scroll", measure, true);
|
|
93
|
+
};
|
|
94
|
+
}, [measure, suggestion]);
|
|
95
|
+
|
|
96
|
+
useEffect(function () {
|
|
97
|
+
var ed = findEditor(sessionId);
|
|
98
|
+
var ph = ed && ed.parentElement ? ed.parentElement.querySelector('[class*="placeholder"]') : null;
|
|
99
|
+
if (ph) ph.style.visibility = "hidden";
|
|
100
|
+
return function () { if (ph) ph.style.visibility = ""; };
|
|
101
|
+
}, [sessionId, suggestion]);
|
|
102
|
+
|
|
103
|
+
if (!pos) return null;
|
|
104
|
+
return h("div", {
|
|
105
|
+
role: "status",
|
|
106
|
+
"aria-live": "polite",
|
|
107
|
+
"data-prompt-predict-ghost": "true",
|
|
108
|
+
style: {
|
|
109
|
+
position: "fixed", left: pos.left, top: pos.top, zIndex: 60,
|
|
110
|
+
display: "flex", alignItems: "baseline", gap: 8, pointerEvents: "none",
|
|
111
|
+
fontFamily: pos.fontFamily, fontSize: pos.fontSize, lineHeight: pos.lineHeight, fontWeight: pos.fontWeight,
|
|
112
|
+
maxWidth: "min(70vw, 72ch)",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
h("span", {
|
|
116
|
+
style: { color: "#9aa3b2", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: "60ch" },
|
|
117
|
+
}, suggestion),
|
|
118
|
+
h("button", {
|
|
119
|
+
type: "button", onClick: onAccept,
|
|
120
|
+
title: "Tab 采纳 · Esc 忽略",
|
|
121
|
+
style: {
|
|
122
|
+
pointerEvents: "auto", flex: "none", padding: "0 7px", height: 19, borderRadius: 5,
|
|
123
|
+
border: "1px solid var(--dsw-alias-border-l1, #d1d5db)", background: "var(--dsw-alias-bg-secondary, #f8fafc)",
|
|
124
|
+
color: "#9aa3b2", fontSize: 10, fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace", cursor: "pointer",
|
|
125
|
+
},
|
|
126
|
+
}, "Tab"),
|
|
127
|
+
h("span", { style: { color: "#b0b7c3", fontSize: 10, whiteSpace: "nowrap" } }, "Esc 忽略"));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ================= 建议(composer dock 仅提供生命周期;显示覆盖到输入框内) =================
|
|
131
|
+
function PredictionDock(props) {
|
|
132
|
+
var sessionId = props.sessionId;
|
|
133
|
+
var useSession = props.useSession;
|
|
134
|
+
var useInput = props.useInput;
|
|
135
|
+
var inputActions = props.inputActions;
|
|
136
|
+
var running = useSession(function (value) { return Boolean(value && value.running); });
|
|
137
|
+
var blank = useSession(function (value) { return Boolean(value && value.blank); });
|
|
138
|
+
var removed = useSession(function (value) { return Boolean(value && value.removed); });
|
|
139
|
+
var draft = useInput(function (value) { return String(value && value.draft || ""); });
|
|
140
|
+
var _s = useState(""), suggestion = _s[0], setSuggestion = _s[1];
|
|
141
|
+
var _en = useState(true), enabled = _en[0], setEnabled = _en[1];
|
|
142
|
+
var armed = useRef(true);
|
|
143
|
+
var abortRef = useRef(null);
|
|
144
|
+
var sessionRef = useRef(sessionId);
|
|
145
|
+
var dismissedByDraft = useRef(false);
|
|
146
|
+
if (draft.trim() !== "") dismissedByDraft.current = true;
|
|
147
|
+
var visibleSuggestion = dismissedByDraft.current ? "" : suggestion;
|
|
148
|
+
|
|
149
|
+
// 首次读取启用状态;设置页保存后通过事件让当前会话立即生效。
|
|
150
|
+
useEffect(function () {
|
|
151
|
+
api("status").then(function (j) { setEnabled(!(j && j.effective && j.effective.enabled === false)); }).catch(function () { });
|
|
152
|
+
}, [sessionId]);
|
|
153
|
+
useEffect(function () {
|
|
154
|
+
var changed = function (event) {
|
|
155
|
+
var on = event.detail?.enabled !== false;
|
|
156
|
+
setEnabled(on);
|
|
157
|
+
if (!on) {
|
|
158
|
+
setSuggestion("");
|
|
159
|
+
if (abortRef.current) abortRef.current.abort();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
window.addEventListener("dsh-prompt-predict:settings", changed);
|
|
163
|
+
return function () { window.removeEventListener("dsh-prompt-predict:settings", changed); };
|
|
164
|
+
}, []);
|
|
165
|
+
|
|
166
|
+
useEffect(function () {
|
|
167
|
+
sessionRef.current = sessionId;
|
|
168
|
+
armed.current = true;
|
|
169
|
+
dismissedByDraft.current = false;
|
|
170
|
+
setSuggestion("");
|
|
171
|
+
if (abortRef.current) abortRef.current.abort();
|
|
172
|
+
}, [sessionId]);
|
|
173
|
+
|
|
174
|
+
useEffect(function () {
|
|
175
|
+
if (!enabled) return;
|
|
176
|
+
if (running) {
|
|
177
|
+
armed.current = true;
|
|
178
|
+
dismissedByDraft.current = false;
|
|
179
|
+
setSuggestion("");
|
|
180
|
+
if (abortRef.current) abortRef.current.abort();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!sessionId || blank || removed || !armed.current) return;
|
|
184
|
+
if (draft.trim() !== "") return;
|
|
185
|
+
armed.current = false;
|
|
186
|
+
var controller = new AbortController();
|
|
187
|
+
abortRef.current = controller;
|
|
188
|
+
var timer = setTimeout(function () {
|
|
189
|
+
api("suggest", { sessionId: sessionId })
|
|
190
|
+
.then(function (result) {
|
|
191
|
+
if (controller.signal.aborted || sessionRef.current !== sessionId) return;
|
|
192
|
+
if (result && result.ok && typeof result.suggestion === "string") setSuggestion(result.suggestion.trim());
|
|
193
|
+
})
|
|
194
|
+
.catch(function () { /* 预测失败静默,不打断正常输入 */ });
|
|
195
|
+
}, 350);
|
|
196
|
+
return function () {
|
|
197
|
+
clearTimeout(timer);
|
|
198
|
+
controller.abort();
|
|
199
|
+
if (abortRef.current === controller) abortRef.current = null;
|
|
200
|
+
};
|
|
201
|
+
}, [sessionId, running, blank, removed, draft, enabled]);
|
|
202
|
+
|
|
203
|
+
var accept = useCallback(function () {
|
|
204
|
+
if (!visibleSuggestion || !inputActions || draft.trim() !== "") return false;
|
|
205
|
+
try {
|
|
206
|
+
var span = inputActions.captureInsertion();
|
|
207
|
+
if (!inputActions.insertText(visibleSuggestion, span)) return false;
|
|
208
|
+
setSuggestion("");
|
|
209
|
+
return true;
|
|
210
|
+
} catch { return false; }
|
|
211
|
+
}, [visibleSuggestion, inputActions, draft]);
|
|
212
|
+
|
|
213
|
+
useEffect(function () {
|
|
214
|
+
if (!visibleSuggestion) return;
|
|
215
|
+
var onKey = function (event) {
|
|
216
|
+
if (event.isComposing) return;
|
|
217
|
+
if (event.key === "Escape") { event.preventDefault(); setSuggestion(""); return; }
|
|
218
|
+
if (event.key !== "Tab" || event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
219
|
+
if (!accept()) return;
|
|
220
|
+
event.preventDefault();
|
|
221
|
+
event.stopPropagation();
|
|
222
|
+
};
|
|
223
|
+
window.addEventListener("keydown", onKey, true);
|
|
224
|
+
return function () { window.removeEventListener("keydown", onKey, true); };
|
|
225
|
+
}, [visibleSuggestion, accept]);
|
|
226
|
+
|
|
227
|
+
// 建议直接以灰色 ghost text 覆盖在 Lexical 输入框首字符处。
|
|
228
|
+
if (!enabled || draft.trim() !== "" || !visibleSuggestion) return null;
|
|
229
|
+
return h(GhostOverlay, { suggestion: visibleSuggestion, sessionId: sessionId, onAccept: accept });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ================= 设置页 =================
|
|
233
|
+
function SettingsSection() {
|
|
234
|
+
var _st = useState(null), st = _st[0], setSt = _st[1];
|
|
235
|
+
var _cat = useState(null), cat = _cat[0], setCat = _cat[1];
|
|
236
|
+
var _pv = useState(""), provider = _pv[0], setProvider = _pv[1];
|
|
237
|
+
var _md = useState(""), model = _md[0], setModel = _md[1];
|
|
238
|
+
var _to = useState(12), timeoutSec = _to[0], setTimeoutSec = _to[1];
|
|
239
|
+
var _ctx = useState(8), ctxSize = _ctx[0], setCtxSize = _ctx[1];
|
|
240
|
+
var _ef = useState("off"), effort = _ef[0], setEffort = _ef[1];
|
|
241
|
+
var _eo = useState([]), effortOptions = _eo[0], setEffortOptions = _eo[1];
|
|
242
|
+
var _busy = useState(false), busy = _busy[0], setBusy = _busy[1];
|
|
243
|
+
var _msg = useState(null), msg = _msg[0], setMsg = _msg[1];
|
|
244
|
+
var _adv = useState(false), adv = _adv[0], setAdv = _adv[1];
|
|
245
|
+
var _dirty = useState(false), dirty = _dirty[0], setDirty = _dirty[1];
|
|
246
|
+
|
|
247
|
+
function loadAll() {
|
|
248
|
+
api("status").then(function (j) {
|
|
249
|
+
if (!j || !j.ok) return;
|
|
250
|
+
setSt(j);
|
|
251
|
+
var eff = j.effective || {};
|
|
252
|
+
setProvider(eff.provider || "");
|
|
253
|
+
setModel(eff.model || "");
|
|
254
|
+
setTimeoutSec(Math.round((eff.timeoutMs || 12000) / 1000));
|
|
255
|
+
setCtxSize(eff.maxMessages || 8);
|
|
256
|
+
setEffort(eff.reasoningEffort || "off");
|
|
257
|
+
setDirty(false);
|
|
258
|
+
}).catch(function () { });
|
|
259
|
+
api("catalog").then(function (j) { if (j && j.ok) setCat(j.groups || []); }).catch(function () { });
|
|
260
|
+
}
|
|
261
|
+
useEffect(function () { loadAll(); }, []);
|
|
262
|
+
|
|
263
|
+
// 思考档位跟随所选模型动态生成(模型不支持的档位不出现,避免保存被拒);
|
|
264
|
+
// 选「跟随当前会话模型」时,按当前会话实际使用的模型取档位。
|
|
265
|
+
useEffect(function () {
|
|
266
|
+
var p = provider, m = model;
|
|
267
|
+
if (p === "auto" || m === "auto") {
|
|
268
|
+
var cur = (st && st.current) || {};
|
|
269
|
+
p = cur.provider; m = cur.model;
|
|
270
|
+
}
|
|
271
|
+
if (!p || !m) { setEffortOptions([]); return; }
|
|
272
|
+
api("efforts?provider=" + encodeURIComponent(p) + "&model=" + encodeURIComponent(m))
|
|
273
|
+
.then(function (j) { setEffortOptions(j && j.ok && Array.isArray(j.efforts) ? j.efforts : []); })
|
|
274
|
+
.catch(function () { setEffortOptions([]); });
|
|
275
|
+
}, [provider, model, st]);
|
|
276
|
+
|
|
277
|
+
function providerGroups() { return (cat || []).filter(function (g) { return g.models && g.models.length; }); }
|
|
278
|
+
function modelsOf(p) {
|
|
279
|
+
var g = (cat || []).find(function (x) { return x.id === p; });
|
|
280
|
+
return g ? g.models : [];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function save() {
|
|
284
|
+
setBusy(true); setMsg(null);
|
|
285
|
+
api("settings", {
|
|
286
|
+
provider: provider, model: model,
|
|
287
|
+
timeoutMs: timeoutSec * 1000,
|
|
288
|
+
maxMessages: ctxSize,
|
|
289
|
+
reasoningEffort: effort,
|
|
290
|
+
}).then(function (j) {
|
|
291
|
+
if (j && j.ok) { notifySettings(j.effective); setMsg({ ok: true, text: "已保存,立即生效" }); setDirty(false); loadAll(); }
|
|
292
|
+
else setMsg({ ok: false, text: (j && j.reason) || "保存失败" });
|
|
293
|
+
}).catch(function (e) { setMsg({ ok: false, text: String(e) }); })
|
|
294
|
+
.finally(function () { setBusy(false); });
|
|
295
|
+
}
|
|
296
|
+
function toggleEnabled(on) {
|
|
297
|
+
setBusy(true); setMsg(null);
|
|
298
|
+
api("settings", { enabled: on }).then(function (j) {
|
|
299
|
+
if (j && j.ok) { notifySettings(j.effective); setSt(Object.assign({}, st, { effective: j.effective })); loadAll(); setMsg({ ok: true, text: on ? "已开启联想" : "已关闭联想" }); }
|
|
300
|
+
else setMsg({ ok: false, text: (j && j.reason) || "操作失败" });
|
|
301
|
+
}).catch(function (e) { setMsg({ ok: false, text: String(e) }); })
|
|
302
|
+
.finally(function () { setBusy(false); });
|
|
303
|
+
}
|
|
304
|
+
function resetDefaults() {
|
|
305
|
+
setBusy(true); setMsg(null);
|
|
306
|
+
api("settings", { reset: true }).then(function (j) {
|
|
307
|
+
if (j && j.ok) { notifySettings(j.effective); setMsg({ ok: true, text: "已恢复默认" }); loadAll(); }
|
|
308
|
+
}).finally(function () { setBusy(false); });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!st) return h("div", { style: SS.card }, h("p", { style: SS.muted }, "加载中…"));
|
|
312
|
+
var eff = st.effective || {};
|
|
313
|
+
var enabled = eff.enabled !== false;
|
|
314
|
+
|
|
315
|
+
return h("div", { style: SS.card },
|
|
316
|
+
h("p", { style: SS.title }, "提示词联想"),
|
|
317
|
+
h("p", { style: SS.muted }, "助手回复结束后,在输入框里直接给出灰色建议(Tab 采纳、Esc 忽略、开始输入自动消失)。"),
|
|
318
|
+
|
|
319
|
+
h("div", { style: SS.row },
|
|
320
|
+
h("span", { style: SS.label }, "启用联想"),
|
|
321
|
+
h("button", { type: "button", style: SS.toggleTrack(enabled), onClick: function () { toggleEnabled(!enabled); }, disabled: busy, "aria-pressed": enabled },
|
|
322
|
+
h("span", { style: SS.toggleThumb(enabled) })),
|
|
323
|
+
h("span", { style: SS.muted }, enabled ? "已开启" : "已关闭")),
|
|
324
|
+
|
|
325
|
+
h("div", { style: SS.divider }),
|
|
326
|
+
|
|
327
|
+
h("div", { style: SS.row },
|
|
328
|
+
h("span", { style: SS.label }, "提供商"),
|
|
329
|
+
h("select", {
|
|
330
|
+
style: SS.select, value: provider, disabled: !enabled || busy,
|
|
331
|
+
onChange: function (e) {
|
|
332
|
+
var p = e.target.value;
|
|
333
|
+
setProvider(p);
|
|
334
|
+
if (p === "auto") { setModel("auto"); }
|
|
335
|
+
else { var ms = modelsOf(p); setModel(ms.length ? ms[0].id : ""); }
|
|
336
|
+
setEffort("auto"); setDirty(true);
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
h("option", { value: "auto" }, "跟随当前会话模型"),
|
|
340
|
+
providerGroups().map(function (g) {
|
|
341
|
+
return h("option", { key: g.id, value: g.id }, g.name + "(" + g.id + ")");
|
|
342
|
+
}))),
|
|
343
|
+
h("div", { style: SS.row },
|
|
344
|
+
h("span", { style: SS.label }, "模型"),
|
|
345
|
+
h("select", {
|
|
346
|
+
style: SS.select, value: model, disabled: !enabled || busy || provider === "auto",
|
|
347
|
+
onChange: function (e) { setModel(e.target.value); setEffort("auto"); setDirty(true); },
|
|
348
|
+
},
|
|
349
|
+
provider === "auto"
|
|
350
|
+
? h("option", { value: "auto" }, "跟随当前会话模型")
|
|
351
|
+
: modelsOf(provider).map(function (m) {
|
|
352
|
+
return h("option", { key: m.id, value: m.id }, m.name + "(" + m.id + ")");
|
|
353
|
+
}))),
|
|
354
|
+
h("p", { style: SS.muted }, "默认跟随当前会话模型;也可固定到某个模型——选快模型更省额度。"),
|
|
355
|
+
|
|
356
|
+
h("p", null, h("button", { style: SS.advBtn, onClick: function () { setAdv(!adv); } }, (adv ? "▾" : "▸") + " 高级")),
|
|
357
|
+
adv ? h("div", null,
|
|
358
|
+
h("div", { style: SS.row },
|
|
359
|
+
h("span", { style: SS.label }, "响应超时"),
|
|
360
|
+
h("input", { style: SS.input, type: "number", min: 1, max: 60, value: timeoutSec, disabled: !enabled || busy, onChange: function (e) { setTimeoutSec(Number(e.target.value)); setDirty(true); } }),
|
|
361
|
+
h("span", { style: SS.muted }, "秒(1–60)")),
|
|
362
|
+
h("div", { style: SS.row },
|
|
363
|
+
h("span", { style: SS.label }, "上下文规模"),
|
|
364
|
+
h("select", { style: SS.select, value: ctxSize, disabled: !enabled || busy, onChange: function (e) { setCtxSize(Number(e.target.value)); setDirty(true); } },
|
|
365
|
+
h("option", { value: 3 }, "少:最近 3 条消息"),
|
|
366
|
+
h("option", { value: 8 }, "标准:最近 8 条(默认)"),
|
|
367
|
+
h("option", { value: 12 }, "多:最近 12 条")),
|
|
368
|
+
h("span", { style: SS.muted }, "越多越准但越慢")),
|
|
369
|
+
h("div", { style: SS.row },
|
|
370
|
+
h("span", { style: SS.label }, "思考深度"),
|
|
371
|
+
h("select", { style: SS.select, value: effort, disabled: !enabled || busy, onChange: function (e) { setEffort(e.target.value); setDirty(true); } },
|
|
372
|
+
h("option", { value: "auto" }, effortOptions.length === 0 ? "跟随模型默认(该模型未公布可调档位)" : "跟随模型默认"),
|
|
373
|
+
(function () {
|
|
374
|
+
var ids = effortOptions.slice();
|
|
375
|
+
if (effort && effort !== "auto" && ids.indexOf(effort) === -1) ids.push(effort); // 保留当前已保存值,避免下拉显示错位
|
|
376
|
+
return ids.map(function (id) {
|
|
377
|
+
return h("option", { key: id, value: id }, (EFFORT_LABELS[id] || id) + (id === "off" ? "(最快最省)" : ""));
|
|
378
|
+
});
|
|
379
|
+
})()),
|
|
380
|
+
h("span", { style: SS.muted }, "联想宜浅;过深会把 token 烧在推理上")) ) : null,
|
|
381
|
+
|
|
382
|
+
h("div", { style: { display: "flex", gap: 10, marginTop: 16 } },
|
|
383
|
+
h("button", { style: dim(SS.btnPrimary, busy || !dirty), onClick: save, disabled: busy || !dirty }, busy ? "保存中…" : "保存"),
|
|
384
|
+
h("button", { style: dim(SS.btnGhost, busy), onClick: resetDefaults, disabled: busy }, "恢复默认")),
|
|
385
|
+
msg ? h("p", { style: msg.ok ? SS.ok : SS.err }, msg.text) : null);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function apply(ctx) {
|
|
389
|
+
ctx.slots.inject("conversation.composer.dock", function () {
|
|
390
|
+
return ctx.slots.register({
|
|
391
|
+
name: "conversation.composer.dock",
|
|
392
|
+
id: "prompt-prediction",
|
|
393
|
+
order: 10,
|
|
394
|
+
}, PredictionDock);
|
|
395
|
+
});
|
|
396
|
+
ctx.slots.inject("settings.section", function () {
|
|
397
|
+
return ctx.slots.register({
|
|
398
|
+
name: "settings.section",
|
|
399
|
+
id: "prompt-predict",
|
|
400
|
+
order: 2,
|
|
401
|
+
label: function () { return "提示词联想"; },
|
|
402
|
+
inject: function () { return {}; },
|
|
403
|
+
}, SettingsSection);
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
module.exports = { name: name, inject: inject, apply: apply };
|
|
408
|
+
return module.exports;
|
|
409
|
+
},
|
|
410
|
+
});
|
package/cordis.patch.yml
ADDED
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// 用户设置持久化(~/.dsh/dsh-prompt-predict/config.json,600)。
|
|
2
|
+
// 只存用户覆盖的字段;未覆盖的走 cordis patch 部署默认 → 内置默认。
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
export function configFilePath() {
|
|
8
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'dsh-prompt-predict', 'config.json');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function readSettings() {
|
|
12
|
+
try {
|
|
13
|
+
const raw = JSON.parse(readFileSync(configFilePath(), 'utf8'));
|
|
14
|
+
return raw && typeof raw === 'object' ? raw : {};
|
|
15
|
+
} catch { return {}; }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function writeSettings(patch) {
|
|
19
|
+
const next = { ...readSettings(), ...patch };
|
|
20
|
+
for (const key of Object.keys(next)) if (next[key] === undefined || next[key] === null) delete next[key];
|
|
21
|
+
mkdirSync(dirname(configFilePath()), { recursive: true, mode: 0o700 });
|
|
22
|
+
writeFileSync(configFilePath(), JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
23
|
+
return next;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function clearSettings() {
|
|
27
|
+
try { rmSync(configFilePath()); } catch { /* 不存在 */ }
|
|
28
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// 同源 HTTP 接口:提示词预测 + 设置页(启用开关、模型选择、高级参数)。
|
|
2
|
+
import { predictNextPrompt } from './predict.mjs';
|
|
3
|
+
import { readSettings, writeSettings, clearSettings } from './config.mjs';
|
|
4
|
+
|
|
5
|
+
const MAX_BODY = 16 * 1024;
|
|
6
|
+
|
|
7
|
+
function readJson(req) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
let raw = '';
|
|
10
|
+
let tooBig = false;
|
|
11
|
+
req.on('data', (chunk) => {
|
|
12
|
+
if (tooBig) return;
|
|
13
|
+
raw += chunk;
|
|
14
|
+
if (raw.length > MAX_BODY) { tooBig = true; raw = ''; }
|
|
15
|
+
});
|
|
16
|
+
req.on('end', () => {
|
|
17
|
+
if (tooBig) return resolve(null);
|
|
18
|
+
try { resolve(raw ? JSON.parse(raw) : {}); } catch { resolve(null); }
|
|
19
|
+
});
|
|
20
|
+
req.on('error', () => resolve(null));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function sendJson(res, status, value) {
|
|
25
|
+
if (res.writableEnded) return;
|
|
26
|
+
const body = JSON.stringify(value);
|
|
27
|
+
res.writeHead(status, {
|
|
28
|
+
'content-type': 'application/json; charset=utf-8',
|
|
29
|
+
'content-length': Buffer.byteLength(body),
|
|
30
|
+
'cache-control': 'no-store',
|
|
31
|
+
});
|
|
32
|
+
res.end(body);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function registerPredictRoute(ctx, cordisConfig = {}) {
|
|
36
|
+
const webServer = ctx?.webServer;
|
|
37
|
+
const connection = ctx?.connection;
|
|
38
|
+
if (!webServer || typeof webServer.register !== 'function') return null;
|
|
39
|
+
|
|
40
|
+
// provider/model 为 'auto' 表示「跟随当前会话模型」:不向辅助调用写死路由,
|
|
41
|
+
// 由 predictNextPrompt 继承会话自身选用的模型。
|
|
42
|
+
const AUTO = 'auto';
|
|
43
|
+
const isAuto = (value) => !value || value === AUTO;
|
|
44
|
+
const routeOf = (provider, model) => (isAuto(provider) || isAuto(model) ? undefined : { provider, model });
|
|
45
|
+
|
|
46
|
+
// 内置默认(可被 cordis patch 部署默认覆盖,再被用户保存的设置覆盖)
|
|
47
|
+
const builtinDefaults = {
|
|
48
|
+
enabled: true,
|
|
49
|
+
provider: AUTO,
|
|
50
|
+
model: AUTO,
|
|
51
|
+
timeoutMs: 12000,
|
|
52
|
+
maxTokens: 64,
|
|
53
|
+
maxMessages: 8,
|
|
54
|
+
maxChars: 6000,
|
|
55
|
+
reasoningEffort: 'off',
|
|
56
|
+
};
|
|
57
|
+
const deployDefaults = { ...builtinDefaults, ...cordisConfig };
|
|
58
|
+
|
|
59
|
+
const currentSelection = () => {
|
|
60
|
+
try { return ctx.agentDefaultModel.currentSelection() ?? {}; } catch { return {}; }
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** 有效设置 = 部署默认 + 用户保存的覆盖(每次请求时读取,改设置立即生效)。 */
|
|
64
|
+
const effective = () => ({ ...deployDefaults, ...readSettings() });
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
async function catalogGroups() {
|
|
68
|
+
const providers = ctx.llm.listProviders();
|
|
69
|
+
return Promise.all(providers.map(async (provider) => {
|
|
70
|
+
try {
|
|
71
|
+
const models = await ctx.llm.listModels(provider.id);
|
|
72
|
+
return { id: provider.id, name: provider.name, models: models.map((m) => ({ id: m.id, name: m.name })) };
|
|
73
|
+
} catch (error) {
|
|
74
|
+
return { id: provider.id, name: provider.name, models: [], error: String(error?.message ?? error) };
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function validateRoute(provider, model) {
|
|
80
|
+
try {
|
|
81
|
+
const models = await ctx.llm.listModels(provider);
|
|
82
|
+
if (!models.some((m) => m.id === model)) return `provider "${provider}" 没有模型 "${model}"`;
|
|
83
|
+
return null;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
return `provider "${provider}" 不可用:${String(error?.message ?? error)}`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function validateEffort(provider, model, effort) {
|
|
90
|
+
if (!['off', 'low', 'high', 'max'].includes(effort)) return '不支持的思考深度';
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
let timer;
|
|
93
|
+
try {
|
|
94
|
+
const info = await Promise.race([
|
|
95
|
+
ctx.llm.resolveModelInfo(provider, model, controller.signal),
|
|
96
|
+
new Promise((_, reject) => {
|
|
97
|
+
timer = setTimeout(() => { controller.abort(); reject(new Error('模型能力查询超时')); }, 3500);
|
|
98
|
+
}),
|
|
99
|
+
]);
|
|
100
|
+
const efforts = info?.reasoning?.efforts?.map((e) => e.id) ?? [];
|
|
101
|
+
if (efforts.length === 0) return `模型 "${model}" 未公布可调思考深度,请选「跟随模型默认」`;
|
|
102
|
+
if (!efforts.includes(effort)) return `模型 "${model}" 不支持思考深度 "${effort}"(可选:${efforts.join('、')})`;
|
|
103
|
+
return null;
|
|
104
|
+
} catch {
|
|
105
|
+
return '暂时无法确认模型支持的思考深度,请选「跟随模型默认」后保存';
|
|
106
|
+
} finally {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const route = {
|
|
112
|
+
kind: 'prefix',
|
|
113
|
+
path: '/dsh-prompt-predict',
|
|
114
|
+
handler: async (req, res) => {
|
|
115
|
+
let rejection;
|
|
116
|
+
if (typeof connection?.requestRejection === 'function') {
|
|
117
|
+
try { rejection = connection.requestRejection(req); } catch { rejection = 403; }
|
|
118
|
+
} else {
|
|
119
|
+
rejection = 403;
|
|
120
|
+
}
|
|
121
|
+
if (rejection !== undefined) {
|
|
122
|
+
sendJson(res, rejection, { ok: false, reason: rejection === 401 ? 'unauthorized' : 'forbidden' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const path = new URL(req.url ?? '/', 'http://x').pathname;
|
|
127
|
+
|
|
128
|
+
if (path === '/dsh-prompt-predict/status' && req.method === 'GET') {
|
|
129
|
+
const current = currentSelection();
|
|
130
|
+
sendJson(res, 200, {
|
|
131
|
+
ok: true,
|
|
132
|
+
current: { provider: current.provider ?? null, model: current.model ?? null },
|
|
133
|
+
effective: effective(),
|
|
134
|
+
saved: readSettings(),
|
|
135
|
+
defaults: deployDefaults,
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (path === '/dsh-prompt-predict/catalog' && req.method === 'GET') {
|
|
141
|
+
sendJson(res, 200, { ok: true, groups: await catalogGroups() });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 某个 provider/model 实际支持的思考档位(设置页据此动态生成下拉,避免出现无效选项)
|
|
146
|
+
if (path === '/dsh-prompt-predict/efforts' && req.method === 'GET') {
|
|
147
|
+
const url = new URL(req.url ?? '/', 'http://x');
|
|
148
|
+
const provider = url.searchParams.get('provider') ?? '';
|
|
149
|
+
const model = url.searchParams.get('model') ?? '';
|
|
150
|
+
if (!provider || !model) { sendJson(res, 400, { ok: false, reason: '缺少 provider 或 model' }); return; }
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
let timer;
|
|
153
|
+
try {
|
|
154
|
+
const info = await Promise.race([
|
|
155
|
+
ctx.llm.resolveModelInfo(provider, model, controller.signal),
|
|
156
|
+
new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new Error('timeout')); }, 3500); }),
|
|
157
|
+
]);
|
|
158
|
+
const efforts = info?.reasoning?.efforts?.map((e) => e.id) ?? [];
|
|
159
|
+
sendJson(res, 200, { ok: true, efforts, default: info?.reasoning?.defaultEffort ?? null });
|
|
160
|
+
} catch {
|
|
161
|
+
sendJson(res, 200, { ok: false, reason: '暂时无法获取该模型支持的思考深度' });
|
|
162
|
+
} finally {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (path === '/dsh-prompt-predict/settings' && req.method === 'POST') {
|
|
169
|
+
const body = await readJson(req);
|
|
170
|
+
if (!body) { sendJson(res, 400, { ok: false, reason: '请求体不是合法 JSON' }); return; }
|
|
171
|
+
if (body.reset === true) {
|
|
172
|
+
clearSettings();
|
|
173
|
+
sendJson(res, 200, { ok: true, effective: effective(), saved: {} });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const patch = {};
|
|
177
|
+
if (typeof body.enabled === 'boolean') patch.enabled = body.enabled;
|
|
178
|
+
if (typeof body.provider === 'string' && typeof body.model === 'string' && body.provider && body.model) {
|
|
179
|
+
if (isAuto(body.provider) || isAuto(body.model)) {
|
|
180
|
+
patch.provider = AUTO;
|
|
181
|
+
patch.model = AUTO;
|
|
182
|
+
} else {
|
|
183
|
+
const problem = await validateRoute(body.provider, body.model);
|
|
184
|
+
if (problem) { sendJson(res, 200, { ok: false, reason: problem }); return; }
|
|
185
|
+
patch.provider = body.provider;
|
|
186
|
+
patch.model = body.model;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (body.timeoutMs !== undefined) {
|
|
190
|
+
const v = Number(body.timeoutMs);
|
|
191
|
+
if (!Number.isSafeInteger(v) || v < 1000 || v > 60000) { sendJson(res, 200, { ok: false, reason: '超时需在 1–60 秒之间' }); return; }
|
|
192
|
+
patch.timeoutMs = v;
|
|
193
|
+
}
|
|
194
|
+
if (body.maxMessages !== undefined) {
|
|
195
|
+
const v = Number(body.maxMessages);
|
|
196
|
+
if (!Number.isSafeInteger(v) || v < 2 || v > 20) { sendJson(res, 200, { ok: false, reason: '上下文条数需在 2–20 之间' }); return; }
|
|
197
|
+
patch.maxMessages = v;
|
|
198
|
+
}
|
|
199
|
+
if (body.reasoningEffort !== undefined) {
|
|
200
|
+
const v = String(body.reasoningEffort);
|
|
201
|
+
if (v === 'auto') { patch.reasoningEffort = 'auto'; }
|
|
202
|
+
else {
|
|
203
|
+
const next = { ...effective(), ...patch };
|
|
204
|
+
// 跟随当前会话模型时模型不固定,无法按具体模型校验档位,直接按用户选择保存。
|
|
205
|
+
const pinned = routeOf(next.provider, next.model);
|
|
206
|
+
const problem = pinned ? await validateEffort(pinned.provider, pinned.model, v) : null;
|
|
207
|
+
if (problem) { sendJson(res, 200, { ok: false, reason: problem }); return; }
|
|
208
|
+
patch.reasoningEffort = v;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
writeSettings(patch);
|
|
212
|
+
sendJson(res, 200, { ok: true, effective: effective(), saved: readSettings() });
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (path !== '/dsh-prompt-predict/suggest' || req.method !== 'POST') {
|
|
217
|
+
sendJson(res, 404, { ok: false, reason: 'not found' });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const eff = effective();
|
|
222
|
+
if (eff.enabled === false) {
|
|
223
|
+
sendJson(res, 200, { ok: false, disabled: true, reason: '提示词联想已关闭' });
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const body = await readJson(req);
|
|
228
|
+
if (!body || typeof body.sessionId !== 'string' || body.sessionId.trim() === '') {
|
|
229
|
+
sendJson(res, 400, { ok: false, reason: '缺少 sessionId' });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const session = ctx.sessions.get(body.sessionId);
|
|
233
|
+
if (!session) {
|
|
234
|
+
sendJson(res, 404, { ok: false, reason: 'Session 不存在' });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const abort = new AbortController();
|
|
239
|
+
const timeout = setTimeout(() => abort.abort('prediction timeout'), Number(eff.timeoutMs) || 12000);
|
|
240
|
+
const onClose = () => { if (!res.writableEnded) abort.abort('client disconnected'); };
|
|
241
|
+
res.on('close', onClose);
|
|
242
|
+
try {
|
|
243
|
+
const result = await predictNextPrompt(ctx, session, {
|
|
244
|
+
signal: abort.signal,
|
|
245
|
+
maxTokens: Number(eff.maxTokens) || 64,
|
|
246
|
+
maxMessages: Number(eff.maxMessages) || 8,
|
|
247
|
+
maxChars: Number(eff.maxChars) || 6000,
|
|
248
|
+
route: (typeof body.provider === 'string' && typeof body.model === 'string' && body.provider && body.model)
|
|
249
|
+
? routeOf(body.provider, body.model)
|
|
250
|
+
: routeOf(eff.provider, eff.model),
|
|
251
|
+
reasoningEffort: typeof body.reasoningEffort === 'string' ? body.reasoningEffort
|
|
252
|
+
: (eff.reasoningEffort && eff.reasoningEffort !== 'auto' ? eff.reasoningEffort : undefined),
|
|
253
|
+
});
|
|
254
|
+
sendJson(res, 200, { ok: true, suggestion: result.suggestion, provider: result.route.provider, model: result.route.model });
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (!res.writableEnded) sendJson(res, 200, { ok: false, reason: String(error?.message ?? error) });
|
|
257
|
+
} finally {
|
|
258
|
+
clearTimeout(timeout);
|
|
259
|
+
res.off('close', onClose);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
return webServer.register(route);
|
|
264
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// dsh-prompt-predict Host 插件入口。
|
|
2
|
+
import { registerPredictRoute } from './host-route.mjs';
|
|
3
|
+
|
|
4
|
+
const name = 'dsh-prompt-predict';
|
|
5
|
+
const inject = ['sessions', 'llm', 'agentDefaultModel', 'connection', 'webServer'];
|
|
6
|
+
|
|
7
|
+
export function apply(ctx, config = {}) {
|
|
8
|
+
// cordis patch 的 config 作为部署默认;用户保存的设置(文件)在每次请求时再覆盖。
|
|
9
|
+
// 内置兜底(provider/model/超时等)由 host-route 负责。
|
|
10
|
+
const dispose = registerPredictRoute(ctx, config);
|
|
11
|
+
ctx?.effect?.(() => () => {
|
|
12
|
+
try { dispose?.(); } catch { /* 忽略 */ }
|
|
13
|
+
}, 'dsh-prompt-predict: unregister HTTP route');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export { name, inject };
|
package/lib/predict.mjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// 提示词预测核心:会话裁剪 → 调当前会话模型 → 归一化为一条可直接发送的建议。
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_SYSTEM = `你是输入框预测器。根据当前对话,预测用户下一条最可能发给助手的消息。
|
|
5
|
+
规则:
|
|
6
|
+
1. 只输出一条可直接发送的消息,不要解释、不要加引号、不要加“建议:”等前缀。
|
|
7
|
+
2. 使用用户近期主要语言;中文通常 8–50 个字,其他语言保持同等简洁。
|
|
8
|
+
3. 建议必须具体、自然、能推进当前任务;优先延续助手刚完成或刚提出的下一步。
|
|
9
|
+
4. 不要复述助手最后一句,不要编造用户未表达的目标,不要输出 Markdown 代码块。`;
|
|
10
|
+
|
|
11
|
+
/** 从消息 content 里只抽取普通文本。 */
|
|
12
|
+
export function messageText(message) {
|
|
13
|
+
if (!message || !Array.isArray(message.content)) return '';
|
|
14
|
+
return message.content
|
|
15
|
+
.filter((block) => block?.type === 'text' && typeof block.text === 'string')
|
|
16
|
+
.map((block) => block.text.trim())
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join('\n')
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 裁出最近的用户/助手文本消息,按总字符预算从后往前保留。
|
|
24
|
+
* 工具、系统、developer、图片和文件块不进入辅助模型请求。
|
|
25
|
+
*/
|
|
26
|
+
export function recentMessages(messages, { maxMessages = 8, maxChars = 6000, perMessageChars = 1600 } = {}) {
|
|
27
|
+
const candidates = [];
|
|
28
|
+
for (const message of Array.isArray(messages) ? messages : []) {
|
|
29
|
+
if (message?.role !== 'user' && message?.role !== 'assistant') continue;
|
|
30
|
+
const text = messageText(message);
|
|
31
|
+
if (!text) continue;
|
|
32
|
+
candidates.push({ role: message.role, text: text.slice(-perMessageChars) });
|
|
33
|
+
}
|
|
34
|
+
const kept = [];
|
|
35
|
+
let used = 0;
|
|
36
|
+
for (let i = candidates.length - 1; i >= 0 && kept.length < maxMessages; i -= 1) {
|
|
37
|
+
const item = candidates[i];
|
|
38
|
+
const cost = item.text.length + 32;
|
|
39
|
+
if (kept.length > 0 && used + cost > maxChars) break;
|
|
40
|
+
kept.unshift(item);
|
|
41
|
+
used += cost;
|
|
42
|
+
}
|
|
43
|
+
return kept;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 清掉模型常见包装,只留下单行建议。 */
|
|
47
|
+
export function normalizeSuggestion(input, maxChars = 180) {
|
|
48
|
+
let text = String(input ?? '').trim();
|
|
49
|
+
text = text.replace(/^```[a-z0-9_-]*\s*/i, '').replace(/\s*```$/i, '').trim();
|
|
50
|
+
text = text.replace(/^(?:建议|预测|下一条(?:消息|提示词)|用户(?:可能会说)?)[::]\s*/i, '');
|
|
51
|
+
text = text.replace(/^["'“‘]+|["'”’]+$/g, '').trim();
|
|
52
|
+
text = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).join(' ');
|
|
53
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
54
|
+
if (text.length > maxChars) text = text.slice(0, maxChars).trim();
|
|
55
|
+
return text;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function selectionFor(ctx, session) {
|
|
59
|
+
try {
|
|
60
|
+
const header = session?.requestHeader?.();
|
|
61
|
+
const config = header?.config;
|
|
62
|
+
if (typeof config?.provider === 'string' && typeof config?.model === 'string') {
|
|
63
|
+
return { provider: config.provider, model: config.model };
|
|
64
|
+
}
|
|
65
|
+
} catch { /* 回退到全局默认 */ }
|
|
66
|
+
return ctx.agentDefaultModel.currentSelection();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 生成一条建议。 */
|
|
70
|
+
export async function predictNextPrompt(ctx, session, {
|
|
71
|
+
signal,
|
|
72
|
+
maxTokens = 64,
|
|
73
|
+
maxMessages = 8,
|
|
74
|
+
maxChars = 6000,
|
|
75
|
+
system = DEFAULT_SYSTEM,
|
|
76
|
+
route: routeOverride,
|
|
77
|
+
reasoningEffort,
|
|
78
|
+
} = {}) {
|
|
79
|
+
const history = recentMessages(session?.deriveMessages?.() ?? [], { maxMessages, maxChars });
|
|
80
|
+
if (history.length === 0) throw new Error('没有可用于预测的会话文本');
|
|
81
|
+
const route = routeOverride ?? selectionFor(ctx, session);
|
|
82
|
+
if (typeof route?.provider !== 'string' || typeof route?.model !== 'string') {
|
|
83
|
+
throw new Error('无法确定联想所用模型,请在「设置 → 提示词联想」里显式选择提供商与模型');
|
|
84
|
+
}
|
|
85
|
+
const framedHistory = history.map((item) => {
|
|
86
|
+
const label = item.role === 'user' ? '用户' : '助手';
|
|
87
|
+
return `[${label}]\n${item.text}`;
|
|
88
|
+
}).join('\n\n');
|
|
89
|
+
// 不能把裁剪后的 assistant 文本伪装成 canonical Assistant Message:DSH 的 Assistant Message
|
|
90
|
+
// 必须带完整 stream/replayState。像 session-title 一样帧化成一条 request-only user input。
|
|
91
|
+
const messages = [{
|
|
92
|
+
id: randomUUID(),
|
|
93
|
+
role: 'user',
|
|
94
|
+
source: { kind: 'dsh-prompt-predict' },
|
|
95
|
+
content: [{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: `以下是当前会话最近内容:\n\n${framedHistory}\n\n请预测我下一条最可能输入的消息。只输出预测内容。`,
|
|
98
|
+
}],
|
|
99
|
+
}];
|
|
100
|
+
let output = '';
|
|
101
|
+
let reasoning = '';
|
|
102
|
+
let finish;
|
|
103
|
+
const options = {
|
|
104
|
+
provider: route.provider,
|
|
105
|
+
model: route.model,
|
|
106
|
+
messages,
|
|
107
|
+
system,
|
|
108
|
+
maxTokens,
|
|
109
|
+
sessionId: session.id,
|
|
110
|
+
purpose: 'prompt-prediction',
|
|
111
|
+
signal,
|
|
112
|
+
};
|
|
113
|
+
// 思考深度:仅在显式设置时传入;联想默认应保持最浅以保证速度/成本。
|
|
114
|
+
if (reasoningEffort) options.reasoningEffort = reasoningEffort;
|
|
115
|
+
for await (const chunk of ctx.llm.stream(options)) {
|
|
116
|
+
if (chunk?.type === 'text-delta' && typeof chunk.text === 'string') output += chunk.text;
|
|
117
|
+
if (chunk?.type === 'reasoning-delta' && typeof chunk.text === 'string') reasoning += chunk.text;
|
|
118
|
+
if (chunk?.type === 'finish') finish = chunk.reason;
|
|
119
|
+
}
|
|
120
|
+
if (finish?.kind === 'error' || finish?.kind === 'aborted') {
|
|
121
|
+
const failure = finish.failure;
|
|
122
|
+
throw new Error(failure?.message ?? failure?.code ?? `模型调用${finish.kind}`);
|
|
123
|
+
}
|
|
124
|
+
const suggestion = normalizeSuggestion(output);
|
|
125
|
+
if (!suggestion) {
|
|
126
|
+
const detail = reasoning.trim() ? `(模型只返回 reasoning:${normalizeSuggestion(reasoning, 80)})` : '';
|
|
127
|
+
throw new Error(`模型未生成有效建议${detail}`);
|
|
128
|
+
}
|
|
129
|
+
return { suggestion, route, historySize: history.length };
|
|
130
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-prompt-predict",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codex-style composer predictions for DeepSeek Harness: suggest the user's next prompt after each reply and accept it with Tab.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": "./client/client.js",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"client",
|
|
17
|
+
"cordis.patch.yml",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node test/predict.mjs",
|
|
22
|
+
"pack": "pnpm pack"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/ferrisching/dsh-prompt-predict.git"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/ferrisching/dsh-prompt-predict#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/ferrisching/dsh-prompt-predict/issues"
|
|
31
|
+
},
|
|
32
|
+
"author": "ferrisching",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"dsh-plugin",
|
|
36
|
+
"deepseek-harness",
|
|
37
|
+
"prompt-prediction",
|
|
38
|
+
"composer",
|
|
39
|
+
"ghost-text"
|
|
40
|
+
],
|
|
41
|
+
"dsh": {
|
|
42
|
+
"bundle": {
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
44
|
+
},
|
|
45
|
+
"client": {
|
|
46
|
+
"platform": "web",
|
|
47
|
+
"immediately": true,
|
|
48
|
+
"inject": [
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|