dsh-armory 1.9.1 → 1.9.2
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/lib/client.js +66 -94
- package/lib/index.js +27 -0
- package/lib/session-persona.js +120 -0
- package/lib/typert.js +5 -0
- package/lib/whats-new.js +13 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -375,9 +375,6 @@ const MCP_CSS = `body{--mcp-accent:var(--dsw-alias-brand-primary);--mcp-ok:var(-
|
|
|
375
375
|
.dsh-mcp-quick-note{padding:7px 11px;margin-bottom:4px;border-radius:var(--mcp-r-sm);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);font-size:11px;line-height:15px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;white-space:normal;word-break:break-word}
|
|
376
376
|
.dsh-mcp-quick-note.err{color:var(--mcp-err)}
|
|
377
377
|
.dsh-mcp-quick-note.ok{color:var(--mcp-ok)}
|
|
378
|
-
.dsh-mcp-quick-new-session{width:calc(100% - 8px);margin:0 4px 6px;border:1px solid color-mix(in srgb,var(--mcp-accent) 35%,transparent);border-radius:var(--mcp-r-sm);background:color-mix(in srgb,var(--mcp-accent) 10%,transparent);color:var(--mcp-accent);font:inherit;font-size:12px;line-height:26px;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap;overflow:hidden}
|
|
379
|
-
.dsh-mcp-quick-new-session:hover{background:color-mix(in srgb,var(--mcp-accent) 18%,transparent)}
|
|
380
|
-
.dsh-mcp-quick-new-session span{overflow:hidden;text-overflow:ellipsis}
|
|
381
378
|
.dsh-mcp-quick-err{color:var(--mcp-err)}
|
|
382
379
|
/* ── 主面板页(main slot,和「插件」同级的入口页)────────────────────────
|
|
383
380
|
入口型页面要用 --dsh-frame-top-clearance 让出窗口顶带(官方 ui-layout 的约定:
|
|
@@ -2859,7 +2856,7 @@ const ExpertMarket = ({ call }) => {
|
|
|
2859
2856
|
const QUICK_MENU_KINDS = [
|
|
2860
2857
|
{ key: 'skill', label: '技能', icon: 'skill', hint: '技能按需触发:选择后以 /技能名 插入输入框' },
|
|
2861
2858
|
{ key: 'mcp', label: 'MCP', icon: 'plug', hint: 'MCP 工具按 server 生效:选择后点名 mcp__服务__* 工具' },
|
|
2862
|
-
{ key: 'expert', label: '专家', icon: 'expert', hint: '
|
|
2859
|
+
{ key: 'expert', label: '专家', icon: 'expert', hint: '专家随时挂/换/摘:点一个即接管本会话,再点别的直接换,点 chip 的 × 取消' },
|
|
2863
2860
|
];
|
|
2864
2861
|
const QUICK_RECENT_LIMIT = 6;
|
|
2865
2862
|
// 引用文本格式(对齐宿主真实语义,避免「同名空文档」):
|
|
@@ -3042,109 +3039,92 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
3042
3039
|
const itemLabel = (kind, item) => (kind === 'expert' ? item.name || item.plugin : item.name);
|
|
3043
3040
|
const itemDesc = (kind, item) => (kind === 'mcp' ? (item.transport ? item.transport : '') : (item.description || '').slice(0, 48));
|
|
3044
3041
|
|
|
3045
|
-
// ── 挂专家(WorkBuddy
|
|
3046
|
-
//
|
|
3047
|
-
//
|
|
3048
|
-
//
|
|
3042
|
+
// ── 挂专家(WorkBuddy 式:随时挂 / 换 / 摘)──────────────────────────────
|
|
3043
|
+
// 主通道是宿主新的会话级人设 RPC expertAttach/expertDetach:它把专家人设作为
|
|
3044
|
+
// systemPrompt 的「本会话作用域 section」注入(与 dsh-persona / dsh-subagent 同款
|
|
3045
|
+
// deployment:persona-prefix 遮蔽机制),因此:
|
|
3046
|
+
// - 不受 agent-preset/locked 限制 —— 会话已开过口也能挂、能换、能摘;
|
|
3047
|
+
// - 只影响这一个会话,不动工具目录、不写任何持久化配置;
|
|
3048
|
+
// - 反复点不同专家 = 直接替换(Host 先摘旧的再装新的),不再是一次性。
|
|
3049
|
+
// 预设通道(agentPresets.select)作为 blank 会话的增强:它连工具目录一起接管,语义更
|
|
3050
|
+
// 彻底;只在「该专家已被召唤 + 会话还是空白」时成功,失败就自动退回人设通道——
|
|
3051
|
+
// 两条路都不通的情况不存在,所以「换专家」永远有出路。
|
|
3049
3052
|
// sessionId 来源:conversation.input.right 是 session 座位,slot 渲染时把 sessionId
|
|
3050
3053
|
// 作为 prop 显式传入(apply() 注册处 inject: (sessionId) => ({ call, sessionId }));
|
|
3051
3054
|
// 会话 summary 行本身不带 id 字段(id 只走 props),所以不能从 useSession 投影里取。
|
|
3052
3055
|
const MODULE = MODULE_CTX.ctx;
|
|
3053
|
-
const [attached, setAttached] = useState(null); // { plugin, name,
|
|
3056
|
+
const [attached, setAttached] = useState(null); // { plugin, name, mode: 'persona'|'preset' }
|
|
3054
3057
|
const [attachBusy, setAttachBusy] = useState(false);
|
|
3055
3058
|
const [attachNote, setAttachNote] = useState('');
|
|
3056
|
-
// locked 后的「带专家新建会话」待办:宿主硬规则是已开始的会话不能换预设
|
|
3057
|
-
// (agent-preset/locked),但官方 hero 预设选择器对 blank 会话可用——所以
|
|
3058
|
-
// 提供「新建会话并挂上该专家」的一键路径:sessions.create() → agentPresets.select() →
|
|
3059
|
-
// uiWorkspace.openSession(),与官方「新会话选预设」语义完全一致,只是自动化了。
|
|
3060
|
-
const [pendingExpert, setPendingExpert] = useState(null);
|
|
3061
3059
|
const sessionId = typeof sessionIdProp === 'string' && sessionIdProp !== '' ? sessionIdProp : null;
|
|
3062
3060
|
const expertPresetIdOf = (item) => 'wbexpert-' + String(item.plugin || '').toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
3063
|
-
const
|
|
3064
|
-
const ctx = MODULE;
|
|
3065
|
-
if (!ctx || typeof ctx.get !== 'function') { setAttachNote('宿主上下文不可用,无法新建会话'); return; }
|
|
3066
|
-
const sessions = ctx.get('sessions');
|
|
3067
|
-
const uiWorkspace = ctx.get('uiWorkspace');
|
|
3068
|
-
const remote = ctx.get('remote.agentPresets');
|
|
3069
|
-
if (!sessions || typeof sessions.create !== 'function') { setAttachNote('宿主未提供会话创建接口(sessions 不可用)'); return; }
|
|
3070
|
-
if (!uiWorkspace || typeof uiWorkspace.openSession !== 'function') { setAttachNote('宿主未提供会话导航接口(uiWorkspace 不可用)'); return; }
|
|
3071
|
-
if (!remote || typeof remote.select !== 'function') { setAttachNote('宿主未提供 agentPresets 接口(remote.agentPresets 不可用)'); return; }
|
|
3072
|
-
setAttachBusy(true); setAttachNote('');
|
|
3073
|
-
try {
|
|
3074
|
-
// 继承当前会话的工作目录(拿不到就让宿主用默认 workspace)。
|
|
3075
|
-
const cwd = sessionId && sessions.list && typeof sessions.list.getSnapshot === 'function'
|
|
3076
|
-
? sessions.list.getSnapshot().byId[sessionId]?.cwd : undefined;
|
|
3077
|
-
// ClientSessions.create 成功直接返回 sessionId 字符串,失败抛 SessionCreateError
|
|
3078
|
-
// (workspace-client 的 reuseOrCreateBlank 同款走法)。
|
|
3079
|
-
const newId = await sessions.create(cwd ? { cwd } : {});
|
|
3080
|
-
if (typeof newId !== 'string' || !newId) throw new Error('创建会话未返回会话 id');
|
|
3081
|
-
const presetId = expertPresetIdOf(item);
|
|
3082
|
-
const selected = await remote.select(newId, presetId);
|
|
3083
|
-
if (!selected || !selected.ok) {
|
|
3084
|
-
throw new Error('专家预设挂载失败:' + ((selected && selected.error && (selected.error.message || selected.error.code)) || '未知错误'));
|
|
3085
|
-
}
|
|
3086
|
-
uiWorkspace.openSession(newId);
|
|
3087
|
-
setAttached({ plugin: item.plugin, name: item.name || item.plugin, presetId: (selected.value && selected.value) || presetId });
|
|
3088
|
-
setPendingExpert(null);
|
|
3089
|
-
setAttachNote('✓ 已新建专家会话:' + (item.name || item.plugin) + '(人设全程接管)');
|
|
3090
|
-
call('wbExpertRecordUse', { kind: 'expert', row: { plugin: item.plugin, name: item.name || item.plugin } }).catch(() => {});
|
|
3091
|
-
} catch (e) {
|
|
3092
|
-
setAttachNote('新建专家会话失败:' + String((e && e.message) || e));
|
|
3093
|
-
} finally {
|
|
3094
|
-
setAttachBusy(false);
|
|
3095
|
-
}
|
|
3096
|
-
};
|
|
3097
|
-
const attachExpert = async (item) => {
|
|
3061
|
+
const attachExpert = async (item, { quiet = false } = {}) => {
|
|
3098
3062
|
setHoverKind(null);
|
|
3099
|
-
|
|
3100
|
-
// 结果显示回菜单内(菜单不关):成功 ✓ + 挂载名;失败显示真实报错。
|
|
3101
|
-
// 上一版把 attachNote 从界面拿掉导致失败无声,用户无从判断「为什么没显示」。
|
|
3102
|
-
if (!sessionId) { setPendingExpert(item); setAttachNote('没有活动会话:可直接带该专家新建会话'); return; }
|
|
3063
|
+
if (!sessionId) { setAttachNote('没有活动会话:请先进入一个会话再挂专家'); return; }
|
|
3103
3064
|
const presetId = expertPresetIdOf(item);
|
|
3104
|
-
const remote = MODULE && typeof MODULE.get === 'function' ? MODULE.get('remote.agentPresets') : null;
|
|
3105
|
-
if (!remote || typeof remote.select !== 'function') { setAttachNote('宿主未提供 agentPresets 接口(remote.agentPresets 不可用)'); return; }
|
|
3106
3065
|
setAttachBusy(true); setAttachNote('');
|
|
3107
3066
|
try {
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3067
|
+
// ① blank 会话 + 已召唤 → 先用预设通道(连工具目录一起接管,最彻底)。
|
|
3068
|
+
const remote = MODULE && typeof MODULE.get === 'function' ? MODULE.get('remote.agentPresets') : null;
|
|
3069
|
+
if (remote && typeof remote.select === 'function') {
|
|
3070
|
+
const result = await remote.select(sessionId, presetId).catch(() => null);
|
|
3071
|
+
if (result && result.ok) {
|
|
3072
|
+
setAttached({ plugin: item.plugin, name: item.name || item.plugin, mode: 'preset' });
|
|
3073
|
+
setAttachNote('✓ 已挂到当前会话:' + (item.name || item.plugin));
|
|
3074
|
+
call('wbExpertRecordUse', { kind: 'expert', row: { plugin: item.plugin, name: item.name || item.plugin } }).catch(() => {});
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
// ② 其余情况(会话已开始 / 未召唤 / 无 agentPresets)→ 会话级人设通道,必定可用。
|
|
3079
|
+
const res = await call('expertAttach', { sessionId, plugin: item.plugin });
|
|
3080
|
+
if (res && res.ok) {
|
|
3081
|
+
setAttached({ plugin: item.plugin, name: res.name || item.name || item.plugin, mode: 'persona' });
|
|
3082
|
+
setAttachNote('✓ 已挂到当前会话:' + (res.name || item.name || item.plugin) + (res.replaced ? '(已替换原专家)' : ''));
|
|
3112
3083
|
call('wbExpertRecordUse', { kind: 'expert', row: { plugin: item.plugin, name: item.name || item.plugin } }).catch(() => {});
|
|
3113
3084
|
} else {
|
|
3114
|
-
|
|
3115
|
-
if (locked) {
|
|
3116
|
-
// 宿主硬规则:会话已开始(有 turn)就不允许换预设。给出一键替代路径
|
|
3117
|
-
// 而不是让用户自己去预设选择器里翻——见 createWithExpert。
|
|
3118
|
-
setPendingExpert(item);
|
|
3119
|
-
setAttachNote('该会话已开始对话,无法中途挂专家(宿主规则:预设只在空白会话可换);可一键带该专家新建会话');
|
|
3120
|
-
} else {
|
|
3121
|
-
setAttachNote('挂载失败:' + ((result && result.error && (result.error.message || result.error.code)) || JSON.stringify(result && result.error) || '未知错误'));
|
|
3122
|
-
}
|
|
3085
|
+
setAttachNote('挂载失败:' + ((res && res.error) || '未知错误'));
|
|
3123
3086
|
}
|
|
3124
3087
|
} catch (e) {
|
|
3125
|
-
setAttachNote('挂载异常:' + String((e && e.message) || e));
|
|
3088
|
+
if (!quiet) setAttachNote('挂载异常:' + String((e && e.message) || e));
|
|
3126
3089
|
} finally {
|
|
3127
3090
|
setAttachBusy(false);
|
|
3128
3091
|
}
|
|
3129
3092
|
};
|
|
3130
3093
|
const detachExpert = async () => {
|
|
3131
|
-
if (!
|
|
3132
|
-
|
|
3133
|
-
if (!remote || typeof remote.list !== 'function' || typeof remote.select !== 'function') { setAttached(null); return; }
|
|
3134
|
-
setAttachBusy(true);
|
|
3094
|
+
if (!sessionId) return;
|
|
3095
|
+
setAttachBusy(true); setAttachNote('');
|
|
3135
3096
|
try {
|
|
3136
|
-
|
|
3137
|
-
const
|
|
3138
|
-
const
|
|
3139
|
-
if (
|
|
3097
|
+
// 会话级人设一定清(不管当初走的是哪条路);预设通道只在当初走预设时回退默认。
|
|
3098
|
+
const res = await call('expertDetach', { sessionId }).catch(() => null);
|
|
3099
|
+
const remote = MODULE && typeof MODULE.get === 'function' ? MODULE.get('remote.agentPresets') : null;
|
|
3100
|
+
if (attached?.mode === 'preset' && remote && typeof remote.list === 'function' && typeof remote.select === 'function') {
|
|
3101
|
+
try {
|
|
3102
|
+
const list = await remote.list();
|
|
3103
|
+
const options = list && list.ok && Array.isArray(list.presets) ? list.presets : [];
|
|
3104
|
+
const fallback = (options.find((row) => row.isDefault) || options[0] || {}).id;
|
|
3105
|
+
if (fallback) await remote.select(sessionId, fallback);
|
|
3106
|
+
} catch { /* 预设回退失败不影响「已摘掉」这一事实 */ }
|
|
3107
|
+
}
|
|
3140
3108
|
setAttached(null);
|
|
3141
|
-
setAttachNote('');
|
|
3109
|
+
setAttachNote(res && res.ok && res.removed ? '✓ 已取消本会话的专家' : '本会话没有挂着专家');
|
|
3142
3110
|
} catch (e) {
|
|
3143
|
-
setAttachNote(String((e && e.message) || e));
|
|
3111
|
+
setAttachNote('取消失败:' + String((e && e.message) || e));
|
|
3144
3112
|
} finally {
|
|
3145
3113
|
setAttachBusy(false);
|
|
3146
3114
|
}
|
|
3147
3115
|
};
|
|
3116
|
+
// 会话切换 / 组件重挂时对齐真实状态:宿主是唯一真源(专家可能由别的入口挂上,
|
|
3117
|
+
// 或会话被重启),不能只信组件自己的 state。
|
|
3118
|
+
useEffect(() => {
|
|
3119
|
+
if (!sessionId) return undefined;
|
|
3120
|
+
let cancelled = false;
|
|
3121
|
+
call('expertActive').then((res) => {
|
|
3122
|
+
if (cancelled || !res || !res.ok || !Array.isArray(res.sessions)) return;
|
|
3123
|
+
const mine = res.sessions.find((row) => row.sessionId === sessionId);
|
|
3124
|
+
setAttached(mine ? { plugin: mine.plugin, name: mine.name, mode: 'persona' } : null);
|
|
3125
|
+
}).catch(() => { /* 对齐失败保持现状:不影响使用 */ });
|
|
3126
|
+
return () => { cancelled = true; };
|
|
3127
|
+
}, [sessionId, call]);
|
|
3148
3128
|
|
|
3149
3129
|
// ── 悬停开合(整树统一管理)───────────────────────────────────────────
|
|
3150
3130
|
// enter/leave 挂组件根,而不是每行:菜单顶部还有提示条/错误行/新建会话按钮,
|
|
@@ -3187,19 +3167,6 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
3187
3167
|
copied ? h('div', { className: 'dsh-mcp-quick-note' }, '已复制,粘贴到输入框:' + copied) : null,
|
|
3188
3168
|
attachBusy ? h('div', { className: 'dsh-mcp-quick-note' }, '正在挂载专家…') : null,
|
|
3189
3169
|
attachNote ? h('div', { className: 'dsh-mcp-quick-note' + (/^✓/.test(attachNote) ? ' ok' : ' err'), role: 'alert' }, attachNote) : null,
|
|
3190
|
-
// locked / 无会话时的一键替代路径:带该专家新建会话(createWithExpert)。
|
|
3191
|
-
// 按钮挂在 attachNote 下方,只在该动作可用时出现;busy 期间禁用防连点。
|
|
3192
|
-
pendingExpert && !attachBusy
|
|
3193
|
-
? h('button', {
|
|
3194
|
-
type: 'button',
|
|
3195
|
-
className: 'dsh-mcp-quick-new-session',
|
|
3196
|
-
title: '新建一个空白会话并立即挂上「' + (pendingExpert.name || pendingExpert.plugin) + '」预设,人设全程接管',
|
|
3197
|
-
onClick: (e) => { e.stopPropagation(); createWithExpert(pendingExpert); },
|
|
3198
|
-
},
|
|
3199
|
-
h(Icon, { d: ICONS.expert, size: 12 }),
|
|
3200
|
-
h('span', null, '带「' + (pendingExpert.name || pendingExpert.plugin) + '」新建会话'),
|
|
3201
|
-
)
|
|
3202
|
-
: null,
|
|
3203
3170
|
listError ? h('div', { className: 'dsh-mcp-quick-err' }, listError) : null,
|
|
3204
3171
|
!recent && !listError ? h('div', { className: 'dsh-mcp-quick-loading' }, '正在读取…') : null,
|
|
3205
3172
|
QUICK_MENU_KINDS.map((kind) => {
|
|
@@ -3237,7 +3204,7 @@ const SummonQuickMenu = ({ call, inputActions, sessionId: sessionIdProp }) => {
|
|
|
3237
3204
|
type: 'button',
|
|
3238
3205
|
role: 'menuitem',
|
|
3239
3206
|
className: 'dsh-mcp-quick-item',
|
|
3240
|
-
title: kind.key === 'expert' ? '
|
|
3207
|
+
title: kind.key === 'expert' ? '挂到当前会话(可随时换或取消);再点别的专家 = 直接替换' : (itemDesc(kind.key, item) || itemLabel(kind.key, item)),
|
|
3241
3208
|
onClick: (e) => { e.stopPropagation(); if (kind.key === 'expert') attachExpert(item); else pick(kind.key, item, quickMentionText(kind.key, item)); },
|
|
3242
3209
|
},
|
|
3243
3210
|
kind.key === 'expert' && attached && attached.plugin === item.plugin
|
|
@@ -4336,7 +4303,12 @@ const MCP_CONTRIBUTION = {
|
|
|
4336
4303
|
{ id: 'dsh-armory#mcpManager/wbExpertDismiss', service: 'mcpManager', namespace: 'mcpManager', method: 'wbExpertDismiss', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
4337
4304
|
{ id: 'dsh-armory#mcpManager/wbExpertInstalled', service: 'mcpManager', namespace: 'mcpManager', method: 'wbExpertInstalled', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('WbExpertInstalledResult') },
|
|
4338
4305
|
{ id: 'dsh-armory#mcpManager/wbExpertRecent', service: 'mcpManager', namespace: 'mcpManager', method: 'wbExpertRecent', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('WbExpertRecentResult') },
|
|
4339
|
-
{ id: 'dsh-armory#mcpManager/wbExpertRecordUse', service: 'mcpManager', namespace: 'mcpManager', method: 'wbExpertRecordUse', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') }
|
|
4306
|
+
{ id: 'dsh-armory#mcpManager/wbExpertRecordUse', service: 'mcpManager', namespace: 'mcpManager', method: 'wbExpertRecordUse', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
4307
|
+
// 会话级专家人设(随时挂 / 换 / 摘):不受 agent-preset/locked 限制。
|
|
4308
|
+
// 与宿主 method、typert.js 三处必须同步,漏一处这条通道就恒 404。
|
|
4309
|
+
{ id: 'dsh-armory#mcpManager/expertAttach', service: 'mcpManager', namespace: 'mcpManager', method: 'expertAttach', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
4310
|
+
{ id: 'dsh-armory#mcpManager/expertDetach', service: 'mcpManager', namespace: 'mcpManager', method: 'expertDetach', invocation: { kind: 'direct' }, parameters: [mcpParam('payload', 'ImportPayload')], result: mcpCodec('NoteResult') },
|
|
4311
|
+
{ id: 'dsh-armory#mcpManager/expertActive', service: 'mcpManager', namespace: 'mcpManager', method: 'expertActive', invocation: { kind: 'direct' }, parameters: [], result: mcpCodec('ExpertActiveResult') }
|
|
4340
4312
|
].map((descriptor) => ({ ...descriptor, sourceLocation: { file: 'dsh-armory/lib/typert.js', line: 7, column: 1 } }))
|
|
4341
4313
|
};
|
|
4342
4314
|
async function apply(ctx) {
|
package/lib/index.js
CHANGED
|
@@ -15,6 +15,7 @@ import { allMcpEntries, entriesForName, projectToolSchema, revisionOf, toolInven
|
|
|
15
15
|
import { WHATS_NEW } from "./whats-new.js";
|
|
16
16
|
import { wbConnectorCatalog, wbConnectorPreview, wbSkillCatalog, wbSkillDetail, wbSkillFiles, wbSkillInstall, wbSkillInstalled, wbSkillUninstall } from "./wb-market.js";
|
|
17
17
|
import { installExpertSkills, listExpertRecent, listExpertSkills, readExpertPresets, recordExpertRecent, removeExpertPreset, uninstallExpertSkills, wbExpertCatalog, wbExpertDetail, writeExpertPreset } from "./expert-market.js";
|
|
18
|
+
import { attachPersona, detachPersona, listPersonas, reconcilePersonas } from "./session-persona.js";
|
|
18
19
|
import { scanSessionLedger } from "./tool-ledger.js";
|
|
19
20
|
import { evaluateEnvExpression, jsExpressionToTemplate } from "./workspace-config.js";
|
|
20
21
|
import { canonicalWorkspacePath, installAgentRuntime, listWorkspaceRecords, liveWorkspaceAgentCount, projectConnectionsView, readWorkspaceConfigCached, reconcileWorkspaceConnections, reconcileWorkspaceRestricts, reconnectWorkspaceServer, withWorkspaceWrite, workspaceConnectionStatus, workspaceMountErrorsView, workspaceRestrictErrorView, workspaceScopeErrorsView, workspaceToolSchemas, writeWorkspaceConfigFile } from "./workspace-runtime.js";
|
|
@@ -1460,6 +1461,32 @@ let McpManagerGateway = (() => {
|
|
|
1460
1461
|
// 数据与安装逻辑都在 lib/expert-market.js;这里只做 RPC 转发与 patch 写盘收尾。
|
|
1461
1462
|
// 召唤 = 往 profile patch 插入一条独立 @deepseek-ai/dsh-agent-preset 行(persona.prefix
|
|
1462
1463
|
// = 专家人设),DSH 热加载后新会话即可在预设选择器里选它——多专家共存,卸载即摘除。
|
|
1464
|
+
// ── 会话级专家人设(随时挂 / 换 / 摘,不受 agent-preset/locked 限制)──────
|
|
1465
|
+
// 机制与边界见 lib/session-persona.js 顶部注释:走 systemPrompt 的会话作用域
|
|
1466
|
+
// section,遮蔽该会话的部署人设前缀;不动工具目录、不写任何持久化配置。
|
|
1467
|
+
async expertAttach(payload) {
|
|
1468
|
+
const sessionId = String(payload?.sessionId || "").trim();
|
|
1469
|
+
const plugin = String(payload?.plugin || "").trim();
|
|
1470
|
+
if (!sessionId) throw new Error("缺少 sessionId");
|
|
1471
|
+
if (!plugin) throw new Error("缺少 plugin");
|
|
1472
|
+
const detail = await wbExpertDetail({ plugin });
|
|
1473
|
+
const result = attachPersona(this.ctx, sessionId, {
|
|
1474
|
+
plugin,
|
|
1475
|
+
name: detail.name,
|
|
1476
|
+
personaText: detail.persona,
|
|
1477
|
+
});
|
|
1478
|
+
await recordExpertRecent("expert", { plugin, name: detail.name, profession: detail.profession, presetId: `session-${plugin}` }).catch(() => {});
|
|
1479
|
+
return { ...result, profession: detail.profession };
|
|
1480
|
+
}
|
|
1481
|
+
async expertDetach(payload) {
|
|
1482
|
+
const sessionId = String(payload?.sessionId || "").trim();
|
|
1483
|
+
if (!sessionId) throw new Error("缺少 sessionId");
|
|
1484
|
+
return detachPersona(this.ctx, sessionId);
|
|
1485
|
+
}
|
|
1486
|
+
async expertActive() {
|
|
1487
|
+
reconcilePersonas(this.ctx);
|
|
1488
|
+
return { sessions: listPersonas() };
|
|
1489
|
+
}
|
|
1463
1490
|
async wbExpertCatalog() {
|
|
1464
1491
|
return wbExpertCatalog();
|
|
1465
1492
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// 会话级专家人设注入 —— 让「专家」像 WorkBuddy 一样可随时挂/换/摘,不受 agent-preset/locked 限制。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要这条独立通道:宿主的 agentPresets.select() 有硬锁(会话一旦开过口就
|
|
4
|
+
// `agent-preset/locked`),因为**预设**决定工具目录与提示词结构,中途换会破坏前后一致。
|
|
5
|
+
// 但用户要的是 WorkBuddy 式体验:任何会话、任何时刻都能换专家或摘掉。可行的原因是
|
|
6
|
+
// 人设(persona)与预设是两件事——preset 的 persona 行只是一段提示词 section:
|
|
7
|
+
//
|
|
8
|
+
// dsh-persona 的注释写明该行是 **scope-only**:挂进某个 agent 预设时,它只遮蔽
|
|
9
|
+
// 「那一个会话」的部署人设。dsh-subagent 也用同一手法给子会话装人设:
|
|
10
|
+
// childCtx.systemPrompt.section({ name: "deployment:persona-prefix",
|
|
11
|
+
// order: childCtx.systemPrompt.getSectionOrder("DEPLOYMENT_PERSONA_PREFIX"),
|
|
12
|
+
// text: persona })
|
|
13
|
+
//
|
|
14
|
+
// 而 systemPrompt.section() 的归属 scope 取自定义层自身 ctx 的 kScope(见 dsh-scope
|
|
15
|
+
// 的 scopeOf / ScopedLayers.effect),cordis 的 tracker 代理会把服务方法的 this.ctx
|
|
16
|
+
// 重绑为**调用方 ctx** —— 所以用 `agent.ctx.systemPrompt.section(...)` 注册,段就落在
|
|
17
|
+
// 该会话自己的 scope 层:只影响这个会话,且 ctx.effect 返回的 disposer 可随时撤销。
|
|
18
|
+
//
|
|
19
|
+
// 与「召唤预设」的分工:召唤写 profile patch,管**新会话的预设选择器**(长期身份);
|
|
20
|
+
// 本模块管**当前会话的即时人设**(会话内挂/换/摘)。两者互不干扰:预设仍在,随时可用;
|
|
21
|
+
// 会话内挂的专家只是这段提示词 section 的替换,不动工具目录与任何持久化配置。
|
|
22
|
+
|
|
23
|
+
/** 与 dsh-persona / dsh-subagent 共用的 section 名(同名即遮蔽,最近 scope 胜出)。 */
|
|
24
|
+
const PERSONA_PREFIX_SECTION = "deployment:persona-prefix";
|
|
25
|
+
/** 单个会话最多保留的撤销记录(防泄漏;正常使用远小于此)。 */
|
|
26
|
+
const MAX_SESSIONS = 64;
|
|
27
|
+
/** sessionId -> { plugin, name, disposer } */
|
|
28
|
+
const activePersonas = new Map();
|
|
29
|
+
|
|
30
|
+
function agentCtxOf(ctx, sessionId) {
|
|
31
|
+
const agents = typeof ctx?.get === "function" ? ctx.get("agents") : undefined;
|
|
32
|
+
if (!agents || typeof agents.get !== "function") return { error: "宿主未提供 agents 服务(agentRegistry 不可用),无法定位会话" };
|
|
33
|
+
const agent = agents.get(sessionId);
|
|
34
|
+
if (agent === undefined) return { error: "会话不在运行中(可能已归档或尚未激活):" + sessionId };
|
|
35
|
+
const agentCtx = agent.ctx;
|
|
36
|
+
if (agentCtx === undefined) return { error: "该会话没有可用的作用域上下文" };
|
|
37
|
+
return { agentCtx };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 摘掉某会话当前注入的人设(若有)。
|
|
42
|
+
* @returns {boolean} 是否真的摘掉了一段。
|
|
43
|
+
*/
|
|
44
|
+
function clearPersona(sessionId) {
|
|
45
|
+
const current = activePersonas.get(sessionId);
|
|
46
|
+
if (!current) return false;
|
|
47
|
+
activePersonas.delete(sessionId);
|
|
48
|
+
try {
|
|
49
|
+
current.disposer?.();
|
|
50
|
+
} catch { /* 撤销失败不回滚状态:section 已随 scope 失效,重复撤销无害 */ }
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 当前挂在某会话上的专家(供客户端 chip 显示真实状态)。 */
|
|
55
|
+
export function personaOf(sessionId) {
|
|
56
|
+
const row = activePersonas.get(sessionId);
|
|
57
|
+
return row ? { plugin: row.plugin, name: row.name } : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 所有会话的挂载快照(会话被销毁后由 reconcile 清理)。 */
|
|
61
|
+
export function listPersonas() {
|
|
62
|
+
return [...activePersonas.entries()].map(([sessionId, row]) => ({ sessionId, plugin: row.plugin, name: row.name }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 会话已不在运行 → 清掉它的撤销记录(disposer 早已随 scope 失效)。 */
|
|
66
|
+
export function reconcilePersonas(ctx) {
|
|
67
|
+
const agents = typeof ctx?.get === "function" ? ctx.get("agents") : undefined;
|
|
68
|
+
if (!agents || typeof agents.get !== "function") return;
|
|
69
|
+
const live = new Set(agents.list().map((agent) => agent.id));
|
|
70
|
+
for (const sessionId of [...activePersonas.keys()]) {
|
|
71
|
+
if (!live.has(sessionId)) activePersonas.delete(sessionId);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 给一个运行中的会话注入(或替换)专家人设。同一会话可反复调用:先摘旧的再装新的。
|
|
77
|
+
* @param {object} ctx - 插件宿主上下文。
|
|
78
|
+
* @param {string} sessionId - 目标会话。
|
|
79
|
+
* @param {object} spec - { plugin, name, personaText }。
|
|
80
|
+
* @returns {{ok: true, plugin: string, name: string, replaced: boolean}}
|
|
81
|
+
*/
|
|
82
|
+
export function attachPersona(ctx, sessionId, spec) {
|
|
83
|
+
const id = String(sessionId || "").trim();
|
|
84
|
+
if (!id) throw new Error("缺少会话 id");
|
|
85
|
+
const text = String(spec?.personaText || "").trim();
|
|
86
|
+
if (!text) throw new Error("人设内容为空,无法挂载");
|
|
87
|
+
const { agentCtx, error } = agentCtxOf(ctx, id);
|
|
88
|
+
if (error) throw new Error(error);
|
|
89
|
+
const prompt = agentCtx.systemPrompt;
|
|
90
|
+
if (!prompt || typeof prompt.section !== "function") {
|
|
91
|
+
throw new Error("该会话没有 systemPrompt 服务,无法注入人设");
|
|
92
|
+
}
|
|
93
|
+
const replaced = clearPersona(id);
|
|
94
|
+
// 与 dsh-persona 同款:同名 section + 同一处 order = 遮蔽该会话的部署人设前缀。
|
|
95
|
+
const disposer = prompt.section({
|
|
96
|
+
name: PERSONA_PREFIX_SECTION,
|
|
97
|
+
order: typeof prompt.getSectionOrder === "function"
|
|
98
|
+
? prompt.getSectionOrder("DEPLOYMENT_PERSONA_PREFIX")
|
|
99
|
+
: 0,
|
|
100
|
+
text,
|
|
101
|
+
});
|
|
102
|
+
activePersonas.set(id, {
|
|
103
|
+
plugin: String(spec?.plugin || ""),
|
|
104
|
+
name: String(spec?.name || spec?.plugin || "专家"),
|
|
105
|
+
disposer,
|
|
106
|
+
});
|
|
107
|
+
// 防泄漏:超出上限时丢掉最早的记录(其 disposer 一并撤销)。
|
|
108
|
+
while (activePersonas.size > MAX_SESSIONS) {
|
|
109
|
+
const oldest = activePersonas.keys().next().value;
|
|
110
|
+
clearPersona(oldest);
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, plugin: String(spec?.plugin || ""), name: String(spec?.name || spec?.plugin || "专家"), replaced };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 摘掉某会话的专家人设。@returns {{ok: true, removed: boolean}} */
|
|
116
|
+
export function detachPersona(ctx, sessionId) {
|
|
117
|
+
const id = String(sessionId || "").trim();
|
|
118
|
+
if (!id) throw new Error("缺少会话 id");
|
|
119
|
+
return { ok: true, removed: clearPersona(id) };
|
|
120
|
+
}
|
package/lib/typert.js
CHANGED
|
@@ -101,6 +101,11 @@ export const TYPERT = {
|
|
|
101
101
|
mv('wbExpertInstalled', [], 'WbExpertInstalledResult'),
|
|
102
102
|
mv('wbExpertRecent', [], 'WbExpertRecentResult'),
|
|
103
103
|
mv('wbExpertRecordUse', [payloadParam], 'NoteResult'),
|
|
104
|
+
// 会话级专家人设(随时挂/换/摘):走 systemPrompt 的会话作用域 section,
|
|
105
|
+
// 不受 agent-preset/locked 限制。payload 交给 lib/session-persona.js 校验。
|
|
106
|
+
mv('expertAttach', [payloadParam], 'NoteResult'),
|
|
107
|
+
mv('expertDetach', [payloadParam], 'NoteResult'),
|
|
108
|
+
mv('expertActive', [], 'ExpertActiveResult'),
|
|
104
109
|
],
|
|
105
110
|
model: { services: [], events: [], objects: [] },
|
|
106
111
|
}
|
package/lib/whats-new.js
CHANGED
|
@@ -229,4 +229,17 @@ export const WHATS_NEW = [
|
|
|
229
229
|
},
|
|
230
230
|
],
|
|
231
231
|
},
|
|
232
|
+
{
|
|
233
|
+
version: '1.9.2',
|
|
234
|
+
items: [
|
|
235
|
+
{
|
|
236
|
+
text: '专家升级为 WorkBuddy 式「随时挂 / 换 / 摘」:任何会话(包括已经聊了一半的)都能挂专家、直接点另一个专家就换掉、点 chip 的 × 立刻取消——不再有「会话已开始,无法中途挂专家」的死路,也不再是一个对话只能引用一次',
|
|
237
|
+
where: '会话输入框工具行右侧的看板小图标 → 专家',
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
text: '换专家会明确告诉你「已替换原专家」,取消后点别的专家又能重新挂上;组件重挂或切换会话时自动向宿主对账,chip 显示的始终是真实状态(专家也能由预设选择器等其他入口挂上)',
|
|
241
|
+
where: '专家 chip 与召唤菜单内的提示条',
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
},
|
|
232
245
|
]
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-armory",
|
|
3
|
-
"version": "1.9.
|
|
4
|
-
"description": "Unified Skill + MCP + Expert board for DeepSeek Harness (desktop app and Web GUI)
|
|
3
|
+
"version": "1.9.2",
|
|
4
|
+
"description": "Unified Skill + MCP + Expert board for DeepSeek Harness (desktop app and Web GUI) 鈥?global panel page, JSON import, WorkBuddy connector & SkillHub & Expert market installs, tool-usage tailor, and profile-backed Host Remote management.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/2026heshao/dsh-armory.git"
|