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,399 @@
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
+
15
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
16
+ import { createSnapshotStore, type SnapshotStore } from './store.js'
17
+
18
+ /**
19
+ * 设置作用域的结构化契约(本地声明,跨 dsh 版本无关——rc.6/0.1.1 的类型
20
+ * 宿主在 dsh-client-runtime,0.1.2 在 dsh-client-ui-settings,追版本不如
21
+ * 只声明自己用到的面)。bind 边界做一次收窄。
22
+ */
23
+ export interface AdvisorSettingsScope<T> {
24
+ getSnapshot(): { status: 'loading' | 'ready' | 'unavailable', value: T | undefined, writable: boolean }
25
+ subscribe(listener: () => void): () => void
26
+ set(field: string, value: unknown): Promise<void>
27
+ }
28
+
29
+ /** 模型目录的一行(llm.models wire schema 的结构子集) */
30
+ export interface CatalogModel {
31
+ id: string
32
+ name: string
33
+ /** 该模型声明的推理档位;null/缺省 = 未声明(配置时不过滤,靠 host 自动降级兜底) */
34
+ reasoning?: { efforts: readonly { id: string, name: string }[] | null } | null
35
+ }
36
+
37
+ export interface CatalogGroup {
38
+ id: string
39
+ name: string
40
+ models: readonly CatalogModel[]
41
+ }
42
+
43
+ interface ModelCatalogResponse {
44
+ groups: CatalogGroup[]
45
+ failures: readonly { id: string }[]
46
+ }
47
+
48
+ /** connection 服务的 llm 面结构收窄(唯一边界) */
49
+ interface ConnectionLlApi {
50
+ llm: {
51
+ models: (payload: Record<string, never>, signal?: AbortSignal) => Promise<
52
+ | { result: { ok: true, value: ModelCatalogResponse } }
53
+ | { result: { ok: false } }
54
+ >
55
+ }
56
+ }
57
+
58
+ /** 一条精确的 provider/model 路由 + 档位 */
59
+ export interface AdvisorRoute {
60
+ provider: string
61
+ model: string
62
+ effort: string
63
+ }
64
+
65
+ /** 卡片暂存的完整段值(路由 + 档位 + 巡逻设置) */
66
+ type SectionDraft = Required<AdvisorSection>
67
+
68
+ /** 目录里的一行(含与生效路由的 join 结果) */
69
+ export interface AdvisorCandidate {
70
+ key: string
71
+ provider: string
72
+ model: string
73
+ providerName: string
74
+ modelName: string
75
+ available: boolean
76
+ selected: boolean
77
+ /** 该模型支持的推理档位(从目录 reasoning 声明提取);undefined = 未声明 */
78
+ effortLevels?: readonly string[]
79
+ }
80
+
81
+ export interface AdvisorCardState {
82
+ /** 设置命名空间就绪 */
83
+ available: boolean
84
+ writable: boolean
85
+ saving: boolean
86
+ failed: boolean
87
+ dirty: boolean
88
+ catalogStatus: 'idle' | 'loading' | 'ready' | 'error'
89
+ catalogPartial: boolean
90
+ /** 生效值(draft 优先,其次已保存值) */
91
+ effective: AdvisorRoute
92
+ /** 巡逻模式的生效值(同 draft 优先) */
93
+ patrol: { enabled: boolean, everySteps: number, immuneTurns: number, investigate: boolean }
94
+ /** 当前路由的推理档位支持面(目录声明) */
95
+ efforts: { known: boolean, levels: readonly string[] }
96
+ groups: readonly { id: string, name: string, candidates: readonly AdvisorCandidate[] }[]
97
+ }
98
+
99
+ export interface AdvisorCardFace {
100
+ hooks: { advisorCard: SnapshotStore<AdvisorCardState> }
101
+ pickRoute: (key: string) => void
102
+ setEffort: (effort: string) => void
103
+ togglePatrol: () => void
104
+ setPatrolEverySteps: (steps: number) => void
105
+ setPatrolImmuneTurns: (n: number) => void
106
+ toggleInvestigate: () => void
107
+ save: () => void
108
+ discard: () => void
109
+ retryCatalog: () => void
110
+ }
111
+
112
+ function routeKey(provider: string, model: string): string {
113
+ return `${provider}\u0000${model}`
114
+ }
115
+
116
+ /** 目录 + 生效路由 → 分组候选行(纯函数;已保存但目录外路由以不可用行展示) */
117
+ export function buildCandidates(
118
+ groups: readonly CatalogGroup[],
119
+ effective: AdvisorRoute,
120
+ ): { groups: { id: string, name: string, candidates: AdvisorCandidate[] }[], stale: boolean } {
121
+ const selectedKey = effective.provider === '' || effective.model === '' ? undefined : routeKey(effective.provider, effective.model)
122
+ const seen = new Set<string>()
123
+ const out = groups.map(group => ({
124
+ id: group.id,
125
+ name: group.name,
126
+ candidates: group.models.map(model => {
127
+ const key = routeKey(group.id, model.id)
128
+ seen.add(key)
129
+ const declared = model.reasoning?.efforts
130
+ return {
131
+ key,
132
+ provider: group.id,
133
+ model: model.id,
134
+ providerName: group.name,
135
+ modelName: model.name,
136
+ available: true,
137
+ selected: key === selectedKey,
138
+ ...(declared === undefined || declared === null ? {} : { effortLevels: declared.map(e => e.id) }),
139
+ }
140
+ }),
141
+ }))
142
+ let stale = false
143
+ // 已保存但目录中不存在的路由:以"不可用"行展示,保证用户能取消选择
144
+ if (selectedKey !== undefined && !seen.has(selectedKey)) {
145
+ stale = true
146
+ out.push({
147
+ id: effective.provider,
148
+ name: effective.provider,
149
+ candidates: [{
150
+ key: selectedKey,
151
+ provider: effective.provider,
152
+ model: effective.model,
153
+ providerName: effective.provider,
154
+ modelName: effective.model,
155
+ available: false,
156
+ selected: true,
157
+ }],
158
+ })
159
+ }
160
+ return { groups: out, stale }
161
+ }
162
+
163
+ /** 当前生效路由的档位支持面(纯函数):known=false 表示目录未声明,不做过滤 */
164
+ export function currentEffortSupport(
165
+ groups: readonly CatalogGroup[],
166
+ route: AdvisorRoute,
167
+ ): { known: boolean, levels: readonly string[] } {
168
+ if (route.provider === '' || route.model === '') return { known: false, levels: [] }
169
+ const declared = groups
170
+ .find(g => g.id === route.provider)
171
+ ?.models.find(m => m.id === route.model)
172
+ ?.reasoning?.efforts
173
+ if (declared === undefined || declared === null) return { known: false, levels: [] }
174
+ return { known: true, levels: declared.map(e => e.id) }
175
+ }
176
+
177
+ export interface AdvisorSection {
178
+ provider?: string
179
+ model?: string
180
+ effort?: string
181
+ patrolEnabled?: boolean
182
+ patrolEverySteps?: number
183
+ patrolImmuneTurns?: number
184
+ investigate?: boolean
185
+ }
186
+
187
+ export class AdvisorCardController {
188
+ private catalogGroups: readonly CatalogGroup[] = []
189
+ private catalogStatus: AdvisorCardState['catalogStatus'] = 'idle'
190
+ private catalogPartial = false
191
+ private draft: SectionDraft | undefined
192
+ private saving = false
193
+ private failed = false
194
+ private disposed = false
195
+ private catalogGeneration = 0
196
+ private readonly store: SnapshotStore<AdvisorCardState>
197
+ private readonly unsubscribe: () => void
198
+
199
+ constructor(
200
+ private readonly scope: AdvisorSettingsScope<AdvisorSection>,
201
+ private readonly ctx: ClientContext,
202
+ ) {
203
+ this.store = createSnapshotStore<AdvisorCardState>(this.projection())
204
+ this.unsubscribe = scope.subscribe(() => {
205
+ if (!this.saving) this.publish()
206
+ })
207
+ if (this.catalogStatus === 'idle') void this.loadCatalog()
208
+ }
209
+
210
+ dispose(): void {
211
+ this.disposed = true
212
+ this.catalogGeneration += 1
213
+ this.unsubscribe()
214
+ }
215
+
216
+ inject(): AdvisorCardFace {
217
+ return {
218
+ hooks: { advisorCard: this.store },
219
+ pickRoute: (key) => this.pickRoute(key),
220
+ setEffort: (effort) => this.setEffort(effort),
221
+ togglePatrol: () => this.togglePatrol(),
222
+ setPatrolEverySteps: (steps) => this.setPatrolEverySteps(steps),
223
+ setPatrolImmuneTurns: (n) => this.setPatrolImmuneTurns(n),
224
+ toggleInvestigate: () => this.toggleInvestigate(),
225
+ save: () => { void this.save() },
226
+ discard: () => this.discard(),
227
+ retryCatalog: () => { void this.loadCatalog() },
228
+ }
229
+ }
230
+
231
+ private saved(): SectionDraft {
232
+ const value = this.scope.getSnapshot().value
233
+ return {
234
+ provider: value?.provider ?? '',
235
+ model: value?.model ?? '',
236
+ effort: value?.effort ?? '',
237
+ patrolEnabled: value?.patrolEnabled ?? true,
238
+ patrolEverySteps: value?.patrolEverySteps ?? 6,
239
+ patrolImmuneTurns: value?.patrolImmuneTurns ?? 3,
240
+ investigate: value?.investigate ?? true,
241
+ }
242
+ }
243
+
244
+ private savedPatrol(): { enabled: boolean, everySteps: number } {
245
+ const saved = this.saved()
246
+ return { enabled: saved.patrolEnabled !== false, everySteps: saved.patrolEverySteps ?? 6 }
247
+ }
248
+
249
+ private effective(): SectionDraft {
250
+ return this.draft ?? this.saved()
251
+ }
252
+
253
+ private pickRoute(key: string): void {
254
+ if (!this.scope.getSnapshot().writable || this.saving) return
255
+ const all = this.catalogGroups.flatMap(g => g.models.map(m => ({ provider: g.id, model: m.id })))
256
+ const hit = all.find(r => routeKey(r.provider, r.model) === key)
257
+ const base = this.effective()
258
+ const currentKey = base.provider === '' || base.model === '' ? undefined : routeKey(base.provider, base.model)
259
+ // 再次点击已选路由 = 取消选择(advisor 回到未武装态);目录外 stale 行同理
260
+ if (currentKey === key) {
261
+ this.draft = { ...base, provider: '', model: '' }
262
+ } else if (hit !== undefined) {
263
+ let draft: SectionDraft = { ...base, provider: hit.provider, model: hit.model }
264
+ // 档位校验:新模型已声明支持面且当前档位不在其中 → 自动清空(走模型默认)
265
+ const support = currentEffortSupport(this.catalogGroups, draft)
266
+ if (support.known && draft.effort !== '' && !support.levels.includes(draft.effort)) {
267
+ console.log(`[dsh-advisor] 档位 ${draft.effort} 不被 ${draft.provider}/${draft.model} 支持,已自动清空`)
268
+ draft = { ...draft, effort: '' }
269
+ }
270
+ this.draft = draft
271
+ }
272
+ this.failed = false
273
+ this.publish()
274
+ }
275
+
276
+ private setEffort(effort: string): void {
277
+ if (!this.scope.getSnapshot().writable || this.saving) return
278
+ // 档位校验:已声明支持面时只接受其中的档位
279
+ const support = currentEffortSupport(this.catalogGroups, this.effective())
280
+ if (support.known && effort !== '' && !support.levels.includes(effort)) return
281
+ this.draft = { ...this.effective(), effort }
282
+ this.failed = false
283
+ this.publish()
284
+ }
285
+
286
+ private togglePatrol(): void {
287
+ if (!this.scope.getSnapshot().writable || this.saving) return
288
+ const current = this.effective()
289
+ this.draft = { ...current, patrolEnabled: !(current.patrolEnabled ?? true) }
290
+ this.failed = false
291
+ this.publish()
292
+ }
293
+
294
+ private setPatrolEverySteps(steps: number): void {
295
+ if (!this.scope.getSnapshot().writable || this.saving) return
296
+ const bounded = Number.isFinite(steps) ? Math.min(500, Math.max(2, Math.round(steps))) : 6
297
+ this.draft = { ...this.effective(), patrolEverySteps: bounded }
298
+ this.failed = false
299
+ this.publish()
300
+ }
301
+
302
+ private setPatrolImmuneTurns(n: number): void {
303
+ if (!this.scope.getSnapshot().writable || this.saving) return
304
+ const bounded = Number.isFinite(n) ? Math.min(20, Math.max(0, Math.round(n))) : 3
305
+ this.draft = { ...this.effective(), patrolImmuneTurns: bounded }
306
+ this.failed = false
307
+ this.publish()
308
+ }
309
+
310
+ private toggleInvestigate(): void {
311
+ if (!this.scope.getSnapshot().writable || this.saving) return
312
+ const current = this.effective()
313
+ this.draft = { ...current, investigate: !(current.investigate ?? true) }
314
+ this.failed = false
315
+ this.publish()
316
+ }
317
+
318
+ private discard(): void {
319
+ if (this.saving) return
320
+ this.draft = undefined
321
+ this.failed = false
322
+ this.publish()
323
+ }
324
+
325
+ private async save(): Promise<void> {
326
+ const desired = this.draft
327
+ if (this.disposed || desired === undefined || this.saving || !this.scope.getSnapshot().writable) return
328
+ this.saving = true
329
+ this.failed = false
330
+ this.publish()
331
+ try {
332
+ // rc.6 scope 是逐字段写:五个字段顺序写入('' 也写,保证旧值被清掉)
333
+ await this.scope.set('provider', desired.provider)
334
+ await this.scope.set('model', desired.model)
335
+ await this.scope.set('effort', desired.effort)
336
+ await this.scope.set('patrolEnabled', desired.patrolEnabled ?? true)
337
+ await this.scope.set('patrolEverySteps', desired.patrolEverySteps ?? 6)
338
+ await this.scope.set('patrolImmuneTurns', desired.patrolImmuneTurns ?? 3)
339
+ await this.scope.set('investigate', desired.investigate ?? true)
340
+ this.draft = undefined
341
+ } catch {
342
+ this.failed = true
343
+ }
344
+ this.saving = false
345
+ this.publish()
346
+ }
347
+
348
+ private async loadCatalog(): Promise<void> {
349
+ if (this.disposed || this.catalogStatus === 'loading') return
350
+ const generation = this.catalogGeneration
351
+ this.catalogStatus = 'loading'
352
+ this.catalogPartial = false
353
+ this.publish()
354
+ try {
355
+ const connection = this.ctx.get('connection') as unknown as { api: ConnectionLlApi }
356
+ const response = await connection.api.llm.models({})
357
+ if (generation !== this.catalogGeneration) return
358
+ if (response.result.ok) {
359
+ this.catalogGroups = response.result.value.groups
360
+ this.catalogPartial = response.result.value.failures.length > 0
361
+ this.catalogStatus = 'ready'
362
+ } else {
363
+ this.catalogStatus = 'error'
364
+ }
365
+ } catch {
366
+ if (generation !== this.catalogGeneration) return
367
+ this.catalogStatus = 'error'
368
+ }
369
+ this.publish()
370
+ }
371
+
372
+ private projection(): AdvisorCardState {
373
+ const snapshot = this.scope.getSnapshot()
374
+ const effective = this.effective()
375
+ const built = buildCandidates(this.catalogGroups, effective)
376
+ return {
377
+ available: snapshot.status === 'ready',
378
+ writable: snapshot.writable,
379
+ saving: this.saving,
380
+ failed: this.failed,
381
+ dirty: this.draft !== undefined,
382
+ catalogStatus: this.catalogStatus,
383
+ catalogPartial: this.catalogPartial,
384
+ effective,
385
+ patrol: {
386
+ enabled: effective.patrolEnabled ?? true,
387
+ everySteps: effective.patrolEverySteps ?? 6,
388
+ immuneTurns: effective.patrolImmuneTurns ?? 3,
389
+ investigate: effective.investigate ?? true,
390
+ },
391
+ efforts: currentEffortSupport(this.catalogGroups, effective),
392
+ groups: built.groups,
393
+ }
394
+ }
395
+
396
+ private publish(): void {
397
+ this.store.set(this.projection())
398
+ }
399
+ }
@@ -0,0 +1,100 @@
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
+
13
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
14
+ import type {} from '@deepseek-ai/dsh-client-locale/client'
15
+ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
16
+ import type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'
17
+ // 0.1.2 起 slots 的 Context/SlotMap 类型增强住在 dsh-client-ui-cordis;
18
+ // ui-tool 携带 tool.call.toolview 槽契约(key 域对插件自有工具开放)
19
+ import type {} from '@deepseek-ai/dsh-client-ui-cordis/client'
20
+ import type {} from '@deepseek-ai/dsh-client-ui-tool/client'
21
+ import type {} from '@deepseek-ai/dsh-client-ui-slots'
22
+ import { AdvisorCard } from './AdvisorCard.js'
23
+ import { AdvisorToolRow } from './AdvisorToolRow.js'
24
+ import { PatrolNodeView } from './PatrolNodeView.js'
25
+ import { AdvisorCardController, type AdvisorSection, type AdvisorSettingsScope } from './controller.js'
26
+ import type { ConversationEventsService } from './patrol-chat.js'
27
+ import { patrolChatDefinition } from './patrol-chat.js'
28
+ import { en, zh, type LocaleKey } from './locales.js'
29
+
30
+ // 声明本插件的 locale 命名空间(值类型 = 文案键联合,官方同款姿势)
31
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
32
+ interface LocaleNamespaceMap {
33
+ 'dsh-advisor.card': LocaleKey
34
+ }
35
+ }
36
+
37
+ // conversationEvents:0.1.1 客户端会话事件→聊天节点 Definition 注册面(巡逻
38
+ // 卡片的物化通道)——cordis 要求先在 inject 声明才能在 ctx 上访问
39
+ export const inject = ['slots', 'locale', 'connection', 'settingsScope', 'conversationEvents']
40
+
41
+ const NS = 'dsh-advisor.card'
42
+
43
+ export function apply(ctx: ClientContext): void {
44
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-advisor: card dictionaries')
45
+
46
+ // bind 边界结构收窄(官方 SettingsScope 类型宿主随版本漂移,见 controller.ts 注释)
47
+ const scope = ctx.settingsScope.bind({ namespace: 'advisor' }) as unknown as AdvisorSettingsScope<AdvisorSection>
48
+ const controller = new AdvisorCardController(scope, ctx)
49
+ ctx.effect(() => () => { controller.dispose() }, 'dsh-advisor: settings card')
50
+
51
+ ctx.slots.inject('settings.plugin.item', function* () {
52
+ // 跨版本兼容:rc.6 用 options.id 标识列表项,0.1.1+ 用 options.key——
53
+ // 两个都传,各自读各自的(多余属性被忽略);类型在边界放宽
54
+ yield ctx.slots.register({
55
+ name: 'settings.plugin.item',
56
+ key: 'advisor',
57
+ id: 'advisor',
58
+ order: 30,
59
+ locale: NS,
60
+ inject: () => {
61
+ const t = ctx.locale.bind(NS) as (key: LocaleKey) => string
62
+ return { ...controller.inject(), t }
63
+ },
64
+ } as never, AdvisorCard)
65
+ })
66
+
67
+ // advisor 工具的专属会话行(keyed 槽按工具名派发,官方对插件自有
68
+ // 工具开放的渲染定制点):🧭 标题 + 状态徽标 + 可展开的建议全文
69
+ ctx.slots.inject('tool.call.toolview', function* () {
70
+ yield ctx.slots.register({
71
+ name: 'tool.call.toolview',
72
+ key: 'advisor',
73
+ locale: NS,
74
+ inject: () => ({ t: ctx.locale.bind(NS) as (key: LocaleKey) => string }),
75
+ } as never, AdvisorToolRow)
76
+ })
77
+
78
+ // 巡逻裁决卡片:conversationEvents 自定义 Definition 把 log-only 的
79
+ // 裁决事件物化为 kind 'advisor-patrol' 聊天节点,keyed 槽按 kind 派发
80
+ // 到本插件的 🧭 卡片(ON-TRACK 不写事件,天然不出卡片)。整个注册包
81
+ // 在 try/catch 里——可观测性增强绝不拖垮设置卡片与工具行
82
+ try {
83
+ const events = (ctx as unknown as { conversationEvents?: ConversationEventsService }).conversationEvents
84
+ if (events !== undefined) {
85
+ ctx.effect(() => events.register(patrolChatDefinition()), 'dsh-advisor: patrol chat definition')
86
+ ctx.slots.inject('conversation.chat.node', function* () {
87
+ yield ctx.slots.register({
88
+ name: 'conversation.chat.node',
89
+ key: 'advisor-patrol',
90
+ locale: NS,
91
+ inject: () => ({ t: ctx.locale.bind(NS) as (key: LocaleKey) => string }),
92
+ } as never, PatrolNodeView)
93
+ })
94
+ } else {
95
+ console.warn('[dsh-advisor] 当前 UI 未提供 conversationEvents 服务,巡逻卡片不可用(裁决仍会写日志与注入)')
96
+ }
97
+ } catch (error) {
98
+ console.warn('[dsh-advisor] 巡逻卡片注册失败(不影响其余功能):', error)
99
+ }
100
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * locales —— Advisor 设置卡片的 zh/en 文案。
3
+ */
4
+
5
+ export const zh = {
6
+ title: 'Advisor 审查模型',
7
+ description: '执行模型卡住/做重大决策/宣告完成前,可调用零参数的 advisor() 工具升级咨询该审查模型,拿回计划、纠正或停止信号。整段会话每次调用都会转发给它——升级不免费。',
8
+ armed: '当前审查模型',
9
+ off: '未选择——advisor 工具对模型隐藏(零提示词成本)',
10
+ offCurrent: '未选择',
11
+ choose: '选择一个 DSH 已配置的模型:',
12
+ loading: '正在加载模型目录…',
13
+ loadFailed: '模型目录加载失败',
14
+ retry: '重试',
15
+ partial: '部分 provider 的模型列表获取失败,列表可能不完整。',
16
+ empty: '没有可配置的模型——请先在 设置 → 模型 里配置 provider。',
17
+ effort: '推理档位',
18
+ effortDefault: '(模型默认)',
19
+ effortHint: '留空走模型默认;该模型未声明支持档位——若配置的档位不被支持,插件会自动降级为默认档(日志可见)。',
20
+ effortHintKnown: '只列出该模型声明支持的档位;留空走模型默认。',
21
+ unavailable: '(已配置的路由不在当前模型目录中)',
22
+ save: '保存',
23
+ saving: '保存中…',
24
+ discard: '放弃更改',
25
+ saved: '已保存',
26
+ saveFailed: '保存失败,请重试',
27
+ readonly: '设置文档只读,无法在此修改。',
28
+ patrolTitle: '巡逻模式(自动检测跑偏)',
29
+ patrolDescription: '执行过程中每隔若干步自动把会话快照发给审查模型检查方向;发现跑偏会在下一步注入纠偏,STOP 时要求执行模型停下向用户报告。每次巡逻整段会话计费一次审查模型。',
30
+ patrolToggle: '启用巡逻',
31
+ patrolEverySteps: '巡逻间隔(步数)',
32
+ patrolEveryStepsHint: '每 N 个模型请求做一次巡逻检查;两次巡逻之间另有至少 90 秒的节流。',
33
+ patrolImmuneTurns: '纠偏冷却(请求数)',
34
+ investigateToggle: '审查者调查工具',
35
+ investigateHint: '允许审查模型在裁决前用只读工具(工作区内搜索/读文件)亲自核实事实;重复建议与空话建议会被自动去重/丢弃。',
36
+ toolTitle: '咨询',
37
+ toolRunning: '进行中',
38
+ toolDone: '完成',
39
+ toolFailed: '失败',
40
+ toolConsulting: '正在向审查模型转发整段会话…',
41
+ toolExpand: '展开建议全文',
42
+ toolCollapse: '收起',
43
+ patrolCardTitle: '巡逻裁决',
44
+ patrolCheckTitle: '巡逻检查',
45
+ patrolVerdictOnTrack: '正常',
46
+ patrolVerdictCorrection: '纠偏',
47
+ patrolVerdictStop: '叫停',
48
+ patrolCardStepPrefix: '第 ',
49
+ patrolCardStepSuffix: ' 步',
50
+ patrolExpand: '展开裁决全文',
51
+ patrolCollapse: '收起',
52
+ patrolDowngradedTag: '冷却降级·未注入',
53
+ } as const
54
+
55
+ export type LocaleKey = keyof typeof zh
56
+
57
+ export const en: Record<LocaleKey, string> = {
58
+ title: 'Advisor reviewer model',
59
+ description: 'The executor model may call the zero-parameter advisor() tool when stuck, before major decisions, or before declaring done. The whole conversation is forwarded to this reviewer each call — escalation is not free.',
60
+ armed: 'Current reviewer',
61
+ off: 'None selected — the advisor tool is hidden from the model (zero prompt cost)',
62
+ offCurrent: 'None selected',
63
+ choose: 'Pick one of the models DSH already has configured:',
64
+ loading: 'Loading model catalog…',
65
+ loadFailed: 'Failed to load the model catalog',
66
+ retry: 'Retry',
67
+ partial: 'Some providers failed to list models; the catalog may be incomplete.',
68
+ empty: 'No configurable models — configure a provider under Settings → Models first.',
69
+ effort: 'Reasoning effort',
70
+ effortDefault: '(model default)',
71
+ effortHint: 'Leave empty for the model default; this model declares no effort support — an unsupported configured effort falls back to the model default automatically (see logs).',
72
+ effortHintKnown: 'Only levels this model declares as supported are listed; empty means the model default.',
73
+ unavailable: '(configured route is not in the current catalog)',
74
+ save: 'Save',
75
+ saving: 'Saving…',
76
+ discard: 'Discard changes',
77
+ saved: 'Saved',
78
+ saveFailed: 'Save failed, please retry',
79
+ readonly: 'The settings document is read-only here.',
80
+ patrolTitle: 'Patrol mode (automatic drift detection)',
81
+ patrolDescription: 'Every few steps the conversation snapshot is sent to the reviewer to check direction; on drift a correction is injected into the next request, and STOP asks the executor to halt and report. Each patrol bills the full conversation to the reviewer once.',
82
+ patrolToggle: 'Enable patrol',
83
+ patrolEverySteps: 'Patrol interval (steps)',
84
+ patrolEveryStepsHint: 'One patrol check per N model requests; throttled to at most one patrol per 90 seconds.',
85
+ patrolImmuneTurns: 'Correction cooldown (requests)',
86
+ investigateToggle: 'Reviewer investigation tools',
87
+ investigateHint: 'Let the reviewer verify facts with read-only tools (workspace search / file read) before ruling; duplicate and content-free advice is auto-deduped/dropped.',
88
+ toolTitle: 'consultation',
89
+ toolRunning: 'running',
90
+ toolDone: 'done',
91
+ toolFailed: 'failed',
92
+ toolConsulting: 'forwarding the whole conversation to the reviewer…',
93
+ toolExpand: 'show guidance',
94
+ toolCollapse: 'collapse',
95
+ patrolCardTitle: 'patrol verdict',
96
+ patrolCheckTitle: 'patrol check',
97
+ patrolVerdictOnTrack: 'on track',
98
+ patrolVerdictCorrection: 'correction',
99
+ patrolVerdictStop: 'stop',
100
+ patrolCardStepPrefix: 'step ',
101
+ patrolCardStepSuffix: '',
102
+ patrolExpand: 'show verdict',
103
+ patrolCollapse: 'collapse',
104
+ patrolDowngradedTag: 'cooldown·not injected',
105
+ }