dsh-advisor-plugin 0.2.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +210 -0
  3. package/cordis.patch.yml +6 -0
  4. package/lib/advisor-prompt.d.ts +28 -0
  5. package/lib/advisor-prompt.js +91 -0
  6. package/lib/client/AdvisorCard.d.ts +15 -0
  7. package/lib/client/AdvisorCard.js +60 -0
  8. package/lib/client/AdvisorToolRow.d.ts +40 -0
  9. package/lib/client/AdvisorToolRow.js +52 -0
  10. package/lib/client/PatrolNodeView.d.ts +18 -0
  11. package/lib/client/PatrolNodeView.js +57 -0
  12. package/lib/client/controller.d.ts +161 -0
  13. package/lib/client/controller.js +287 -0
  14. package/lib/client/index.d.ts +20 -0
  15. package/lib/client/index.js +841 -0
  16. package/lib/client/locales.d.ts +54 -0
  17. package/lib/client/locales.js +101 -0
  18. package/lib/client/patrol-chat.d.ts +78 -0
  19. package/lib/client/patrol-chat.js +54 -0
  20. package/lib/client/store.d.ts +11 -0
  21. package/lib/client/store.js +23 -0
  22. package/lib/command.d.ts +7 -0
  23. package/lib/command.js +52 -0
  24. package/lib/config.d.ts +37 -0
  25. package/lib/config.js +83 -0
  26. package/lib/emission-guard.d.ts +32 -0
  27. package/lib/emission-guard.js +74 -0
  28. package/lib/gating.d.ts +8 -0
  29. package/lib/gating.js +48 -0
  30. package/lib/history.d.ts +20 -0
  31. package/lib/history.js +95 -0
  32. package/lib/index.d.ts +25 -0
  33. package/lib/index.js +96 -0
  34. package/lib/llm-call.d.ts +69 -0
  35. package/lib/llm-call.js +483 -0
  36. package/lib/patrol-event.d.ts +88 -0
  37. package/lib/patrol-event.js +68 -0
  38. package/lib/patrol.d.ts +51 -0
  39. package/lib/patrol.js +202 -0
  40. package/lib/prompt-section.d.ts +7 -0
  41. package/lib/prompt-section.js +16 -0
  42. package/lib/settings.d.ts +18 -0
  43. package/lib/settings.js +50 -0
  44. package/lib/tool.d.ts +22 -0
  45. package/lib/tool.js +91 -0
  46. package/package.json +91 -0
  47. package/src/advisor-prompt.ts +102 -0
  48. package/src/client/AdvisorCard.tsx +253 -0
  49. package/src/client/AdvisorToolRow.tsx +83 -0
  50. package/src/client/PatrolNodeView.tsx +85 -0
  51. package/src/client/controller.ts +399 -0
  52. package/src/client/index.ts +100 -0
  53. package/src/client/locales.ts +105 -0
  54. package/src/client/patrol-chat.ts +109 -0
  55. package/src/client/store.ts +29 -0
  56. package/src/command.ts +61 -0
  57. package/src/config.ts +113 -0
  58. package/src/emission-guard.ts +76 -0
  59. package/src/gating.ts +54 -0
  60. package/src/history.ts +102 -0
  61. package/src/index.ts +106 -0
  62. package/src/llm-call.ts +552 -0
  63. package/src/patrol-event.ts +115 -0
  64. package/src/patrol.ts +229 -0
  65. package/src/prompt-section.ts +21 -0
  66. package/src/settings.ts +74 -0
  67. package/src/tool.ts +114 -0
