@morehao/dsh-skills-console 0.5.5 → 0.5.7
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.en.md +5 -3
- package/README.md +5 -3
- package/client.js +142 -54
- package/lib/core/intro-store.js +5 -2
- package/lib/core/intro.js +112 -18
- package/lib/core/plugin-update.js +59 -25
- package/lib/core/service.js +17 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -94,7 +94,7 @@ Some skills only have an English description, and some titles alone don't tell y
|
|
|
94
94
|
The **refresh button** next to the identity capsule in the page header checks whether *Skills Console itself* has a new release. This is a different axis from skill updates — it checks this plugin's own npm releases:
|
|
95
95
|
|
|
96
96
|
- **How it checks**: follows the registry plan from the host's `pluginManager.registries()` (inheriting the profile's mirror / private source / token config) and asks `<registry>/<package>/latest` in turn (8s timeout, version only; falls back to the official `registry.npmjs.org` only if none answer), then compares with the installed version; the host caches for 5 minutes so repeated opens don't hit the network. About 20 seconds after startup it warms the cache once, silently.
|
|
97
|
-
- **One-click update**: when the desktop host exposes `pluginManager`, clicking "Update to x.y.z" reinstalls this plugin to the latest version (installed from the registry that reported it); **restart DSH to apply** (no auto-reload).
|
|
97
|
+
- **One-click update**: when the desktop host exposes `pluginManager`, clicking "Update to x.y.z" reinstalls this plugin to the latest version (installed from the registry that reported it). With HMR, the host refuses to overwrite an installed package by name with `already-installed`; the plugin follows the official update path (`removeBundle` then `installBundle`); **restart DSH to apply** (no auto-reload).
|
|
98
98
|
- **Manual fallback**: if one-click update is unavailable, the dialog shows a copyable command (`dsh plugin --profile <profile> add <package>@latest`).
|
|
99
99
|
- **When a new version exists**: a small dot lights up on the refresh button; open it to see current / latest version and the update button, plus the release's **GitHub Release notes** body pulled for that version (if unavailable, a "view release notes on GitHub" link instead).
|
|
100
100
|
- **Boundaries**: it only queries registries and only (re)installs this plugin itself, touching no other plugin's state directory; failures degrade silently to "could not reach the registry" and never affect any skill-management feature.
|
|
@@ -157,8 +157,10 @@ AI intro settings (in the plugin `config` of the profile / `cordis.patch.yml`):
|
|
|
157
157
|
| `intro.enabled` | `true` | `false` hides the ✨ entry and the API returns 501 |
|
|
158
158
|
| `intro.provider` / `intro.model` | empty | empty uses DSH's current default model; both must be set together |
|
|
159
159
|
| `intro.maxInputBytes` | `24576` | cap on the `SKILL.md` sent to the model (8–48 KB); longer input is truncated and marked in the prompt |
|
|
160
|
-
| `intro.maxOutputTokens` | `
|
|
161
|
-
| `intro.timeoutMs` | `
|
|
160
|
+
| `intro.maxOutputTokens` | `4096` | output ceiling (96–8192). This is a runaway guard, not a length target; reasoning tokens count toward it, and the effective floor is raised per model |
|
|
161
|
+
| `intro.timeoutMs` | `60000` | per-generation timeout (5–60 s); timeouts return 504 |
|
|
162
|
+
|
|
163
|
+
The intro length is controlled by the 160-character prompt target. Do not treat `maxOutputTokens` as a length target; lowering it does not save cost, it only leaves a reasoning model no room for visible text.
|
|
162
164
|
|
|
163
165
|
## Development and publishing
|
|
164
166
|
|
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ DSH(DeepSeek Harness)的技能控制台:**安装、更新、删除、启
|
|
|
94
94
|
页头身份胶囊旁的**刷新按钮**用来检查「技能控制台」这个插件自己有没有新版本,而管的是另一个维度——它不是检查技能的上游,而是检查本插件在 npm 上的发布:
|
|
95
95
|
|
|
96
96
|
- **怎么查**:按宿主 `pluginManager.registries()` 给的 registry 计划(继承 profile 的镜像 / 私有源 / token 配置)依次问 `<registry>/<包名>/latest`(8 秒超时,只取版本号,取不到才回退官方 `registry.npmjs.org`),与本机安装的版本比较;Host 端 5 分钟缓存,连续打开不重复打网络。启动后约 20 秒会静默热身一次,不打扰你。
|
|
97
|
-
- **一键更新**:桌面端有 `pluginManager` 服务时,弹窗里点「更新到 x.y.z」即可把本插件重装到最新版(装到查到最新版的那个 registry
|
|
97
|
+
- **一键更新**:桌面端有 `pluginManager` 服务时,弹窗里点「更新到 x.y.z」即可把本插件重装到最新版(装到查到最新版的那个 registry)。HMR 开启时宿主会以 `already-installed` 拒绝覆盖同名已安装包,插件按官方更新路径先 `removeBundle` 再 `installBundle`;更新成功后**重启 DSH 生效**(不自动刷新)。
|
|
98
98
|
- **手动兜底**:环境不支持一键更新时,弹窗给出可复制的手动命令(`dsh plugin --profile <profile> add <包名>@latest`)。
|
|
99
99
|
- **有新版本时**:刷新按钮上会亮一个小圆点,点开即见当前版本 / 最新版本与更新按钮;弹窗还会拉取该版本在 GitHub 上的 **Release 发布说明**正文一并展示(拉不到时给「在 GitHub 查看发布说明」链接)。
|
|
100
100
|
- **边界**:只问 registry、只装本插件自身,不读写其他插件的状态目录;失败静默降级为「暂时无法获取最新版本」,不影响技能管理的任何功能。
|
|
@@ -157,8 +157,10 @@ plugin_manager install_bundle(target = <本仓库绝对路径>)
|
|
|
157
157
|
| `intro.enabled` | `true` | `false` 时界面隐藏 ✨ 入口,接口返回 501 |
|
|
158
158
|
| `intro.provider` / `intro.model` | 空 | 留空用 DSH 当前默认模型;两个必须同时给 |
|
|
159
159
|
| `intro.maxInputBytes` | `24576` | 发给模型的 `SKILL.md` 上限(8–48 KB),超出截断并在提示词里标注 |
|
|
160
|
-
| `intro.maxOutputTokens` | `
|
|
161
|
-
| `intro.timeoutMs` | `
|
|
160
|
+
| `intro.maxOutputTokens` | `4096` | 输出上限(96–8192)。这是防跑飞的预算,不是长度目标;推理模型的思考 token 也计入,实际会按模型自动抬高保底 |
|
|
161
|
+
| `intro.timeoutMs` | `60000` | 单次生成超时(5–60 秒),超时返回 504 |
|
|
162
|
+
|
|
163
|
+
简介长度由提示词里的 160 字控制,不要把 `maxOutputTokens` 当成目标长度;调小它不会省钱,只会让推理模型没有输出正文的空间。
|
|
162
164
|
|
|
163
165
|
## 开发与发布
|
|
164
166
|
|
package/client.js
CHANGED
|
@@ -244,6 +244,7 @@ var DICT = {
|
|
|
244
244
|
"form.subpath": "子目录(可空)",
|
|
245
245
|
"form.hint": "会立即下载并写入 ~/.agents/skills,并按「新安装时自动共享」的选择共享至对应 Agent。",
|
|
246
246
|
"form.subHint": "只记录你关注这个仓库,不会安装任何技能。",
|
|
247
|
+
"form.submitFailed": "添加失败,请重试",
|
|
247
248
|
"subscription.defaultRef": "默认分支",
|
|
248
249
|
"confirm.deleteTitle": "删除技能",
|
|
249
250
|
"confirm.deleteBody": "会从本机移除,断开本插件建立的其他 Agent 链接并清理记录;目录先备份到回收站(保留 7 天)。不会删除你的源仓库。",
|
|
@@ -356,6 +357,9 @@ var DICT = {
|
|
|
356
357
|
"intro.error.timeout": "生成超时,请重试",
|
|
357
358
|
"intro.error.auth": "模型提供方认证失败",
|
|
358
359
|
"intro.error.empty": "模型没有返回内容",
|
|
360
|
+
"intro.error.truncated": "简介被输出上限截断,请重试",
|
|
361
|
+
"intro.error.reasoningOnly": "模型只输出了思考过程,没有输出正文;请提高输出上限或换非推理模型",
|
|
362
|
+
"intro.error.noText": "模型没有输出文字简介,请重试",
|
|
359
363
|
"intro.error.busy": "正在生成,请稍候",
|
|
360
364
|
"intro.error.source": "读不到远端的 SKILL.md(仓库私有、已删除或网络问题)",
|
|
361
365
|
"intro.error.noSource": "订阅快照里没有这个技能,请先点「检查更新」",
|
|
@@ -525,6 +529,7 @@ var DICT = {
|
|
|
525
529
|
"form.subpath": "Subdirectory (optional)",
|
|
526
530
|
"form.hint": "Downloads into ~/.agents/skills and shares it with agents per the auto-share setting.",
|
|
527
531
|
"form.subHint": "Only records the repository; installs nothing.",
|
|
532
|
+
"form.submitFailed": "Failed; please try again",
|
|
528
533
|
"subscription.defaultRef": "default branch",
|
|
529
534
|
"confirm.deleteTitle": "Delete skill",
|
|
530
535
|
"confirm.deleteBody": "Removes it locally, disconnects the agent links this plugin created, and clears the record. A snapshot stays in trash for 7 days. Your source repo is untouched.",
|
|
@@ -638,6 +643,9 @@ var DICT = {
|
|
|
638
643
|
"intro.error.timeout": "Generation timed out; try again",
|
|
639
644
|
"intro.error.auth": "The model provider rejected the credentials",
|
|
640
645
|
"intro.error.empty": "The model returned no content",
|
|
646
|
+
"intro.error.truncated": "The intro hit the output limit and was truncated; try again",
|
|
647
|
+
"intro.error.reasoningOnly": "The model only produced reasoning, not intro text; raise the output limit or use a non-reasoning model",
|
|
648
|
+
"intro.error.noText": "The model returned no text intro; try again",
|
|
641
649
|
"intro.error.busy": "Already generating; please wait",
|
|
642
650
|
"intro.error.source": "Could not read the remote SKILL.md (private repo, deleted, or network)",
|
|
643
651
|
"intro.error.noSource": "This skill is not in the subscription snapshot; run “Check for updates” first",
|
|
@@ -1152,6 +1160,7 @@ var CSS = `
|
|
|
1152
1160
|
/* 标题不参与收窄:窄面板里让身份胶囊先省略,标题本身不折行 */
|
|
1153
1161
|
.dshsc-title { font-size: 17px; font-weight: 600; letter-spacing: -.01em; margin: 0; flex: none; white-space: nowrap; }
|
|
1154
1162
|
.dshsc-sub { font-size: 12px; line-height: 1.55; color: var(--dsw-alias-label-secondary); margin: 0; max-width: 64ch; }
|
|
1163
|
+
.dshsc-form-error { font-size: 12px; line-height: 1.55; margin: 8px 0 0; max-width: 64ch; color: var(--dsw-alias-state-error-primary); }
|
|
1155
1164
|
.dshsc-spacer { flex: 1; }
|
|
1156
1165
|
.dshsc-head-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; flex-wrap: wrap; margin-left: auto; }
|
|
1157
1166
|
/* 标题行恒为一行:身份胶囊只在面板极窄(连标题行都放不下)时才省略,工具组不被挤走。
|
|
@@ -1984,26 +1993,38 @@ function useSkillActions({ t, rows, reload, run, setBusy, setToast, setModal })
|
|
|
1984
1993
|
return apiCall("POST", "/probe", { source: String(source || "").trim() });
|
|
1985
1994
|
}
|
|
1986
1995
|
async function installSkills({ source, skills }) {
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
+
try {
|
|
1997
|
+
await run("install", async () => {
|
|
1998
|
+
const result = await apiCall("POST", "/install", { source: String(source || "").trim(), skills });
|
|
1999
|
+
const warnings = result.warnings?.length || 0;
|
|
2000
|
+
setToast({
|
|
2001
|
+
kind: warnings ? "err" : "ok",
|
|
2002
|
+
text: `${result.installed?.length || 0} ${t("skillCount")}${warnings ? ` · ${warnings} 条提示` : ""}`
|
|
2003
|
+
});
|
|
2004
|
+
return result;
|
|
2005
|
+
}, { toastError: false });
|
|
2006
|
+
setModal(null);
|
|
2007
|
+
return { ok: true };
|
|
2008
|
+
} catch (err) {
|
|
2009
|
+
return { ok: false, error: err?.message || String(err) };
|
|
2010
|
+
}
|
|
1996
2011
|
}
|
|
1997
2012
|
async function addSubscription(form) {
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2013
|
+
try {
|
|
2014
|
+
const result = await run("subscribe", async () => {
|
|
2015
|
+
const res = await apiCall("POST", "/subscriptions", {
|
|
2016
|
+
source: form.source.trim(),
|
|
2017
|
+
ref: form.ref.trim() || void 0,
|
|
2018
|
+
subpath: form.subpath.trim() || void 0
|
|
2019
|
+
});
|
|
2020
|
+
setToast({ kind: "ok", text: res.subscription?.source || form.source });
|
|
2021
|
+
return res;
|
|
2022
|
+
}, { toastError: false });
|
|
2023
|
+
setModal(null);
|
|
2024
|
+
return { ok: true, result };
|
|
2025
|
+
} catch (err) {
|
|
2026
|
+
return { ok: false, error: err?.message || String(err) };
|
|
2027
|
+
}
|
|
2007
2028
|
}
|
|
2008
2029
|
async function refreshSubscription(row) {
|
|
2009
2030
|
await run(`refresh:${row.id}`, async () => {
|
|
@@ -2070,14 +2091,14 @@ function useSkillsData() {
|
|
|
2070
2091
|
return () => clearTimeout(timer);
|
|
2071
2092
|
}, [toast]);
|
|
2072
2093
|
const run = import_react2.default.useCallback(
|
|
2073
|
-
async (label, task) => {
|
|
2094
|
+
async (label, task, { toastError = true } = {}) => {
|
|
2074
2095
|
setBusy(label);
|
|
2075
2096
|
try {
|
|
2076
2097
|
const result = await task();
|
|
2077
2098
|
await reload();
|
|
2078
2099
|
return result;
|
|
2079
2100
|
} catch (err) {
|
|
2080
|
-
setToast({ kind: "err", text: err.message });
|
|
2101
|
+
if (toastError) setToast({ kind: "err", text: err.message });
|
|
2081
2102
|
throw err;
|
|
2082
2103
|
} finally {
|
|
2083
2104
|
setBusy(null);
|
|
@@ -2590,12 +2611,13 @@ function Toolbar({
|
|
|
2590
2611
|
var import_react4 = __toESM(require("react"), 1);
|
|
2591
2612
|
|
|
2592
2613
|
// client/views/overlay.js
|
|
2593
|
-
function Overlay({ t, title, children, footer, onCancel }) {
|
|
2594
|
-
useOverlayLayer(true, onCancel)
|
|
2614
|
+
function Overlay({ t, title, children, footer, onCancel, dismissible = true }) {
|
|
2615
|
+
useOverlayLayer(true, dismissible ? onCancel : () => {
|
|
2616
|
+
});
|
|
2595
2617
|
return portalize(h(
|
|
2596
2618
|
"div",
|
|
2597
2619
|
{ className: "dshsc-overlay", onClick: (event) => {
|
|
2598
|
-
if (event.target === event.currentTarget) onCancel?.();
|
|
2620
|
+
if (dismissible && event.target === event.currentTarget) onCancel?.();
|
|
2599
2621
|
} },
|
|
2600
2622
|
h(
|
|
2601
2623
|
"div",
|
|
@@ -2603,9 +2625,10 @@ function Overlay({ t, title, children, footer, onCancel }) {
|
|
|
2603
2625
|
h("button", {
|
|
2604
2626
|
className: "dshsc-modal-close",
|
|
2605
2627
|
"aria-label": t("action.cancel"),
|
|
2628
|
+
disabled: !dismissible,
|
|
2606
2629
|
onClick: (event) => {
|
|
2607
2630
|
event.stopPropagation();
|
|
2608
|
-
onCancel?.();
|
|
2631
|
+
if (dismissible) onCancel?.();
|
|
2609
2632
|
}
|
|
2610
2633
|
}, "×"),
|
|
2611
2634
|
h("h3", null, title),
|
|
@@ -2624,6 +2647,9 @@ var ERROR_KEY = {
|
|
|
2624
2647
|
"error.skills.intro-timeout": "intro.error.timeout",
|
|
2625
2648
|
"error.skills.intro-auth": "intro.error.auth",
|
|
2626
2649
|
"error.skills.intro-empty": "intro.error.empty",
|
|
2650
|
+
"error.skills.intro-truncated": "intro.error.truncated",
|
|
2651
|
+
"error.skills.intro-reasoning-only": "intro.error.reasoningOnly",
|
|
2652
|
+
"error.skills.intro-no-text": "intro.error.noText",
|
|
2627
2653
|
"error.skills.intro-busy": "intro.error.busy",
|
|
2628
2654
|
"error.skills.intro-source": "intro.error.source",
|
|
2629
2655
|
"error.skills.subscription-not-found": "intro.error.subscription",
|
|
@@ -3614,6 +3640,8 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
|
|
|
3614
3640
|
const [manual, setManual] = import_react6.default.useState("");
|
|
3615
3641
|
const [skipped, setSkipped] = import_react6.default.useState(0);
|
|
3616
3642
|
const [error, setError] = import_react6.default.useState(null);
|
|
3643
|
+
const [pending, setPending] = import_react6.default.useState(false);
|
|
3644
|
+
const [submitError, setSubmitError] = import_react6.default.useState(null);
|
|
3617
3645
|
const reset = (value) => {
|
|
3618
3646
|
setSource(value);
|
|
3619
3647
|
setPhase("idle");
|
|
@@ -3621,12 +3649,14 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
|
|
|
3621
3649
|
setSelected(/* @__PURE__ */ new Set());
|
|
3622
3650
|
setSkipped(0);
|
|
3623
3651
|
setError(null);
|
|
3652
|
+
setSubmitError(null);
|
|
3624
3653
|
};
|
|
3625
3654
|
const probe = async () => {
|
|
3626
3655
|
const value = source.trim();
|
|
3627
|
-
if (!value || phase === "probing") return;
|
|
3656
|
+
if (!value || phase === "probing" || pending) return;
|
|
3628
3657
|
setPhase("probing");
|
|
3629
3658
|
setError(null);
|
|
3659
|
+
setSubmitError(null);
|
|
3630
3660
|
try {
|
|
3631
3661
|
const result = await onProbe(value);
|
|
3632
3662
|
const found = Array.isArray(result?.skills) ? result.skills : [];
|
|
@@ -3639,20 +3669,33 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
|
|
|
3639
3669
|
setPhase("failed");
|
|
3640
3670
|
}
|
|
3641
3671
|
};
|
|
3642
|
-
const toggle = (name) =>
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3672
|
+
const toggle = (name) => {
|
|
3673
|
+
setSubmitError(null);
|
|
3674
|
+
setSelected((prev) => {
|
|
3675
|
+
const next = new Set(prev);
|
|
3676
|
+
if (next.has(name)) next.delete(name);
|
|
3677
|
+
else next.add(name);
|
|
3678
|
+
return next;
|
|
3679
|
+
});
|
|
3680
|
+
};
|
|
3648
3681
|
const names = manual.split(/[,,\s]+/).map((item) => item.trim()).filter(Boolean);
|
|
3649
3682
|
const skippedText = skipped > 0 ? t("form.skipped").replace("{n}", String(skipped)) : null;
|
|
3650
3683
|
const manualMode = phase === "empty" || phase === "failed";
|
|
3651
3684
|
const skills = phase === "picked" ? [...selected] : names;
|
|
3652
3685
|
const canSubmit = phase === "picked" ? selected.size > 0 : manualMode ? names.length > 0 : false;
|
|
3653
|
-
const submit = () => {
|
|
3654
|
-
if (!canSubmit) return;
|
|
3655
|
-
|
|
3686
|
+
const submit = async () => {
|
|
3687
|
+
if (!canSubmit || pending) return;
|
|
3688
|
+
setPending(true);
|
|
3689
|
+
setSubmitError(null);
|
|
3690
|
+
let res;
|
|
3691
|
+
try {
|
|
3692
|
+
res = await onInstall({ source: source.trim(), skills });
|
|
3693
|
+
} catch (err) {
|
|
3694
|
+
res = { ok: false, error: err?.message || String(err) };
|
|
3695
|
+
}
|
|
3696
|
+
if (res?.ok) return;
|
|
3697
|
+
setPending(false);
|
|
3698
|
+
setSubmitError(res?.error || t("form.submitFailed"));
|
|
3656
3699
|
};
|
|
3657
3700
|
return h(
|
|
3658
3701
|
Overlay,
|
|
@@ -3660,14 +3703,16 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
|
|
|
3660
3703
|
t,
|
|
3661
3704
|
title: t("action.addSkill"),
|
|
3662
3705
|
onCancel,
|
|
3706
|
+
// 提交安装进行中禁止关闭:请求不可取消,允许关闭只是假装取消
|
|
3707
|
+
dismissible: !pending,
|
|
3663
3708
|
footer: [
|
|
3664
|
-
h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
|
|
3709
|
+
h("button", { key: "c", className: "dshsc-btn ghost", disabled: pending, onClick: onCancel }, t("action.cancel")),
|
|
3665
3710
|
h("button", {
|
|
3666
3711
|
key: "s",
|
|
3667
3712
|
className: "dshsc-btn primary",
|
|
3668
|
-
disabled: !canSubmit,
|
|
3669
|
-
onClick: submit
|
|
3670
|
-
}, t("action.save"))
|
|
3713
|
+
disabled: !canSubmit || pending,
|
|
3714
|
+
onClick: () => void submit()
|
|
3715
|
+
}, pending ? t("busy.working") : t("action.save"))
|
|
3671
3716
|
]
|
|
3672
3717
|
},
|
|
3673
3718
|
h("p", { className: "dshsc-sub" }, t("form.pickHint")),
|
|
@@ -3681,18 +3726,20 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
|
|
|
3681
3726
|
h("input", {
|
|
3682
3727
|
value: source,
|
|
3683
3728
|
placeholder: t("form.sourceHint"),
|
|
3729
|
+
disabled: pending,
|
|
3684
3730
|
onChange: (event) => reset(event.target.value),
|
|
3685
3731
|
onKeyDown: (event) => {
|
|
3686
|
-
if (event.key === "Enter") void probe();
|
|
3732
|
+
if (event.key === "Enter" && !pending) void probe();
|
|
3687
3733
|
}
|
|
3688
3734
|
}),
|
|
3689
3735
|
h("button", {
|
|
3690
3736
|
className: "dshsc-btn",
|
|
3691
|
-
disabled: !source.trim() || phase === "probing",
|
|
3737
|
+
disabled: !source.trim() || phase === "probing" || pending,
|
|
3692
3738
|
onClick: () => void probe()
|
|
3693
3739
|
}, phase === "probing" ? t("form.probing") : t("form.probe"))
|
|
3694
3740
|
)
|
|
3695
3741
|
),
|
|
3742
|
+
submitError ? h("p", { className: "dshsc-form-error", role: "alert" }, submitError) : null,
|
|
3696
3743
|
phase === "picked" ? h(
|
|
3697
3744
|
"div",
|
|
3698
3745
|
{ key: "picked" },
|
|
@@ -3702,11 +3749,19 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
|
|
|
3702
3749
|
{ style: { display: "flex", gap: 6, marginBottom: 6 } },
|
|
3703
3750
|
h("button", {
|
|
3704
3751
|
className: "dshsc-btn ghost",
|
|
3705
|
-
|
|
3752
|
+
disabled: pending,
|
|
3753
|
+
onClick: () => {
|
|
3754
|
+
setSubmitError(null);
|
|
3755
|
+
setSelected(new Set(options.map((item) => item.name)));
|
|
3756
|
+
}
|
|
3706
3757
|
}, t("form.selectAll")),
|
|
3707
3758
|
h("button", {
|
|
3708
3759
|
className: "dshsc-btn ghost",
|
|
3709
|
-
|
|
3760
|
+
disabled: pending,
|
|
3761
|
+
onClick: () => {
|
|
3762
|
+
setSubmitError(null);
|
|
3763
|
+
setSelected(/* @__PURE__ */ new Set());
|
|
3764
|
+
}
|
|
3710
3765
|
}, t("form.selectNone"))
|
|
3711
3766
|
),
|
|
3712
3767
|
h("div", { className: "dshsc-panel" }, options.map((option) => h(
|
|
@@ -3715,6 +3770,7 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
|
|
|
3715
3770
|
h("input", {
|
|
3716
3771
|
type: "checkbox",
|
|
3717
3772
|
checked: selected.has(option.name),
|
|
3773
|
+
disabled: pending,
|
|
3718
3774
|
onChange: () => toggle(option.name)
|
|
3719
3775
|
}),
|
|
3720
3776
|
h(
|
|
@@ -3739,7 +3795,11 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
|
|
|
3739
3795
|
h("input", {
|
|
3740
3796
|
value: manual,
|
|
3741
3797
|
placeholder: "git-kit, handoff",
|
|
3742
|
-
|
|
3798
|
+
disabled: pending,
|
|
3799
|
+
onChange: (event) => {
|
|
3800
|
+
setManual(event.target.value);
|
|
3801
|
+
if (submitError) setSubmitError(null);
|
|
3802
|
+
}
|
|
3743
3803
|
})
|
|
3744
3804
|
)
|
|
3745
3805
|
) : null,
|
|
@@ -3750,23 +3810,41 @@ function AddSkillModal({ t, onCancel, onProbe, onInstall }) {
|
|
|
3750
3810
|
// client/views/modals.js
|
|
3751
3811
|
function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
|
|
3752
3812
|
const [form, setForm] = import_react7.default.useState(() => Object.fromEntries(fields.map((field) => [field.key, ""])));
|
|
3813
|
+
const [pending, setPending] = import_react7.default.useState(false);
|
|
3814
|
+
const [error, setError] = import_react7.default.useState(null);
|
|
3753
3815
|
const incomplete = fields.some(
|
|
3754
3816
|
(field) => field.required !== false && !String(form[field.key] || "").trim()
|
|
3755
3817
|
);
|
|
3818
|
+
const submit = async () => {
|
|
3819
|
+
if (incomplete || pending) return;
|
|
3820
|
+
setPending(true);
|
|
3821
|
+
setError(null);
|
|
3822
|
+
let res;
|
|
3823
|
+
try {
|
|
3824
|
+
res = await onSubmit(form);
|
|
3825
|
+
} catch (err) {
|
|
3826
|
+
res = { ok: false, error: err?.message || String(err) };
|
|
3827
|
+
}
|
|
3828
|
+
if (res?.ok) return;
|
|
3829
|
+
setPending(false);
|
|
3830
|
+
setError(res?.error || t("form.submitFailed"));
|
|
3831
|
+
};
|
|
3756
3832
|
return h(
|
|
3757
3833
|
Overlay,
|
|
3758
3834
|
{
|
|
3759
3835
|
t,
|
|
3760
3836
|
title,
|
|
3761
3837
|
onCancel,
|
|
3838
|
+
// 提交进行中禁止关闭:请求不可取消,允许关闭只是假装取消
|
|
3839
|
+
dismissible: !pending,
|
|
3762
3840
|
footer: [
|
|
3763
|
-
h("button", { key: "c", className: "dshsc-btn ghost", onClick: onCancel }, t("action.cancel")),
|
|
3841
|
+
h("button", { key: "c", className: "dshsc-btn ghost", disabled: pending, onClick: onCancel }, t("action.cancel")),
|
|
3764
3842
|
h("button", {
|
|
3765
3843
|
key: "s",
|
|
3766
3844
|
className: "dshsc-btn primary",
|
|
3767
|
-
disabled: incomplete,
|
|
3768
|
-
onClick: () =>
|
|
3769
|
-
}, t("action.save"))
|
|
3845
|
+
disabled: incomplete || pending,
|
|
3846
|
+
onClick: () => void submit()
|
|
3847
|
+
}, pending ? t("busy.working") : t("action.save"))
|
|
3770
3848
|
]
|
|
3771
3849
|
},
|
|
3772
3850
|
hint ? h("p", { className: "dshsc-sub" }, hint) : null,
|
|
@@ -3777,9 +3855,14 @@ function FormModal({ t, title, hint, fields, onCancel, onSubmit }) {
|
|
|
3777
3855
|
h("input", {
|
|
3778
3856
|
value: form[field.key],
|
|
3779
3857
|
placeholder: field.placeholder,
|
|
3780
|
-
|
|
3858
|
+
disabled: pending,
|
|
3859
|
+
onChange: (event) => {
|
|
3860
|
+
setForm((prev) => ({ ...prev, [field.key]: event.target.value }));
|
|
3861
|
+
if (error) setError(null);
|
|
3862
|
+
}
|
|
3781
3863
|
})
|
|
3782
|
-
))
|
|
3864
|
+
)),
|
|
3865
|
+
error ? h("p", { className: "dshsc-form-error", role: "alert" }, error) : null
|
|
3783
3866
|
);
|
|
3784
3867
|
}
|
|
3785
3868
|
function ConfirmModal({ t, title, body, onCancel, onConfirm, danger, confirmLabel, hideCancel = false }) {
|
|
@@ -3894,7 +3977,7 @@ function Modals({ t, modal, data, autoLinkAgents, actions, onClose, onSaveAutoLi
|
|
|
3894
3977
|
{ key: "subpath", label: t("form.subpath"), placeholder: "skills", required: false }
|
|
3895
3978
|
],
|
|
3896
3979
|
onCancel: onClose,
|
|
3897
|
-
onSubmit: (form) =>
|
|
3980
|
+
onSubmit: (form) => actions.addSubscription(form)
|
|
3898
3981
|
});
|
|
3899
3982
|
}
|
|
3900
3983
|
if (modal.type === "exports") {
|
|
@@ -5000,6 +5083,11 @@ function SkillReader({
|
|
|
5000
5083
|
|
|
5001
5084
|
// client/views/plugin-update-modal.js
|
|
5002
5085
|
var import_react9 = __toESM(require("react"), 1);
|
|
5086
|
+
function flattenDetails(text) {
|
|
5087
|
+
return String(text ?? "").replace(/<details[^>]*>\s*<summary>([\s\S]*?)<\/summary>/gi, (_, summary) => `**${String(summary).trim()}**
|
|
5088
|
+
|
|
5089
|
+
`).replace(/<\/details>/gi, "");
|
|
5090
|
+
}
|
|
5003
5091
|
function PluginUpdateModal({ t, update, onClose, repoUrl }) {
|
|
5004
5092
|
const { status, checking, applying, checkError, applyError, check, apply: apply2, release, loadRelease } = update;
|
|
5005
5093
|
const [copied, setCopied] = import_react9.default.useState(false);
|
|
@@ -5018,7 +5106,7 @@ function PluginUpdateModal({ t, update, onClose, repoUrl }) {
|
|
|
5018
5106
|
}, [available, latest, loadRelease]);
|
|
5019
5107
|
const releaseData = release?.version === latest ? release.data : null;
|
|
5020
5108
|
const releaseLoading = release?.version === latest && release.loading;
|
|
5021
|
-
const releaseBody = typeof releaseData?.body === "string" ? releaseData.body.trim() : "";
|
|
5109
|
+
const releaseBody = typeof releaseData?.body === "string" ? flattenDetails(releaseData.body.trim()).trim() : "";
|
|
5022
5110
|
const releaseUrl = releaseData?.htmlUrl || (repoUrl ? `${repoUrl}/releases` : null);
|
|
5023
5111
|
const copy = () => {
|
|
5024
5112
|
if (!manual || typeof navigator === "undefined" || !navigator.clipboard?.writeText) return;
|
|
@@ -5031,7 +5119,7 @@ function PluginUpdateModal({ t, update, onClose, repoUrl }) {
|
|
|
5031
5119
|
let stateLine;
|
|
5032
5120
|
if (checking && !status) stateLine = t("pluginUpdate.checking");
|
|
5033
5121
|
else if (checkError || status?.checkFailed) stateLine = t("pluginUpdate.checkFailed");
|
|
5034
|
-
else if (restartRequired) stateLine = t("pluginUpdate.updated"
|
|
5122
|
+
else if (restartRequired) stateLine = t("pluginUpdate.updated").replace("{version}", String(updatedVersion || latest || ""));
|
|
5035
5123
|
else if (available) stateLine = t("pluginUpdate.available");
|
|
5036
5124
|
else if (status) stateLine = t("pluginUpdate.upToDate");
|
|
5037
5125
|
else stateLine = t("pluginUpdate.checking");
|
|
@@ -5050,7 +5138,7 @@ function PluginUpdateModal({ t, update, onClose, repoUrl }) {
|
|
|
5050
5138
|
className: "dshsc-btn primary",
|
|
5051
5139
|
disabled: applying,
|
|
5052
5140
|
onClick: () => void apply2()
|
|
5053
|
-
}, applying ? t("pluginUpdate.applying") : t("pluginUpdate.update"
|
|
5141
|
+
}, applying ? t("pluginUpdate.applying") : t("pluginUpdate.update").replace("{version}", String(latest || ""))));
|
|
5054
5142
|
}
|
|
5055
5143
|
footer.push(h("button", { key: "close", className: "dshsc-btn ghost", onClick: onClose }, t("pluginUpdate.close")));
|
|
5056
5144
|
return h(
|
|
@@ -5069,7 +5157,7 @@ function PluginUpdateModal({ t, update, onClose, repoUrl }) {
|
|
|
5069
5157
|
available ? h(
|
|
5070
5158
|
"div",
|
|
5071
5159
|
{ className: "dshsc-release" },
|
|
5072
|
-
h("div", { className: "dshsc-release-head" }, t("pluginUpdate.whatsNew"
|
|
5160
|
+
h("div", { className: "dshsc-release-head" }, t("pluginUpdate.whatsNew").replace("{version}", String(latest || ""))),
|
|
5073
5161
|
releaseLoading ? h("p", { className: "dshsc-sub" }, t("pluginUpdate.releaseLoading")) : releaseBody ? h("div", { className: "dshsc-release-body" }, h(Markdown, { text: releaseBody })) : h(
|
|
5074
5162
|
"p",
|
|
5075
5163
|
{ className: "dshsc-sub" },
|
package/lib/core/intro-store.js
CHANGED
|
@@ -33,6 +33,8 @@ function sanitizeEntry(entry) {
|
|
|
33
33
|
inputBytes: Number.isFinite(entry.inputBytes) ? entry.inputBytes : null,
|
|
34
34
|
truncated: Boolean(entry.truncated),
|
|
35
35
|
sourceBytes: Number.isFinite(entry.sourceBytes) ? entry.sourceBytes : null,
|
|
36
|
+
maxTokens: Number.isFinite(entry.maxTokens) ? entry.maxTokens : null,
|
|
37
|
+
finish: typeof entry.finish === 'string' ? entry.finish : 'stop',
|
|
36
38
|
};
|
|
37
39
|
}
|
|
38
40
|
|
|
@@ -125,10 +127,11 @@ export function createIntroStore({ paths, logger = () => {} }) {
|
|
|
125
127
|
return problem;
|
|
126
128
|
},
|
|
127
129
|
load,
|
|
128
|
-
/** 读一条缓存;没有就是 null
|
|
130
|
+
/** 读一条缓存;没有就是 null。只有 `finish: stop` 的完整简介才会被返回。 */
|
|
129
131
|
get(name, lang) {
|
|
130
132
|
const entry = load().items[name]?.[lang];
|
|
131
|
-
|
|
133
|
+
if (!entry || entry.finish !== 'stop') return null;
|
|
134
|
+
return { ...entry };
|
|
132
135
|
},
|
|
133
136
|
/** 写一条缓存;写盘失败不影响返回值(介绍已经拿到了)。 */
|
|
134
137
|
put(name, lang, entry) {
|
package/lib/core/intro.js
CHANGED
|
@@ -19,9 +19,10 @@ import { sha256 } from './hash.js';
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* 简介缓存的格式版本(与 state 的版本无关,坏了直接当空表)。
|
|
22
|
-
*
|
|
22
|
+
* v4 = 只缓存 `finish: stop` 的完整简介;v3 及更早的条目(含 300 token 上限下
|
|
23
|
+
* 产生的截断文本)全部丢弃重建。
|
|
23
24
|
*/
|
|
24
|
-
export const INTRO_STORE_VERSION =
|
|
25
|
+
export const INTRO_STORE_VERSION = 4;
|
|
25
26
|
|
|
26
27
|
/** 介绍语言:跟随界面语言,只支持这两档。 */
|
|
27
28
|
export const INTRO_LANGS = Object.freeze(['zh', 'en']);
|
|
@@ -31,20 +32,30 @@ export const INTRO_LIMITS = Object.freeze({
|
|
|
31
32
|
minInputBytes: 8 * 1024,
|
|
32
33
|
maxInputBytes: 48 * 1024,
|
|
33
34
|
minOutputTokens: 96,
|
|
34
|
-
maxOutputTokens:
|
|
35
|
+
maxOutputTokens: 8_192,
|
|
35
36
|
minTimeoutMs: 5_000,
|
|
36
37
|
maxTimeoutMs: 60_000,
|
|
37
38
|
/** 单条简介的字符上限(提示词要 160 字并含触发句,留出余量后兜底) */
|
|
38
39
|
maxIntroChars: 400,
|
|
39
40
|
});
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 输出预算的保底值:`maxOutputTokens` 是「防跑飞上限」,不是长度目标。
|
|
44
|
+
* 推理模型的思考 token 与正文共用同一份输出预算,300 这种小上限会被思考吃光;
|
|
45
|
+
* 因此推理模型至少 4096,普通模型至少 1024(400 字符中文也可能超过 300 token)。
|
|
46
|
+
*/
|
|
47
|
+
export const INTRO_OUTPUT_FLOOR = Object.freeze({
|
|
48
|
+
plain: 1_024,
|
|
49
|
+
reasoning: 4_096,
|
|
50
|
+
});
|
|
51
|
+
|
|
41
52
|
export const DEFAULT_INTRO_CONFIG = Object.freeze({
|
|
42
53
|
enabled: true,
|
|
43
54
|
provider: null,
|
|
44
55
|
model: null,
|
|
45
56
|
maxInputBytes: 24 * 1024,
|
|
46
|
-
maxOutputTokens:
|
|
47
|
-
timeoutMs:
|
|
57
|
+
maxOutputTokens: 4_096,
|
|
58
|
+
timeoutMs: 60_000,
|
|
48
59
|
});
|
|
49
60
|
|
|
50
61
|
/** 调用/准备阶段的失败,由 Service 转成带 HTTP 状态的错误。 */
|
|
@@ -326,7 +337,7 @@ function failureFrom(error) {
|
|
|
326
337
|
* @param {{system: string, input: string}} options.prompt
|
|
327
338
|
* @param {number} options.maxOutputTokens
|
|
328
339
|
* @param {AbortSignal} [options.signal]
|
|
329
|
-
* @returns {Promise<{ text: string, usage: object|null, provider: string, model: string }>}
|
|
340
|
+
* @returns {Promise<{ text: string, usage: object|null, provider: string, model: string, maxTokens: number, reasoningEffort: string|null, finish: string }>}
|
|
330
341
|
*/
|
|
331
342
|
export async function runIntroCall({ llm, route, prompt, maxOutputTokens, signal }) {
|
|
332
343
|
if (!llm || (typeof llm.prepareCall !== 'function' && typeof llm.stream !== 'function')) {
|
|
@@ -345,16 +356,29 @@ export async function runIntroCall({ llm, route, prompt, maxOutputTokens, signal
|
|
|
345
356
|
},
|
|
346
357
|
];
|
|
347
358
|
|
|
359
|
+
// 输出上限是「防跑飞预算」,不是长度目标:推理 token 也计入这里。
|
|
360
|
+
// 推理模型至少 4096,普通模型至少 1024,避免思考吃光预算后正文为空。
|
|
361
|
+
const requestedMaxTokens = Math.max(
|
|
362
|
+
Number.isFinite(maxOutputTokens) ? maxOutputTokens : DEFAULT_INTRO_CONFIG.maxOutputTokens,
|
|
363
|
+
INTRO_OUTPUT_FLOOR.plain,
|
|
364
|
+
);
|
|
365
|
+
|
|
348
366
|
let call = null;
|
|
349
367
|
try {
|
|
350
368
|
if (typeof llm.prepareCall === 'function') {
|
|
351
369
|
call = await llm.prepareCall(
|
|
352
|
-
{ provider: route.provider, model: route.model, maxTokens:
|
|
370
|
+
{ provider: route.provider, model: route.model, maxTokens: requestedMaxTokens },
|
|
353
371
|
signal,
|
|
354
372
|
(controls, model) => {
|
|
373
|
+
const floor = model?.reasoning ? INTRO_OUTPUT_FLOOR.reasoning : INTRO_OUTPUT_FLOOR.plain;
|
|
374
|
+
const modelCap = Number.isFinite(model?.defaultMaxTokens) ? model.defaultMaxTokens : Infinity;
|
|
355
375
|
// 介绍不需要长思考:取适配器公布的最低推理强度,压低首字延迟与成本。
|
|
356
376
|
const lowest = model?.reasoning?.efforts?.[0]?.id;
|
|
357
|
-
return
|
|
377
|
+
return {
|
|
378
|
+
...controls,
|
|
379
|
+
maxTokens: Math.max(controls.maxTokens ?? 0, Math.min(floor, modelCap)),
|
|
380
|
+
...(lowest ? { reasoningEffort: lowest } : {}),
|
|
381
|
+
};
|
|
358
382
|
},
|
|
359
383
|
);
|
|
360
384
|
}
|
|
@@ -362,42 +386,96 @@ export async function runIntroCall({ llm, route, prompt, maxOutputTokens, signal
|
|
|
362
386
|
throw failureFrom(error);
|
|
363
387
|
}
|
|
364
388
|
|
|
389
|
+
const fallbackMaxTokens = Math.max(requestedMaxTokens, INTRO_OUTPUT_FLOOR.reasoning);
|
|
365
390
|
const options = call
|
|
366
391
|
? { ...call.config, messages, system: prompt.system, signal }
|
|
367
392
|
: {
|
|
368
393
|
provider: route.provider,
|
|
369
394
|
model: route.model,
|
|
370
|
-
maxTokens:
|
|
395
|
+
maxTokens: fallbackMaxTokens,
|
|
371
396
|
messages,
|
|
372
397
|
system: prompt.system,
|
|
373
398
|
signal,
|
|
374
399
|
};
|
|
375
400
|
|
|
376
|
-
let
|
|
401
|
+
let deltaText = '';
|
|
402
|
+
let sawTextDelta = false;
|
|
403
|
+
const textBlocks = new Map();
|
|
404
|
+
let reasoningChars = 0;
|
|
377
405
|
let usage = null;
|
|
378
406
|
let finish = null;
|
|
379
407
|
try {
|
|
380
408
|
const stream = call ? call.stream(options) : llm.stream(options);
|
|
381
409
|
for await (const chunk of stream) {
|
|
382
410
|
if (!chunk || typeof chunk !== 'object') continue;
|
|
383
|
-
if (chunk.type === 'text-delta')
|
|
384
|
-
|
|
385
|
-
|
|
411
|
+
if (chunk.type === 'text-delta') {
|
|
412
|
+
sawTextDelta = true;
|
|
413
|
+
if (typeof chunk.text === 'string') deltaText += chunk.text;
|
|
414
|
+
} else if (chunk.type === 'reasoning-delta') {
|
|
415
|
+
if (typeof chunk.text === 'string') reasoningChars += chunk.text.length;
|
|
416
|
+
} else if (chunk.type === 'block-end') {
|
|
417
|
+
const block = chunk.block;
|
|
418
|
+
if (block?.type === 'text' && typeof block.text === 'string') {
|
|
419
|
+
textBlocks.set(chunk.index, block.text);
|
|
420
|
+
} else if (block?.type === 'reasoning' && typeof block.text === 'string') {
|
|
421
|
+
reasoningChars = Math.max(reasoningChars, block.text.length);
|
|
422
|
+
}
|
|
423
|
+
} else if (chunk.type === 'usage') {
|
|
424
|
+
usage = chunk.usage || null;
|
|
425
|
+
} else if (chunk.type === 'finish') {
|
|
426
|
+
finish = chunk.reason || null;
|
|
427
|
+
}
|
|
386
428
|
}
|
|
387
429
|
} catch (error) {
|
|
388
430
|
throw failureFrom(error);
|
|
389
431
|
}
|
|
390
432
|
|
|
391
|
-
|
|
392
|
-
|
|
433
|
+
const effectiveMaxTokens = Number.isFinite(options.maxTokens) ? options.maxTokens : requestedMaxTokens;
|
|
434
|
+
const kind = finish?.kind || 'stop';
|
|
435
|
+
|
|
436
|
+
if (kind === 'aborted') {
|
|
437
|
+
throw failureFrom({ failure: finish?.failure || null, code: 'ABORTED' });
|
|
438
|
+
}
|
|
439
|
+
if (kind === 'error') {
|
|
440
|
+
throw failureFrom({ failure: finish?.failure || null, code: null });
|
|
441
|
+
}
|
|
442
|
+
if (kind === 'max-tokens') {
|
|
443
|
+
throw new IntroError(
|
|
444
|
+
'error.skills.intro-truncated',
|
|
445
|
+
'简介达到输出上限被截断,请重试或调大 intro.maxOutputTokens',
|
|
446
|
+
{ finish: kind, maxTokens: effectiveMaxTokens, usage },
|
|
447
|
+
502,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (kind === 'tool-calls') {
|
|
451
|
+
throw new IntroError(
|
|
452
|
+
'error.skills.intro-no-text',
|
|
453
|
+
'模型没有输出文字简介(尝试调用工具),请重试',
|
|
454
|
+
{ finish: kind, maxTokens: effectiveMaxTokens, usage },
|
|
455
|
+
502,
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (kind !== 'stop') {
|
|
459
|
+
throw failureFrom({ failure: finish?.failure || null, code: null });
|
|
393
460
|
}
|
|
394
461
|
|
|
462
|
+
// 块式适配器可能不给 text-delta,回退用 block-end 里的 text block。
|
|
463
|
+
const text = sawTextDelta ? deltaText : [...textBlocks.values()].join('');
|
|
395
464
|
const cleaned = cleanIntroText(text);
|
|
396
465
|
if (!cleaned) {
|
|
466
|
+
const produced = reasoningChars > 0 || Number(usage?.outputTokens) > 0;
|
|
467
|
+
if (produced) {
|
|
468
|
+
throw new IntroError(
|
|
469
|
+
'error.skills.intro-reasoning-only',
|
|
470
|
+
'模型只输出了思考过程、没有输出简介正文,请提高输出上限或换一个非推理模型',
|
|
471
|
+
{ finish: kind, maxTokens: effectiveMaxTokens, usage, reasoningChars },
|
|
472
|
+
502,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
397
475
|
throw new IntroError(
|
|
398
476
|
'error.skills.intro-empty',
|
|
399
|
-
'
|
|
400
|
-
{ finish:
|
|
477
|
+
'模型没有返回内容,请重试',
|
|
478
|
+
{ finish: kind, maxTokens: effectiveMaxTokens, usage },
|
|
401
479
|
502,
|
|
402
480
|
);
|
|
403
481
|
}
|
|
@@ -407,6 +485,9 @@ export async function runIntroCall({ llm, route, prompt, maxOutputTokens, signal
|
|
|
407
485
|
usage,
|
|
408
486
|
provider: route.provider,
|
|
409
487
|
model: route.model,
|
|
488
|
+
maxTokens: effectiveMaxTokens,
|
|
489
|
+
reasoningEffort: options.reasoningEffort || null,
|
|
490
|
+
finish: kind,
|
|
410
491
|
};
|
|
411
492
|
}
|
|
412
493
|
|
|
@@ -416,7 +497,18 @@ export function introKey(name, lang) {
|
|
|
416
497
|
}
|
|
417
498
|
|
|
418
499
|
/** 汇总一次生成的元信息(缓存条目与响应共用)。 */
|
|
419
|
-
export function introEntry({
|
|
500
|
+
export function introEntry({
|
|
501
|
+
text,
|
|
502
|
+
hash,
|
|
503
|
+
provider,
|
|
504
|
+
model,
|
|
505
|
+
usage,
|
|
506
|
+
inputBytes,
|
|
507
|
+
truncated,
|
|
508
|
+
sourceBytes,
|
|
509
|
+
maxTokens = null,
|
|
510
|
+
finish = 'stop',
|
|
511
|
+
}) {
|
|
420
512
|
return {
|
|
421
513
|
hash,
|
|
422
514
|
text,
|
|
@@ -427,6 +519,8 @@ export function introEntry({ text, hash, provider, model, usage, inputBytes, tru
|
|
|
427
519
|
inputBytes,
|
|
428
520
|
truncated: Boolean(truncated),
|
|
429
521
|
sourceBytes: sourceBytes ?? inputBytes,
|
|
522
|
+
maxTokens: Number.isFinite(maxTokens) ? maxTokens : null,
|
|
523
|
+
finish,
|
|
430
524
|
};
|
|
431
525
|
}
|
|
432
526
|
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* 设计取舍(对齐本机同类插件 archive-manager / ds-bot 的轻量做法,但更保守):
|
|
14
14
|
* - 依次问 registry 计划里的 `<base><pkg>/latest`,8s 超时,只取 `version` 字段;失败不抛,返回 null。
|
|
15
15
|
* - 版本比较手写(本仓 runtime 依赖只有 tar,不引入 semver);结构非法一律判「不更新」。
|
|
16
|
-
* - 安装只走宿主 `pluginManager.installBundle
|
|
17
|
-
* `
|
|
18
|
-
* -
|
|
16
|
+
* - 安装只走宿主 `pluginManager.installBundle`(桌面端官方能力);HMR 下已安装的包会被以
|
|
17
|
+
* `already-installed` 拒绝,此时按官方更新路径先 `removeBundle` 再 `installBundle`;拿不到
|
|
18
|
+
* 这些能力就抛 `error.plugin-update.manual`,把手动命令放进 detail 让前端展示/复制。
|
|
19
|
+
* - 不落盘、不加锁:本模块用 `applying` 单飞(进入 `apply()` 立即置位,覆盖查询 + 安装全程);重启由用户决定。
|
|
19
20
|
*/
|
|
20
21
|
import { readFileSync } from 'node:fs';
|
|
21
22
|
|
|
@@ -101,6 +102,26 @@ function manualCommand(meta, profileName) {
|
|
|
101
102
|
: `dsh plugin add ${spec}`;
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 宿主 `installBundle` / `removeBundle` 的结果两种形态都要接:
|
|
107
|
+
* 本地服务直接返回 `{ application, error? }`;远程代理可能再包一层 `{ ok, value | error }`。
|
|
108
|
+
*/
|
|
109
|
+
function unwrapResult(result) {
|
|
110
|
+
if (
|
|
111
|
+
result && typeof result === 'object'
|
|
112
|
+
&& typeof result.ok === 'boolean'
|
|
113
|
+
&& !('application' in result)
|
|
114
|
+
&& ('value' in result || 'error' in result)
|
|
115
|
+
) {
|
|
116
|
+
return result.ok ? result.value : { application: 'failed', error: result.error ?? { code: 'operation-error' } };
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function failureMessage(result) {
|
|
122
|
+
return result?.error?.diagnostic || result?.error?.message || result?.error?.code || '更新失败';
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
/** registry 归一成 `https://host/` 形式;非法返回 null。 */
|
|
105
126
|
function normalizeRegistry(value) {
|
|
106
127
|
if (typeof value !== 'string' || value.trim() === '') return null;
|
|
@@ -241,33 +262,45 @@ export function createPluginUpdate({ getPluginManager, getProfileName, fetchImpl
|
|
|
241
262
|
if (applying) {
|
|
242
263
|
throw new ServiceError('error.plugin-update.busy', '插件正在更新,请稍候', null, 409);
|
|
243
264
|
}
|
|
244
|
-
|
|
245
|
-
if (latest === null) {
|
|
246
|
-
throw new ServiceError('error.plugin-update.check-failed', '暂时无法获取最新版本,请稍后再试', manualCommand(meta, profileName()), 503);
|
|
247
|
-
}
|
|
248
|
-
if (!isNewerVersion(meta.version, latest)) {
|
|
249
|
-
return { ...view({ latest }), updatedVersion: null };
|
|
250
|
-
}
|
|
251
|
-
const pm = manager();
|
|
252
|
-
if (pm === null) {
|
|
253
|
-
throw new ServiceError(
|
|
254
|
-
'error.plugin-update.manual',
|
|
255
|
-
'当前环境不支持自动更新,请手动执行命令',
|
|
256
|
-
manualCommand(meta, profileName()),
|
|
257
|
-
409,
|
|
258
|
-
);
|
|
259
|
-
}
|
|
265
|
+
// 进入即置位:先查最新版也有 await,若等查到再置位,两次并发点击会都通过守卫、装两遍
|
|
260
266
|
applying = true;
|
|
261
267
|
try {
|
|
268
|
+
const latest = await latestVersion();
|
|
269
|
+
if (latest === null) {
|
|
270
|
+
throw new ServiceError('error.plugin-update.check-failed', '暂时无法获取最新版本,请稍后再试', manualCommand(meta, profileName()), 503);
|
|
271
|
+
}
|
|
272
|
+
if (!isNewerVersion(meta.version, latest)) {
|
|
273
|
+
return { ...view({ latest }), updatedVersion: null, applying: false };
|
|
274
|
+
}
|
|
275
|
+
const pm = manager();
|
|
276
|
+
if (pm === null) {
|
|
277
|
+
throw new ServiceError(
|
|
278
|
+
'error.plugin-update.manual',
|
|
279
|
+
'当前环境不支持自动更新,请手动执行命令',
|
|
280
|
+
manualCommand(meta, profileName()),
|
|
281
|
+
409,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
262
284
|
// 用查到最新版的那个 registry 去装,和「问」保持一致;没有就用宿主默认(不传 options)
|
|
263
285
|
const registry = cache?.version === latest ? cache.registry : undefined;
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
const spec = `${meta.name}@${latest}`;
|
|
287
|
+
const installOptions = registry ? { registry } : undefined;
|
|
288
|
+
let result = unwrapResult(await pm.installBundle(spec, installOptions));
|
|
289
|
+
// HMR 开启时,installBundle 对已安装的包名会在跑 pnpm 前直接拒绝 already-installed;
|
|
290
|
+
// 官方页面的「更新」就是先 removeBundle 再 installBundle,这里照同一路径做显式更新。
|
|
291
|
+
if (result?.error?.code === 'already-installed' && typeof pm.removeBundle === 'function') {
|
|
292
|
+
const removed = unwrapResult(await pm.removeBundle(meta.name));
|
|
293
|
+
if (removed?.error || removed?.application === 'failed') {
|
|
294
|
+
const code = removed?.error?.code;
|
|
295
|
+
const message = code === 'stop-profile'
|
|
296
|
+
? '插件正在运行且当前 profile 未启用 HMR,请完全退出 DSH 后手动更新'
|
|
297
|
+
: failureMessage(removed);
|
|
298
|
+
throw new ServiceError('error.plugin-update.install-failed', String(message).slice(0, 600), manualCommand(meta, profileName()), 500);
|
|
299
|
+
}
|
|
300
|
+
result = unwrapResult(await pm.installBundle(spec, installOptions));
|
|
301
|
+
}
|
|
268
302
|
if (result?.error || !['applied', 'restart-required', 'overridden'].includes(result?.application)) {
|
|
269
|
-
|
|
270
|
-
throw new ServiceError('error.plugin-update.install-failed', String(message).slice(0, 600), manualCommand(meta, profileName()), 500);
|
|
303
|
+
throw new ServiceError('error.plugin-update.install-failed', String(failureMessage(result)).slice(0, 600), manualCommand(meta, profileName()), 500);
|
|
271
304
|
}
|
|
272
305
|
cache = { version: latest, expiresAt: now() + CACHE_TTL_MS, registry };
|
|
273
306
|
return {
|
|
@@ -275,6 +308,7 @@ export function createPluginUpdate({ getPluginManager, getProfileName, fetchImpl
|
|
|
275
308
|
updateAvailable: false,
|
|
276
309
|
updatedVersion: latest,
|
|
277
310
|
restartRequired: true,
|
|
311
|
+
applying: false,
|
|
278
312
|
};
|
|
279
313
|
} finally {
|
|
280
314
|
applying = false;
|
package/lib/core/service.js
CHANGED
|
@@ -1514,6 +1514,8 @@ export function createService({
|
|
|
1514
1514
|
inputBytes: prompt.inputBytes,
|
|
1515
1515
|
truncated: prompt.truncated,
|
|
1516
1516
|
sourceBytes: prompt.sourceBytes,
|
|
1517
|
+
maxTokens: result.maxTokens,
|
|
1518
|
+
finish: result.finish,
|
|
1517
1519
|
});
|
|
1518
1520
|
introStore.put(name, language, entry);
|
|
1519
1521
|
logger('intro.generate', {
|
|
@@ -1521,6 +1523,9 @@ export function createService({
|
|
|
1521
1523
|
lang: language,
|
|
1522
1524
|
provider: result.provider,
|
|
1523
1525
|
model: result.model,
|
|
1526
|
+
effort: result.reasoningEffort || null,
|
|
1527
|
+
maxTokens: result.maxTokens ?? null,
|
|
1528
|
+
finish: result.finish || null,
|
|
1524
1529
|
inputBytes: prompt.inputBytes,
|
|
1525
1530
|
truncated: prompt.truncated,
|
|
1526
1531
|
durationMs: Date.now() - startedAt,
|
|
@@ -1536,7 +1541,12 @@ export function createService({
|
|
|
1536
1541
|
};
|
|
1537
1542
|
} catch (error) {
|
|
1538
1543
|
if (!(error instanceof ServiceError)) {
|
|
1539
|
-
logger('intro.generate.failed', {
|
|
1544
|
+
logger('intro.generate.failed', {
|
|
1545
|
+
name,
|
|
1546
|
+
message: error?.message,
|
|
1547
|
+
code: error?.code,
|
|
1548
|
+
detail: error?.detail || null,
|
|
1549
|
+
});
|
|
1540
1550
|
}
|
|
1541
1551
|
throw error;
|
|
1542
1552
|
}
|
|
@@ -1648,6 +1658,8 @@ export function createService({
|
|
|
1648
1658
|
inputBytes: prompt.inputBytes,
|
|
1649
1659
|
truncated: prompt.truncated,
|
|
1650
1660
|
sourceBytes: prompt.sourceBytes,
|
|
1661
|
+
maxTokens: result.maxTokens,
|
|
1662
|
+
finish: result.finish,
|
|
1651
1663
|
});
|
|
1652
1664
|
introStore.put(target.cacheKey, language, entry);
|
|
1653
1665
|
logger('intro.generate.remote', {
|
|
@@ -1657,6 +1669,9 @@ export function createService({
|
|
|
1657
1669
|
commit: target.commit,
|
|
1658
1670
|
provider: result.provider,
|
|
1659
1671
|
model: result.model,
|
|
1672
|
+
effort: result.reasoningEffort || null,
|
|
1673
|
+
maxTokens: result.maxTokens ?? null,
|
|
1674
|
+
finish: result.finish || null,
|
|
1660
1675
|
inputBytes: prompt.inputBytes,
|
|
1661
1676
|
truncated: prompt.truncated,
|
|
1662
1677
|
durationMs: Date.now() - startedAt,
|
|
@@ -1681,6 +1696,7 @@ export function createService({
|
|
|
1681
1696
|
name,
|
|
1682
1697
|
message: error?.message,
|
|
1683
1698
|
code: error?.code,
|
|
1699
|
+
detail: error?.detail || null,
|
|
1684
1700
|
});
|
|
1685
1701
|
}
|
|
1686
1702
|
throw error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morehao/dsh-skills-console",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "统一管理本地技能的安装、更新、删除与接入,让同一份技能在多个 Agent 间复用。A skill manager for DeepSeek Harness: install, update, remove, link and toggle agent skills.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "morehao <morehao@qq.com>",
|