@joekytc/dsh-swarm 0.3.12 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/client/ConfigSection.tsx +127 -18
  2. package/client/ConfigSelect.tsx +1 -0
  3. package/client/config-store.ts +6 -3
  4. package/client/config.css +89 -1
  5. package/client/index.ts +8 -4
  6. package/client/session-bridge.ts +26 -2
  7. package/cordis.patch.yml +10 -52
  8. package/lib/client.js +215 -37
  9. package/lib/config.d.ts +7 -0
  10. package/lib/config.js +7 -1
  11. package/lib/dispatcher/agent-runner.d.ts +4 -0
  12. package/lib/dispatcher/agent-runner.js +28 -17
  13. package/lib/dispatcher/chain-auditor.d.ts +3 -2
  14. package/lib/dispatcher/chain-auditor.js +2 -2
  15. package/lib/dispatcher/model-candidates.d.ts +9 -3
  16. package/lib/dispatcher/model-candidates.js +27 -6
  17. package/lib/dispatcher/model-chain.d.ts +19 -0
  18. package/lib/dispatcher/model-chain.js +110 -0
  19. package/lib/dispatcher/session-events.d.ts +9 -0
  20. package/lib/dispatcher/session-events.js +28 -0
  21. package/lib/dispatcher/v-orchestrator.d.ts +2 -0
  22. package/lib/dispatcher/v-orchestrator.js +14 -9
  23. package/lib/domain/config-override.d.ts +20 -0
  24. package/lib/domain/config-override.js +46 -2
  25. package/lib/index.js +13 -3
  26. package/lib/roles/preset-installer.d.ts +50 -5
  27. package/lib/roles/preset-installer.js +154 -20
  28. package/lib/routes/kanban-http.js +24 -3
  29. package/lib/services/config-provider.js +5 -0
  30. package/package.json +23 -17
  31. package/personas/kanban-d/agent.cordis.yml +3 -1
  32. package/personas/kanban-dt/agent.cordis.yml +2 -1
  33. package/personas/kanban-p/agent.cordis.yml +1 -1
  34. package/personas/kanban-pt/agent.cordis.yml +1 -1
  35. package/personas/kanban-v/agent.cordis.yml +1 -1
  36. package/personas/kanban-w/agent.cordis.yml +1 -1
  37. package/personas/swarm/agent.cordis.yml +1 -1
@@ -1,9 +1,15 @@
1
1
  import { useEffect, useRef, useState, useSyncExternalStore } from 'react';
2
- import { createConfigStore, type EditableSnapshot, type OcrStatus } from './config-store.js';
3
- import { ConfigSelect } from './ConfigSelect.js';
2
+ import { createConfigStore, type EditableSnapshot, type ModelFallback, type OcrStatus } from './config-store.js';
3
+ import { ConfigSelect, type ConfigSelectOption } from './ConfigSelect.js';
4
4
 
5
5
  const ROLES = ['v', 'p', 'w', 'd', 'pt', 'dt'] as const;
6
6
  type Model = { provider: string; model: string; reasoningEffort: string };
7
+ /** 单角色降级候选上限(与服务端 schema/派发链合成同口径)。 */
8
+ const MAX_FALLBACKS = 2;
9
+ /** 角色名(对齐 personas 各 kanban preset 目录 preset.yml 的 name 字段)。 */
10
+ const ROLE_NAMES: Record<(typeof ROLES)[number], string> = {
11
+ v: '看板编排官(V)', p: '规划官(P)', w: '知识官(W)', d: '全栈开发官(D)', pt: '计划审查官(PT)', dt: '交付评审官(DT)',
12
+ };
7
13
 
8
14
  /** ocr 卡横幅文案:null=已安装无横幅;安装中不显示横幅(spinner 行替代)。 */
