pi-web-ui 0.60.0 → 0.61.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.
- package/dist/server/agent-service.js +142 -1
- package/dist/server/client-state.js +52 -0
- package/dist/server/dsh/dsh-agent-service.js +15 -1
- package/dist/server/index.js +12 -0
- package/dist/server/model-admin.js +360 -35
- package/package.json +1 -1
- package/web/dist/assets/{TerminalPanel-CSzqnWV2.js → TerminalPanel-CYq-pKeS.js} +1 -1
- package/web/dist/assets/index-BVuOleif.js +321 -0
- package/web/dist/assets/index-CxB1f4vG.css +10 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-D9iD9G_p.css +0 -10
- package/web/dist/assets/index-TZDcJFdi.js +0 -321
|
@@ -733,6 +733,8 @@ export class ClientSession {
|
|
|
733
733
|
}
|
|
734
734
|
}
|
|
735
735
|
await cs.bindSession();
|
|
736
|
+
await cs.restoreProjectProviderKeysForCwd(cwd);
|
|
737
|
+
await cs.restoreProjectModelForCwd(cwd);
|
|
736
738
|
return cs;
|
|
737
739
|
}
|
|
738
740
|
/**
|
|
@@ -928,6 +930,9 @@ export class ClientSession {
|
|
|
928
930
|
// Reconnect: push the background-task list — it must survive reconnects
|
|
929
931
|
// and outlive the conversation that started the tasks.
|
|
930
932
|
this.bg.push();
|
|
933
|
+
// Reconnect: push the built-in provider key list (multi-key grouping in the
|
|
934
|
+
// model picker needs it even before the client asks).
|
|
935
|
+
this.modelAdmin.listProviderKeys();
|
|
931
936
|
// PTYs are conversation-owned and survive a socket reconnect.
|
|
932
937
|
this.pushTerminals();
|
|
933
938
|
}
|
|
@@ -1813,6 +1818,120 @@ export class ClientSession {
|
|
|
1813
1818
|
deleteModelConfig(providerId) {
|
|
1814
1819
|
return this.modelAdmin.deleteModelConfig(providerId);
|
|
1815
1820
|
}
|
|
1821
|
+
listProviderKeys() {
|
|
1822
|
+
return this.modelAdmin.listProviderKeys();
|
|
1823
|
+
}
|
|
1824
|
+
async addProviderKey(provider, apiKey, name) {
|
|
1825
|
+
await this.modelAdmin.addProviderKey(provider, apiKey, name);
|
|
1826
|
+
const active = this.modelAdmin.getActiveKeyName(provider);
|
|
1827
|
+
if (active)
|
|
1828
|
+
this.stateStore.saveProjectProviderKey(this.clientId, this.cwd, provider, active);
|
|
1829
|
+
}
|
|
1830
|
+
async activateProviderKey(provider, keyName) {
|
|
1831
|
+
await this.modelAdmin.activateProviderKey(provider, keyName);
|
|
1832
|
+
this.stateStore.saveProjectProviderKey(this.clientId, this.cwd, provider, keyName);
|
|
1833
|
+
}
|
|
1834
|
+
async removeProviderKey(provider, keyName) {
|
|
1835
|
+
await this.modelAdmin.removeProviderKey(provider, keyName);
|
|
1836
|
+
const saved = this.stateStore.getProjectProviderKey(this.clientId, this.cwd, provider);
|
|
1837
|
+
if (saved === keyName) {
|
|
1838
|
+
const active = this.modelAdmin.getActiveKeyName(provider);
|
|
1839
|
+
if (active)
|
|
1840
|
+
this.stateStore.saveProjectProviderKey(this.clientId, this.cwd, provider, active);
|
|
1841
|
+
else
|
|
1842
|
+
this.stateStore.deleteProjectProviderKey(this.clientId, this.cwd, provider);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
/** Restore per-project provider keys when entering a project. For each
|
|
1846
|
+
* provider that has a saved key for `cwd`, activate it if it differs from
|
|
1847
|
+
* the current global active. Silent — no notice spam on project switch. */
|
|
1848
|
+
async restoreProjectProviderKeysForCwd(cwd) {
|
|
1849
|
+
const saved = this.stateStore.getProjectProviderKeys(this.clientId, cwd);
|
|
1850
|
+
if (!saved)
|
|
1851
|
+
return;
|
|
1852
|
+
for (const [provider, keyName] of Object.entries(saved)) {
|
|
1853
|
+
const cur = this.modelAdmin.getActiveKeyName(provider);
|
|
1854
|
+
if (cur === keyName)
|
|
1855
|
+
continue;
|
|
1856
|
+
try {
|
|
1857
|
+
await this.modelAdmin.activateProviderKey(provider, keyName);
|
|
1858
|
+
}
|
|
1859
|
+
catch {
|
|
1860
|
+
// saved key may have been deleted — ignore
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
/** When a model is set, ensure its provider's per-project key is restored. */
|
|
1865
|
+
async restoreKeyForModel(modelId, cwd) {
|
|
1866
|
+
const slash = modelId.indexOf("/");
|
|
1867
|
+
if (slash <= 0)
|
|
1868
|
+
return;
|
|
1869
|
+
const provider = modelId.slice(0, slash);
|
|
1870
|
+
const saved = this.stateStore.getProjectProviderKey(this.clientId, cwd, provider);
|
|
1871
|
+
if (!saved)
|
|
1872
|
+
return;
|
|
1873
|
+
const cur = this.modelAdmin.getActiveKeyName(provider);
|
|
1874
|
+
if (cur === saved)
|
|
1875
|
+
return;
|
|
1876
|
+
try {
|
|
1877
|
+
await this.modelAdmin.activateProviderKey(provider, saved);
|
|
1878
|
+
}
|
|
1879
|
+
catch { }
|
|
1880
|
+
}
|
|
1881
|
+
/** Remember the just-selected model (and the key that was active for its
|
|
1882
|
+
* provider) for the current project. Called IMMEDIATELY on model selection —
|
|
1883
|
+
* not only after a turn — so switching back to the project restores the exact
|
|
1884
|
+
* {model, key} left behind, even for a fresh conversation with no assistant
|
|
1885
|
+
* message yet (the SDK only flushes a model_change to disk once one exists). */
|
|
1886
|
+
rememberProjectModel(modelId) {
|
|
1887
|
+
const cwd = this.cwd;
|
|
1888
|
+
this.stateStore.saveProjectModel(this.clientId, cwd, modelId);
|
|
1889
|
+
const slash = modelId.indexOf("/");
|
|
1890
|
+
if (slash <= 0)
|
|
1891
|
+
return;
|
|
1892
|
+
const provider = modelId.slice(0, slash);
|
|
1893
|
+
const active = this.modelAdmin.getActiveKeyName(provider);
|
|
1894
|
+
if (active)
|
|
1895
|
+
this.stateStore.saveProjectProviderKey(this.clientId, cwd, provider, active);
|
|
1896
|
+
}
|
|
1897
|
+
/** Restore the project's remembered model (and its provider's key) onto the
|
|
1898
|
+
* ACTIVE conversation — but ONLY for a conversation the user hasn't really
|
|
1899
|
+
* started (no messages yet). A conversation that already has content keeps its
|
|
1900
|
+
* own per-session model: switching back to a RUNNING / completed chat must not
|
|
1901
|
+
* silently overwrite its model with the project default. So a fresh chat in the
|
|
1902
|
+
* project gets the remembered model; an in-progress one keeps what it had and
|
|
1903
|
+
* the user switches via the picker. Silent on failure (model no longer in catalog). */
|
|
1904
|
+
async restoreProjectModelForCwd(cwd) {
|
|
1905
|
+
const savedModel = this.stateStore.getProjectModel(this.clientId, cwd);
|
|
1906
|
+
if (!savedModel)
|
|
1907
|
+
return;
|
|
1908
|
+
try {
|
|
1909
|
+
if (this.conv.session.getSessionStats().totalMessages > 0)
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
catch {
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
try {
|
|
1916
|
+
const mr = this.runtime.services.modelRuntime;
|
|
1917
|
+
const slash = savedModel.indexOf("/");
|
|
1918
|
+
if (slash <= 0 || slash === savedModel.length - 1)
|
|
1919
|
+
return;
|
|
1920
|
+
const model = mr.getModel(savedModel.slice(0, slash), savedModel.slice(slash + 1));
|
|
1921
|
+
if (!model)
|
|
1922
|
+
return;
|
|
1923
|
+
const cur = this.session.model;
|
|
1924
|
+
const curId = cur ? `${cur.provider}/${cur.id}` : null;
|
|
1925
|
+
// Restore the model's provider key first so setModel's auth check passes.
|
|
1926
|
+
await this.restoreKeyForModel(savedModel, cwd);
|
|
1927
|
+
if (curId === savedModel)
|
|
1928
|
+
return;
|
|
1929
|
+
await this.session.setModel(model);
|
|
1930
|
+
}
|
|
1931
|
+
catch {
|
|
1932
|
+
// model no longer resolvable / key gone — keep the conversation default
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1816
1935
|
// ---------------------------------------------------------------------------
|
|
1817
1936
|
// Settings (system prompt / skills / extensions / presets)
|
|
1818
1937
|
// ---------------------------------------------------------------------------
|
|
@@ -2266,6 +2385,9 @@ export class ClientSession {
|
|
|
2266
2385
|
if (prevModel && this.sharedModelRuntime) {
|
|
2267
2386
|
try {
|
|
2268
2387
|
await this.session.setModel(prevModel);
|
|
2388
|
+
const p = prevModel.provider;
|
|
2389
|
+
const mid = `${p}/${prevModel.id}`;
|
|
2390
|
+
await this.restoreKeyForModel(mid, this.cwd);
|
|
2269
2391
|
}
|
|
2270
2392
|
catch {
|
|
2271
2393
|
// model no longer resolvable — keep the default
|
|
@@ -2363,6 +2485,8 @@ export class ClientSession {
|
|
|
2363
2485
|
void this.pushSlashCommands();
|
|
2364
2486
|
if (cwdChanged) {
|
|
2365
2487
|
this.cwd = newCwd;
|
|
2488
|
+
await this.restoreProjectProviderKeysForCwd(newCwd);
|
|
2489
|
+
await this.restoreProjectModelForCwd(newCwd);
|
|
2366
2490
|
// Mirror set_cwd's project-switch side-effects so the whole UI follows
|
|
2367
2491
|
// the new workspace, not just the chat pane.
|
|
2368
2492
|
try {
|
|
@@ -2645,6 +2769,8 @@ export class ClientSession {
|
|
|
2645
2769
|
this.removeConversation(displaced.id);
|
|
2646
2770
|
await this.bindSession();
|
|
2647
2771
|
this.cwd = targetCwd;
|
|
2772
|
+
await this.restoreProjectProviderKeysForCwd(targetCwd);
|
|
2773
|
+
await this.restoreProjectModelForCwd(targetCwd);
|
|
2648
2774
|
this.conv.lastActiveAt = Date.now();
|
|
2649
2775
|
this.webUi.refresh();
|
|
2650
2776
|
this.emitConversations();
|
|
@@ -2746,6 +2872,8 @@ export class ClientSession {
|
|
|
2746
2872
|
if (prevModel && this.sharedModelRuntime) {
|
|
2747
2873
|
try {
|
|
2748
2874
|
await this.session.setModel(prevModel);
|
|
2875
|
+
const pm = prevModel;
|
|
2876
|
+
await this.restoreKeyForModel(`${pm.provider}/${pm.id}`, this.cwd);
|
|
2749
2877
|
}
|
|
2750
2878
|
catch {
|
|
2751
2879
|
// model no longer resolvable — keep the default
|
|
@@ -2861,7 +2989,13 @@ export class ClientSession {
|
|
|
2861
2989
|
}
|
|
2862
2990
|
async cycleModel() {
|
|
2863
2991
|
try {
|
|
2864
|
-
await this.session.cycleModel();
|
|
2992
|
+
const result = await this.session.cycleModel();
|
|
2993
|
+
if (result?.model) {
|
|
2994
|
+
const mid = `${result.model.provider}/${result.model.id}`;
|
|
2995
|
+
await this.restoreKeyForModel(mid, this.cwd);
|
|
2996
|
+
// Remember per-project like setModel — cycling is also a model switch.
|
|
2997
|
+
this.rememberProjectModel(mid);
|
|
2998
|
+
}
|
|
2865
2999
|
}
|
|
2866
3000
|
catch (err) {
|
|
2867
3001
|
this.emit({
|
|
@@ -2942,6 +3076,8 @@ export class ClientSession {
|
|
|
2942
3076
|
this.conv.promptedSinceActive = false;
|
|
2943
3077
|
this.conv.lastActiveAt = Date.now();
|
|
2944
3078
|
this.cwd = abs;
|
|
3079
|
+
await this.restoreProjectProviderKeysForCwd(abs);
|
|
3080
|
+
await this.restoreProjectModelForCwd(abs);
|
|
2945
3081
|
// 工作区跟随型插件(编辑器文件树等)同步切根。
|
|
2946
3082
|
try {
|
|
2947
3083
|
this.onCwdChanged?.(abs);
|
|
@@ -3041,6 +3177,11 @@ export class ClientSession {
|
|
|
3041
3177
|
if (!model)
|
|
3042
3178
|
throw new Error(`模型不存在:${modelId}`);
|
|
3043
3179
|
await this.session.setModel(model);
|
|
3180
|
+
await this.restoreKeyForModel(modelId, this.cwd);
|
|
3181
|
+
// Immediately remember the model + the key it uses for the current
|
|
3182
|
+
// project (not only after a turn). This is what makes project switching
|
|
3183
|
+
// restore both the model and the provider key.
|
|
3184
|
+
this.rememberProjectModel(modelId);
|
|
3044
3185
|
}
|
|
3045
3186
|
catch (err) {
|
|
3046
3187
|
this.emit({
|
|
@@ -231,4 +231,56 @@ export class ClientStateStore {
|
|
|
231
231
|
state.presets = presets;
|
|
232
232
|
this.save();
|
|
233
233
|
}
|
|
234
|
+
/** Get per-project provider keys for a cwd, or undefined. */
|
|
235
|
+
getProjectProviderKeys(clientId, cwd) {
|
|
236
|
+
return this.load()[clientId]?.projectProviderKeys?.[cwd];
|
|
237
|
+
}
|
|
238
|
+
/** Get a single provider's saved key for a project. */
|
|
239
|
+
getProjectProviderKey(clientId, cwd, provider) {
|
|
240
|
+
return this.load()[clientId]?.projectProviderKeys?.[cwd]?.[provider];
|
|
241
|
+
}
|
|
242
|
+
/** Remember which key was last used for a provider in a project. */
|
|
243
|
+
saveProjectProviderKey(clientId, cwd, provider, keyName) {
|
|
244
|
+
const all = this.load();
|
|
245
|
+
const state = (all[clientId] ??= { projects: [] });
|
|
246
|
+
const map = (state.projectProviderKeys ??= {});
|
|
247
|
+
const inner = (map[cwd] ??= {});
|
|
248
|
+
inner[provider] = keyName;
|
|
249
|
+
this.save();
|
|
250
|
+
}
|
|
251
|
+
/** Delete a per-project provider key (e.g. when the key is removed). */
|
|
252
|
+
deleteProjectProviderKey(clientId, cwd, provider) {
|
|
253
|
+
const all = this.load();
|
|
254
|
+
const inner = all[clientId]?.projectProviderKeys?.[cwd];
|
|
255
|
+
if (!inner || !(provider in inner))
|
|
256
|
+
return;
|
|
257
|
+
delete inner[provider];
|
|
258
|
+
if (Object.keys(inner).length === 0) {
|
|
259
|
+
delete all[clientId].projectProviderKeys[cwd];
|
|
260
|
+
}
|
|
261
|
+
this.save();
|
|
262
|
+
}
|
|
263
|
+
/** Get the model the user last selected in a project, or undefined. */
|
|
264
|
+
getProjectModel(clientId, cwd) {
|
|
265
|
+
return this.load()[clientId]?.projectModels?.[cwd];
|
|
266
|
+
}
|
|
267
|
+
/** Remember the model last selected in a project (immediate, not after a turn). */
|
|
268
|
+
saveProjectModel(clientId, cwd, modelId) {
|
|
269
|
+
const all = this.load();
|
|
270
|
+
const state = (all[clientId] ??= { projects: [] });
|
|
271
|
+
(state.projectModels ??= {})[cwd] = modelId;
|
|
272
|
+
this.save();
|
|
273
|
+
}
|
|
274
|
+
/** Drop the per-project model memory for a project (e.g. when the model is
|
|
275
|
+
* removed from the catalog). */
|
|
276
|
+
deleteProjectModel(clientId, cwd) {
|
|
277
|
+
const all = this.load();
|
|
278
|
+
const map = all[clientId]?.projectModels;
|
|
279
|
+
if (!map || !(cwd in map))
|
|
280
|
+
return;
|
|
281
|
+
delete map[cwd];
|
|
282
|
+
if (Object.keys(map).length === 0)
|
|
283
|
+
delete all[clientId].projectModels;
|
|
284
|
+
this.save();
|
|
285
|
+
}
|
|
234
286
|
}
|
|
@@ -2526,6 +2526,18 @@ export class DshClientSession {
|
|
|
2526
2526
|
],
|
|
2527
2527
|
});
|
|
2528
2528
|
}
|
|
2529
|
+
listProviderKeys() {
|
|
2530
|
+
this.emit({ type: "provider_keys", keys: {} });
|
|
2531
|
+
}
|
|
2532
|
+
async addProviderKey(provider, apiKey, name) {
|
|
2533
|
+
this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持多密钥" });
|
|
2534
|
+
}
|
|
2535
|
+
async activateProviderKey(provider, keyName) {
|
|
2536
|
+
this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持多密钥" });
|
|
2537
|
+
}
|
|
2538
|
+
async removeProviderKey(provider, keyName) {
|
|
2539
|
+
this.emit({ type: "notice", level: "warning", text: "DSH 引擎不支持多密钥" });
|
|
2540
|
+
}
|
|
2529
2541
|
async fetchModelsList(reqId, baseUrl, apiKey, authHeader, api) {
|
|
2530
2542
|
this.emit({ type: "fetch_models_result", reqId, ok: false, error: "DSH 引擎不支持自定义 provider 探测" });
|
|
2531
2543
|
}
|
|
@@ -2533,7 +2545,9 @@ export class DshClientSession {
|
|
|
2533
2545
|
this.emit({ type: "refresh_provider_result", reqId, ok: false, error: "DSH 引擎不支持自定义 provider" });
|
|
2534
2546
|
}
|
|
2535
2547
|
async cloneProvider(provider, reqId) {
|
|
2536
|
-
|
|
2548
|
+
const error = "DSH 引擎不支持自定义 provider";
|
|
2549
|
+
this.emit({ type: "notice", level: "error", text: error });
|
|
2550
|
+
this.emit({ type: "clone_provider_result", reqId, ok: false, error });
|
|
2537
2551
|
}
|
|
2538
2552
|
// -----------------------------------------------------------------------
|
|
2539
2553
|
// 其他
|
package/dist/server/index.js
CHANGED
|
@@ -694,6 +694,18 @@ wss.on("connection", (ws) => {
|
|
|
694
694
|
case "clone_provider":
|
|
695
695
|
void cs.cloneProvider(msg.provider, msg.reqId);
|
|
696
696
|
break;
|
|
697
|
+
case "list_provider_keys":
|
|
698
|
+
cs.listProviderKeys();
|
|
699
|
+
break;
|
|
700
|
+
case "add_provider_key":
|
|
701
|
+
void cs.addProviderKey(msg.provider, msg.apiKey, msg.name);
|
|
702
|
+
break;
|
|
703
|
+
case "activate_provider_key":
|
|
704
|
+
void cs.activateProviderKey(msg.provider, msg.keyName);
|
|
705
|
+
break;
|
|
706
|
+
case "remove_provider_key":
|
|
707
|
+
void cs.removeProviderKey(msg.provider, msg.keyName);
|
|
708
|
+
break;
|
|
697
709
|
case "terminal_create": {
|
|
698
710
|
const tm = cs.getTerminalManager(msg.conversationId);
|
|
699
711
|
if (tm)
|