dsh-plan-completion 0.1.5 → 0.1.7
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/CHANGELOG.md +29 -0
- package/client.js +237 -2
- package/docs/publish-assessment.md +7 -1
- package/index.js +174 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,33 @@
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning: SemVer.
|
|
5
5
|
|
|
6
|
+
## [0.1.7] - 2026-11-02
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- Settings page model lists now load per provider (`GET /settings?provider=…`), so third-party
|
|
10
|
+
providers' models appear after switching Provider; fixed an internal error caused by a
|
|
11
|
+
provider-scope bug in the settings snapshot.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Settings page redesign matched to DSH General Settings: flat 66px rows (title + description left,
|
|
15
|
+
control right), filled controls without borders, DSH-sized toggle, right-aligned save button,
|
|
16
|
+
unified typography/theme tokens for light and dark themes.
|
|
17
|
+
|
|
18
|
+
## [0.1.6] - 2026-11-02
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Model settings page registered at `settings.plugins.tab` (Settings → Plugins → 计划补全):
|
|
22
|
+
radio group for `default` / `fixed` / `auto` model policy, Provider/Model ID inputs for fixed
|
|
23
|
+
mode, a `modelCalls` toggle, current host-default route hint, and save/status feedback.
|
|
24
|
+
- Host settings route `GET/POST /plan-completion/settings` (authenticated) persists the policy to
|
|
25
|
+
`<storageDir>/settings.json` and loads it on apply; `pickModel` honours `default`/`fixed`/`auto`.
|
|
26
|
+
- Client transport test for the settings page (GET/POST, failure handling).
|
|
27
|
+
- Dev-only suggestion log lives outside the release: `research/suggestion-log-local/` keeps a local
|
|
28
|
+
copy for tuning; the published plugin does not contain or enable suggestion-content logging.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- `npm test` now reports **138/138** tests.
|
|
32
|
+
|
|
6
33
|
## [0.1.5] - 2026-11-02
|
|
7
34
|
|
|
8
35
|
### Fixed
|
|
@@ -136,3 +163,5 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning: Se
|
|
|
136
163
|
- Debug endpoints (`theme`, `experiment`, `autojob`, `steps`, `reset`) are only registered when
|
|
137
164
|
`enableDebugRoutes: true`; production endpoints are `state`, `debug`, `diag`, `replan`, `warm`.
|
|
138
165
|
- No credentials are stored in the plugin; model calls use the host `llm` service.
|
|
166
|
+
|
|
167
|
+
|
package/client.js
CHANGED
|
@@ -105,7 +105,25 @@ window.__ModuleLoader__.load({
|
|
|
105
105
|
'verify.clean': '未发现明显差异。',
|
|
106
106
|
'verify.next': '建议:{value}',
|
|
107
107
|
'verify.failed': '核对失败:{value}',
|
|
108
|
-
'verify.no-session': '当前还没有可用会话'
|
|
108
|
+
'verify.no-session': '当前还没有可用会话',
|
|
109
|
+
'settings.title': '计划补全',
|
|
110
|
+
'settings.model': '模型选择',
|
|
111
|
+
'settings.policy.default': '跟随宿主默认',
|
|
112
|
+
'settings.policy.fixed': '固定模型',
|
|
113
|
+
'settings.policy.auto': '自动(优先非推理)',
|
|
114
|
+
'settings.provider': 'Provider',
|
|
115
|
+
'settings.modelId': 'Model ID',
|
|
116
|
+
'settings.modelCalls': '允许本插件调用模型',
|
|
117
|
+
'settings.modelDesc': '选择插件生成计划与建议时使用的模型来源',
|
|
118
|
+
'settings.providerDesc': '固定使用的 provider',
|
|
119
|
+
'settings.callsDesc': '关闭后插件只使用启发式,不向模型发送内容',
|
|
120
|
+
'settings.defaultRoute': '宿主默认:{value}',
|
|
121
|
+
'settings.save': '保存',
|
|
122
|
+
'settings.saving': '保存中…',
|
|
123
|
+
'settings.saved': '已保存',
|
|
124
|
+
'settings.loading': '加载中…',
|
|
125
|
+
'settings.error': '设置加载失败',
|
|
126
|
+
'settings.saveError': '保存失败'
|
|
109
127
|
},
|
|
110
128
|
en: {
|
|
111
129
|
'entry.aria': 'Open the session plan panel',
|
|
@@ -148,7 +166,25 @@ window.__ModuleLoader__.load({
|
|
|
148
166
|
'verify.clean': 'No obvious mismatches found.',
|
|
149
167
|
'verify.next': 'Next: {value}',
|
|
150
168
|
'verify.failed': 'Verification failed: {value}',
|
|
151
|
-
'verify.no-session': 'No session available yet'
|
|
169
|
+
'verify.no-session': 'No session available yet',
|
|
170
|
+
'settings.title': 'Plan Completion',
|
|
171
|
+
'settings.model': 'Model selection',
|
|
172
|
+
'settings.policy.default': 'Follow host default',
|
|
173
|
+
'settings.policy.fixed': 'Fixed model',
|
|
174
|
+
'settings.policy.auto': 'Auto (prefer non-reasoning)',
|
|
175
|
+
'settings.provider': 'Provider',
|
|
176
|
+
'settings.modelId': 'Model ID',
|
|
177
|
+
'settings.modelCalls': 'Allow this plugin to call models',
|
|
178
|
+
'settings.modelDesc': 'Model source used for plan and suggestion generation',
|
|
179
|
+
'settings.providerDesc': 'Provider used by the fixed policy',
|
|
180
|
+
'settings.callsDesc': 'When off, the plugin only uses heuristics and sends nothing',
|
|
181
|
+
'settings.defaultRoute': 'Host default: {value}',
|
|
182
|
+
'settings.save': 'Save',
|
|
183
|
+
'settings.saving': 'Saving…',
|
|
184
|
+
'settings.saved': 'Saved',
|
|
185
|
+
'settings.loading': 'Loading…',
|
|
186
|
+
'settings.error': 'Failed to load settings',
|
|
187
|
+
'settings.saveError': 'Save failed'
|
|
152
188
|
}
|
|
153
189
|
};
|
|
154
190
|
|
|
@@ -787,6 +823,53 @@ window.__ModuleLoader__.load({
|
|
|
787
823
|
});
|
|
788
824
|
}
|
|
789
825
|
|
|
826
|
+
/**
|
|
827
|
+
* Host settings used by the settings page (model policy + modelCalls).
|
|
828
|
+
*/
|
|
829
|
+
function fetchSettings(provider) {
|
|
830
|
+
try {
|
|
831
|
+
const query = typeof provider === 'string' && provider ? '?provider=' + encodeURIComponent(provider) : '';
|
|
832
|
+
return Promise.resolve(fetch('/plan-completion/settings' + query, {
|
|
833
|
+
method: 'GET',
|
|
834
|
+
credentials: 'same-origin',
|
|
835
|
+
headers: { accept: 'application/json' }
|
|
836
|
+
}))
|
|
837
|
+
.then(function readResponse(response) {
|
|
838
|
+
return response && response.ok === true ? Promise.resolve(response.json()) : null;
|
|
839
|
+
})
|
|
840
|
+
.then(function parsed(json) {
|
|
841
|
+
return json && typeof json === 'object' ? json : null;
|
|
842
|
+
})
|
|
843
|
+
.catch(function networkError() {
|
|
844
|
+
return null;
|
|
845
|
+
});
|
|
846
|
+
} catch (error) {
|
|
847
|
+
return Promise.resolve(null);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function saveSettings(payload) {
|
|
852
|
+
try {
|
|
853
|
+
return Promise.resolve(fetch('/plan-completion/settings', {
|
|
854
|
+
method: 'POST',
|
|
855
|
+
credentials: 'same-origin',
|
|
856
|
+
headers: { 'content-type': 'application/json' },
|
|
857
|
+
body: JSON.stringify(payload || {})
|
|
858
|
+
}))
|
|
859
|
+
.then(function readResponse(response) {
|
|
860
|
+
return response && response.ok === true ? Promise.resolve(response.json()) : null;
|
|
861
|
+
})
|
|
862
|
+
.then(function parsed(json) {
|
|
863
|
+
return json && typeof json === 'object' ? json : null;
|
|
864
|
+
})
|
|
865
|
+
.catch(function networkError() {
|
|
866
|
+
return null;
|
|
867
|
+
});
|
|
868
|
+
} catch (error) {
|
|
869
|
+
return Promise.resolve(null);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
790
873
|
/**
|
|
791
874
|
* P0 boundary: read the current plan state. Keep this signature stable —
|
|
792
875
|
* swapping the transport (HTTP route / projection / wire view) only changes
|
|
@@ -2055,6 +2138,146 @@ window.__ModuleLoader__.load({
|
|
|
2055
2138
|
);
|
|
2056
2139
|
}
|
|
2057
2140
|
|
|
2141
|
+
// =====================================================================
|
|
2142
|
+
// Settings page (settings.plugins.tab)
|
|
2143
|
+
// =====================================================================
|
|
2144
|
+
const SETTINGS_SLOT = 'settings.plugins.tab';
|
|
2145
|
+
const SETTINGS_ID = 'plan-completion-settings';
|
|
2146
|
+
const SETTINGS_ORDER = 60;
|
|
2147
|
+
|
|
2148
|
+
function settingsCss() {
|
|
2149
|
+
return [
|
|
2150
|
+
'.dpc-settings-page{display:flex;flex-direction:column;padding:8px 24px 20px;max-width:720px;box-sizing:border-box;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary)}',
|
|
2151
|
+
'.dpc-settings-head{display:flex;flex-direction:column;gap:4px;padding:6px 0 10px}',
|
|
2152
|
+
'.dpc-settings-title{font-size:16px;font-weight:600;line-height:26px}',
|
|
2153
|
+
'.dpc-settings-sub{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}',
|
|
2154
|
+
'.dpc-settings-row{display:flex;align-items:center;justify-content:space-between;gap:24px;min-height:66px;padding:12px 0;box-sizing:border-box;border-bottom:1px solid var(--dsw-alias-border-l1)}',
|
|
2155
|
+
'.dpc-settings-row:last-of-type{border-bottom:0}',
|
|
2156
|
+
'.dpc-settings-row-main{display:flex;flex-direction:column;gap:3px;min-width:0}',
|
|
2157
|
+
'.dpc-settings-row-label{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}',
|
|
2158
|
+
'.dpc-settings-row-desc{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}',
|
|
2159
|
+
'.dpc-settings-control{flex:none;display:flex;justify-content:flex-end;align-items:center;min-width:132px;max-width:240px}',
|
|
2160
|
+
'.dpc-settings-select{width:100%;border:1px solid transparent;border-radius:8px;padding:7px 12px;font-size:13px;line-height:20px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);cursor:pointer;appearance:auto}',
|
|
2161
|
+
'.dpc-settings-select:hover{background:var(--dsw-alias-bg-layer-1)}',
|
|
2162
|
+
'.dpc-settings-select:focus{outline:none;border-color:var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary))}',
|
|
2163
|
+
'.dpc-settings-toggle{width:40px;height:24px;border-radius:999px;border:0;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));position:relative;cursor:pointer;padding:0;flex:none}',
|
|
2164
|
+
'.dpc-settings-toggle[data-on="true"]{background:var(--dsw-alias-brand-primary,var(--dsw-alias-state-business-primary))}',
|
|
2165
|
+
'.dpc-settings-toggle span{position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:999px;background:#fff;transition:left .12s ease;box-shadow:0 1px 2px rgba(0,0,0,.18)}',
|
|
2166
|
+
'.dpc-settings-toggle[data-on="true"] span{left:18px}',
|
|
2167
|
+
'.dpc-settings-footer{display:flex;align-items:center;justify-content:flex-end;gap:12px;padding-top:18px}',
|
|
2168
|
+
'.dpc-settings-status{font-size:12px;line-height:18px;color:var(--dsw-alias-state-success-primary,var(--dsw-alias-label-secondary));margin-right:auto}',
|
|
2169
|
+
'.dpc-settings-error{font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary,var(--dsw-alias-label-secondary));margin-right:auto}',
|
|
2170
|
+
'.dpc-settings-save{border:0;border-radius:8px;padding:7px 16px;font-size:13px;line-height:20px;cursor:pointer;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary);flex:none}',
|
|
2171
|
+
'.dpc-settings-save:hover{background:var(--dsw-alias-bg-layer-1)}',
|
|
2172
|
+
'.dpc-settings-save:disabled{opacity:.5;cursor:default}',
|
|
2173
|
+
'.dpc-settings-loading{padding:18px 24px;font-size:13px;color:var(--dsw-alias-label-secondary)}'
|
|
2174
|
+
].join('\n');
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
function PlanSettingsPage(props) {
|
|
2178
|
+
const t = makeT(props);
|
|
2179
|
+
const [settings, setSettings] = React.useState(null);
|
|
2180
|
+
const [status, setStatus] = React.useState('loading');
|
|
2181
|
+
const [saving, setSaving] = React.useState(false);
|
|
2182
|
+
const [error, setError] = React.useState('');
|
|
2183
|
+
const [saved, setSaved] = React.useState(false);
|
|
2184
|
+
|
|
2185
|
+
React.useEffect(function load() {
|
|
2186
|
+
let alive = true;
|
|
2187
|
+
fetchSettings().then(function applied(result) {
|
|
2188
|
+
if (!alive) return;
|
|
2189
|
+
if (!result || result.ok !== true || !result.settings) { setStatus('error'); return; }
|
|
2190
|
+
setSettings(result.settings);
|
|
2191
|
+
setStatus('ready');
|
|
2192
|
+
});
|
|
2193
|
+
return function cleanup() { alive = false; };
|
|
2194
|
+
}, []);
|
|
2195
|
+
|
|
2196
|
+
function patch(next) {
|
|
2197
|
+
setSaved(false);
|
|
2198
|
+
setSettings(function keep(previous) { return previous ? { ...previous, ...next } : previous; });
|
|
2199
|
+
}
|
|
2200
|
+
function chooseProvider(provider) {
|
|
2201
|
+
patch({ fixedProvider: provider, fixedModel: '', models: [] });
|
|
2202
|
+
fetchSettings(provider).then(function applied(result) {
|
|
2203
|
+
if (result && result.ok === true && result.settings) patch({ models: result.settings.models || [] });
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
function save() {
|
|
2207
|
+
if (!settings || saving) return;
|
|
2208
|
+
setSaving(true); setError('');
|
|
2209
|
+
saveSettings({
|
|
2210
|
+
modelPolicy: settings.modelPolicy,
|
|
2211
|
+
fixedProvider: settings.fixedProvider || '',
|
|
2212
|
+
fixedModel: settings.fixedModel || '',
|
|
2213
|
+
modelCalls: settings.modelCalls === true
|
|
2214
|
+
}).then(function applied(result) {
|
|
2215
|
+
setSaving(false);
|
|
2216
|
+
if (!result || result.ok !== true) {
|
|
2217
|
+
const detail = result && typeof result.error === 'string' ? result.error : '';
|
|
2218
|
+
setError(detail ? t('settings.saveError') + ': ' + detail : t('settings.saveError'));
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
setSettings(result.settings || settings);
|
|
2222
|
+
setSaved(true); setError('');
|
|
2223
|
+
});
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
const styleTag = h('style', { key: 'style', 'data-plan-completion-style': 'settings' }, settingsCss());
|
|
2227
|
+
if (status === 'loading') return h(React.Fragment, null, styleTag, h('div', { key: 'loading', className: 'dpc-settings-loading' }, t('settings.loading')));
|
|
2228
|
+
if (status === 'error' || !settings) return h(React.Fragment, null, styleTag, h('div', { key: 'error', className: 'dpc-settings-loading dpc-settings-error' }, t('settings.error')));
|
|
2229
|
+
|
|
2230
|
+
const defaultRoute = settings.defaultSelection ? settings.defaultSelection.provider + '/' + settings.defaultSelection.model : '-';
|
|
2231
|
+
const providers = settings.providers || [];
|
|
2232
|
+
const models = settings.models || [];
|
|
2233
|
+
const row = function row(key, label, desc, control) {
|
|
2234
|
+
return h('div', { key: key, className: 'dpc-settings-row' },
|
|
2235
|
+
h('div', { key: 'main', className: 'dpc-settings-row-main' },
|
|
2236
|
+
h('div', { key: 'label', className: 'dpc-settings-row-label' }, label),
|
|
2237
|
+
desc ? h('div', { key: 'desc', className: 'dpc-settings-row-desc' }, desc) : null),
|
|
2238
|
+
h('div', { key: 'control', className: 'dpc-settings-control' }, control));
|
|
2239
|
+
};
|
|
2240
|
+
|
|
2241
|
+
return h(React.Fragment, null, styleTag,
|
|
2242
|
+
h('div', { key: 'page', className: 'dpc-settings-page' },
|
|
2243
|
+
h('div', { key: 'head', className: 'dpc-settings-head' },
|
|
2244
|
+
h('div', { key: 'title', className: 'dpc-settings-title' }, t('settings.title')),
|
|
2245
|
+
h('div', { key: 'sub', className: 'dpc-settings-sub' }, t('settings.defaultRoute', { value: defaultRoute }))),
|
|
2246
|
+
row('policy', t('settings.model'), t('settings.modelDesc'),
|
|
2247
|
+
h('select', { className: 'dpc-settings-select', value: settings.modelPolicy,
|
|
2248
|
+
onChange: function onPolicy(event) { patch({ modelPolicy: event.target.value }); } },
|
|
2249
|
+
[h('option', { key: 'default', value: 'default' }, t('settings.policy.default')),
|
|
2250
|
+
h('option', { key: 'fixed', value: 'fixed' }, t('settings.policy.fixed')),
|
|
2251
|
+
h('option', { key: 'auto', value: 'auto' }, t('settings.policy.auto'))])),
|
|
2252
|
+
settings.modelPolicy === 'fixed'
|
|
2253
|
+
? row('provider', t('settings.provider'), t('settings.providerDesc'),
|
|
2254
|
+
h('select', { className: 'dpc-settings-select', value: settings.fixedProvider || '',
|
|
2255
|
+
onChange: function onProvider(event) { chooseProvider(event.target.value); } },
|
|
2256
|
+
[h('option', { key: 'empty', value: '' }, '—')].concat(providers.map(function option(p) {
|
|
2257
|
+
return h('option', { key: p.id, value: p.id }, p.name || p.id);
|
|
2258
|
+
}))))
|
|
2259
|
+
: null,
|
|
2260
|
+
settings.modelPolicy === 'fixed'
|
|
2261
|
+
? row('model', t('settings.modelId'), t('settings.modelDesc'),
|
|
2262
|
+
h('select', { className: 'dpc-settings-select', value: settings.fixedModel || '',
|
|
2263
|
+
onChange: function onModel(event) { patch({ fixedModel: event.target.value }); } },
|
|
2264
|
+
[h('option', { key: 'empty', value: '' }, '—')].concat(models.map(function option(m) {
|
|
2265
|
+
return h('option', { key: m.id, value: m.id }, m.name || m.id);
|
|
2266
|
+
}))))
|
|
2267
|
+
: null,
|
|
2268
|
+
row('calls', t('settings.modelCalls'), t('settings.callsDesc'),
|
|
2269
|
+
h('button', { type: 'button', className: 'dpc-settings-toggle', 'data-on': settings.modelCalls === true ? 'true' : 'false',
|
|
2270
|
+
'aria-pressed': settings.modelCalls === true ? 'true' : 'false',
|
|
2271
|
+
onClick: function onCalls() { patch({ modelCalls: settings.modelCalls !== true }); } },
|
|
2272
|
+
h('span', { key: 'knob' }))),
|
|
2273
|
+
h('div', { key: 'footer', className: 'dpc-settings-footer' },
|
|
2274
|
+
error ? h('div', { key: 'e', className: 'dpc-settings-error' }, error)
|
|
2275
|
+
: saved ? h('div', { key: 's', className: 'dpc-settings-status' }, t('settings.saved')) : null,
|
|
2276
|
+
h('button', { key: 'save', type: 'button', className: 'dpc-settings-save', disabled: saving, onClick: save },
|
|
2277
|
+
saving ? t('settings.saving') : t('settings.save')))));
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
|
|
2058
2281
|
// =====================================================================
|
|
2059
2282
|
// Plugin entry
|
|
2060
2283
|
// =====================================================================
|
|
@@ -2090,6 +2313,13 @@ window.__ModuleLoader__.load({
|
|
|
2090
2313
|
return ctx.slots.register({ name: OVERLAY_SLOT, id: OVERLAY_ID, order: OVERLAY_ORDER }, PlanPopover);
|
|
2091
2314
|
});
|
|
2092
2315
|
|
|
2316
|
+
ctx.slots.inject(SETTINGS_SLOT, function registerSettingsPage() {
|
|
2317
|
+
return ctx.slots.register(
|
|
2318
|
+
{ name: SETTINGS_SLOT, id: SETTINGS_ID, order: SETTINGS_ORDER, label: '计划补全' },
|
|
2319
|
+
PlanSettingsPage
|
|
2320
|
+
);
|
|
2321
|
+
});
|
|
2322
|
+
|
|
2093
2323
|
ctx.effect(function installKeyboard() {
|
|
2094
2324
|
if (typeof window === 'undefined') return undefined;
|
|
2095
2325
|
const onKeyDown = function onKeyDown(event) {
|
|
@@ -2181,3 +2411,8 @@ window.__ModuleLoader__.load({
|
|
|
2181
2411
|
// hot-reload marker 2026-11-01T13:57Z
|
|
2182
2412
|
|
|
2183
2413
|
|
|
2414
|
+
|
|
2415
|
+
|
|
2416
|
+
|
|
2417
|
+
|
|
2418
|
+
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# GitHub 发布评估 · 会话计划补全插件(2026-11-02)
|
|
2
2
|
|
|
3
|
-
> 历史文档:以下保留 v0.1.0 的交付/评估/计划记录。**当前最新为 v0.1.
|
|
3
|
+
> 历史文档:以下保留 v0.1.0 的交付/评估/计划记录。**当前最新为 v0.1.6**(计划停更检测 + 建议文本保真 + 核对进度 + 本地计划持久化 + 鉴权/状态一致性/隐私边界修复),详见 [security.md](security.md) 与 [CHANGELOG](../CHANGELOG.md);旧记录中的“无鉴权”、原始诊断字段与独立评审不代表当前实现。
|
|
4
4
|
>
|
|
5
|
+
> v0.1.6 发布记录(2026-11-02):模型设置页(settings.plugins.tab)+ settings 持久化;Tab 建议日志仅本地不发布。tag `v0.1.6`;npm `dsh-plan-completion@0.1.6`;138/138 tests;CI run 41393937067。
|
|
6
|
+
>
|
|
7
|
+
> v0.1.5 发布记录(2026-11-02):计划停更检测——连续 2 次 op=none 后建议变为 `/replan …` 并标记 stale。tag `v0.1.5` + [Release](https://github.com/Xumi0920/dsh-plan-completion/releases/tag/v0.1.5);Actions [run 41391402601](https://github.com/Xumi0920/dsh-plan-completion/actions/runs/41391402601)(四 job 全绿,133/133 tests);npm `dsh-plan-completion@0.1.5`(shasum `c53c6d7ef79d8634913054db79fa9e5b1e86b182`)。
|
|
8
|
+
>
|
|
5
9
|
> v0.1.4 发布记录(2026-11-02):建议文本保真修复(标识符/路径/命令保留 + 语义化截断)。tag `v0.1.4` + [Release](https://github.com/Xumi0920/dsh-plan-completion/releases/tag/v0.1.4);GitHub Actions [run 41389845733](https://github.com/Xumi0920/dsh-plan-completion/actions/runs/41389845733)(四 job 全绿,132/132 tests);npm `dsh-plan-completion@0.1.4`(31 files,shasum `a6f8953445e330081243f9f9d6b429fd4402b341`)。
|
|
6
10
|
>
|
|
7
11
|
> v0.1.3 发布记录(2026-11-02):tag `v0.1.3` + [Release](https://github.com/Xumi0920/dsh-plan-completion/releases/tag/v0.1.3);GitHub Actions [run 41368609382](https://github.com/Xumi0920/dsh-plan-completion/actions/runs/41368609382)(Ubuntu/Windows × Node20/22 四 job 全绿,126/126 tests);npm `dsh-plan-completion@0.1.3`(31 files,shasum `11adee0fe9acff199a180be511ca8bc600afdf6f`);canary 验证 `/plan-completion/verify` 未认证 401、客户端鉴权请求正常。
|
|
@@ -108,3 +112,5 @@ git tag v0.1.0 && git push --tags
|
|
|
108
112
|
|
|
109
113
|
|
|
110
114
|
|
|
115
|
+
|
|
116
|
+
|
package/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* The Client half only talks to this half through `/plan-completion/*`
|
|
12
12
|
* (verified contract: same-origin fetch from the Electron page works and the body is readable).
|
|
13
13
|
*/
|
|
14
|
-
import { appendFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
14
|
+
import { appendFileSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'
|
|
15
15
|
import { homedir } from 'node:os'
|
|
16
16
|
|
|
17
17
|
import { DEFAULTS } from './host/config.js'
|
|
@@ -63,6 +63,38 @@ function expandStorageDir(value, envValue) {
|
|
|
63
63
|
return raw
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
const MODEL_POLICIES = new Set(['default', 'fixed', 'auto'])
|
|
67
|
+
|
|
68
|
+
function normalizeModelPolicy(value) {
|
|
69
|
+
const policy = typeof value === 'string' ? value.trim() : ''
|
|
70
|
+
return MODEL_POLICIES.has(policy) ? policy : 'default'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Settings written through the model-settings page (persisted next to the plan store). */
|
|
74
|
+
function readSavedSettings(dir) {
|
|
75
|
+
if (!dir) return {}
|
|
76
|
+
try {
|
|
77
|
+
const data = JSON.parse(readFileSync(join(dir, 'settings.json'), 'utf8'))
|
|
78
|
+
return data && typeof data === 'object' && !Array.isArray(data) ? data : {}
|
|
79
|
+
} catch {
|
|
80
|
+
return {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeSavedSettings(dir, data) {
|
|
85
|
+
if (!dir) return false
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(dir, { recursive: true })
|
|
88
|
+
const target = join(dir, 'settings.json')
|
|
89
|
+
const tmp = join(dir, `.settings-${process.pid}-${Date.now()}.json.tmp`)
|
|
90
|
+
writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8')
|
|
91
|
+
renameSync(tmp, target)
|
|
92
|
+
return true
|
|
93
|
+
} catch {
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
66
98
|
function readJsonBody(req) {
|
|
67
99
|
return new Promise(resolve => {
|
|
68
100
|
let size = 0
|
|
@@ -154,11 +186,56 @@ function normalizePromptMessages(prompt) {
|
|
|
154
186
|
/**
|
|
155
187
|
* Use the user's current default route only. Never send session evidence to
|
|
156
188
|
* another configured provider/model just because it may respond faster.
|
|
189
|
+
*
|
|
190
|
+
* v0.1.6 model policy:
|
|
191
|
+
* default — follow the host's current default model (previous behaviour)
|
|
192
|
+
* fixed — always use `fixedProvider`/`fixedModel` (only if resolvable)
|
|
193
|
+
* auto — within the default provider, prefer a model without reasoning efforts
|
|
194
|
+
* Without a host default route the generator falls back to heuristics (null).
|
|
157
195
|
*/
|
|
158
|
-
async function pickModel(services) {
|
|
196
|
+
async function pickModel(services, policy = null) {
|
|
159
197
|
const llm = services.llm
|
|
160
198
|
const selection = services.agentDefaultModel?.currentSelection?.()
|
|
161
|
-
if (!llm
|
|
199
|
+
if (!llm) return null
|
|
200
|
+
const mode = policy?.modelPolicy === 'fixed' ? 'fixed' : policy?.modelPolicy === 'auto' ? 'auto' : 'default'
|
|
201
|
+
|
|
202
|
+
if (mode === 'fixed' && policy?.fixedProvider && policy?.fixedModel) {
|
|
203
|
+
const route = { provider: policy.fixedProvider, model: policy.fixedModel, source: 'fixed' }
|
|
204
|
+
try {
|
|
205
|
+
const info = await llm.resolveModelInfo?.(route.provider, route.model)
|
|
206
|
+
if (info) {
|
|
207
|
+
const efforts = info?.reasoning?.efforts
|
|
208
|
+
if (Array.isArray(efforts) && efforts.length) route.reasoningEffort = efforts[0]?.id
|
|
209
|
+
return route
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
/* fixed route unavailable: fall back to the host default below */
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!selection?.provider || !selection?.model) return null
|
|
217
|
+
|
|
218
|
+
if (mode === 'auto') {
|
|
219
|
+
try {
|
|
220
|
+
const models = typeof llm.listModels === 'function' ? await llm.listModels(selection.provider) : []
|
|
221
|
+
for (const model of Array.isArray(models) ? models : []) {
|
|
222
|
+
const id = String(model?.id ?? '')
|
|
223
|
+
if (!id) continue
|
|
224
|
+
try {
|
|
225
|
+
const info = await llm.resolveModelInfo?.(selection.provider, id)
|
|
226
|
+
const efforts = info?.reasoning?.efforts
|
|
227
|
+
if (!Array.isArray(efforts) || efforts.length === 0) {
|
|
228
|
+
return { provider: selection.provider, model: id, source: 'auto' }
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
/* try the next catalog entry */
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
/* fall back to the host default */
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
162
239
|
const route = { provider: selection.provider, model: selection.model, source: 'session-default' }
|
|
163
240
|
try {
|
|
164
241
|
const info = await llm.resolveModelInfo?.(route.provider, route.model)
|
|
@@ -247,14 +324,19 @@ export function apply(ctx, config = {}) {
|
|
|
247
324
|
return Math.min(max, Math.max(min, number))
|
|
248
325
|
}
|
|
249
326
|
const rawConfig = config && typeof config === 'object' ? config : {}
|
|
327
|
+
const storageDir = expandStorageDir(rawConfig.storageDir, process.env.DSH_PLAN_COMPLETION_STORE_DIR)
|
|
328
|
+
const savedSettings = readSavedSettings(storageDir)
|
|
250
329
|
const settings = {
|
|
251
330
|
evidenceChars: readInt(rawConfig.evidenceChars, 1500, 400, 8000),
|
|
252
331
|
maxTokensPatch: readInt(rawConfig.maxTokensPatch, 400, 120, 4000),
|
|
253
332
|
maxTokensRewrite: readInt(rawConfig.maxTokensRewrite, 1200, 400, 8000),
|
|
254
333
|
warmBackfill: rawConfig.warmBackfill !== false,
|
|
255
|
-
modelCalls: rawConfig.modelCalls !== false,
|
|
334
|
+
modelCalls: (typeof savedSettings.modelCalls === 'boolean' ? savedSettings.modelCalls : true) && rawConfig.modelCalls !== false,
|
|
335
|
+
modelPolicy: normalizeModelPolicy(savedSettings.modelPolicy ?? rawConfig.modelPolicy),
|
|
336
|
+
fixedProvider: typeof savedSettings.fixedProvider === 'string' ? savedSettings.fixedProvider.trim().slice(0, 120) : '',
|
|
337
|
+
fixedModel: typeof savedSettings.fixedModel === 'string' ? savedSettings.fixedModel.trim().slice(0, 120) : '',
|
|
256
338
|
persistPlans: rawConfig.persistPlans !== false,
|
|
257
|
-
storageDir
|
|
339
|
+
storageDir,
|
|
258
340
|
maxTokensVerify: readInt(rawConfig.maxTokensVerify, DEFAULTS.maxTokensVerify, 200, 2000),
|
|
259
341
|
enableDebugRoutes: rawConfig.enableDebugRoutes === true,
|
|
260
342
|
debugDir: typeof rawConfig.debugDir === 'string' && rawConfig.debugDir.trim() !== ''
|
|
@@ -337,6 +419,38 @@ export function apply(ctx, config = {}) {
|
|
|
337
419
|
}
|
|
338
420
|
}
|
|
339
421
|
|
|
422
|
+
async function settingsSnapshot(providerHint = '') {
|
|
423
|
+
const selection = services.agentDefaultModel?.currentSelection?.() ?? null
|
|
424
|
+
let providers = []
|
|
425
|
+
try {
|
|
426
|
+
providers = services.llm?.listProviders?.() ?? []
|
|
427
|
+
} catch {
|
|
428
|
+
providers = []
|
|
429
|
+
}
|
|
430
|
+
let models = []
|
|
431
|
+
let modelsProvider = ''
|
|
432
|
+
try {
|
|
433
|
+
modelsProvider = providerHint || selection?.provider || (Array.isArray(providers) ? providers[0]?.id : '')
|
|
434
|
+
if (modelsProvider && typeof services.llm?.listModels === 'function') {
|
|
435
|
+
models = (await services.llm.listModels(modelsProvider))?.map(m => ({ provider: modelsProvider, id: m?.id ?? '', name: m?.name ?? m?.id ?? '' })) ?? []
|
|
436
|
+
}
|
|
437
|
+
} catch { models = [] }
|
|
438
|
+
return {
|
|
439
|
+
modelPolicy: settings.modelPolicy,
|
|
440
|
+
fixedProvider: settings.fixedProvider,
|
|
441
|
+
fixedModel: settings.fixedModel,
|
|
442
|
+
modelCalls: settings.modelCalls,
|
|
443
|
+
defaultSelection: selection && selection.provider && selection.model
|
|
444
|
+
? { provider: selection.provider, model: selection.model }
|
|
445
|
+
: null,
|
|
446
|
+
models,
|
|
447
|
+
modelsProvider,
|
|
448
|
+
providers: Array.isArray(providers)
|
|
449
|
+
? providers.map(provider => ({ id: provider?.id ?? '', name: provider?.name ?? provider?.id ?? '' }))
|
|
450
|
+
: [],
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
340
454
|
function publishState(sessionId, patch) {
|
|
341
455
|
const previous = states.get(sessionId) ?? { plan: null, suggestion: null, stale: false }
|
|
342
456
|
states.set(sessionId, { ...previous, ...patch })
|
|
@@ -633,7 +747,7 @@ export function apply(ctx, config = {}) {
|
|
|
633
747
|
]
|
|
634
748
|
}
|
|
635
749
|
try {
|
|
636
|
-
const route = await pickModel(services)
|
|
750
|
+
const route = await pickModel(services, settings)
|
|
637
751
|
if (!isCurrent()) return
|
|
638
752
|
debug.model = route
|
|
639
753
|
debugLog({ phase: 'model-choice', sessionId, route })
|
|
@@ -1009,7 +1123,7 @@ export function apply(ctx, config = {}) {
|
|
|
1009
1123
|
jsonResponse(res, 200, { ok: false, error: 'in-progress' })
|
|
1010
1124
|
return
|
|
1011
1125
|
}
|
|
1012
|
-
const route = await pickModel(services)
|
|
1126
|
+
const route = await pickModel(services, settings)
|
|
1013
1127
|
if (!route) {
|
|
1014
1128
|
jsonResponse(res, 200, { ok: false, error: 'no-model-route' })
|
|
1015
1129
|
return
|
|
@@ -1077,6 +1191,54 @@ export function apply(ctx, config = {}) {
|
|
|
1077
1191
|
}
|
|
1078
1192
|
return
|
|
1079
1193
|
}
|
|
1194
|
+
if (pathname === `${ROUTE_PREFIX}settings` && method === 'GET') {
|
|
1195
|
+
jsonResponse(res, 200, { ok: true, settings: await settingsSnapshot(url?.searchParams?.get('provider') ?? '') })
|
|
1196
|
+
return
|
|
1197
|
+
}
|
|
1198
|
+
if (pathname === `${ROUTE_PREFIX}settings` && method === 'POST') {
|
|
1199
|
+
const body = (await readJsonBody(req)) ?? {}
|
|
1200
|
+
const next = { ...(await settingsSnapshot()) }
|
|
1201
|
+
if (body.modelPolicy !== undefined) {
|
|
1202
|
+
const policy = normalizeModelPolicy(body.modelPolicy)
|
|
1203
|
+
if (policy !== String(body.modelPolicy).trim()) {
|
|
1204
|
+
jsonResponse(res, 200, { ok: false, error: 'bad-model-policy' })
|
|
1205
|
+
return
|
|
1206
|
+
}
|
|
1207
|
+
next.modelPolicy = policy
|
|
1208
|
+
}
|
|
1209
|
+
if (body.modelCalls !== undefined) {
|
|
1210
|
+
if (typeof body.modelCalls !== 'boolean') {
|
|
1211
|
+
jsonResponse(res, 200, { ok: false, error: 'bad-model-calls' })
|
|
1212
|
+
return
|
|
1213
|
+
}
|
|
1214
|
+
next.modelCalls = body.modelCalls
|
|
1215
|
+
}
|
|
1216
|
+
for (const key of ['fixedProvider', 'fixedModel']) {
|
|
1217
|
+
if (body[key] === undefined) continue
|
|
1218
|
+
if (typeof body[key] !== 'string') {
|
|
1219
|
+
jsonResponse(res, 200, { ok: false, error: 'bad-' + key })
|
|
1220
|
+
return
|
|
1221
|
+
}
|
|
1222
|
+
next[key] = body[key].trim().slice(0, 120)
|
|
1223
|
+
}
|
|
1224
|
+
if (next.modelPolicy === 'fixed' && (!next.fixedProvider || !next.fixedModel)) {
|
|
1225
|
+
jsonResponse(res, 200, { ok: false, error: 'missing-fixed-route' })
|
|
1226
|
+
return
|
|
1227
|
+
}
|
|
1228
|
+
settings.modelPolicy = next.modelPolicy
|
|
1229
|
+
settings.fixedProvider = next.fixedProvider
|
|
1230
|
+
settings.fixedModel = next.fixedModel
|
|
1231
|
+
settings.modelCalls = next.modelCalls
|
|
1232
|
+
const saved = writeSavedSettings(settings.storageDir, {
|
|
1233
|
+
modelPolicy: settings.modelPolicy,
|
|
1234
|
+
fixedProvider: settings.fixedProvider,
|
|
1235
|
+
fixedModel: settings.fixedModel,
|
|
1236
|
+
modelCalls: settings.modelCalls,
|
|
1237
|
+
})
|
|
1238
|
+
debugLog({ phase: 'settings-updated', modelPolicy: settings.modelPolicy, saved })
|
|
1239
|
+
jsonResponse(res, 200, { ok: true, saved, settings: await settingsSnapshot() })
|
|
1240
|
+
return
|
|
1241
|
+
}
|
|
1080
1242
|
if (pathname === `${ROUTE_PREFIX}state` && method === 'GET') {
|
|
1081
1243
|
const sessionId = url?.searchParams?.get('sessionId') ?? ''
|
|
1082
1244
|
await hydrateSession(sessionId)
|
|
@@ -1297,7 +1459,7 @@ export function apply(ctx, config = {}) {
|
|
|
1297
1459
|
experimentState.error = null
|
|
1298
1460
|
void (async () => {
|
|
1299
1461
|
try {
|
|
1300
|
-
const route = await pickModel(services)
|
|
1462
|
+
const route = await pickModel(services, settings)
|
|
1301
1463
|
if (!route) throw new Error('no-model-route')
|
|
1302
1464
|
const judgeRoute = await pickJudgeRoute(services, route)
|
|
1303
1465
|
debugLog({ phase: 'experiment-start', sessionId, route, judgeRoute })
|
|
@@ -1390,6 +1552,7 @@ export function apply(ctx, config = {}) {
|
|
|
1390
1552
|
`${ROUTE_PREFIX}replan`,
|
|
1391
1553
|
`${ROUTE_PREFIX}warm`,
|
|
1392
1554
|
`${ROUTE_PREFIX}verify`,
|
|
1555
|
+
`${ROUTE_PREFIX}settings`,
|
|
1393
1556
|
]
|
|
1394
1557
|
if (settings.enableDebugRoutes) {
|
|
1395
1558
|
paths.push(
|
|
@@ -1494,3 +1657,6 @@ export function apply(ctx, config = {}) {
|
|
|
1494
1657
|
})
|
|
1495
1658
|
}
|
|
1496
1659
|
|
|
1660
|
+
|
|
1661
|
+
|
|
1662
|
+
|