@morehao/dsh-skills-console 0.5.5 → 0.5.6
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 +4 -2
- package/README.md +4 -2
- package/client.js +133 -50
- package/lib/core/intro-store.js +5 -2
- package/lib/core/intro.js +112 -18
- package/lib/core/service.js +17 -1
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -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
|
@@ -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") {
|
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
|
|
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.6",
|
|
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>",
|