dsh-bce 0.1.0 → 0.1.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 +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +34 -41
- package/lib/index.js +43 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**Context-aware prompt enhancement for the DeepSeek Harness composer, powered by your Better Context Engine MCP.**
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-bce) [](#license) [](#requirements) [](https://github.com/deepseek-ai/deepseek-harness)
|
|
10
10
|
|
|
11
11
|
English · [简体中文](README.zh.md)
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@ English · [简体中文](README.zh.md)
|
|
|
19
19
|
`dsh-bce` adds an enhance icon next to the composer's attach button. One click incrementally indexes the current workspace through the Better Context Engine (BCE) MCP server already configured in your profile, then rewrites the draft into a context-rich prompt.
|
|
20
20
|
|
|
21
21
|
> [!NOTE]
|
|
22
|
-
> The plugin stores no BCE base URL or token. Credentials
|
|
22
|
+
> The plugin stores no BCE base URL or token. Credentials live only in the active profile's MCP entry and are used by bce-tool on the host; they are never sent to the UI (the web page or the Desktop window).
|
|
23
23
|
|
|
24
24
|
## Demo
|
|
25
25
|
|
|
@@ -175,7 +175,7 @@ dsh plugin --profile web add "$(pwd)"
|
|
|
175
175
|
| Cmd/Ctrl+Z does not revert | `setDraft` is outside the composer undo stack | Use **Restore original**. |
|
|
176
176
|
| Markdown is not rendered in the composer | The composer is a plain-text draft with file-reference chips | Keep enhancer output as plain text. |
|
|
177
177
|
|
|
178
|
-
**Why
|
|
178
|
+
**Why doesn't the UI call BCE directly?** The BCE service has no CORS layer, and the token must stay on the host. The enhance icon calls the host RPC, which executes the configured MCP tools.
|
|
179
179
|
|
|
180
180
|
## License
|
|
181
181
|
|
package/README.zh.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**为 DeepSeek Harness Composer 提供基于工作区上下文的提示增强,复用你已配置的 Better Context Engine MCP。**
|
|
8
8
|
|
|
9
|
-
[](https://www.npmjs.com/package/dsh-bce) [](#许可) [](#环境要求) [](https://github.com/deepseek-ai/deepseek-harness)
|
|
10
10
|
|
|
11
11
|
[English](README.md) · 简体中文
|
|
12
12
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
`dsh-bce` 在 Composer 附件按钮旁增加一个增强图标。点击后,插件通过当前 profile 已配置的 Better Context Engine(BCE)MCP 对工作区做增量索引,再把草稿重写为带有上下文的提示词。
|
|
20
20
|
|
|
21
21
|
> [!NOTE]
|
|
22
|
-
> 插件不保存 BCE 服务地址或 Token
|
|
22
|
+
> 插件不保存 BCE 服务地址或 Token。凭证只保存在当前 profile 的 MCP 配置中,仅由 Host 端的 bce-tool 使用,不会下发到前端界面(web 页面或 Desktop 窗口)。
|
|
23
23
|
|
|
24
24
|
## 演示
|
|
25
25
|
|
|
@@ -175,7 +175,7 @@ dsh plugin --profile web add "$(pwd)"
|
|
|
175
175
|
| Cmd/Ctrl+Z 无法撤销 | `setDraft` 不进入 Composer 撤销栈 | 使用 **还原**。 |
|
|
176
176
|
| 输入框不渲染 Markdown | Composer 是纯文本草稿加文件引用 chip | 增强结果使用纯文本。 |
|
|
177
177
|
|
|
178
|
-
|
|
178
|
+
**为什么前端不直连 BCE?** BCE 服务端没有 CORS,而且 Token 只能留在 Host 端。增强图标通过 Host RPC 调用已配置的 MCP 工具。
|
|
179
179
|
|
|
180
180
|
## 许可
|
|
181
181
|
|
package/lib/client.js
CHANGED
|
@@ -40,8 +40,10 @@ window.__ModuleLoader__.load({
|
|
|
40
40
|
"wand.mcpMissing": "BCE MCP was not found. Add bce-tool to this profile with --no-webbrowser-enhance-prompt.",
|
|
41
41
|
"wand.notGit": "This folder is not a Git repository. bce-tool only indexes Git projects.",
|
|
42
42
|
"wand.tooLarge": "This project is too large to index. Trim it with .gitignore or .bceignore.",
|
|
43
|
-
"wand.timeout": "Enhancement timed out (90s). Add --no-webbrowser-enhance-prompt to the bce-tool MCP args.",
|
|
43
|
+
"wand.timeout": "Enhancement timed out (90s). Add --no-webbrowser-enhance-prompt to the bce-tool MCP args, or switch to a faster enhancement model in BCE.",
|
|
44
44
|
"wand.indexFailed": "Indexing failed. Check the bce-tool MCP logs.",
|
|
45
|
+
"wand.indexTimeout": "Indexing timed out. Raise toolCallTimeoutMs on the bce-tool MCP entry.",
|
|
46
|
+
"wand.draftChanged": "The draft changed while enhancing, so it was not replaced.",
|
|
45
47
|
"wand.upstream": "Enhancement failed. Check MCP tool permissions and bce-tool logs.",
|
|
46
48
|
"wand.noIndex": "Enhanced without a fresh index (search_context was not found).",
|
|
47
49
|
"wand.done": "Prompt enhanced.",
|
|
@@ -56,8 +58,10 @@ window.__ModuleLoader__.load({
|
|
|
56
58
|
"wand.mcpMissing": "未检测到 BCE MCP。请在当前 profile 配置 bce-tool,并加上 --no-webbrowser-enhance-prompt。",
|
|
57
59
|
"wand.notGit": "当前目录不是 Git 仓库。bce-tool 默认只索引 Git 项目。",
|
|
58
60
|
"wand.tooLarge": "项目过大,无法索引。请用 .gitignore 或 .bceignore 排除大目录后再试。",
|
|
59
|
-
"wand.timeout": "增强超时(90 秒)。请确认 MCP 启动参数包含 --no-webbrowser-enhance-prompt
|
|
61
|
+
"wand.timeout": "增强超时(90 秒)。请确认 MCP 启动参数包含 --no-webbrowser-enhance-prompt,或在 BCE 中换用更快的增强模型。",
|
|
60
62
|
"wand.indexFailed": "索引失败。请查看 bce-tool MCP 日志。",
|
|
63
|
+
"wand.indexTimeout": "索引超时。请在 bce-tool MCP 配置中调大 toolCallTimeoutMs。",
|
|
64
|
+
"wand.draftChanged": "增强期间草稿已修改,未覆盖。",
|
|
61
65
|
"wand.upstream": "增强失败。请检查 MCP 工具权限和 bce-tool 日志。",
|
|
62
66
|
"wand.noIndex": "已增强,但未做索引(未找到 search_context)。",
|
|
63
67
|
"wand.done": "提示已增强。",
|
|
@@ -263,50 +267,27 @@ window.__ModuleLoader__.load({
|
|
|
263
267
|
return;
|
|
264
268
|
}
|
|
265
269
|
}
|
|
266
|
-
function
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
try {
|
|
270
|
-
return hook((state) => state);
|
|
271
|
-
} catch {
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
270
|
+
function sessionsPath(sessions, sessionId) {
|
|
271
|
+
const id = sessionId ?? nonEmpty(sessions?.current);
|
|
272
|
+
return id === void 0 ? void 0 : cwdOf(sessions?.byId?.[id]);
|
|
274
273
|
}
|
|
275
|
-
function
|
|
276
|
-
const session = hookValue(props, "useSession") ?? props.session;
|
|
277
|
-
const sessions = hookValue(props, "useSessions") ?? snapshotOf(extras.sessions);
|
|
278
|
-
const workspaces = hookValue(props, "useWorkspaces") ?? snapshotOf(extras.workspaces);
|
|
279
|
-
const sessionId = nonEmpty(props.sessionId) ?? nonEmpty(sessions?.current);
|
|
280
|
-
const fromSession = cwdOf(session);
|
|
281
|
-
if (fromSession !== void 0) return {
|
|
282
|
-
path: fromSession,
|
|
283
|
-
sessionId
|
|
284
|
-
};
|
|
285
|
-
if (sessionId !== void 0) {
|
|
286
|
-
const rowCwd = cwdOf(sessions?.byId?.[sessionId]);
|
|
287
|
-
if (rowCwd !== void 0) return {
|
|
288
|
-
path: rowCwd,
|
|
289
|
-
sessionId
|
|
290
|
-
};
|
|
291
|
-
}
|
|
274
|
+
function workspacesPath(workspaces, sessionId) {
|
|
292
275
|
const items = workspaces?.items ?? [];
|
|
293
276
|
if (sessionId !== void 0) {
|
|
294
|
-
const
|
|
295
|
-
if (
|
|
296
|
-
path: ownedPath,
|
|
297
|
-
sessionId
|
|
298
|
-
};
|
|
277
|
+
const owned = nonEmpty(items.find((item) => item.sessionIds?.includes(sessionId))?.path);
|
|
278
|
+
if (owned !== void 0) return owned;
|
|
299
279
|
}
|
|
300
280
|
const recentId = nonEmpty(workspaces?.recentWorkspaceId);
|
|
301
|
-
if (recentId
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
281
|
+
if (recentId === void 0) return void 0;
|
|
282
|
+
return nonEmpty(items.find((item) => item.workspaceId === recentId || item.id === recentId)?.path);
|
|
283
|
+
}
|
|
284
|
+
/** Reads store snapshots only; hook values must be captured during render. */
|
|
285
|
+
function resolveWorkspace(props, hooked, extras) {
|
|
286
|
+
const sessions = snapshotOf(extras.sessions);
|
|
287
|
+
const workspaces = snapshotOf(extras.workspaces);
|
|
288
|
+
const sessionId = nonEmpty(props.sessionId) ?? nonEmpty(sessions?.current);
|
|
308
289
|
return {
|
|
309
|
-
path:
|
|
290
|
+
path: hooked.session ?? cwdOf(props.session) ?? hooked.sessions ?? sessionsPath(sessions, sessionId) ?? hooked.workspaces ?? workspacesPath(workspaces, sessionId),
|
|
310
291
|
sessionId
|
|
311
292
|
};
|
|
312
293
|
}
|
|
@@ -319,6 +300,7 @@ window.__ModuleLoader__.load({
|
|
|
319
300
|
case "too_large": return t("wand.tooLarge");
|
|
320
301
|
case "timeout": return t("wand.timeout");
|
|
321
302
|
case "index_failed": return t("wand.indexFailed");
|
|
303
|
+
case "index_timeout": return t("wand.indexTimeout");
|
|
322
304
|
case "no_rpc": return t("wand.noRpc");
|
|
323
305
|
default: return t("wand.upstream");
|
|
324
306
|
}
|
|
@@ -383,11 +365,18 @@ window.__ModuleLoader__.load({
|
|
|
383
365
|
const lock = react.default.useRef(false);
|
|
384
366
|
const draftFromHook = props.useInput?.((state) => state.draft);
|
|
385
367
|
const draft = typeof draftFromHook === "string" ? draftFromHook : props.input?.draft ?? "";
|
|
368
|
+
const draftRef = react.default.useRef(draft);
|
|
369
|
+
draftRef.current = draft;
|
|
386
370
|
const extras = {
|
|
387
371
|
sessions: sessionsStore,
|
|
388
372
|
workspaces: workspacesStore
|
|
389
373
|
};
|
|
390
374
|
const sessionId = nonEmpty(props.sessionId);
|
|
375
|
+
const hooked = {
|
|
376
|
+
session: props.useSession?.((state) => cwdOf(state)),
|
|
377
|
+
sessions: props.useSessions?.((state) => sessionsPath(state, sessionId)),
|
|
378
|
+
workspaces: props.useWorkspaces?.((state) => workspacesPath(state, sessionId))
|
|
379
|
+
};
|
|
391
380
|
const empty = draft.trim().length === 0;
|
|
392
381
|
const disabled = busy || empty;
|
|
393
382
|
const title = empty ? t("wand.empty") : status.length > 0 ? status : t("wand.label");
|
|
@@ -411,7 +400,7 @@ window.__ModuleLoader__.load({
|
|
|
411
400
|
setBusy(true);
|
|
412
401
|
announce("busy", t("wand.enhancing"));
|
|
413
402
|
const prompt = draft.trim();
|
|
414
|
-
const workspace = resolveWorkspace(props, extras);
|
|
403
|
+
const workspace = resolveWorkspace(props, hooked, extras);
|
|
415
404
|
try {
|
|
416
405
|
if (workspace.path === void 0 && workspace.sessionId === void 0) {
|
|
417
406
|
announce("error", t("wand.noWorkspace"));
|
|
@@ -428,6 +417,10 @@ window.__ModuleLoader__.load({
|
|
|
428
417
|
sessionId: workspace.sessionId ?? sessionId
|
|
429
418
|
}));
|
|
430
419
|
if (parsed?.ok === true) {
|
|
420
|
+
if (draftRef.current.trim() !== prompt) {
|
|
421
|
+
announce("error", t("wand.draftChanged"));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
431
424
|
inputActions?.setDraft(parsed.value.text);
|
|
432
425
|
const previous = getRestore();
|
|
433
426
|
const original = (previous === void 0 || previous.sessionId === void 0 || previous.sessionId === sessionId) && previous !== void 0 && prompt === previous.enhanced ? previous.original : prompt;
|
package/lib/index.js
CHANGED
|
@@ -95,13 +95,19 @@ function resolveWorkspacePath(ctx, record) {
|
|
|
95
95
|
if (sessionId === void 0) return void 0;
|
|
96
96
|
return workspacePathFromRegistry(ctx, sessionId);
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
/** `ctx.tools.execute` reports tool failures and aborts as results, never by throwing. */
|
|
99
|
+
function isFailure(result, text) {
|
|
100
|
+
return result.isError || text.startsWith("Error:");
|
|
101
|
+
}
|
|
102
|
+
/** Only meaningful for failed results: successful text may quote these phrases. */
|
|
103
|
+
function classifyFailure(text) {
|
|
99
104
|
const lower = text.toLowerCase();
|
|
100
105
|
if (lower.includes("not managed by git") || lower.includes("not a git repository")) return "not_git";
|
|
101
106
|
if (lower.includes("too large to index") || lower.includes("exceeds the") && lower.includes("mb")) return "too_large";
|
|
102
|
-
if (
|
|
103
|
-
if (lower.includes("tool unavailable")) return "upstream";
|
|
107
|
+
if (lower.includes("timed out") || lower.includes("timeout") || lower.includes("tool call aborted")) return "timeout";
|
|
104
108
|
}
|
|
109
|
+
const INDEX_TIMEOUT_MESSAGE = "Indexing timed out. Raise toolCallTimeoutMs on the bce-tool MCP entry.";
|
|
110
|
+
const ENHANCE_TIMEOUT_MESSAGE = `Enhancement timed out (${ENHANCE_TIMEOUT_MS / 1e3}s). Add --no-webbrowser-enhance-prompt to the bce-tool MCP args if the review page is blocking.`;
|
|
105
111
|
function isAbortError(error) {
|
|
106
112
|
if (error instanceof Error && error.name === "AbortError") return true;
|
|
107
113
|
if (typeof error === "object" && error !== null && "name" in error && error.name === "AbortError") return true;
|
|
@@ -134,19 +140,26 @@ async function enhancePrompt(ctx, payload, signal) {
|
|
|
134
140
|
query: prompt
|
|
135
141
|
}, signal);
|
|
136
142
|
} catch (error) {
|
|
137
|
-
if (isAbortError(error)) return fail("
|
|
143
|
+
if (isAbortError(error)) return fail("index_timeout", INDEX_TIMEOUT_MESSAGE);
|
|
138
144
|
return fail("index_failed", error instanceof Error ? error.message : String(error));
|
|
139
145
|
}
|
|
140
146
|
const searchText = contentText(searchResult);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
if (isFailure(searchResult, searchText)) {
|
|
148
|
+
const searchCode = classifyFailure(searchText);
|
|
149
|
+
if (searchCode === "timeout") return fail("index_timeout", INDEX_TIMEOUT_MESSAGE);
|
|
150
|
+
if (searchCode !== void 0) return fail(searchCode, searchText);
|
|
151
|
+
return fail("index_failed", searchText || "search_context failed");
|
|
152
|
+
}
|
|
144
153
|
} else warning = "no_index";
|
|
145
154
|
const enhanceAbort = new AbortController();
|
|
146
155
|
const onParentAbort = () => enhanceAbort.abort();
|
|
147
156
|
if (signal.aborted) enhanceAbort.abort();
|
|
148
157
|
else signal.addEventListener("abort", onParentAbort, { once: true });
|
|
149
|
-
|
|
158
|
+
let timedOut = false;
|
|
159
|
+
const timer = setTimeout(() => {
|
|
160
|
+
timedOut = true;
|
|
161
|
+
enhanceAbort.abort();
|
|
162
|
+
}, ENHANCE_TIMEOUT_MS);
|
|
150
163
|
let enhanceResult;
|
|
151
164
|
try {
|
|
152
165
|
enhanceResult = await callTool(ctx, tools.enhance, {
|
|
@@ -155,16 +168,20 @@ async function enhancePrompt(ctx, payload, signal) {
|
|
|
155
168
|
conversation_history: ""
|
|
156
169
|
}, enhanceAbort.signal);
|
|
157
170
|
} catch (error) {
|
|
158
|
-
if (isAbortError(error)
|
|
171
|
+
if (timedOut || isAbortError(error)) return fail("timeout", ENHANCE_TIMEOUT_MESSAGE);
|
|
159
172
|
return fail("upstream", error instanceof Error ? error.message : String(error));
|
|
160
173
|
} finally {
|
|
161
174
|
clearTimeout(timer);
|
|
162
175
|
signal.removeEventListener("abort", onParentAbort);
|
|
163
176
|
}
|
|
177
|
+
if (timedOut) return fail("timeout", ENHANCE_TIMEOUT_MESSAGE);
|
|
164
178
|
const text = contentText(enhanceResult);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
179
|
+
if (isFailure(enhanceResult, text)) {
|
|
180
|
+
const enhanceCode = classifyFailure(text);
|
|
181
|
+
if (enhanceCode === "timeout") return fail("timeout", ENHANCE_TIMEOUT_MESSAGE);
|
|
182
|
+
if (enhanceCode !== void 0) return fail(enhanceCode, text);
|
|
183
|
+
return fail("upstream", text || "enhance_prompt failed");
|
|
184
|
+
}
|
|
168
185
|
if (text.length === 0) return fail("upstream", "enhance_prompt returned empty text");
|
|
169
186
|
return ok(warning === void 0 ? { text } : {
|
|
170
187
|
text,
|
|
@@ -235,7 +252,20 @@ function fetchRouteFor(handler) {
|
|
|
235
252
|
function registerEnhanceRpc(ctx) {
|
|
236
253
|
ctx.inject(["connection"], (wired) => {
|
|
237
254
|
const connection = wired.get("connection");
|
|
238
|
-
const handler = (payload, signal) =>
|
|
255
|
+
const handler = async (payload, signal) => {
|
|
256
|
+
try {
|
|
257
|
+
return await enhancePrompt(ctx, payload, signal);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
error: {
|
|
262
|
+
code: "upstream",
|
|
263
|
+
message: error instanceof Error ? error.message : String(error),
|
|
264
|
+
details: {}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
};
|
|
239
269
|
const register = connection.fetch?.register;
|
|
240
270
|
if (typeof register === "function") {
|
|
241
271
|
wired.effect(() => register(fetchRouteFor(handler)), "dsh-bce: /api/dsh-bce.enhance fetch route");
|
package/package.json
CHANGED