dsh-superinput 0.0.0-stage → 0.7.0

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.
@@ -0,0 +1,218 @@
1
+ /* dsh-superinput · hub 模块客户端:设置页「输入栏」总览 + 运行时调节。 */
2
+ function createHubClient(require) {
3
+ const React = require('react')
4
+ const h = React.createElement
5
+
6
+ const STYLE_ID = 'dsh-superinput-hub-style'
7
+ const ENDPOINT = '/api/dsh-superinput/hub'
8
+
9
+ const CSS = `
10
+ .dsh-hub { display: flex; flex-direction: column; gap: 12px; padding: 4px 2px 24px; font-size: 13px; color: var(--dsw-alias-label-primary); }
11
+ .dsh-hub-note { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); padding: 8px 12px; border-radius: 8px; background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.04)); }
12
+ .dsh-hub-card { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: 10px; background: var(--dsw-alias-bg-layer-1, transparent); }
13
+ .dsh-hub-title { font-size: 12px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-secondary); }
14
+ .dsh-hub-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 8px 16px; }
15
+ .dsh-hub-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; }
16
+ .dsh-hub-key { font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary); flex: 0 0 auto; }
17
+ .dsh-hub-ctl { flex: 0 1 auto; min-width: 0; display: flex; justify-content: flex-end; }
18
+ .dsh-hub-num {
19
+ width: 80px; box-sizing: border-box; padding: 2px 6px;
20
+ font-size: 12px; line-height: 18px; text-align: right;
21
+ color: var(--dsw-alias-label-primary);
22
+ background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
23
+ border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
24
+ border-radius: 5px; outline: none; font-variant-numeric: tabular-nums;
25
+ }
26
+ .dsh-hub-num:focus { border-color: #3a83f7; }
27
+ .dsh-hub-sel {
28
+ padding: 2px 4px; font-size: 12px; line-height: 18px;
29
+ color: var(--dsw-alias-label-primary);
30
+ background: var(--dsw-alias-bg-layer-1, rgba(255, 255, 255, 0.06));
31
+ border: 0.5px solid var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12));
32
+ border-radius: 5px; outline: none;
33
+ }
34
+ .dsh-hub-sw {
35
+ width: 34px; height: 18px; border-radius: 9px; border: none; padding: 0;
36
+ position: relative; cursor: pointer; transition: background 0.15s;
37
+ background: var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.18));
38
+ }
39
+ .dsh-hub-sw[data-on="true"] { background: #3a83f7; }
40
+ .dsh-hub-sw::after {
41
+ content: ''; position: absolute; top: 2px; left: 2px;
42
+ width: 14px; height: 14px; border-radius: 50%; background: #fff;
43
+ transition: transform 0.15s;
44
+ }
45
+ .dsh-hub-sw[data-on="true"]::after { transform: translateX(16px); }
46
+ .dsh-hub-val { font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
47
+ .dsh-hub-empty { font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary); }
48
+ .dsh-hub-msg { font-size: 11px; line-height: 16px; color: #f87171; }
49
+ `
50
+
51
+ function ensureStyle() {
52
+ if (typeof document === 'undefined') return
53
+ if (document.getElementById(STYLE_ID)) return
54
+ const style = document.createElement('style')
55
+ style.id = STYLE_ID
56
+ style.textContent = CSS
57
+ document.head.appendChild(style)
58
+ }
59
+
60
+ // 每个模块:endpoint 是它自己的 /settings 写入地址;fields 只列可运行时改的项。
61
+ const MODULE_META = [
62
+ {
63
+ key: 'enhancer',
64
+ label: '提示词增强',
65
+ endpoint: '/api/prompt-enhancer/settings',
66
+ fields: [
67
+ { key: 'level', label: '强度', type: 'select', options: ['off', 'low', 'high', 'max'] },
68
+ { key: 'mode', label: '模式', type: 'select', options: ['auto', 'manual'] },
69
+ { key: 'timeoutMs', label: '超时(ms)', type: 'number', min: 1000, max: 120000 },
70
+ { key: 'minPromptLength', label: '最小字数', type: 'number', min: 1, max: 200 },
71
+ ],
72
+ },
73
+ {
74
+ key: 'followup',
75
+ label: '跟进建议',
76
+ endpoint: '/api/dsh-followup/settings',
77
+ fields: [
78
+ { key: 'maxSuggestions', label: '条数', type: 'number', min: 1, max: 3 },
79
+ { key: 'idleDelayMs', label: '延迟(ms)', type: 'number', min: 200, max: 10000 },
80
+ { key: 'timeoutMs', label: '超时(ms)', type: 'number', min: 1000, max: 120000 },
81
+ ],
82
+ },
83
+ {
84
+ key: 'picker',
85
+ label: '模型选择器',
86
+ endpoint: '/api/dsh-power-picker/settings',
87
+ fields: [
88
+ { key: 'enabled', label: '启用', type: 'bool' },
89
+ ],
90
+ },
91
+ {
92
+ key: 'prompts',
93
+ label: '模板库',
94
+ endpoint: '/api/dsh-superinput/prompts/settings',
95
+ fields: [
96
+ { key: 'maxItems', label: '上限', type: 'number', min: 20, max: 5000 },
97
+ ],
98
+ },
99
+ {
100
+ key: 'cost',
101
+ label: '费用引擎',
102
+ endpoint: '/api/dsh-cost/settings',
103
+ fields: [
104
+ { key: 'budgetEnabled', label: '预算开关', type: 'bool' },
105
+ { key: 'budgetAmount', label: '预算额', type: 'number', min: 0, max: 1000000 },
106
+ { key: 'budgetPeriod', label: '预算周期', type: 'select', options: ['day', 'month'] },
107
+ { key: 'exchangeRate', label: '汇率', type: 'number', min: 1, max: 20, step: 0.1 },
108
+ { key: 'peakAlertEnabled', label: '峰时提醒', type: 'bool' },
109
+ ],
110
+ },
111
+ { key: 'voice', label: '语音输入', endpoint: null, fields: [] },
112
+ ]
113
+
114
+ function HubSection() {
115
+ const [state, setState] = React.useState(null)
116
+ const [saving, setSaving] = React.useState('')
117
+ const [error, setError] = React.useState('')
118
+
119
+ const reload = React.useCallback(() => {
120
+ fetch(`${ENDPOINT}/state`, { cache: 'no-store' })
121
+ .then((res) => (res.ok ? res.json() : null))
122
+ .then((value) => setState(value))
123
+ .catch(() => {})
124
+ }, [])
125
+ React.useEffect(() => { reload() }, [reload])
126
+
127
+ const modules = state?.modules ?? {}
128
+
129
+ const save = async (meta, field, value) => {
130
+ if (!meta.endpoint) return
131
+ const id = `${meta.key}.${field.key}`
132
+ setSaving(id)
133
+ setError('')
134
+ try {
135
+ const res = await fetch(meta.endpoint, {
136
+ method: 'POST',
137
+ headers: { 'content-type': 'application/json' },
138
+ body: JSON.stringify({ [field.key]: value }),
139
+ })
140
+ if (!res.ok) throw new Error(`HTTP ${res.status}`)
141
+ setState((prev) => {
142
+ if (!prev) return prev
143
+ const next = { modules: { ...prev.modules } }
144
+ next.modules[meta.key] = { ...(prev.modules?.[meta.key] ?? {}), [field.key]: value }
145
+ return next
146
+ })
147
+ reload()
148
+ } catch (err) {
149
+ setError(`${meta.label} · ${field.label} 保存失败:${err?.message ?? err}`)
150
+ } finally {
151
+ setSaving('')
152
+ }
153
+ }
154
+
155
+ const renderControl = (meta, field, value) => {
156
+ if (field.type === 'bool') {
157
+ return h('button', {
158
+ className: 'dsh-hub-sw',
159
+ 'data-on': String(value === true),
160
+ disabled: saving === `${meta.key}.${field.key}`,
161
+ onClick: () => save(meta, field, value !== true),
162
+ })
163
+ }
164
+ if (field.type === 'select') {
165
+ return h('select', {
166
+ className: 'dsh-hub-sel',
167
+ value: value ?? '',
168
+ onChange: (event) => save(meta, field, event.target.value),
169
+ }, field.options.map((option) => h('option', { key: option, value: option }, option)))
170
+ }
171
+ if (field.type === 'number') {
172
+ return h('input', {
173
+ className: 'dsh-hub-num',
174
+ type: 'number',
175
+ min: field.min,
176
+ max: field.max,
177
+ step: field.step ?? 1,
178
+ defaultValue: value ?? '',
179
+ onBlur: (event) => {
180
+ const next = Number(event.target.value)
181
+ if (Number.isFinite(next) && next !== value) save(meta, field, next)
182
+ },
183
+ onKeyDown: (event) => { if (event.key === 'Enter') event.currentTarget.blur() },
184
+ })
185
+ }
186
+ return h('span', { className: 'dsh-hub-val' }, String(value ?? '—'))
187
+ }
188
+
189
+ return h('div', { className: 'dsh-hub' },
190
+ h('div', { className: 'dsh-hub-note' },
191
+ '运行时调节立即生效,重启后回到配置文件里的值。要长期生效请改 profile 的 cordis.patch.yml。'),
192
+ error ? h('div', { className: 'dsh-hub-msg' }, error) : null,
193
+ ...MODULE_META.map((meta) => {
194
+ const cfg = modules[meta.key]
195
+ return h('div', { className: 'dsh-hub-card', key: meta.key },
196
+ h('div', { className: 'dsh-hub-title' }, meta.label),
197
+ meta.fields.length === 0
198
+ ? h('div', { className: 'dsh-hub-empty' }, '无配置项')
199
+ : h('div', { className: 'dsh-hub-grid' },
200
+ meta.fields.map((field) => h('div', { className: 'dsh-hub-item', key: field.key },
201
+ h('span', { className: 'dsh-hub-key' }, field.label),
202
+ h('span', { className: 'dsh-hub-ctl' }, cfg ? renderControl(meta, field, cfg[field.key]) : h('span', { className: 'dsh-hub-val' }, '—'))))))
203
+ }))
204
+ }
205
+
206
+ return {
207
+ inject: ['slots'],
208
+ apply(ctx) {
209
+ ensureStyle()
210
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
211
+ name: 'settings.section',
212
+ id: 'dsh-superinput-hub',
213
+ order: 29,
214
+ label: '输入栏',
215
+ }, HubSection))
216
+ },
217
+ }
218
+ }
package/modules/hub.js ADDED
@@ -0,0 +1,78 @@
1
+ /**
2
+ * dsh-superinput · hub 模块(设置总览)
3
+ *
4
+ * 把各模块的**最终生效配置**聚合成一张表,供设置页展示。
5
+ *
6
+ * 为什么不做写入:dsh 的插件 Config 在启动时由宿主读取,运行时改不了。
7
+ * 各模块自己的 /settings 端点改的是内存 live 副本 + storages 持久化,
8
+ * 重启后由模块自己回放。这里只回答「现在是什么状态」。
9
+ *
10
+ * 数据来源两层:apply 时聚合层塞进来的 snapshot(defaults 与宿主配置合并),
11
+ * 叠加各模块落盘的显式改动(<storages>/dsh-superinput/<mod>.settings.json)——
12
+ * 只吐快照会在重启后与实际运行值不符(快照不知道回放)。
13
+ */
14
+ import Schema from '@deepseek-ai/schemastery'
15
+ import { readFileSync } from 'node:fs'
16
+ import { homedir } from 'node:os'
17
+ import { join } from 'node:path'
18
+
19
+ export const inject = []
20
+
21
+ export const Config = Schema.object({
22
+ enabled: Schema.boolean().default(true).description('在设置页显示「输入栏」总览'),
23
+ })
24
+
25
+ /** 各模块持久化文件名(与各模块 SETTINGS_PATH 保持一致)。 */
26
+ const PERSISTED_FILES = [
27
+ ['cost', 'cost.settings.json'],
28
+ ['enhancer', 'enhancer.settings.json'],
29
+ ['followup', 'followup.settings.json'],
30
+ ['picker', 'picker.settings.json'],
31
+ ['prompts', 'prompts.settings.json'],
32
+ ]
33
+
34
+ function loadPersisted() {
35
+ const persisted = {}
36
+ const dir = join(homedir(), '.dsh', 'storages', 'dsh-superinput')
37
+ for (const [mod, file] of PERSISTED_FILES) {
38
+ try {
39
+ const saved = JSON.parse(readFileSync(join(dir, file), 'utf8'))
40
+ if (saved && typeof saved === 'object' && Object.keys(saved).length > 0) persisted[mod] = saved
41
+ } catch { /* 模块无存档:跟随配置 */ }
42
+ }
43
+ return persisted
44
+ }
45
+
46
+ export function apply(ctx, config) {
47
+ if (!config.enabled) return
48
+ const snapshot = config.snapshot ?? {}
49
+
50
+ ctx.inject(['webServer'], (webCtx) => {
51
+ webCtx.effect(() => webCtx.webServer.register({
52
+ kind: 'prefix',
53
+ path: '/api/dsh-superinput/hub',
54
+ handler: async (req, res) => {
55
+ const send = (status, body) => {
56
+ res.statusCode = status
57
+ res.setHeader('content-type', 'application/json')
58
+ res.end(JSON.stringify(body))
59
+ }
60
+ const path = (req.url ?? '').split('?')[0].replace(/^\/api\/dsh-superinput\/hub/, '')
61
+ if (path === '/state' && req.method === 'GET') {
62
+ const persisted = loadPersisted()
63
+ const modules = {}
64
+ for (const [mod, cfg] of Object.entries(snapshot)) modules[mod] = { ...cfg, ...(persisted[mod] ?? {}) }
65
+ for (const [mod, saved] of Object.entries(persisted)) {
66
+ if (!modules[mod]) modules[mod] = { ...saved }
67
+ else modules[mod].persistedKeys = Object.keys(saved)
68
+ }
69
+ send(200, { modules })
70
+ return
71
+ }
72
+ send(404, { error: 'not-found' })
73
+ },
74
+ }), 'dsh-superinput: hub endpoint')
75
+ })
76
+ }
77
+
78
+ export default { Config, apply }
@@ -0,0 +1,133 @@
1
+ /* 由 scripts/build-suite.mjs 从 dsh-power-picker/index.js 机械生成;勿手改,重跑脚本再生成。 */
2
+ /**
3
+ * dsh-power-picker — Codex 风格的模型 + 推理强度选择器(DeepSeek Harness / dsh)
4
+ *
5
+ * 服务端半很薄:`enabled` 开关 + 诊断落盘。所有真正的逻辑都在客户端——
6
+ * 以更低 priority 注册 `conversation.input.model` 槽位遮蔽内置选择器,
7
+ * 数据走官方 modelDirectories 会话模型目录。服务端唯一的职责:
8
+ * 1. 让「关闭插件」有一个不用卸载的开关(客户端注册遮蔽前先来确认);
9
+ * 2. 接收客户端诊断事件,落到 ~/.dsh/power-picker-debug.log,
10
+ * 座位异常时看这份文件就能定位卡在哪一环(服务端起没起、客户端
11
+ * 模块加载没有、inject 是否失败、快照字段长什么样)。
12
+ *
13
+ * 已核对的 dsh API(0.2.1-rc.3 已装运行时反推 + dsh-reference 对照):
14
+ * - 客户端 ctx.modelDirectories.directoryFor(sessionId) → 目录
15
+ * - 组件拿到的是 directory.store(SnapshotStore):subscribe/getSnapshot
16
+ * - 快照:{ current: {provider, model, reasoningEffort?} | null,
17
+ * groups: [{ id, name, models: [{ id, name, reasoning? }] }],
18
+ * retainedEffort?, pending, ... }
19
+ * 档位阶梯在 groups[].models[].reasoning = { defaultEffort?, efforts: [{id, name}] }
20
+ * - directory.select({ provider, model, reasoningEffort? }) 官方切换
21
+ * - conversation.input.model 是 single 槽位,多注册按 priority 只渲染最低者
22
+ */
23
+
24
+ import Schema from '@deepseek-ai/schemastery'
25
+ import { appendFile, mkdir } from 'node:fs/promises'
26
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
27
+ import { homedir } from 'node:os'
28
+ import { dirname, join } from 'node:path'
29
+
30
+ const name = 'power-picker'
31
+
32
+ const DIAG_LOG = join(homedir(), '.dsh', 'power-picker-debug.log')
33
+
34
+ export const Config = Schema.object({
35
+ /** 关闭后客户端不再注册任何入口,模型选择恢复原样(无需卸载插件)。 */
36
+ enabled: Schema.boolean().default(true).description('是否启用 Codex 风格选择器'),
37
+ /**
38
+ * Codex 实测形态:入口就是输入栏右下角「选择推理强度」按钮,面板从其
39
+ * 上方弹出。默认 true;false 时输入栏不渲染入口(无任何入口)。
40
+ */
41
+ composerSeat: Schema.boolean().default(true).description('输入栏保留模型入口(Codex 同位:右下角,发送键旁)'),
42
+ })
43
+
44
+ export function apply(ctx, config) {
45
+ // 运行时可变副本:POST /settings 改这里,立即生效;显式改过的键持久化
46
+ // 到 storages、启动时回放(没改过的键继续跟随宿主配置)。
47
+ const live = { ...config }
48
+
49
+ // 设置持久化:只落与 config 不同的键,防抖 400ms 原子写。
50
+ const SETTINGS_KEYS = ['enabled', 'composerSeat']
51
+ const SETTINGS_PATH = join(homedir(), '.dsh', 'storages', 'dsh-power-picker', 'settings.json')
52
+ try {
53
+ const saved = JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'))
54
+ for (const key of SETTINGS_KEYS) if (saved?.[key] !== undefined) live[key] = saved[key]
55
+ } catch { /* 无存档:全部跟随配置 */ }
56
+ let settingsTimer = null
57
+ const persistSettings = () => {
58
+ if (settingsTimer) return
59
+ settingsTimer = setTimeout(() => {
60
+ settingsTimer = null
61
+ try {
62
+ mkdirSync(dirname(SETTINGS_PATH), { recursive: true })
63
+ const out = {}
64
+ for (const key of SETTINGS_KEYS) if (live[key] !== config[key]) out[key] = live[key]
65
+ const tmp = `${SETTINGS_PATH}.tmp`
66
+ writeFileSync(tmp, JSON.stringify(out, null, 2))
67
+ renameSync(tmp, SETTINGS_PATH)
68
+ } catch { /* ignore */ }
69
+ }, 400)
70
+ }
71
+
72
+ const say = (message) => {
73
+ try { console.info(`[dsh-power-picker] ${message}`) } catch { /* ignore */ }
74
+ }
75
+ const report = async (line) => {
76
+ try {
77
+ await mkdir(dirname(DIAG_LOG), { recursive: true })
78
+ await appendFile(DIAG_LOG, `${new Date().toISOString()} ${line}\n`, 'utf8')
79
+ } catch { /* ignore */ }
80
+ }
81
+
82
+ say(`server boot, enabled=${live.enabled !== false}`)
83
+ void report(`server boot enabled=${live.enabled !== false}`)
84
+
85
+ ctx.inject(['webServer'], (webCtx) => {
86
+ webCtx.effect(() => webCtx.webServer.register({
87
+ kind: 'prefix',
88
+ path: '/api/dsh-power-picker',
89
+ handler: async (req, res) => {
90
+ const send = (status, body) => {
91
+ res.statusCode = status
92
+ res.setHeader('content-type', 'application/json')
93
+ res.end(JSON.stringify(body))
94
+ }
95
+ const path = (req.url ?? '').split('?')[0].replace(/^\/api\/dsh-power-picker/, '')
96
+ if (path === '/settings' && req.method === 'POST') {
97
+ let raw = ''
98
+ for await (const chunk of req) raw += chunk
99
+ let body
100
+ try { body = JSON.parse(raw) } catch { send(400, { error: 'invalid-json' }); return }
101
+ if (body.enabled !== undefined) live.enabled = body.enabled === true
102
+ if (typeof body.composerSeat === 'string') live.composerSeat = body.composerSeat.slice(0, 40)
103
+ persistSettings()
104
+ send(200, { ...live })
105
+ return
106
+ }
107
+
108
+
109
+ // 客户端注册前读取设置:enabled 总开关 + composerSeat 入口位置。
110
+ if (path === '/settings' && req.method === 'GET') {
111
+ send(200, {
112
+ enabled: live.enabled !== false,
113
+ composerSeat: live.composerSeat === true,
114
+ name,
115
+ })
116
+ return
117
+ }
118
+
119
+ // 诊断上报:一行一条 JSON,出问题时看 ~/.dsh/power-picker-debug.log。
120
+ if (path === '/diag' && req.method === 'POST') {
121
+ let raw = ''
122
+ for await (const chunk of req) raw += chunk
123
+ await report(raw.slice(0, 2000))
124
+ send(200, { ok: true })
125
+ return
126
+ }
127
+
128
+ send(404, { error: 'not-found' })
129
+ },
130
+ }), 'dsh-power-picker: /api/dsh-power-picker/*')
131
+ })
132
+ }
133
+