@@ -0,0 +1,161 @@
1
+ /**
2
+ * controller —— Advisor 设置卡片的暂存编辑器(浏览器半)。
3
+ *
4
+ * 数据面(均为 rc.6 客户端服务):
5
+ * - 设置:ctx.settingsScope.bind({namespace:'advisor'}) —— 快照/订阅/
6
+ * set(field, value) 逐字段写入(写的是用户层 ~/.dsh/settings.yaml)
7
+ * - 模型目录:ctx.get('connection').api.llm.models({}) —— 枚举 DSH 已
8
+ * 配置的全部 provider 与模型,正是"选择 DSH 已经配置好的模型"的数据源
9
+ *
10
+ * 暂存语义:改动先进 draft,save 一次性逐字段写入;discard 丢掉 draft。
11
+ * 类型上不依赖官方 client 包的模块增强(rc 阶段漂移面大),
12
+ * 连接 API 在边界做一次结构化收窄——白皮书第 3/4 章的边界放宽纪律。
13
+ */
14
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
15
+ import { type SnapshotStore } from './store.js';
16
+ /**
17
+ * 设置作用域的结构化契约(本地声明,跨 dsh 版本无关——rc.6/0.1.1 的类型
18
+ * 宿主在 dsh-client-runtime,0.1.2 在 dsh-client-ui-settings,追版本不如
19
+ * 只声明自己用到的面)。bind 边界做一次收窄。
20
+ */
21
+ export interface AdvisorSettingsScope<T> {
22
+ getSnapshot(): {
23
+ status: 'loading' | 'ready' | 'unavailable';
24
+ value: T | undefined;
25
+ writable: boolean;
26
+ };
27
+ subscribe(listener: () => void): () => void;
28
+ set(field: string, value: unknown): Promise<void>;
29
+ }
30
+ /** 模型目录的一行(llm.models wire schema 的结构子集) */
31
+ export interface CatalogModel {
32
+ id: string;
33
+ name: string;
34
+ /** 该模型声明的推理档位;null/缺省 = 未声明(配置时不过滤,靠 host 自动降级兜底) */
35
+ reasoning?: {
36
+ efforts: readonly {
37
+ id: string;
38
+ name: string;
39
+ }[] | null;
40
+ } | null;
41
+ }
42
+ export interface CatalogGroup {
43
+ id: string;
44
+ name: string;
45
+ models: readonly CatalogModel[];
46
+ }
47
+ /** 一条精确的 provider/model 路由 + 档位 */
48
+ export interface AdvisorRoute {
49
+ provider: string;
50
+ model: string;
51
+ effort: string;
52
+ }
53
+ /** 目录里的一行(含与生效路由的 join 结果) */
54
+ export interface AdvisorCandidate {
55
+ key: string;
56
+ provider: string;
57
+ model: string;
58
+ providerName: string;
59
+ modelName: string;
60
+ available: boolean;
61
+ selected: boolean;
62
+ /** 该模型支持的推理档位(从目录 reasoning 声明提取);undefined = 未声明 */
63
+ effortLevels?: readonly string[];
64
+ }
65
+ export interface AdvisorCardState {
66
+ /** 设置命名空间就绪 */
67
+ available: boolean;
68
+ writable: boolean;
69
+ saving: boolean;
70
+ failed: boolean;
71
+ dirty: boolean;
72
+ catalogStatus: 'idle' | 'loading' | 'ready' | 'error';
73
+ catalogPartial: boolean;
74
+ /** 生效值(draft 优先,其次已保存值) */
75
+ effective: AdvisorRoute;
76
+ /** 巡逻模式的生效值(同 draft 优先) */
77
+ patrol: {
78
+ enabled: boolean;
79
+ everySteps: number;
80
+ immuneTurns: number;
81
+ investigate: boolean;
82
+ };
83
+ /** 当前路由的推理档位支持面(目录声明) */
84
+ efforts: {
85
+ known: boolean;
86
+ levels: readonly string[];
87
+ };
88
+ groups: readonly {
89
+ id: string;
90
+ name: string;
91
+ candidates: readonly AdvisorCandidate[];
92
+ }[];
93
+ }
94
+ export interface AdvisorCardFace {
95
+ hooks: {
96
+ advisorCard: SnapshotStore<AdvisorCardState>;
97
+ };
98
+ pickRoute: (key: string) => void;
99
+ setEffort: (effort: string) => void;
100
+ togglePatrol: () => void;
101
+ setPatrolEverySteps: (steps: number) => void;
102
+ setPatrolImmuneTurns: (n: number) => void;
103
+ toggleInvestigate: () => void;
104
+ save: () => void;
105
+ discard: () => void;
106
+ retryCatalog: () => void;
107
+ }
108
+ /** 目录 + 生效路由 → 分组候选行(纯函数;已保存但目录外路由以不可用行展示) */
109
+ export declare function buildCandidates(groups: readonly CatalogGroup[], effective: AdvisorRoute): {
110
+ groups: {
111
+ id: string;
112
+ name: string;
113
+ candidates: AdvisorCandidate[];
114
+ }[];
115
+ stale: boolean;
116
+ };
117
+ /** 当前生效路由的档位支持面(纯函数):known=false 表示目录未声明,不做过滤 */
118
+ export declare function currentEffortSupport(groups: readonly CatalogGroup[], route: AdvisorRoute): {
119
+ known: boolean;
120
+ levels: readonly string[];
121
+ };
122
+ export interface AdvisorSection {
123
+ provider?: string;
124
+ model?: string;
125
+ effort?: string;
126
+ patrolEnabled?: boolean;
127
+ patrolEverySteps?: number;
128
+ patrolImmuneTurns?: number;
129
+ investigate?: boolean;
130
+ }
131
+ export declare class AdvisorCardController {
132
+ private readonly scope;
133
+ private readonly ctx;
134
+ private catalogGroups;
135
+ private catalogStatus;
136
+ private catalogPartial;
137
+ private draft;
138
+ private saving;
139
+ private failed;
140
+ private disposed;
141
+ private catalogGeneration;
142
+ private readonly store;
143
+ private readonly unsubscribe;
144
+ constructor(scope: AdvisorSettingsScope<AdvisorSection>, ctx: ClientContext);
145
+ dispose(): void;
146
+ inject(): AdvisorCardFace;
147
+ private saved;
148
+ private savedPatrol;
149
+ private effective;
150
+ private pickRoute;
151
+ private setEffort;
152
+ private togglePatrol;
153
+ private setPatrolEverySteps;
154
+ private setPatrolImmuneTurns;
155
+ private toggleInvestigate;
156
+ private discard;
157
+ private save;
158
+ private loadCatalog;
159
+ private projection;
160
+ private publish;
161
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * controller —— Advisor 设置卡片的暂存编辑器(浏览器半)。
3
+ *
4
+ * 数据面(均为 rc.6 客户端服务):
5
+ * - 设置:ctx.settingsScope.bind({namespace:'advisor'}) —— 快照/订阅/
6
+ * set(field, value) 逐字段写入(写的是用户层 ~/.dsh/settings.yaml)
7
+ * - 模型目录:ctx.get('connection').api.llm.models({}) —— 枚举 DSH 已
8
+ * 配置的全部 provider 与模型,正是"选择 DSH 已经配置好的模型"的数据源
9
+ *
10
+ * 暂存语义:改动先进 draft,save 一次性逐字段写入;discard 丢掉 draft。
11
+ * 类型上不依赖官方 client 包的模块增强(rc 阶段漂移面大),
12
+ * 连接 API 在边界做一次结构化收窄——白皮书第 3/4 章的边界放宽纪律。
13
+ */
14
+ import { createSnapshotStore } from './store.js';
15
+ function routeKey(provider, model) {
16
+ return `${provider}\u0000${model}`;
17
+ }
18
+ /** 目录 + 生效路由 → 分组候选行(纯函数;已保存但目录外路由以不可用行展示) */
19
+ export function buildCandidates(groups, effective) {
20
+ const selectedKey = effective.provider === '' || effective.model === '' ? undefined : routeKey(effective.provider, effective.model);
21
+ const seen = new Set();
22
+ const out = groups.map(group => ({
23
+ id: group.id,
24
+ name: group.name,
25
+ candidates: group.models.map(model => {
26
+ const key = routeKey(group.id, model.id);
27
+ seen.add(key);
28
+ const declared = model.reasoning?.efforts;
29
+ return {
30
+ key,
31
+ provider: group.id,
32
+ model: model.id,
33
+ providerName: group.name,
34
+ modelName: model.name,
35
+ available: true,
36
+ selected: key === selectedKey,
37
+ ...(declared === undefined || declared === null ? {} : { effortLevels: declared.map(e => e.id) }),
38
+ };
39
+ }),
40
+ }));
41
+ let stale = false;
42
+ // 已保存但目录中不存在的路由:以"不可用"行展示,保证用户能取消选择
43
+ if (selectedKey !== undefined && !seen.has(selectedKey)) {
44
+ stale = true;
45
+ out.push({
46
+ id: effective.provider,
47
+ name: effective.provider,
48
+ candidates: [{
49
+ key: selectedKey,
50
+ provider: effective.provider,
51
+ model: effective.model,
52
+ providerName: effective.provider,
53
+ modelName: effective.model,
54
+ available: false,
55
+ selected: true,
56
+ }],
57
+ });
58
+ }
59
+ return { groups: out, stale };
60
+ }
61
+ /** 当前生效路由的档位支持面(纯函数):known=false 表示目录未声明,不做过滤 */
62
+ export function currentEffortSupport(groups, route) {
63
+ if (route.provider === '' || route.model === '')
64
+ return { known: false, levels: [] };
65
+ const declared = groups
66
+ .find(g => g.id === route.provider)
67
+ ?.models.find(m => m.id === route.model)
68
+ ?.reasoning?.efforts;
69
+ if (declared === undefined || declared === null)
70
+ return { known: false, levels: [] };
71
+ return { known: true, levels: declared.map(e => e.id) };
72
+ }
73
+ export class AdvisorCardController {
74
+ scope;
75
+ ctx;
76
+ catalogGroups = [];
77
+ catalogStatus = 'idle';
78
+ catalogPartial = false;
79
+ draft;
80
+ saving = false;
81
+ failed = false;
82
+ disposed = false;
83
+ catalogGeneration = 0;
84
+ store;
85
+ unsubscribe;
86
+ constructor(scope, ctx) {
87
+ this.scope = scope;
88
+ this.ctx = ctx;
89
+ this.store = createSnapshotStore(this.projection());
90
+ this.unsubscribe = scope.subscribe(() => {
91
+ if (!this.saving)
92
+ this.publish();
93
+ });
94
+ if (this.catalogStatus === 'idle')
95
+ void this.loadCatalog();
96
+ }
97
+ dispose() {
98
+ this.disposed = true;
99
+ this.catalogGeneration += 1;
100
+ this.unsubscribe();
101
+ }
102
+ inject() {
103
+ return {
104
+ hooks: { advisorCard: this.store },
105
+ pickRoute: (key) => this.pickRoute(key),
106
+ setEffort: (effort) => this.setEffort(effort),
107
+ togglePatrol: () => this.togglePatrol(),
108
+ setPatrolEverySteps: (steps) => this.setPatrolEverySteps(steps),
109
+ setPatrolImmuneTurns: (n) => this.setPatrolImmuneTurns(n),
110
+ toggleInvestigate: () => this.toggleInvestigate(),
111
+ save: () => { void this.save(); },
112
+ discard: () => this.discard(),
113
+ retryCatalog: () => { void this.loadCatalog(); },
114
+ };
115
+ }
116
+ saved() {
117
+ const value = this.scope.getSnapshot().value;
118
+ return {
119
+ provider: value?.provider ?? '',
120
+ model: value?.model ?? '',
121
+ effort: value?.effort ?? '',
122
+ patrolEnabled: value?.patrolEnabled ?? true,
123
+ patrolEverySteps: value?.patrolEverySteps ?? 6,
124
+ patrolImmuneTurns: value?.patrolImmuneTurns ?? 3,
125
+ investigate: value?.investigate ?? true,
126
+ };
127
+ }
128
+ savedPatrol() {
129
+ const saved = this.saved();
130
+ return { enabled: saved.patrolEnabled !== false, everySteps: saved.patrolEverySteps ?? 6 };
131
+ }
132
+ effective() {
133
+ return this.draft ?? this.saved();
134
+ }
135
+ pickRoute(key) {
136
+ if (!this.scope.getSnapshot().writable || this.saving)
137
+ return;
138
+ const all = this.catalogGroups.flatMap(g => g.models.map(m => ({ provider: g.id, model: m.id })));
139
+ const hit = all.find(r => routeKey(r.provider, r.model) === key);
140
+ const base = this.effective();
141
+ const currentKey = base.provider === '' || base.model === '' ? undefined : routeKey(base.provider, base.model);
142
+ // 再次点击已选路由 = 取消选择(advisor 回到未武装态);目录外 stale 行同理
143
+ if (currentKey === key) {
144
+ this.draft = { ...base, provider: '', model: '' };
145
+ }
146
+ else if (hit !== undefined) {
147
+ let draft = { ...base, provider: hit.provider, model: hit.model };
148
+ // 档位校验:新模型已声明支持面且当前档位不在其中 → 自动清空(走模型默认)
149
+ const support = currentEffortSupport(this.catalogGroups, draft);
150
+ if (support.known && draft.effort !== '' && !support.levels.includes(draft.effort)) {
151
+ console.log(`[dsh-advisor] 档位 ${draft.effort} 不被 ${draft.provider}/${draft.model} 支持,已自动清空`);
152
+ draft = { ...draft, effort: '' };
153
+ }
154
+ this.draft = draft;
155
+ }
156
+ this.failed = false;
157
+ this.publish();
158
+ }
159
+ setEffort(effort) {
160
+ if (!this.scope.getSnapshot().writable || this.saving)
161
+ return;
162
+ // 档位校验:已声明支持面时只接受其中的档位
163
+ const support = currentEffortSupport(this.catalogGroups, this.effective());
164
+ if (support.known && effort !== '' && !support.levels.includes(effort))
165
+ return;
166
+ this.draft = { ...this.effective(), effort };
167
+ this.failed = false;
168
+ this.publish();
169
+ }
170
+ togglePatrol() {
171
+ if (!this.scope.getSnapshot().writable || this.saving)
172
+ return;
173
+ const current = this.effective();
174
+ this.draft = { ...current, patrolEnabled: !(current.patrolEnabled ?? true) };
175
+ this.failed = false;
176
+ this.publish();
177
+ }
178
+ setPatrolEverySteps(steps) {
179
+ if (!this.scope.getSnapshot().writable || this.saving)
180
+ return;
181
+ const bounded = Number.isFinite(steps) ? Math.min(500, Math.max(2, Math.round(steps))) : 6;
182
+ this.draft = { ...this.effective(), patrolEverySteps: bounded };
183
+ this.failed = false;
184
+ this.publish();
185
+ }
186
+ setPatrolImmuneTurns(n) {
187
+ if (!this.scope.getSnapshot().writable || this.saving)
188
+ return;
189
+ const bounded = Number.isFinite(n) ? Math.min(20, Math.max(0, Math.round(n))) : 3;
190
+ this.draft = { ...this.effective(), patrolImmuneTurns: bounded };
191
+ this.failed = false;
192
+ this.publish();
193
+ }
194
+ toggleInvestigate() {
195
+ if (!this.scope.getSnapshot().writable || this.saving)
196
+ return;
197
+ const current = this.effective();
198
+ this.draft = { ...current, investigate: !(current.investigate ?? true) };
199
+ this.failed = false;
200
+ this.publish();
201
+ }
202
+ discard() {
203
+ if (this.saving)
204
+ return;
205
+ this.draft = undefined;
206
+ this.failed = false;
207
+ this.publish();
208
+ }
209
+ async save() {
210
+ const desired = this.draft;
211
+ if (this.disposed || desired === undefined || this.saving || !this.scope.getSnapshot().writable)
212
+ return;
213
+ this.saving = true;
214
+ this.failed = false;
215
+ this.publish();
216
+ try {
217
+ // rc.6 scope 是逐字段写:五个字段顺序写入('' 也写,保证旧值被清掉)
218
+ await this.scope.set('provider', desired.provider);
219
+ await this.scope.set('model', desired.model);
220
+ await this.scope.set('effort', desired.effort);
221
+ await this.scope.set('patrolEnabled', desired.patrolEnabled ?? true);
222
+ await this.scope.set('patrolEverySteps', desired.patrolEverySteps ?? 6);
223
+ await this.scope.set('patrolImmuneTurns', desired.patrolImmuneTurns ?? 3);
224
+ await this.scope.set('investigate', desired.investigate ?? true);
225
+ this.draft = undefined;
226
+ }
227
+ catch {
228
+ this.failed = true;
229
+ }
230
+ this.saving = false;
231
+ this.publish();
232
+ }
233
+ async loadCatalog() {
234
+ if (this.disposed || this.catalogStatus === 'loading')
235
+ return;
236
+ const generation = this.catalogGeneration;
237
+ this.catalogStatus = 'loading';
238
+ this.catalogPartial = false;
239
+ this.publish();
240
+ try {
241
+ const connection = this.ctx.get('connection');
242
+ const response = await connection.api.llm.models({});
243
+ if (generation !== this.catalogGeneration)
244
+ return;
245
+ if (response.result.ok) {
246
+ this.catalogGroups = response.result.value.groups;
247
+ this.catalogPartial = response.result.value.failures.length > 0;
248
+ this.catalogStatus = 'ready';
249
+ }
250
+ else {
251
+ this.catalogStatus = 'error';
252
+ }
253
+ }
254
+ catch {
255
+ if (generation !== this.catalogGeneration)
256
+ return;
257
+ this.catalogStatus = 'error';
258
+ }
259
+ this.publish();
260
+ }
261
+ projection() {
262
+ const snapshot = this.scope.getSnapshot();
263
+ const effective = this.effective();
264
+ const built = buildCandidates(this.catalogGroups, effective);
265
+ return {
266
+ available: snapshot.status === 'ready',
267
+ writable: snapshot.writable,
268
+ saving: this.saving,
269
+ failed: this.failed,
270
+ dirty: this.draft !== undefined,
271
+ catalogStatus: this.catalogStatus,
272
+ catalogPartial: this.catalogPartial,
273
+ effective,
274
+ patrol: {
275
+ enabled: effective.patrolEnabled ?? true,
276
+ everySteps: effective.patrolEverySteps ?? 6,
277
+ immuneTurns: effective.patrolImmuneTurns ?? 3,
278
+ investigate: effective.investigate ?? true,
279
+ },
280
+ efforts: currentEffortSupport(this.catalogGroups, effective),
281
+ groups: built.groups,
282
+ };
283
+ }
284
+ publish() {
285
+ this.store.set(this.projection());
286
+ }
287
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * dsh-advisor —— 浏览器半(client half)入口。
3
+ *
4
+ * 职责:把 Advisor 设置卡片注册进设置页"插件"栏的 `settings.plugin.item`
5
+ * 槽(rc.6 形态:id='advisor' 的列表项卡片)。卡片数据 = 'advisor' 设置
6
+ * 作用域 + connection.api.llm.models 模型目录。
7
+ *
8
+ * 依赖(dsh.client.inject):locale / ui-settings / connection / runtime /
9
+ * ui-settings-plugins(槽类型归属包)——全部由 web 应用提供;本包零
10
+ * 运行时值导入(@deepseek-ai/* 只做类型导入,react 走平台 jsx-runtime)。
11
+ */
12
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
13
+ import { type LocaleKey } from './locales.js';
14
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
15
+ interface LocaleNamespaceMap {
16
+ 'dsh-advisor.card': LocaleKey;
17
+ }
18
+ }
19
+ export declare const inject: string[];
20
+ export declare function apply(ctx: ClientContext): void;