@yfwu2020/dsh-selection-explain 0.4.1 → 0.7.1
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/README.md +6 -2
- package/lib/client.js +811 -26
- package/lib/index.js +112 -4
- package/lib/index.js.map +1 -1
- package/lib/types/index.d.ts +18 -0
- package/package.json +3 -2
- package/scripts/gen-star-path.mjs +71 -0
- package/scripts/smoke-panel.mjs +709 -1
- package/scripts/test-client.mjs +12 -2
- package/scripts/test-format.mjs +93 -0
- package/src/client/index.js +811 -26
- package/src/index.ts +117 -4
package/lib/index.js
CHANGED
|
@@ -23,6 +23,24 @@ import z from '@deepseek-ai/schemastery';
|
|
|
23
23
|
import { CHAT_SYSTEM_PROMPT, CODE_PROMPT, DETAIL_PROMPT, TRANSLATION_PROMPT, } from './prompts.js';
|
|
24
24
|
/** 插件名(= package.json name,客户端 bundle 的模块 id 也是它)。 */
|
|
25
25
|
export const name = '@yfwu2020/dsh-selection-explain';
|
|
26
|
+
/**
|
|
27
|
+
* 升格时那条「上下文注入」消息的 `source.kind` —— 生产者**自有**的来源标识。
|
|
28
|
+
*
|
|
29
|
+
* 会话格式 v4 起(本机实测内核 0.2.0-rc.2),消息来源必须是生产者自报的 kind;
|
|
30
|
+
* v3 那种 `plugin` 包装(`kind: 'plugin'` 配一个 `plugin` 字段)已经退役,会被
|
|
31
|
+
* v4 准入当场拒掉:
|
|
32
|
+
* SessionFormatError: format v4 message requires a producer-owned source kind
|
|
33
|
+
* 症状极具迷惑性:`↗ 升格` 的 HTTP 请求返回 `ok: true`(那时只写进了内存里的会话面),
|
|
34
|
+
* 随后那轮 opener 运行失败、整个 artifact 一个字都没落盘 —— 新会话只剩表头。
|
|
35
|
+
* 离线测试也照样全绿(校验发生在真实内核里,不在构建期),所以另配了
|
|
36
|
+
* scripts/test-format.mjs 钉住这个 shape。
|
|
37
|
+
*
|
|
38
|
+
* 第三方插件的规范形态是 `plugin:<插件名>`:内核 v3→v4 迁移把那种 `plugin` 包装
|
|
39
|
+
* 重写成 `plugin:<完整插件名>`(`producerKind()` 对不在同名表里的插件就是这么算的),
|
|
40
|
+
* 所以这里写的与迁移结果逐字节一致 —— 新旧会话归因相同,且 v3 内核下同样合法
|
|
41
|
+
* (直写的 kind 迁移时原样保留)。
|
|
42
|
+
*/
|
|
43
|
+
export const PLUGIN_SOURCE_KIND = `plugin:${name}`;
|
|
26
44
|
/** 路由注册与模型调用所需的服务。 */
|
|
27
45
|
export const inject = ['webServer', 'llm'];
|
|
28
46
|
/** 路由前缀。 */
|
|
@@ -1098,6 +1116,93 @@ export function apply(ctx, rawConfig) {
|
|
|
1098
1116
|
let historyLoaded = false;
|
|
1099
1117
|
let historyTimer = null;
|
|
1100
1118
|
const historyFile = () => join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'selection-explain', 'history.json');
|
|
1119
|
+
/**
|
|
1120
|
+
* 悬浮胶囊"拖到别处"后的位置(用户要求记住,存 host 上 —— 不用 localStorage)。
|
|
1121
|
+
* 只记自由摆放的**右边缘 + 垂直中心**:收球/展开都以右边缘为锚(client 半的 applyFreePill 用 CSS right 钉住)。
|
|
1122
|
+
* 吸附回右下角(anchor)时会清掉 —— 那是默认位置,不需要记。
|
|
1123
|
+
*/
|
|
1124
|
+
const pillPosFile = () => join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'selection-explain', 'pill.json');
|
|
1125
|
+
let pillPos = null;
|
|
1126
|
+
let pillPosLoaded = false;
|
|
1127
|
+
let pillPosTimer = null;
|
|
1128
|
+
/** 位置校验:只接受有限数值,并夹到一个"任何屏幕都放得下"的量级。 */
|
|
1129
|
+
function normalizePillPos(input) {
|
|
1130
|
+
const value = input;
|
|
1131
|
+
const right = typeof value?.right === 'number' && Number.isFinite(value.right) ? value.right : NaN;
|
|
1132
|
+
const y = typeof value?.y === 'number' && Number.isFinite(value.y) ? value.y : NaN;
|
|
1133
|
+
if (!Number.isFinite(right) || !Number.isFinite(y))
|
|
1134
|
+
return null;
|
|
1135
|
+
const clamp = (n) => Math.round(Math.min(40000, Math.max(-40000, n)));
|
|
1136
|
+
return { right: clamp(right), y: clamp(y) };
|
|
1137
|
+
}
|
|
1138
|
+
async function loadPillPos() {
|
|
1139
|
+
if (pillPosLoaded)
|
|
1140
|
+
return;
|
|
1141
|
+
pillPosLoaded = true;
|
|
1142
|
+
try {
|
|
1143
|
+
const parsed = JSON.parse(await readFile(pillPosFile(), 'utf8'));
|
|
1144
|
+
pillPos = normalizePillPos(parsed?.position);
|
|
1145
|
+
}
|
|
1146
|
+
catch {
|
|
1147
|
+
pillPos = null; // 没存过 / 坏了都当作"没记过"
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function schedulePillPosWrite() {
|
|
1151
|
+
if (pillPosTimer)
|
|
1152
|
+
clearTimeout(pillPosTimer);
|
|
1153
|
+
pillPosTimer = setTimeout(() => {
|
|
1154
|
+
pillPosTimer = null;
|
|
1155
|
+
void (async () => {
|
|
1156
|
+
try {
|
|
1157
|
+
await mkdir(join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'selection-explain'), { recursive: true });
|
|
1158
|
+
await writeFile(pillPosFile(), JSON.stringify({ version: 1, position: pillPos }, null, 0), 'utf8');
|
|
1159
|
+
}
|
|
1160
|
+
catch (error) {
|
|
1161
|
+
ctx.logger?.warn?.(`[${name}] 写入胶囊位置失败:${String(error?.message ?? error)}`);
|
|
1162
|
+
}
|
|
1163
|
+
})();
|
|
1164
|
+
}, 300);
|
|
1165
|
+
pillPosTimer.unref?.();
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* 胶囊位置:GET 读回来(挂载时恢复)、POST 存({right,y})或清({clear:true},吸附回右下角时调)。
|
|
1169
|
+
*/
|
|
1170
|
+
const handlePillPos = (req, res) => {
|
|
1171
|
+
void (async () => {
|
|
1172
|
+
await loadPillPos();
|
|
1173
|
+
if (req.method === 'GET') {
|
|
1174
|
+
sendJson(res, 200, { ok: true, position: pillPos });
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
if (req.method !== 'POST') {
|
|
1178
|
+
sendJson(res, 405, { ok: false, error: '只支持 GET / POST' });
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
const raw = await readBody(req, 4_000);
|
|
1182
|
+
let body = {};
|
|
1183
|
+
try {
|
|
1184
|
+
body = raw ? JSON.parse(raw) : {};
|
|
1185
|
+
}
|
|
1186
|
+
catch {
|
|
1187
|
+
sendJson(res, 400, { ok: false, error: '请求体不是合法 JSON' });
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (body.clear === true) {
|
|
1191
|
+
pillPos = null;
|
|
1192
|
+
schedulePillPosWrite();
|
|
1193
|
+
sendJson(res, 200, { ok: true, position: null });
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const next = normalizePillPos(body.position ?? body);
|
|
1197
|
+
if (!next) {
|
|
1198
|
+
sendJson(res, 400, { ok: false, error: 'position 需要 { right, y } 两个有限数值' });
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
pillPos = next;
|
|
1202
|
+
schedulePillPosWrite();
|
|
1203
|
+
sendJson(res, 200, { ok: true, position: pillPos });
|
|
1204
|
+
})();
|
|
1205
|
+
};
|
|
1101
1206
|
const clampText = (value, max) => (typeof value === 'string' ? value.slice(0, max) : '');
|
|
1102
1207
|
async function loadHistory() {
|
|
1103
1208
|
if (historyLoaded)
|
|
@@ -1758,7 +1863,7 @@ export function apply(ctx, rawConfig) {
|
|
|
1758
1863
|
};
|
|
1759
1864
|
/**
|
|
1760
1865
|
* 把划词解读小窗「升格」成正式会话(建在来源会话同一个项目下):
|
|
1761
|
-
* ① 建会话 → ②
|
|
1866
|
+
* ① 建会话 → ② 注入上下文消息(选中文字/所在位置/上下文片段,生产者自有 kind 的 context 消息)
|
|
1762
1867
|
* → ③ 把首轮解读写成助手消息 → ④ 把追问逐轮写成真实的 user/assistant 历史消息
|
|
1763
1868
|
* → ⑤ 送一条短开场消息让 agent 接手(这样整段历史才会落盘)。
|
|
1764
1869
|
* 这样新会话里看到的就是一段真实的多轮对话,而不是一大坨打包文本。
|
|
@@ -1875,7 +1980,10 @@ export function apply(ctx, rawConfig) {
|
|
|
1875
1980
|
live.append?.('step/end', { turn, step: 1 });
|
|
1876
1981
|
live.append?.('turn/end', { turn, reason: { kind: 'completed' } });
|
|
1877
1982
|
};
|
|
1878
|
-
// 第 1
|
|
1983
|
+
// 第 1 轮:上下文消息(生产者自有的 source.kind + snapshot 呈现)+ 首轮解读
|
|
1984
|
+
// ⚠️ kind 必须是 PLUGIN_SOURCE_KIND(`plugin:<插件名>`):会话格式 v4 起不再
|
|
1985
|
+
// 接受退役的 `plugin` 包装(`kind: 'plugin'` 加一个 `plugin` 字段),否则整场升格
|
|
1986
|
+
// 会死在 "format v4 message requires a producer-owned source kind" 上(见文件头常量注释)。
|
|
1879
1987
|
const contextBody = [
|
|
1880
1988
|
`选中文字:${text}`,
|
|
1881
1989
|
`所在位置:${label}`,
|
|
@@ -1889,8 +1997,7 @@ export function apply(ctx, rawConfig) {
|
|
|
1889
1997
|
id: `sel-ctx-${stamp}`,
|
|
1890
1998
|
role: 'user',
|
|
1891
1999
|
source: {
|
|
1892
|
-
kind:
|
|
1893
|
-
plugin: name,
|
|
2000
|
+
kind: PLUGIN_SOURCE_KIND,
|
|
1894
2001
|
form: 'snapshot',
|
|
1895
2002
|
sections: [
|
|
1896
2003
|
{ name: '划词解读 · 选中文字', text },
|
|
@@ -2668,6 +2775,7 @@ export function apply(ctx, rawConfig) {
|
|
|
2668
2775
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/analyze`, handler: handleAnalyze }), `${name}: analyze route`);
|
|
2669
2776
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/ping`, handler: handlePing }), `${name}: ping route`);
|
|
2670
2777
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/history`, handler: handleHistory }), `${name}: history route`);
|
|
2778
|
+
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/pill`, handler: handlePillPos }), `${name}: pill position route`);
|
|
2671
2779
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/models`, handler: handleModels }), `${name}: models route`);
|
|
2672
2780
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/promote`, handler: handlePromote }), `${name}: promote route`);
|
|
2673
2781
|
ctx.effect(() => ctx.webServer.register({ kind: 'exact', path: `${API_PREFIX}/quote-context`, handler: handleQuoteContext }), `${name}: quote-context route`);
|