9
15
  function ocrBanner(ocr: OcrStatus | null, installing: boolean): string | null {
@@ -14,6 +20,8 @@ function ocrBanner(ocr: OcrStatus | null, installing: boolean): string | null {
14
20
  }
15
21
 
16
22
  /** settings.section 配置面板——本地 draft 编辑态、下拉选中即存、级联下拉、来源徽章。
23
+ * 第二卡「模型链」:每角色主模型级联下拉 + 「+ 降级」追加降级行(provider→model→effort 级联 + 删除,
24
+ * 顺序=优先序、上限 2)+「复制本链到全部角色」快捷;官方兜底为全局只读尾行(恒垫链尾,编辑入口在部署配置)。
17
25
  * 第三卡「评审引擎(ocr)」:安装单飞(组件持轮询定时器 1.5s)、模式切换、托管提供方/模型级联、应用到 ocr。
18
26
  * 无「重置」:各用户模型配置不同,无有意义的公共默认值可回退(历史教训:重置按钮已移除,勿再加回)。 */
19
27
  export function ConfigSection({ fetchImpl, close }: { fetchImpl?: typeof fetch; close: () => void }) {
@@ -23,6 +31,8 @@ export function ConfigSection({ fetchImpl, close }: { fetchImpl?: typeof fetch;
23
31
  const state = useSyncExternalStore(store.subscribe, store.get);
24
32
  const [draft, setDraft] = useState<EditableSnapshot | null>(null);
25
33
  const [wireResult, setWireResult] = useState<{ ok: boolean; log: string } | null>(null);
34
+ // 手风琴展开态:undefined=用户未点过(默认展开第一个已配置角色);null=用户全部收起;字符串=用户点开的角色。
35
+ const [openRole, setOpenRole] = useState<string | null | undefined>(undefined);
26
36
 
27
37
  useEffect(() => {
28
38
  void store.load().then(() => setDraft(store.get().effective));
@@ -42,14 +52,58 @@ export function ConfigSection({ fetchImpl, close }: { fetchImpl?: typeof fetch;
42
52
  if (!draft) return <div className="dsh-kb-config">加载中…</div>;
43
53
 
44
54
  const commit = (next: EditableSnapshot) => { setDraft(next); void store.save(next); };
45
- const setModel = (r: string, patch: Partial<Model>) => {
46
- const prev = draft.roles.models[r] ?? { provider: '', model: '', reasoningEffort: '' };
47
- commit({ ...draft, roles: { models: { ...draft.roles.models, [r]: { ...prev, ...patch } } } });
55
+ const roleEntry = (r: string) => draft.roles.models[r] ?? { provider: '', model: '', reasoningEffort: '', fallbacks: [] as ModelFallback[] };
56
+ const setModel = (r: string, patch: Partial<Model> & { fallbacks?: ModelFallback[] }) => {
57
+ commit({ ...draft, roles: { models: { ...draft.roles.models, [r]: { ...roleEntry(r), ...patch } } } });
58
+ };
59
+ const setFallback = (r: string, idx: number, patch: Partial<Model>) => {
60
+ const fbs = [...roleEntry(r).fallbacks];
61
+ const cur = fbs[idx] ?? { provider: '', model: '', reasoningEffort: '' };
62
+ fbs[idx] = { ...cur, ...patch };
63
+ setModel(r, { fallbacks: fbs });
64
+ };
65
+ const addFallback = (r: string) => {
66
+ const fbs = roleEntry(r).fallbacks;
67
+ if (fbs.length >= MAX_FALLBACKS) return;
68
+ setModel(r, { fallbacks: [...fbs, { provider: '', model: '', reasoningEffort: '' }] });
69
+ };
70
+ const removeFallback = (r: string, idx: number) => {
71
+ setModel(r, { fallbacks: roleEntry(r).fallbacks.filter((_, i) => i !== idx) });
72
+ };
73
+ /** 主模型+降级链整体深拷贝到其余角色(顺序=优先序原样保留)。 */
74
+ const copyChainToAll = (r: string) => {
75
+ const src = roleEntry(r);
76
+ if (!(src.provider && src.model)) return;
77
+ const models = { ...draft.roles.models };
78
+ for (const o of ROLES) {
79
+ if (o === r) continue;
80
+ models[o] = { provider: src.provider, model: src.model, reasoningEffort: src.reasoningEffort, fallbacks: src.fallbacks.map((f) => ({ ...f })) };
81
+ }
82
+ commit({ ...draft, roles: { models } });
48
83
  };
49
84
  const commitReviewEngine = (mode: 'delegate' | 'managed', managed: { provider: string; model: string }) => {
50
85
  commit({ ...draft, reviewEngine: { mode, managed } });
51
86
  };
52
87
  const onBlur = () => { void store.save(draft); };
88
+ /** 链字段:label 在上 + 全宽下拉(宿主 provider 卡排版)。 */
89
+ const chainField = (label: string, value: string, placeholder: string, options: ConfigSelectOption[], onPick: (v: string) => void) => (
90
+ <label className="dsh-kb-config__field">
91
+ <span className="dsh-kb-config__field-label">{label}</span>
92
+ <ConfigSelect value={value} placeholder={placeholder} options={options} onChange={onPick} />
93
+ </label>
94
+ );
95
+ /** 折叠态链摘要:provider/model · N 降级;未配置明示。 */
96
+ const chainSummary = (m: { provider: string; model: string; fallbacks?: ModelFallback[] }) => {
97
+ if (!(m.provider && m.model)) return '未配置';
98
+ const n = (m.fallbacks ?? []).filter((f) => f.provider && f.model).length;
99
+ return `${m.provider} / ${m.model}` + (n ? ` · ${n} 降级` : '');
100
+ };
101
+ const providerOptions = state.catalog.providers.map((p) => ({ value: p.id, label: p.name }));
102
+ // 手风琴:同一时刻至多一个角色展开;用户未点过时默认展开第一个已配置角色。
103
+ const expandedRole = openRole === undefined ? ROLES.find((x) => {
104
+ const e = draft.roles.models[x];
105
+ return Boolean(e?.provider && e?.model);
106
+ }) ?? null : openRole;
53
107
 
54
108
  const ocr = state.ocrStatus;
55
109
  const banner = ocrBanner(ocr, installing);
@@ -57,6 +111,8 @@ export function ConfigSection({ fetchImpl, close }: { fetchImpl?: typeof fetch;
57
111
  const ocrBlocked = installing || ocr === null || !ocr.installed;
58
112
  const re = draft.reviewEngine;
59
113
  const managedFieldsEnabled = !ocrBlocked && re.mode === 'managed';
114
+ // 版本倾斜防御:旧服务端 bundle 快照缺 chainFallback/fallbacks 字段时按空值处理,不崩面板。
115
+ const cf = draft.chainFallback ?? { provider: '', model: '', reasoningEffort: 'high' };
60
116
 
61
117
  return (
62
118
  <div className="dsh-kb-config">
@@ -74,25 +130,78 @@ export function ConfigSection({ fetchImpl, close }: { fetchImpl?: typeof fetch;
74
130
  <section className="dsh-kb-config__card">
75
131
  <h3>模型链</h3>
76
132
  {ROLES.map((r) => {
77
- const m = draft.roles.models[r] ?? { provider: '', model: '', reasoningEffort: '' };
133
+ const m = roleEntry(r);
134
+ const fbs = m.fallbacks ?? [];
135
+ const configured = Boolean(m.provider && m.model);
136
+ const open = expandedRole === r;
78
137
  const providerModels = state.catalog.models[m.provider] ?? [];
79
138
  const efforts = providerModels.find((x) => x.id === m.model)?.efforts ?? [];
80
139
  return (
81
- <div key={r} className="dsh-kb-config__role">
82
- <strong>{r}</strong>
83
- <ConfigSelect value={m.provider} placeholder="选择 provider"
84
- options={state.catalog.providers.map((p) => ({ value: p.id, label: p.name }))}
85
- onChange={(v) => { if (v !== m.provider) setModel(r, { provider: v, model: '', reasoningEffort: '' }); }} />
86
- <ConfigSelect value={m.model} placeholder="选择模型"
87
- options={providerModels.map((x) => ({ value: x.id, label: x.name }))}
88
- onChange={(v) => { if (v !== m.model) setModel(r, { model: v, reasoningEffort: '' }); }} />
89
- <ConfigSelect value={m.reasoningEffort} placeholder="跟随默认"
90
- options={efforts.map((x) => ({ value: x.id, label: x.name }))}
91
- onChange={(v) => { if (v !== m.reasoningEffort) setModel(r, { reasoningEffort: v }); }} />
92
- <span className="dsh-kb-config__src">{state.sources[`roles.models.${r}.model`] === 'override' ? '已覆盖' : '继承'}</span>
140
+ <div key={r} className="dsh-kb-config__role-block">
141
+ <button type="button" className="dsh-kb-config__role-head" aria-expanded={open}
142
+ onClick={() => setOpenRole(open ? null : r)}>
143
+ <span className={'dsh-kb-config__chevron' + (open ? ' dsh-kb-config__chevron--open' : '')} aria-hidden="true">
144
+ <svg width="14" height="14" viewBox="0 0 16 16">
145
+ <path d="M6 3.5 10.5 8 6 12.5" fill="none" stroke="currentColor" strokeWidth="1.5"
146
+ strokeLinecap="round" strokeLinejoin="round" />
147
+ </svg>
148
+ </span>
149
+ <strong>{ROLE_NAMES[r]}</strong>
150
+ {!open && <span className="dsh-kb-config__role-summary">{chainSummary(m)}</span>}
151
+ </button>
152
+ {open && (
153
+ <div className="dsh-kb-config__role-body">
154
+ <span className="dsh-kb-config__group-label">主模型</span>
155
+ {chainField('供应商', m.provider, '选择 provider', providerOptions,
156
+ (v) => { if (v !== m.provider) setModel(r, { provider: v, model: '', reasoningEffort: '' }); })}
157
+ {chainField('模型', m.model, '选择模型', providerModels.map((x) => ({ value: x.id, label: x.name })),
158
+ (v) => { if (v !== m.model) setModel(r, { model: v, reasoningEffort: '' }); })}
159
+ {chainField('思考等级', m.reasoningEffort, '跟随默认', efforts.map((x) => ({ value: x.id, label: x.name })),
160
+ (v) => { if (v !== m.reasoningEffort) setModel(r, { reasoningEffort: v }); })}
161
+ <span className="dsh-kb-config__group-label">降级(最多 2 个,顺序 = 切换优先序)</span>
162
+ {fbs.map((f, i) => {
163
+ const fProviderModels = state.catalog.models[f.provider] ?? [];
164
+ const fEfforts = fProviderModels.find((x) => x.id === f.model)?.efforts ?? [];
165
+ return (
166
+ <div key={i} className="dsh-kb-config__fb-group">
167
+ <div className="dsh-kb-config__fb-head">
168
+ <span className="dsh-kb-config__fb-title">降级 {i + 1}</span>
169
+ <button type="button" className="dsh-kb-config__row-btn" title="删除该降级候选"
170
+ onClick={() => removeFallback(r, i)}>删除</button>
171
+ </div>
172
+ {chainField('供应商', f.provider, '选择 provider', providerOptions,
173
+ (v) => { if (v !== f.provider) setFallback(r, i, { provider: v, model: '', reasoningEffort: '' }); })}
174
+ {chainField('模型', f.model, '选择模型', fProviderModels.map((x) => ({ value: x.id, label: x.name })),
175
+ (v) => { if (v !== f.model) setFallback(r, i, { model: v, reasoningEffort: '' }); })}
176
+ {chainField('思考等级', f.reasoningEffort, '跟随默认', fEfforts.map((x) => ({ value: x.id, label: x.name })),
177
+ (v) => { if (v !== f.reasoningEffort) setFallback(r, i, { reasoningEffort: v }); })}
178
+ </div>
179
+ );
180
+ })}
181
+ <div className="dsh-kb-config__role-actions">
182
+ {fbs.length < MAX_FALLBACKS && (
183
+ <button type="button" className="dsh-kb-config__row-btn" disabled={!configured}
184
+ title={configured ? '追加一个降级候选' : '先选好主模型(provider+模型)再追加降级'}
185
+ onClick={() => addFallback(r)}>+ 降级</button>
186
+ )}
187
+ <button type="button" className="dsh-kb-config__row-btn" disabled={!configured}
188
+ title="把该角色的主模型+降级链整体复制到其余角色"
189
+ onClick={() => copyChainToAll(r)}>复制本链到全部角色</button>
190
+ </div>
191
+ </div>
192
+ )}
93
193
  </div>
94
194
  );
95
195
  })}
196
+ <div className="dsh-kb-config__role dsh-kb-config__role--official">
197
+ <span className="dsh-kb-config__badge">官方兜底</span>
198
+ {cf.provider && cf.model
199
+ ? <span>{cf.provider} / {cf.model}(reasoningEffort={cf.reasoningEffort})</span>
200
+ : <span className="dsh-kb-config__muted">未配置——角色链全空时走宿主默认模型</span>}
201
+ </div>
202
+ <span className="dsh-kb-config__help">
203
+ 派发顺序:主模型 → 降级(顺序=优先序,最多 2 个)→ 官方兜底(恒垫链尾)→ 宿主默认模型;候选不可用(含官方账号未登录)静默切换并留 [model-fallback] 评论
204
+ </span>
96
205
  </section>
97
206
  <section className="dsh-kb-config__card">
98
207
  <h3>评审引擎(ocr)</h3>
@@ -36,6 +36,7 @@ export function ConfigSelect({ value, options, placeholder, onChange, onCommit,
36
36
  <div className="dsh-kb-config__select" ref={rootRef}>
37
37
  <button type="button" className="dsh-kb-config__select-trigger" aria-haspopup="listbox" aria-expanded={open}
38
38
  disabled={disabled}
39
+ title={current?.label ?? placeholder}
39
40
  onClick={() => setOpen((v) => !v)}>
40
41
  <span className={current ? undefined : 'dsh-kb-config__select-placeholder'}>{current?.label ?? placeholder}</span>
41
42
  <span className="dsh-kb-config__select-chevron" aria-hidden="true">
@@ -1,11 +1,14 @@
1
- /** settings.section 插槽注册 id(T9)。 */
1
+ /** settings.section 插槽注册 id。 */
2
2
  export const SWARM_CONFIG_NS = 'swarm-config';
3
3
 
4
4
  /** 手写本地类型:与 src/domain/config-override.ts 的 EditableSnapshot 同形,客户端不 import 服务端。 */
5
- export interface EditableModelSnapshot { provider: string; model: string; reasoningEffort: string; }
5
+ export interface ModelFallback { provider: string; model: string; reasoningEffort: string; }
6
+ export interface EditableModelSnapshot { provider: string; model: string; reasoningEffort: string; fallbacks: ModelFallback[]; }
6
7
  export interface EditableSnapshot {
7
8
  wikiVault: { baseUrl: string; pagePrefix: string };
8
9
  roles: { models: Partial<Record<string, EditableModelSnapshot>> };
10
+ /** 官方兜底(全局只读投影):编辑入口在部署配置,面板仅展示链尾并原样回传。 */
11
+ chainFallback: ModelFallback;
9
12
  reviewEngine: { mode: 'delegate' | 'managed'; managed: { provider: string; model: string } };
10
13
  /** 默认投递机器人(预设未命中的落点)。本面板不编辑:用户经 /sms 交互写入,原样回传避免清空。 */
11
14
  imDelivery: { fallbackBotId: string };
@@ -25,7 +28,7 @@ export interface ConfigState {
25
28
 
26
29
  /** 配置外部 store(GET /kanban/config + /kanban/llm-catalog + ocr 运维面;PUT 保存;POST reset),fetch 注入便于测试。 */
27
30
  export function createConfigStore(fetchImpl: typeof fetch) {
28
- let state: ConfigState = { effective: { wikiVault: { baseUrl: '', pagePrefix: '' }, roles: { models: {} }, reviewEngine: { mode: 'delegate', managed: { provider: '', model: '' } }, imDelivery: { fallbackBotId: '' } }, sources: {}, catalog: { providers: [], models: {} }, ocrStatus: null, install: { phase: 'idle', log: '' }, saving: false, error: null };
31
+ let state: ConfigState = { effective: { wikiVault: { baseUrl: '', pagePrefix: '' }, roles: { models: {} }, chainFallback: { provider: '', model: '', reasoningEffort: 'high' }, reviewEngine: { mode: 'delegate', managed: { provider: '', model: '' } }, imDelivery: { fallbackBotId: '' } }, sources: {}, catalog: { providers: [], models: {} }, ocrStatus: null, install: { phase: 'idle', log: '' }, saving: false, error: null };
29
32
  const listeners = new Set<() => void>();
30
33
  const setState = (patch: Partial<ConfigState>) => { state = { ...state, ...patch }; for (const l of [...listeners]) l(); };
31
34
  const get = () => state;
package/client/config.css CHANGED
@@ -88,7 +88,7 @@
88
88
  }
89
89
  .dsh-kb-config__option:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }
90
90
  .dsh-kb-config__option[data-selected='true'] { color: var(--dsw-alias-state-business-primary, #4b7bff); }
91
- .dsh-kb-config__option > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
91
+ .dsh-kb-config__option > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: normal; word-break: break-word; }
92
92
  .dsh-kb-config__option-check { flex: 0 0 auto; display: inline-flex; }
93
93
 
94
94
  .dsh-kb-config__label { display: block; margin: 12px 0 6px; font-weight: 600; }
@@ -108,3 +108,91 @@
108
108
  }
109
109
  .dsh-kb-config__footer button:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }
110
110
  .dsh-kb-config__error { color: var(--dsw-alias-state-error-primary, #e5484d); }
111
+
112
+ /* 模型链卡:宿主 provider 卡展开风——角色头可折叠、字段 label 在上 + 控件全宽、降级为条目组 */
113
+ .dsh-kb-config__role-block { margin-bottom: 4px; }
114
+ .dsh-kb-config__role-head {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 8px;
118
+ width: 100%;
119
+ padding: 10px 10px;
120
+ border: none;
121
+ border-radius: var(--dsh-kb-config-radius-md);
122
+ background: transparent;
123
+ color: inherit;
124
+ font: inherit;
125
+ text-align: left;
126
+ cursor: pointer;
127
+ }
128
+ .dsh-kb-config__role-head:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }
129
+ .dsh-kb-config__chevron { flex: 0 0 auto; display: inline-flex; color: var(--dsw-alias-label-tertiary, #999); transition: transform 150ms ease; }
130
+ .dsh-kb-config__chevron--open { transform: rotate(90deg); }
131
+ .dsh-kb-config__role-summary {
132
+ margin-left: auto;
133
+ color: var(--dsw-alias-label-secondary, #888);
134
+ font-size: 12px;
135
+ overflow: hidden;
136
+ text-overflow: ellipsis;
137
+ white-space: nowrap;
138
+ }
139
+ /* 展开体背景色:与折叠角色头形成块级区分(同宿主设置内嵌面板层次) */
140
+ .dsh-kb-config__role-body {
141
+ padding: 12px 14px 16px;
142
+ margin: 8px 0 4px;
143
+ background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.08));
144
+ border-radius: var(--dsh-kb-config-radius-md);
145
+ }
146
+ .dsh-kb-config__group-label { display: block; margin: 10px 0 8px; font-weight: 600; }
147
+ .dsh-kb-config__group-label:first-child { margin-top: 0; }
148
+ .dsh-kb-config__field { display: block; margin: 0 0 10px; }
149
+ .dsh-kb-config__field-label { display: block; margin: 0 0 6px; color: var(--dsw-alias-label-secondary, #888); font-size: 12px; }
150
+ .dsh-kb-config__fb-group {
151
+ margin: 0 0 10px;
152
+ padding: 10px 12px;
153
+ border: 1px solid var(--dsh-kb-config-border);
154
+ border-radius: var(--dsh-kb-config-radius-md);
155
+ }
156
+ .dsh-kb-config__fb-head {
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: space-between;
160
+ margin: 0 0 8px;
161
+ }
162
+ .dsh-kb-config__fb-title { color: var(--dsw-alias-label-secondary, #888); font-size: 12px; }
163
+ .dsh-kb-config__row-btn {
164
+ padding: 6px 10px;
165
+ border: 1px solid var(--dsh-kb-config-border);
166
+ border-radius: var(--dsh-kb-config-radius-md);
167
+ background: transparent;
168
+ color: inherit;
169
+ font: inherit;
170
+ font-size: 12px;
171
+ white-space: nowrap;
172
+ cursor: pointer;
173
+ transition: background-color 150ms ease;
174
+ }
175
+ .dsh-kb-config__row-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12)); }
176
+ .dsh-kb-config__row-btn:disabled { opacity: 0.5; cursor: not-allowed; }
177
+ .dsh-kb-config__role-actions { display: flex; gap: 8px; margin-top: 12px; }
178
+ .dsh-kb-config__role--official {
179
+ display: flex;
180
+ gap: 10px;
181
+ align-items: center;
182
+ margin: 12px 0 0;
183
+ padding: 8px 10px;
184
+ border: 1px dashed var(--dsh-kb-config-border);
185
+ border-radius: var(--dsh-kb-config-radius-md);
186
+ color: var(--dsw-alias-label-secondary, #888);
187
+ font-size: 12px;
188
+ }
189
+ .dsh-kb-config__badge {
190
+ flex: 0 0 auto;
191
+ padding: 2px 8px;
192
+ border-radius: 999px;
193
+ background: var(--dsw-alias-interactive-bg-hover, rgba(127, 127, 127, 0.12));
194
+ color: var(--dsw-alias-label-primary, inherit);
195
+ font-size: 12px;
196
+ white-space: nowrap;
197
+ }
198
+ .dsh-kb-config__muted { color: var(--dsw-alias-label-tertiary, #999); }
package/client/index.ts CHANGED
@@ -15,14 +15,15 @@ declare module '@deepseek-ai/cordis' {
15
15
  }
16
16
  import { ConfigSection } from './ConfigSection.js';
17
17
  import { SWARM_CONFIG_NS } from './config-store.js';
18
- import { setSessionsService } from './session-bridge.js';
18
+ import { setSessionsService, setSessionNavigator } from './session-bridge.js';
19
19
  import css from './kanban.css';
20
20
  import configCss from './config.css';
21
21
 
22
22
  export const name = 'kanban-board';
23
23
 
24
- /** 所需 client 服务(cordis fiber inject——loader 把模块导出当作对象插件传入)。 */
25
- export const inject = ['slots', 'sessions'];
24
+ /** 所需 client 服务(cordis fiber inject——loader 把模块导出当作对象插件传入)。
25
+ * uiWorkspace:0.1.7 起宿主内置的视图所有者导航(会话跳转新接缝),fiber inject 会等待其就绪。 */
26
+ export const inject = ['slots', 'sessions', 'uiWorkspace'];
26
27
 
27
28
  /** 浏览器半入口(roster 行 id: kanban-board):把看板挂到 conversation.view(会话中心 tab,additive)。
28
29
  * 对齐 DSH 原生注册:对话(id=chat, order=0) → 轨迹(id=trajectory, order=10) → 看板(id=kanban, order=20)。
@@ -32,6 +33,9 @@ export function apply(ctx: Context): (() => void) | void {
32
33
  // cast:@deepseek-ai/dsh-session(服务端包)也 merge 了 cordis Context.sessions: SessionStore,
33
34
  // 类型面被其覆盖;浏览器半运行时注入的实为 dsh-api-session-controller 的 ISessions。
34
35
  setSessionsService(ctx.sessions as unknown as ISessions);
36
+ // 会话跳转导航:0.2.0+ 宿主提供 uiWorkspace(视图所有者),但注册晚于 client apply——
37
+ // 只传惰性 getter(点击时解析+try/catch 降级),apply 期绝不触碰属性,防 cordis 未就绪 throw 炸掉入口注册。
38
+ setSessionNavigator(() => (ctx as { uiWorkspace?: { openSession(target: string): void } }).uiWorkspace ?? null);
35
39
  let style: HTMLStyleElement | null = null;
36
40
  let configStyle: HTMLStyleElement | null = null;
37
41
  if (typeof document !== 'undefined') {
@@ -62,5 +66,5 @@ export function apply(ctx: Context): (() => void) | void {
62
66
  ConfigSection as never,
63
67
  ),
64
68
  );
65
- return () => { if (style) style.remove(); if (configStyle) configStyle.remove(); setSessionsService(null); };
69
+ return () => { if (style) style.remove(); if (configStyle) configStyle.remove(); setSessionsService(null); setSessionNavigator(null); };
66
70
  }
@@ -32,8 +32,32 @@ export function useSessionIds(): ReadonlySet<string> {
32
32
  );
33
33
  }
34
34
 
35
- /** 应用内跳转到指定会话(宿主 ISessions.open 对不在列表的 id fail loud,调用方须先经 useSessionIds 门控)。 */
35
+ /** 新宿主导航接缝:dsh 0.2.0 起 ISessions.open 移除,会话导航归视图所有者(uiWorkspace.openSession)。
36
+ * 宿主以 getter 惰性供给(uiWorkspace 注册晚于 client apply,apply 期属性访问会在 cordis 上 throw);
37
+ * 0.1.7 底线宿主无此服务,getter 返回 null 走旧 open 转发。 */
38
+ let navigator: (() => { openSession(target: string): void } | null) | null = null;
39
+
40
+ export function setSessionNavigator(next: (() => { openSession(target: string): void } | null) | null): void {
41
+ navigator = next;
42
+ }
43
+
44
+ /** 应用内跳转到指定会话(调用方须先经 useSessionIds 门控,宿主对不在列表的 id fail loud)。 */
36
45
  export function openSession(id: string): void {
46
+ // 优先 0.2.0+ 视图所有者导航;解析失败视同服务缺席;旧宿主退回 ISessions.open 转发;两者皆缺则响亮报错。
47
+ if (navigator) {
48
+ let viaWorkspace: { openSession(target: string): void } | null = null;
49
+ try {
50
+ viaWorkspace = navigator();
51
+ } catch {
52
+ viaWorkspace = null;
53
+ }
54
+ if (viaWorkspace) {
55
+ viaWorkspace.openSession(id);
56
+ return;
57
+ }
58
+ }
37
59
  if (!service) throw new Error('sessions service unavailable');
38
- service.open(id as never);
60
+ const open = (service as { open?: (target: string) => void }).open;
61
+ if (!open) throw new Error('host exposes neither uiWorkspace nor sessions.open; session jump unavailable');
62
+ open(id);
39
63
  }
package/cordis.patch.yml CHANGED
@@ -26,58 +26,16 @@
26
26
  staleTimeoutSeconds: 14400
27
27
  maxRetries: 3
28
28
  heartbeatIntervalSeconds: 300
29
- # 角色模型候选链(Task 12):逐角色 primary + fallbacks,主模型不可用静默切换([model-fallback] 证据),
30
- # 全候选不可用 → block(model-unavailable) 抛用户;reasoningEffort 缺省 high。部署实际值以本文件为准。
29
+ # 角色模型链:GUI 配置面板(模型链卡)为唯一编辑入口——逐角色 primary + fallbacks(≤2),
30
+ # 主模型不可用静默切换([model-fallback] 证据),全候选不可用 → block(model-unavailable) 抛用户;
31
+ # reasoningEffort 缺省 high。本 patch 不再硬编码角色模型,仅保留全局官方兜底:
32
+ # GUI 未配角色链 → 官方兜底单候选直用;有配置 → 官方兜底恒垫链尾(官方账号路由,登录态授权;
33
+ # 未登录 MISSING_CREDENTIAL 归入候选切换判定,不误判任务失败)。
31
34
  roles:
32
- models:
33
- v:
34
- provider: 'ark'
35
- model: 'deepseek-v4-flash'
36
- reasoningEffort: 'high'
37
- fallbacks:
38
- - provider: 'openai'
39
- model: 'gpt-5.6-sol'
40
- reasoningEffort: 'high'
41
- p:
42
- provider: 'ark'
43
- model: 'deepseek-v4-flash'
44
- reasoningEffort: 'high'
45
- fallbacks:
46
- - provider: 'openai'
47
- model: 'gpt-5.6-sol'
48
- reasoningEffort: 'high'
49
- w:
50
- provider: 'ark'
51
- model: 'deepseek-v4-flash'
52
- reasoningEffort: 'high'
53
- fallbacks:
54
- - provider: 'openai'
55
- model: 'gpt-5.6-sol'
56
- reasoningEffort: 'high'
57
- d:
58
- provider: 'ark'
59
- model: 'deepseek-v4-flash'
60
- reasoningEffort: 'high'
61
- fallbacks:
62
- - provider: 'openai'
63
- model: 'gpt-5.6-sol'
64
- reasoningEffort: 'high'
65
- pt:
66
- provider: 'ark'
67
- model: 'deepseek-v4-flash'
68
- reasoningEffort: 'high'
69
- fallbacks:
70
- - provider: 'openai'
71
- model: 'gpt-5.6-sol'
72
- reasoningEffort: 'high'
73
- dt:
74
- provider: 'ark'
75
- model: 'deepseek-v4-flash'
76
- reasoningEffort: 'high'
77
- fallbacks:
78
- - provider: 'openai'
79
- model: 'gpt-5.6-sol'
80
- reasoningEffort: 'high'
35
+ chainFallback:
36
+ provider: 'deepseek-account'
37
+ model: 'deepseek-flash'
38
+ reasoningEffort: 'high'
81
39
  prefixRoutes:
82
40
  plan: '/plan:'
83
41
  openspec: '/openspec:'
@@ -85,4 +43,4 @@
85
43
  enabled: true
86
44
  contentMinWidth: 715
87
45
  contentMaxWidth: 780
88
- sseHeartbeatSeconds: 20
46
+ sseHeartbeatSeconds: 10