pi-web-ui 0.86.2 → 0.87.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/CHANGELOG.md +59 -21
- package/README.md +1 -1
- package/bin/pi-web-ui.mjs +312 -40
- package/dist/server/agent-service.js +297 -6
- package/dist/server/client-state.js +8 -0
- package/dist/server/composer-drafts.js +138 -0
- package/dist/server/dsh/dsh-agent-service.js +531 -54
- package/dist/server/dsh/dsh-client.js +28 -0
- package/dist/server/dsh/dsh-sessions.js +28 -0
- package/dist/server/dsh/dsh-usage.js +82 -0
- package/dist/server/dsh/preset-clones.js +260 -0
- package/dist/server/dsh/runtime/custom-prompt.mjs +33 -0
- package/dist/server/dsh/runtime/goal-rpc.mjs +406 -22
- package/dist/server/dsh/runtime/launcher.mjs +17 -0
- package/dist/server/dsh/runtime/override.patch.yml +19 -1
- package/dist/server/files-service.js +259 -1
- package/dist/server/index.js +219 -3
- package/dist/server/mcp-bridge.js +126 -22
- package/dist/server/mcp-hot-reload.js +117 -0
- package/dist/server/model-admin.js +93 -3
- package/dist/server/plugin-dom.js +83 -0
- package/dist/server/plugin-facilities.js +15 -1
- package/dist/server/plugin-installer.js +6 -0
- package/dist/server/plugins.js +781 -74
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/provider-oauth-flow.js +157 -0
- package/package.json +1 -1
- package/plugins/catalog.json +9 -0
- package/themes/dark-teal.css +63 -22
- package/web/dist/assets/index-DePnXpq-.js +374 -0
- package/web/dist/assets/index-F86qWlJy.css +41 -0
- package/web/dist/index.html +3 -2
- package/web/dist/assets/TerminalPanel-BytY8dx7.js +0 -6
- package/web/dist/assets/TerminalPanel-DOrYoP_4.css +0 -32
- package/web/dist/assets/index-CKoVyDkP.css +0 -10
- package/web/dist/assets/index-Dmwji4Cr.js +0 -361
|
@@ -31,7 +31,7 @@ import { homedir } from "node:os";
|
|
|
31
31
|
import { BgServerTracker } from "../bg-servers.js";
|
|
32
32
|
import { ClientStateStore, DEFAULT_RETRY_MAX_ATTEMPTS } from "../client-state.js";
|
|
33
33
|
import { normalizeUiLayout } from "../client-state.js";
|
|
34
|
-
import { FilesService, workspacePath } from "../files-service.js";
|
|
34
|
+
import { FilesService, workspacePath, desktopDirWire } from "../files-service.js";
|
|
35
35
|
import { QuiesceRejectedError } from "../agent-service.js";
|
|
36
36
|
import { NATIVE_COMMANDS, parseSlash } from "../slash-commands.js";
|
|
37
37
|
import { bilingual, pick, resolveServerLang } from "../i18n.js";
|
|
@@ -42,9 +42,19 @@ import { previewKind } from "../text-sniff.js";
|
|
|
42
42
|
import { launchOrigin, toServiceInfo } from "../launch-origin.js";
|
|
43
43
|
import { DshRuntime, loadDeepSeekKey } from "./dsh-client.js";
|
|
44
44
|
import { DshStreamAccumulator, assistantMessageEventToUiMessage, toolResultEventToUiMessage, userMessageEventToUiMessage, } from "./dsh-serialize.js";
|
|
45
|
-
import { firstUserText, findSessionFilesForCwd, readSessionLog, replayEventsToMessages } from "./dsh-sessions.js";
|
|
45
|
+
import { firstUserText, findSessionFilesForCwd, readSessionLog, replayEventsToMessages, sessionLogPermission, sessionLogPreset, } from "./dsh-sessions.js";
|
|
46
|
+
import { generatePresetClones, PRESET_DEFAULT_ID } from "./preset-clones.js";
|
|
47
|
+
import { dshContextUsage, lastUsageFromEvents, normalizeDshUsage } from "./dsh-usage.js";
|
|
46
48
|
const SNAPSHOT_INTERVAL_MS = 60;
|
|
49
|
+
/** 用户主目录(wire 格式):进程内不变,模块加载时求值一次(右栏 🏠)。 */
|
|
50
|
+
const HOME_WIRE = homedir().replace(/\\/g, "/");
|
|
51
|
+
/** 桌面目录(wire 格式):进程内不变,不存在则空串 → 前端不渲染 🖥️。 */
|
|
52
|
+
const DESKTOP_WIRE = desktopDirWire(HOME_WIRE);
|
|
47
53
|
const MAX_OPEN_CONVERSATIONS = 8;
|
|
54
|
+
/** 新会话默认权限预设(沙箱内 + 无审批弹窗;无头运行的当前行为,保持不变)。 */
|
|
55
|
+
const PERMISSION_DEFAULT_PRESET = "workspace-write-never";
|
|
56
|
+
/** 前端提供的三档(官方 workspace-write 走 ask,无应答者时是死路,不提供)。 */
|
|
57
|
+
const PERMISSION_OFFERED = ["read-only", "workspace-write-never", "danger-full-access"];
|
|
48
58
|
const DEFAULT_CONV_TITLE = "新对话";
|
|
49
59
|
const DEFAULT_CONV_TITLE_EN = "New chat";
|
|
50
60
|
const DEFAULT_MODEL = "deepseek-v4-flash";
|
|
@@ -119,6 +129,8 @@ const DEFAULT_SETTINGS = {
|
|
|
119
129
|
disabledPlugins: [],
|
|
120
130
|
uiLayout: {},
|
|
121
131
|
reviewPrompt: "",
|
|
132
|
+
defaultAgentPreset: PRESET_DEFAULT_ID,
|
|
133
|
+
defaultPermissionPreset: PERMISSION_DEFAULT_PRESET,
|
|
122
134
|
quickPhrases: [],
|
|
123
135
|
quickPhrasesEnabled: true,
|
|
124
136
|
};
|
|
@@ -173,6 +185,16 @@ export class DshClientSession {
|
|
|
173
185
|
settings = { ...DEFAULT_SETTINGS };
|
|
174
186
|
/** 最近一次从运行时拉取的技能清单(UiSkillInfo,含 enabled 由 disabledSkills 推导)。 */
|
|
175
187
|
skillsCache = [];
|
|
188
|
+
/** 运行时世代(每次 start 成功 +1;conv.assignedGen 用它判断 preset/assign 是否过期)。 */
|
|
189
|
+
runtimeGen = 0;
|
|
190
|
+
/** Agent 预设名录缓存(运行时 preset/list;unavailable/空 = legacy,UI 隐藏预设条)。 */
|
|
191
|
+
agentPresets = [];
|
|
192
|
+
agentPresetDefault = PRESET_DEFAULT_ID;
|
|
193
|
+
agentPresetsAvailable = false;
|
|
194
|
+
/** 权限选项表缓存(组合静态;空 = 未就绪/legacy,前端隐藏权限条)。 */
|
|
195
|
+
permissionOptions = [];
|
|
196
|
+
/** clone 出来的预设 id(roster 名录校验用;空 = 未生成,走 legacy)。 */
|
|
197
|
+
presetCloneIds = [];
|
|
176
198
|
files;
|
|
177
199
|
bg;
|
|
178
200
|
/** 插件扩展点(index.ts 注入)。 */
|
|
@@ -253,20 +275,28 @@ export class DshClientSession {
|
|
|
253
275
|
reviewPrompt: savedSettings.reviewPrompt,
|
|
254
276
|
quickPhrases: savedSettings.quickPhrases ?? [],
|
|
255
277
|
quickPhrasesEnabled: savedSettings.quickPhrasesEnabled ?? true,
|
|
278
|
+
defaultAgentPreset: savedSettings.defaultAgentPreset ?? PRESET_DEFAULT_ID,
|
|
279
|
+
defaultPermissionPreset: savedSettings.defaultPermissionPreset && PERMISSION_OFFERED.includes(savedSettings.defaultPermissionPreset)
|
|
280
|
+
? savedSettings.defaultPermissionPreset
|
|
281
|
+
: PERMISSION_DEFAULT_PRESET,
|
|
256
282
|
};
|
|
257
283
|
}
|
|
258
284
|
// 第一个 conversation = 新会话(每客户端独立 sessionId,避免多标签页/多
|
|
259
285
|
// 客户端共享同一 JSONL 互相串会话)。历史会话经 switch_session 恢复。
|
|
260
286
|
cs.makeRuntime();
|
|
261
|
-
const first = cs.addConversation(`web-${randomUUID().slice(0, 12)}`, cwd, false);
|
|
287
|
+
const first = cs.addConversation(`web-${randomUUID().slice(0, 12)}`, cwd, false, cs.settings.defaultAgentPreset);
|
|
262
288
|
cs.activeId = first.id;
|
|
263
289
|
cs.attachRuntimeEvents();
|
|
264
290
|
// 每次启动成功(含初次/换模型/watchdog 重启)后重新注册插件工具桥,
|
|
265
291
|
// 因为重 spawn 后的 ctx.tools 是全新的,需要重新 sync 插件工具。
|
|
266
292
|
cs.runtime.onStarted = () => {
|
|
293
|
+
cs.runtimeGen += 1;
|
|
267
294
|
void cs.syncPluginTools();
|
|
268
295
|
void cs.pushDisabledSkillsToRuntime();
|
|
269
296
|
void cs.refreshSkillsFromRuntime();
|
|
297
|
+
void cs.refreshAgentPresets();
|
|
298
|
+
void cs.refreshPermissionOptions().catch(() => { });
|
|
299
|
+
void cs.refreshActivePermission().catch(() => { });
|
|
270
300
|
};
|
|
271
301
|
// P0-1 watchdog:意外退出(非 kill/close 主动触发)→ 限频自动重启,保持可用。
|
|
272
302
|
cs.runtime.onExit = (code, signal, intentional) => {
|
|
@@ -322,6 +352,8 @@ export class DshClientSession {
|
|
|
322
352
|
for (const conv of this.convs.values()) {
|
|
323
353
|
conv.isStreaming = false;
|
|
324
354
|
conv.streaming = null;
|
|
355
|
+
// 直播帧能力跟着运行时进程走:重启后重新探测(换运行时版本也能回落到持久 assistant/chunk)。
|
|
356
|
+
conv.liveChunks = false;
|
|
325
357
|
}
|
|
326
358
|
const now = Date.now();
|
|
327
359
|
if (now - this.runtimeRestart.windowStart > DshClientSession.RUNTIME_RESTART_WINDOW_MS) {
|
|
@@ -447,12 +479,37 @@ export class DshClientSession {
|
|
|
447
479
|
}
|
|
448
480
|
}
|
|
449
481
|
makeRuntime() {
|
|
482
|
+
// Agent 预设 clone(file: 改写 shipped 预设,供 preset-plane patch 的 roster 用)。
|
|
483
|
+
// 成功 → 下发 patch(预设时代);失败/null → 不下发(回落 legacy 单组合)。
|
|
484
|
+
let presetPatch;
|
|
485
|
+
try {
|
|
486
|
+
const clones = generatePresetClones(this.dataDir);
|
|
487
|
+
if (clones) {
|
|
488
|
+
presetPatch = clones.patchFile;
|
|
489
|
+
if (clones.warnings.length > 0) {
|
|
490
|
+
console.error(`[dsh] preset clone warnings: ${clones.warnings.join("; ")}`);
|
|
491
|
+
}
|
|
492
|
+
this.presetCloneIds = clones.presetIds;
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
this.presetCloneIds = [];
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
console.error(`[dsh] preset clone 生成失败,回落 legacy: ${err.message}`);
|
|
500
|
+
this.presetCloneIds = [];
|
|
501
|
+
}
|
|
450
502
|
this.runtime = new DshRuntime({
|
|
451
503
|
cwd: this.cwd,
|
|
452
504
|
provider: "deepseek-official",
|
|
453
505
|
model: this.model,
|
|
454
506
|
sessionRoot: this.sessionRoot,
|
|
455
507
|
dataDir: this.dataDir,
|
|
508
|
+
agentDir: this.agentDir,
|
|
509
|
+
env: {
|
|
510
|
+
...(presetPatch ? { PI_WEB_DSH_PRESET_PATCH: presetPatch } : {}),
|
|
511
|
+
PI_WEB_DSH_CUSTOM_PROMPT: this.settings.customSystemPrompt.trim(),
|
|
512
|
+
},
|
|
456
513
|
});
|
|
457
514
|
}
|
|
458
515
|
attachRuntimeEvents() {
|
|
@@ -463,6 +520,11 @@ export class DshClientSession {
|
|
|
463
520
|
if (method === "session.event") {
|
|
464
521
|
this.handleSessionEvent(params);
|
|
465
522
|
}
|
|
523
|
+
else if (method === "assistant.stream") {
|
|
524
|
+
// 新版运行时(0.1.1-rc.2 之后)的直播流:wrapper 把 agent/assistant-stream 帧转成
|
|
525
|
+
// assistant.stream 通知(老运行时的持久 assistant/chunk 已不存在)。
|
|
526
|
+
this.handleAssistantStream(params);
|
|
527
|
+
}
|
|
466
528
|
else if (method === "session.status") {
|
|
467
529
|
this.handleSessionStatus(params);
|
|
468
530
|
}
|
|
@@ -683,11 +745,15 @@ export class DshClientSession {
|
|
|
683
745
|
return title === DEFAULT_CONV_TITLE || title === DEFAULT_CONV_TITLE_EN;
|
|
684
746
|
}
|
|
685
747
|
/** 新建(或切换)一个 conversation。existing 的 sessionId 续聊最近 JSONL。 */
|
|
686
|
-
addConversation(sessionId, cwd, replay = true) {
|
|
748
|
+
addConversation(sessionId, cwd, replay = true, preset) {
|
|
687
749
|
const id = this.nextConversationId();
|
|
688
750
|
const conv = {
|
|
689
751
|
id,
|
|
690
752
|
sessionId,
|
|
753
|
+
agentPreset: this.resolvePreset(preset),
|
|
754
|
+
presetLocked: false,
|
|
755
|
+
assignedGen: -1,
|
|
756
|
+
permissionPreset: null,
|
|
691
757
|
dsGoal: null,
|
|
692
758
|
goal: this.makeGoalStatus(),
|
|
693
759
|
title: this.defaultTitle(),
|
|
@@ -707,17 +773,36 @@ export class DshClientSession {
|
|
|
707
773
|
() => this.getLang()),
|
|
708
774
|
toolStartTimes: new Map(),
|
|
709
775
|
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
776
|
+
usageSeen: false,
|
|
777
|
+
liveChunks: false,
|
|
778
|
+
streamAnchor: 0,
|
|
779
|
+
streamTurn: 0,
|
|
710
780
|
};
|
|
711
781
|
if (replay) {
|
|
712
782
|
// 从磁盘 JSONL 回放历史消息(DSH 事件流不重放历史)。
|
|
713
783
|
try {
|
|
714
784
|
const files = findSessionFilesForCwd(this.sessionRoot, cwd).filter((f) => basename(dirname(f)) === sessionId);
|
|
715
785
|
if (files.length > 0) {
|
|
716
|
-
const { events } = readSessionLog(files[0]);
|
|
786
|
+
const { header, events } = readSessionLog(files[0]);
|
|
717
787
|
conv.messages = replayEventsToMessages(events);
|
|
718
788
|
for (const m of conv.messages)
|
|
719
789
|
conv.messageIds.add(m.id);
|
|
720
790
|
conv.title = firstUserText(events, this.getLang());
|
|
791
|
+
// 回放定预设:日志记录(selected 事件/header)> 调用方指定 > 默认。
|
|
792
|
+
const logged = sessionLogPreset({ header, events });
|
|
793
|
+
conv.agentPreset = this.resolvePreset(logged ?? preset);
|
|
794
|
+
// 权限预设同理:permission/preset 事件最后一条为准(无则 null,激活时拉取)。
|
|
795
|
+
conv.permissionPreset = sessionLogPermission({ header, events });
|
|
796
|
+
// 底栏统计:日志里最后一次 usage(新版在 assistant/message.usage,0.1.1 在
|
|
797
|
+
// assistant/chunk 的 usage chunk)→ 切回历史会话立刻有上下文 / 缓存命中数字。
|
|
798
|
+
const loggedUsage = lastUsageFromEvents(events);
|
|
799
|
+
if (loggedUsage) {
|
|
800
|
+
conv.tokens = loggedUsage;
|
|
801
|
+
conv.usageSeen = true;
|
|
802
|
+
}
|
|
803
|
+
// 有历史消息 = 已开始的会话,预设锁定(官方语义)。
|
|
804
|
+
if (conv.messages.length > 0)
|
|
805
|
+
conv.presetLocked = true;
|
|
721
806
|
}
|
|
722
807
|
}
|
|
723
808
|
catch {
|
|
@@ -806,6 +891,8 @@ export class DshClientSession {
|
|
|
806
891
|
conv.streaming = null;
|
|
807
892
|
this.appendMessage(conv, msg);
|
|
808
893
|
}
|
|
894
|
+
// 新版运行时的 usage 挂在这条持久事件上(老运行时没有 → undefined 保持原统计)。
|
|
895
|
+
this.applyUsage(conv, ev.data.usage);
|
|
809
896
|
break;
|
|
810
897
|
}
|
|
811
898
|
case "tool/result": {
|
|
@@ -838,38 +925,14 @@ export class DshClientSession {
|
|
|
838
925
|
break;
|
|
839
926
|
}
|
|
840
927
|
case "assistant/chunk": {
|
|
928
|
+
// 老运行时(0.1.1-rc.2)的持久直播 chunk;新版运行时没有这个事件(改发
|
|
929
|
+
// agent/assistant-stream 直播帧)→ 过渡期两边都在时只认直播帧,避免喂两遍。
|
|
930
|
+
if (conv.liveChunks)
|
|
931
|
+
break;
|
|
841
932
|
const chunk = ev.data?.chunk;
|
|
842
933
|
if (!chunk)
|
|
843
934
|
break;
|
|
844
|
-
|
|
845
|
-
const u = chunk.usage;
|
|
846
|
-
conv.tokens.input = u.inputTokens ?? 0;
|
|
847
|
-
conv.tokens.output = u.outputTokens ?? 0;
|
|
848
|
-
conv.tokens.cacheRead = u.cacheReadTokens ?? 0;
|
|
849
|
-
conv.tokens.cacheWrite = u.cacheWriteTokens ?? 0;
|
|
850
|
-
break;
|
|
851
|
-
}
|
|
852
|
-
if (!conv.streaming) {
|
|
853
|
-
conv.streaming = new DshStreamAccumulator(ev.seq, ev.data?.turn ?? 0);
|
|
854
|
-
}
|
|
855
|
-
conv.streaming.apply(chunk);
|
|
856
|
-
// message_delta 实时通道:本机快速完成时 60ms 延迟快照总被
|
|
857
|
-
// assistant/message 抢跑(streaming 从未被捕捉)——delta 直接
|
|
858
|
-
// 走独立通道,保证逐 token 渲染(前端 patch streamingMessage)。
|
|
859
|
-
if (conv.id === this.conv.id && (chunk.type === "text-delta" || chunk.type === "reasoning-delta")) {
|
|
860
|
-
this.emit({
|
|
861
|
-
type: "message_delta",
|
|
862
|
-
conversationId: conv.id,
|
|
863
|
-
seq: ++conv.deltaSeq,
|
|
864
|
-
messageId: conv.streaming.id,
|
|
865
|
-
usage: null,
|
|
866
|
-
assistantMessageEvent: {
|
|
867
|
-
type: chunk.type === "text-delta" ? "text_delta" : "thinking_delta",
|
|
868
|
-
contentIndex: chunk.index,
|
|
869
|
-
delta: chunk.text,
|
|
870
|
-
},
|
|
871
|
-
});
|
|
872
|
-
}
|
|
935
|
+
this.applyStreamChunk(conv, chunk, ev.seq, ev.data?.turn ?? 0);
|
|
873
936
|
break;
|
|
874
937
|
}
|
|
875
938
|
case "tool/call": {
|
|
@@ -939,6 +1002,85 @@ export class DshClientSession {
|
|
|
939
1002
|
this.scheduleSnapshot();
|
|
940
1003
|
}
|
|
941
1004
|
}
|
|
1005
|
+
/**
|
|
1006
|
+
* 新版运行时的直播帧处理。运行时逐 chunk 发 `agent/assistant-stream`(agent scope,
|
|
1007
|
+
* host 侧要 `{ global: true }` 才收得到),wrapper(goal-rpc.mjs)转成本通知:
|
|
1008
|
+
* { type: "start" | "chunk" | "end", attemptId, revision, index, time, chunk }
|
|
1009
|
+
* chunk 形状与老运行时的 `assistant/chunk.data.chunk` 同构 → 共用 applyStreamChunk。
|
|
1010
|
+
*/
|
|
1011
|
+
handleAssistantStream(params) {
|
|
1012
|
+
const conv = this.findConv(params.sessionId);
|
|
1013
|
+
const frame = params.frame;
|
|
1014
|
+
// 别家客户端的会话 / 子代理会话(不在本客户端 convs 里)→ 忽略。
|
|
1015
|
+
if (!conv || !frame || typeof frame.type !== "string")
|
|
1016
|
+
return;
|
|
1017
|
+
conv.lastEventAt = Date.now();
|
|
1018
|
+
if (frame.type === "start") {
|
|
1019
|
+
// 新 attempt(含重试)→ 换锚点,丢掉上一 attempt 的累计内容(与官方 dsh-web 一致)。
|
|
1020
|
+
conv.liveChunks = true;
|
|
1021
|
+
conv.streaming = null;
|
|
1022
|
+
conv.streamAnchor = typeof frame.time === "number" && Number.isFinite(frame.time) ? frame.time : Date.now();
|
|
1023
|
+
conv.streamTurn = typeof frame.turn === "number" ? frame.turn : 0;
|
|
1024
|
+
this.scheduleSnapshot();
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (frame.type !== "chunk")
|
|
1028
|
+
return; // end:结算由 assistant/message / turn/end 清
|
|
1029
|
+
conv.liveChunks = true;
|
|
1030
|
+
const chunk = frame.chunk;
|
|
1031
|
+
if (!chunk)
|
|
1032
|
+
return;
|
|
1033
|
+
if (!conv.streamAnchor) {
|
|
1034
|
+
// 没收到 start(重连晚到)→ 用首帧补锚点。
|
|
1035
|
+
conv.streamAnchor = typeof frame.time === "number" && Number.isFinite(frame.time) ? frame.time : Date.now();
|
|
1036
|
+
conv.streamTurn = typeof frame.turn === "number" ? frame.turn : 0;
|
|
1037
|
+
}
|
|
1038
|
+
this.applyStreamChunk(conv, chunk, conv.streamAnchor, conv.streamTurn);
|
|
1039
|
+
// 60ms 节流快照:与持久事件那条路一样,让 isStreaming / streamingMessage 进快照
|
|
1040
|
+
// (delta 通道只负责内容增量,靠它保证状态一致 + 背压时仍能收敛)。
|
|
1041
|
+
this.scheduleSnapshot();
|
|
1042
|
+
}
|
|
1043
|
+
/**
|
|
1044
|
+
* 一个 model stream chunk → 会话:usage 进底栏统计,其余进 streamingMessage 累计器 +
|
|
1045
|
+
* 实时 delta 通道(message_delta)。两条投递路径(老 assistant/chunk / 新直播帧)共用。
|
|
1046
|
+
*
|
|
1047
|
+
* @param anchorId - streamingMessage id 的锚(老 = chunk 事件 seq;新版 = 首帧 time)。
|
|
1048
|
+
* @param turn - 所属 turn(累计器 id 用)。
|
|
1049
|
+
*/
|
|
1050
|
+
applyStreamChunk(conv, chunk, anchorId, turn) {
|
|
1051
|
+
if (chunk.type === "usage") {
|
|
1052
|
+
this.applyUsage(conv, chunk.usage);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (!conv.streaming)
|
|
1056
|
+
conv.streaming = new DshStreamAccumulator(anchorId, turn);
|
|
1057
|
+
conv.streaming.apply(chunk);
|
|
1058
|
+
// message_delta 实时通道:本机快速完成时 60ms 延迟快照总被 assistant/message 抢跑
|
|
1059
|
+
// (streaming 从未被捕捉)→ delta 直接走独立通道,保证逐 token 渲染
|
|
1060
|
+
// (前端 patch streamingMessage)。只推给当前对话,其他对话靠快照。
|
|
1061
|
+
if (conv.id === this.conv.id && (chunk.type === "text-delta" || chunk.type === "reasoning-delta")) {
|
|
1062
|
+
this.emit({
|
|
1063
|
+
type: "message_delta",
|
|
1064
|
+
conversationId: conv.id,
|
|
1065
|
+
seq: ++conv.deltaSeq,
|
|
1066
|
+
messageId: conv.streaming.id,
|
|
1067
|
+
usage: null,
|
|
1068
|
+
assistantMessageEvent: {
|
|
1069
|
+
type: chunk.type === "text-delta" ? "text_delta" : "thinking_delta",
|
|
1070
|
+
contentIndex: chunk.index,
|
|
1071
|
+
delta: chunk.text,
|
|
1072
|
+
},
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/** usage → 底栏统计(最近一次模型调用口径;缺字段 / 非对象保持原值)。 */
|
|
1077
|
+
applyUsage(conv, usage) {
|
|
1078
|
+
const next = normalizeDshUsage(usage);
|
|
1079
|
+
if (!next)
|
|
1080
|
+
return;
|
|
1081
|
+
conv.tokens = next;
|
|
1082
|
+
conv.usageSeen = true;
|
|
1083
|
+
}
|
|
942
1084
|
handleSessionStatus(params) {
|
|
943
1085
|
const conv = this.findConv(params.sessionId);
|
|
944
1086
|
if (!conv)
|
|
@@ -1047,18 +1189,15 @@ export class DshClientSession {
|
|
|
1047
1189
|
total: tokens.input + tokens.output + tokens.cacheRead + tokens.cacheWrite,
|
|
1048
1190
|
},
|
|
1049
1191
|
cost: estimateCost(tokens),
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
contextWindow: DSH_CONTEXT_WINDOW,
|
|
1053
|
-
percent: DSH_CONTEXT_WINDOW > 0
|
|
1054
|
-
? Math.min(100, ((tokens.input + tokens.output + tokens.cacheRead) / DSH_CONTEXT_WINDOW) * 100)
|
|
1055
|
-
: null,
|
|
1056
|
-
},
|
|
1192
|
+
// 最近一次请求的 prompt + 输出(usageSeen=false → tokens=null,前端显示 `—`)。
|
|
1193
|
+
contextUsage: dshContextUsage(conv.usageSeen ? tokens : null, DSH_CONTEXT_WINDOW),
|
|
1057
1194
|
};
|
|
1058
1195
|
return {
|
|
1059
1196
|
clientId: this.clientId,
|
|
1060
1197
|
cwd: this.cwd,
|
|
1061
1198
|
workspaceRoots: this.roots,
|
|
1199
|
+
homeDir: HOME_WIRE,
|
|
1200
|
+
desktopDir: DESKTOP_WIRE,
|
|
1062
1201
|
sessionId: conv.sessionId,
|
|
1063
1202
|
conversationId: this.activeId,
|
|
1064
1203
|
rev,
|
|
@@ -1073,12 +1212,15 @@ export class DshClientSession {
|
|
|
1073
1212
|
},
|
|
1074
1213
|
thinkingLevel: this.thinkingLevel,
|
|
1075
1214
|
availableThinkingLevels: ["high"],
|
|
1215
|
+
agentPreset: { id: conv.agentPreset, name: this.presetName(conv.agentPreset), locked: conv.presetLocked },
|
|
1216
|
+
permission: conv.permissionPreset ?? null,
|
|
1076
1217
|
queue: { steering: conv.queue.steering, followUp: conv.queue.followUp },
|
|
1077
1218
|
pendingQuestion: this.pendingQuestionForSnapshot(),
|
|
1078
1219
|
tools: [],
|
|
1079
1220
|
version: ++this.version,
|
|
1080
|
-
piConfigured:
|
|
1081
|
-
|
|
1221
|
+
piConfigured: this.isDshConfigured(),
|
|
1222
|
+
// DSH 不需要 pi CLI:报 true 让 PiSetupModal 跳过“安装 pi”步骤,直达填 key 表单。
|
|
1223
|
+
piAgentInstalled: true,
|
|
1082
1224
|
stats,
|
|
1083
1225
|
};
|
|
1084
1226
|
}
|
|
@@ -1093,6 +1235,10 @@ export class DshClientSession {
|
|
|
1093
1235
|
this.emitConversations();
|
|
1094
1236
|
this.emitGoalStatus();
|
|
1095
1237
|
this.pushSettings();
|
|
1238
|
+
this.pushAgentPresets();
|
|
1239
|
+
this.pushPermission();
|
|
1240
|
+
void this.refreshPermissionOptions().catch(() => { });
|
|
1241
|
+
void this.refreshActivePermission().catch(() => { });
|
|
1096
1242
|
this.bg.push();
|
|
1097
1243
|
this.pushTerminals();
|
|
1098
1244
|
}
|
|
@@ -1195,6 +1341,9 @@ export class DshClientSession {
|
|
|
1195
1341
|
messageCount: conv.messages.length,
|
|
1196
1342
|
isStreaming: conv.isStreaming,
|
|
1197
1343
|
isSubagent: false,
|
|
1344
|
+
// DSH Agent 预设(左栏徽标/详情用;pi 引擎不填)。
|
|
1345
|
+
agentPreset: conv.agentPreset,
|
|
1346
|
+
presetLocked: conv.presetLocked,
|
|
1198
1347
|
});
|
|
1199
1348
|
}
|
|
1200
1349
|
// issue #145:流式集合签名变化 → 通知其他客户端重推(左栏「另一处正在运行」近实时)
|
|
@@ -1216,13 +1365,17 @@ export class DshClientSession {
|
|
|
1216
1365
|
});
|
|
1217
1366
|
}
|
|
1218
1367
|
/** 语义同 pi 引擎的 newChat:true = 当前活动对话是可接收首条的空白新对话
|
|
1219
|
-
* (/new <prompt> 靠它决定要不要把首条提示发出去)。
|
|
1220
|
-
|
|
1368
|
+
* (/new <prompt> 靠它决定要不要把首条提示发出去)。preset = 新会话预设
|
|
1369
|
+
* (复用空白会话时等价一次空白切换)。 */
|
|
1370
|
+
async newChat(preset) {
|
|
1221
1371
|
if (this.quiesceBlocked())
|
|
1222
1372
|
return false;
|
|
1223
1373
|
const active = this.conv;
|
|
1224
1374
|
if (active.messages.length === 0 && active.terminals.list().length === 0) {
|
|
1225
|
-
|
|
1375
|
+
if (preset)
|
|
1376
|
+
await this.selectAgentPreset(preset);
|
|
1377
|
+
else
|
|
1378
|
+
this.flushSnapshot();
|
|
1226
1379
|
return true;
|
|
1227
1380
|
}
|
|
1228
1381
|
for (const conv of this.convs.values()) {
|
|
@@ -1230,7 +1383,10 @@ export class DshClientSession {
|
|
|
1230
1383
|
continue;
|
|
1231
1384
|
if (conv.messages.length === 0) {
|
|
1232
1385
|
this.switchConversation(conv.id);
|
|
1233
|
-
|
|
1386
|
+
if (preset)
|
|
1387
|
+
await this.selectAgentPreset(preset);
|
|
1388
|
+
else
|
|
1389
|
+
this.flushSnapshot();
|
|
1234
1390
|
return true;
|
|
1235
1391
|
}
|
|
1236
1392
|
}
|
|
@@ -1247,13 +1403,14 @@ export class DshClientSession {
|
|
|
1247
1403
|
// 旧对话保留(listed 生命周期简化:不主动移除)。
|
|
1248
1404
|
const prevModel = this.model;
|
|
1249
1405
|
active.listed = active.isStreaming || active.terminals.list().length > 0 || active.promptedSinceActive;
|
|
1250
|
-
const conv = this.addConversation(`chat-${randomUUID().slice(0, 12)}`, this.cwd, false);
|
|
1406
|
+
const conv = this.addConversation(`chat-${randomUUID().slice(0, 12)}`, this.cwd, false, preset);
|
|
1251
1407
|
this.activeId = conv.id;
|
|
1252
1408
|
this.model = prevModel;
|
|
1253
1409
|
this.emitConversations();
|
|
1254
1410
|
this.emitGoalStatus();
|
|
1255
1411
|
this.pushTerminals();
|
|
1256
1412
|
this.flushSnapshot();
|
|
1413
|
+
void this.refreshActivePermission().catch(() => { });
|
|
1257
1414
|
return true;
|
|
1258
1415
|
}
|
|
1259
1416
|
async switchConversation(id) {
|
|
@@ -1286,6 +1443,8 @@ export class DshClientSession {
|
|
|
1286
1443
|
this.emitGoalStatus();
|
|
1287
1444
|
this.pushTerminals();
|
|
1288
1445
|
this.flushSnapshot(true);
|
|
1446
|
+
// 切会话带上权限值(cwd 变化走运行时重启,onStarted 会重拉)。
|
|
1447
|
+
void this.refreshActivePermission().catch(() => { });
|
|
1289
1448
|
}
|
|
1290
1449
|
removeConversation(id) {
|
|
1291
1450
|
const conv = this.convs.get(id);
|
|
@@ -1438,6 +1597,20 @@ export class DshClientSession {
|
|
|
1438
1597
|
return;
|
|
1439
1598
|
conv.promptedSinceActive = true;
|
|
1440
1599
|
conv.lastEventAt = Date.now();
|
|
1600
|
+
// Agent 预设:运行时世代过期 → 重登 preset/assign(创建时消费;
|
|
1601
|
+
// 已存在会话的登记永不消费,重启后新运行时建会话时正好用上)。
|
|
1602
|
+
if (conv.assignedGen !== this.runtimeGen) {
|
|
1603
|
+
try {
|
|
1604
|
+
await this.runtime.assignPreset(conv.sessionId, conv.agentPreset);
|
|
1605
|
+
conv.assignedGen = this.runtimeGen;
|
|
1606
|
+
}
|
|
1607
|
+
catch {
|
|
1608
|
+
/* legacy/运行时未就绪:创建走默认;best effort */
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
// 新会话默认权限预设(与当前一致时 apply 无事件零噪音)。
|
|
1612
|
+
await this.ensurePermissionDefault(conv);
|
|
1613
|
+
this.flushSnapshot();
|
|
1441
1614
|
const blocks = await this.buildContentBlocks(sysPrefix ? `${sysPrefix}${text}` : text, attachments);
|
|
1442
1615
|
// 乐观落地用户消息(id 用暂定值;user/message 事件到达时按内容去重)。
|
|
1443
1616
|
const optimistic = {
|
|
@@ -1463,6 +1636,11 @@ export class DshClientSession {
|
|
|
1463
1636
|
else {
|
|
1464
1637
|
await this.runtime.prompt(conv.sessionId, blocks);
|
|
1465
1638
|
}
|
|
1639
|
+
// 首轮用户发言送达 → 预设锁定(官方语义;回放会话建时已锁)。
|
|
1640
|
+
if (!conv.presetLocked) {
|
|
1641
|
+
conv.presetLocked = true;
|
|
1642
|
+
this.emitConversations();
|
|
1643
|
+
}
|
|
1466
1644
|
}
|
|
1467
1645
|
catch (err) {
|
|
1468
1646
|
this.emit({
|
|
@@ -1486,7 +1664,7 @@ export class DshClientSession {
|
|
|
1486
1664
|
}
|
|
1487
1665
|
/** 新建 fork 会话并切换到它(DSH 无法原地续聊旧会话)。 */
|
|
1488
1666
|
forkConversation(prev) {
|
|
1489
|
-
const fork = this.addConversation(`fork-${randomUUID().slice(0, 12)}`, this.cwd, false);
|
|
1667
|
+
const fork = this.addConversation(`fork-${randomUUID().slice(0, 12)}`, this.cwd, false, prev.agentPreset);
|
|
1490
1668
|
fork.title = prev.title;
|
|
1491
1669
|
this.activeId = fork.id;
|
|
1492
1670
|
// P2-19:原会话有 active goal(DSH same-session 语义)→ 提示随会话存档。
|
|
@@ -2240,6 +2418,19 @@ export class DshClientSession {
|
|
|
2240
2418
|
async uploadFile(dirRel, name, data) {
|
|
2241
2419
|
await this.files.uploadFile(dirRel, name, data);
|
|
2242
2420
|
}
|
|
2421
|
+
/** 文件树右键菜单:新建/重命名/删除/复制移动(FilesService 直透传)。 */
|
|
2422
|
+
async createEntry(dir, name, kind) {
|
|
2423
|
+
await this.files.createEntry(dir, name, kind);
|
|
2424
|
+
}
|
|
2425
|
+
async renameEntry(path, newName) {
|
|
2426
|
+
await this.files.renameEntry(path, newName);
|
|
2427
|
+
}
|
|
2428
|
+
async deleteEntry(path) {
|
|
2429
|
+
await this.files.deleteEntry(path);
|
|
2430
|
+
}
|
|
2431
|
+
async copyEntry(src, destDir, move) {
|
|
2432
|
+
await this.files.copyEntry(src, destDir, move);
|
|
2433
|
+
}
|
|
2243
2434
|
async completePath(input) {
|
|
2244
2435
|
await this.files.completePath(input);
|
|
2245
2436
|
}
|
|
@@ -2481,6 +2672,7 @@ export class DshClientSession {
|
|
|
2481
2672
|
reviewPrompt: this.settings.reviewPrompt,
|
|
2482
2673
|
quickPhrases: this.settings.quickPhrases,
|
|
2483
2674
|
quickPhrasesEnabled: this.settings.quickPhrasesEnabled,
|
|
2675
|
+
defaultAgentPreset: this.settings.defaultAgentPreset,
|
|
2484
2676
|
});
|
|
2485
2677
|
// 仅系统提示词变化才重启运行时(DSH_PERSONA 由 launcher env 注入);
|
|
2486
2678
|
// 其他设置(开关/隐藏插件等)只存不回写运行时。
|
|
@@ -2502,7 +2694,10 @@ export class DshClientSession {
|
|
|
2502
2694
|
: this.settings.promptMode === "append" && custom
|
|
2503
2695
|
? `\n\n${custom}`
|
|
2504
2696
|
: "";
|
|
2505
|
-
|
|
2697
|
+
// 自定义提示词双通道:DSH_PERSONA(host 部署人设;预设 persona 会 shadow 它,
|
|
2698
|
+
// minimal 更全压住)+ PI_WEB_DSH_CUSTOM_PROMPT(独立 host section,随
|
|
2699
|
+
// standard/ptc/cordis 下发;minimal 下被 complete 压住,官方语义)。
|
|
2700
|
+
this.runtime.env = { ...this.runtime.env, DSH_PERSONA: persona, PI_WEB_DSH_CUSTOM_PROMPT: custom };
|
|
2506
2701
|
if (this.runtime.alive) {
|
|
2507
2702
|
return this.runtime.restart(this.model).catch(() => {
|
|
2508
2703
|
/* keep old runtime */
|
|
@@ -2519,6 +2714,255 @@ export class DshClientSession {
|
|
|
2519
2714
|
textEn: "The DSH engine does not support pi extension hot-reload",
|
|
2520
2715
|
});
|
|
2521
2716
|
}
|
|
2717
|
+
// -----------------------------------------------------------------------
|
|
2718
|
+
// Agent 预设(dsh-web 四模式;Node 侧是真相源,运行时只负责挂载)
|
|
2719
|
+
// -----------------------------------------------------------------------
|
|
2720
|
+
/** 从运行时拉取预设名录(每次启动后跑;失败/legacy → UI 隐藏预设条)。 */
|
|
2721
|
+
async refreshAgentPresets() {
|
|
2722
|
+
try {
|
|
2723
|
+
const res = await this.runtime.listPresets();
|
|
2724
|
+
if (res.unavailable || !Array.isArray(res.presets) || res.presets.length === 0) {
|
|
2725
|
+
this.agentPresets = [];
|
|
2726
|
+
this.agentPresetsAvailable = false;
|
|
2727
|
+
this.agentPresetDefault = PRESET_DEFAULT_ID;
|
|
2728
|
+
}
|
|
2729
|
+
else {
|
|
2730
|
+
this.agentPresets = res.presets;
|
|
2731
|
+
this.agentPresetDefault = res.defaultPreset || PRESET_DEFAULT_ID;
|
|
2732
|
+
this.agentPresetsAvailable = true;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
catch {
|
|
2736
|
+
this.agentPresets = [];
|
|
2737
|
+
this.agentPresetsAvailable = false;
|
|
2738
|
+
this.agentPresetDefault = PRESET_DEFAULT_ID;
|
|
2739
|
+
}
|
|
2740
|
+
this.pushAgentPresets();
|
|
2741
|
+
this.flushSnapshot();
|
|
2742
|
+
}
|
|
2743
|
+
/** 校验一个预设 id(名录 healthy 优先;legacy/未知 → 默认)。 */
|
|
2744
|
+
resolvePreset(requested) {
|
|
2745
|
+
if (requested) {
|
|
2746
|
+
const hit = this.agentPresets.find((p) => p.id === requested && !p.broken);
|
|
2747
|
+
if (hit)
|
|
2748
|
+
return hit.id;
|
|
2749
|
+
// 名录还没拉到(启动中)但 clone 有它 → 先信 clone,挂载期再定。
|
|
2750
|
+
if (this.presetCloneIds.includes(requested))
|
|
2751
|
+
return requested;
|
|
2752
|
+
}
|
|
2753
|
+
const def = this.settings.defaultAgentPreset;
|
|
2754
|
+
if (def) {
|
|
2755
|
+
const hit = this.agentPresets.find((p) => p.id === def && !p.broken);
|
|
2756
|
+
if (hit)
|
|
2757
|
+
return hit.id;
|
|
2758
|
+
if (this.presetCloneIds.includes(def))
|
|
2759
|
+
return def;
|
|
2760
|
+
}
|
|
2761
|
+
return PRESET_DEFAULT_ID;
|
|
2762
|
+
}
|
|
2763
|
+
/** 推送预设名录 + 默认(attach/变化后;legacy 下 presets 空,前端隐藏)。 */
|
|
2764
|
+
pushAgentPresets() {
|
|
2765
|
+
this.emit({
|
|
2766
|
+
type: "dsh_presets",
|
|
2767
|
+
presets: this.agentPresets.map((p) => ({ ...p })),
|
|
2768
|
+
defaultPreset: this.resolvePreset(),
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
/** 新会话默认预设(设置面板;非法 id 拒绝并提示)。 */
|
|
2772
|
+
async setDefaultAgentPreset(preset) {
|
|
2773
|
+
const hit = this.agentPresets.find((p) => p.id === preset && !p.broken);
|
|
2774
|
+
const valid = hit ? hit.id : this.presetCloneIds.includes(preset) ? preset : null;
|
|
2775
|
+
if (!valid) {
|
|
2776
|
+
this.emit({
|
|
2777
|
+
type: "notice",
|
|
2778
|
+
level: "warning",
|
|
2779
|
+
text: `未知预设「${preset}」,默认预设未改`,
|
|
2780
|
+
textEn: `Unknown preset "${preset}"; default preset unchanged`,
|
|
2781
|
+
});
|
|
2782
|
+
return;
|
|
2783
|
+
}
|
|
2784
|
+
this.settings.defaultAgentPreset = valid;
|
|
2785
|
+
this.stateStore.saveSettings(this.clientId, { defaultAgentPreset: valid });
|
|
2786
|
+
this.pushSettings();
|
|
2787
|
+
this.pushAgentPresets();
|
|
2788
|
+
this.flushSnapshot();
|
|
2789
|
+
}
|
|
2790
|
+
/** 空白会话切换预设(首轮后锁定,官方语义;失败按 code 出双语 notice)。 */
|
|
2791
|
+
async selectAgentPreset(preset) {
|
|
2792
|
+
const conv = this.conv;
|
|
2793
|
+
if (conv.presetLocked || conv.messages.length > 0) {
|
|
2794
|
+
conv.presetLocked = true;
|
|
2795
|
+
this.emit({
|
|
2796
|
+
type: "notice",
|
|
2797
|
+
level: "warning",
|
|
2798
|
+
text: `会话已开始,预设锁定为「${this.presetName(conv.agentPreset)}」(空白会话可切换)`,
|
|
2799
|
+
textEn: `Session already started; preset locked to "${this.presetName(conv.agentPreset)}" (only blank sessions can switch)`,
|
|
2800
|
+
});
|
|
2801
|
+
this.flushSnapshot();
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
const target = this.resolvePreset(preset);
|
|
2805
|
+
if (target === conv.agentPreset) {
|
|
2806
|
+
this.flushSnapshot();
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
try {
|
|
2810
|
+
const res = await this.runtime.selectPreset(conv.sessionId, target);
|
|
2811
|
+
if (!res.ok) {
|
|
2812
|
+
if (res.code === "agent-preset/locked")
|
|
2813
|
+
conv.presetLocked = true;
|
|
2814
|
+
this.emit({
|
|
2815
|
+
type: "notice",
|
|
2816
|
+
level: "warning",
|
|
2817
|
+
text: `预设切换失败:${res.code === "agent-preset/locked" ? "会话已开始,预设已锁定" : (res.message ?? res.code ?? "未知错误")}`,
|
|
2818
|
+
textEn: `Preset switch failed: ${res.code === "agent-preset/locked" ? "session already started; preset is locked" : (res.message ?? res.code ?? "unknown error")}`,
|
|
2819
|
+
});
|
|
2820
|
+
this.flushSnapshot();
|
|
2821
|
+
return;
|
|
2822
|
+
}
|
|
2823
|
+
conv.agentPreset = res.preset ?? target;
|
|
2824
|
+
this.emitConversations();
|
|
2825
|
+
this.flushSnapshot();
|
|
2826
|
+
}
|
|
2827
|
+
catch (err) {
|
|
2828
|
+
this.emit({
|
|
2829
|
+
type: "notice",
|
|
2830
|
+
level: "error",
|
|
2831
|
+
text: `预设切换失败:${err.message}`,
|
|
2832
|
+
textEn: `Preset switch failed: ${err.message}`,
|
|
2833
|
+
});
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
/** 预设显示名(名录 name > id;供 notice/前端回退)。 */
|
|
2837
|
+
presetName(id) {
|
|
2838
|
+
return this.agentPresets.find((p) => p.id === id)?.name ?? id;
|
|
2839
|
+
}
|
|
2840
|
+
// -----------------------------------------------------------------------
|
|
2841
|
+
// 权限预设(三档;官方 /permission 弹窗同源,Node 侧只做值守 + 转发)
|
|
2842
|
+
// -----------------------------------------------------------------------
|
|
2843
|
+
/** 校验三档值(非法 → 默认;组合里没有也照默认,前端按 options 取交集展示)。 */
|
|
2844
|
+
resolvePermissionPreset(requested) {
|
|
2845
|
+
if (requested && PERMISSION_OFFERED.includes(requested))
|
|
2846
|
+
return requested;
|
|
2847
|
+
const def = this.settings.defaultPermissionPreset;
|
|
2848
|
+
if (def && PERMISSION_OFFERED.includes(def))
|
|
2849
|
+
return def;
|
|
2850
|
+
return PERMISSION_DEFAULT_PRESET;
|
|
2851
|
+
}
|
|
2852
|
+
/** 推选项表 + 默认(组合静态,options 为空 = 运行时未就绪/legacy,前端隐藏)。 */
|
|
2853
|
+
pushPermission() {
|
|
2854
|
+
this.emit({
|
|
2855
|
+
type: "dsh_permission",
|
|
2856
|
+
options: this.permissionOptions.map((o) => ({ ...o })),
|
|
2857
|
+
defaultPreset: this.settings.defaultPermissionPreset,
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2860
|
+
/** 拉取权限选项表(运行时启动/重建后跑一次;失败 → 空,前端隐藏)。 */
|
|
2861
|
+
async refreshPermissionOptions() {
|
|
2862
|
+
try {
|
|
2863
|
+
const res = await this.runtime.getPermission(this.conv.sessionId);
|
|
2864
|
+
this.permissionOptions = Array.isArray(res.options) ? res.options : [];
|
|
2865
|
+
}
|
|
2866
|
+
catch {
|
|
2867
|
+
this.permissionOptions = [];
|
|
2868
|
+
}
|
|
2869
|
+
this.pushPermission();
|
|
2870
|
+
}
|
|
2871
|
+
/** 拉取当前会话的权限预设值(激活/创建/切换/重启后跑;失败保持 null)。 */
|
|
2872
|
+
async refreshActivePermission() {
|
|
2873
|
+
const conv = this.conv;
|
|
2874
|
+
try {
|
|
2875
|
+
const res = await this.runtime.getPermission(conv.sessionId);
|
|
2876
|
+
const cur = typeof res.currentValue === "string" ? res.currentValue : null;
|
|
2877
|
+
if (cur && cur !== conv.permissionPreset) {
|
|
2878
|
+
conv.permissionPreset = cur;
|
|
2879
|
+
this.flushSnapshot();
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
if (cur)
|
|
2883
|
+
conv.permissionPreset = cur;
|
|
2884
|
+
}
|
|
2885
|
+
catch {
|
|
2886
|
+
/* 运行时未就绪:保持 null/旧值 */
|
|
2887
|
+
}
|
|
2888
|
+
this.flushSnapshot();
|
|
2889
|
+
}
|
|
2890
|
+
/** 新会话默认权限预设(设置面板;非法值拒绝并提示)。 */
|
|
2891
|
+
async setDefaultPermissionPreset(preset) {
|
|
2892
|
+
if (!PERMISSION_OFFERED.includes(preset)) {
|
|
2893
|
+
this.emit({
|
|
2894
|
+
type: "notice",
|
|
2895
|
+
level: "warning",
|
|
2896
|
+
text: `未知权限预设「${preset}」,默认未改`,
|
|
2897
|
+
textEn: `Unknown permission preset "${preset}"; default unchanged`,
|
|
2898
|
+
});
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
this.settings.defaultPermissionPreset = preset;
|
|
2902
|
+
this.stateStore.saveSettings(this.clientId, { defaultPermissionPreset: preset });
|
|
2903
|
+
this.pushPermission();
|
|
2904
|
+
this.flushSnapshot();
|
|
2905
|
+
}
|
|
2906
|
+
/** 首轮 prompt 前:新会话应用 per-client 默认(与当前一致时 apply 无事件零噪音)。 */
|
|
2907
|
+
async ensurePermissionDefault(conv) {
|
|
2908
|
+
const target = this.resolvePermissionPreset();
|
|
2909
|
+
try {
|
|
2910
|
+
if (!conv.permissionPreset) {
|
|
2911
|
+
const res = await this.runtime.getPermission(conv.sessionId);
|
|
2912
|
+
if (typeof res.currentValue === "string" && res.currentValue)
|
|
2913
|
+
conv.permissionPreset = res.currentValue;
|
|
2914
|
+
}
|
|
2915
|
+
if (conv.permissionPreset && conv.permissionPreset !== target) {
|
|
2916
|
+
const res = await this.runtime.setPermission(conv.sessionId, target);
|
|
2917
|
+
if (res.ok)
|
|
2918
|
+
conv.permissionPreset = res.currentValue || target;
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
catch {
|
|
2922
|
+
/* legacy/运行时未就绪:best effort */
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
/** 当前会话切换权限预设(热切换,无需重启;失败按 code 出双语 notice)。 */
|
|
2926
|
+
async setPermissionPreset(preset) {
|
|
2927
|
+
const conv = this.conv;
|
|
2928
|
+
const target = this.resolvePermissionPreset(preset);
|
|
2929
|
+
if (preset !== target) {
|
|
2930
|
+
this.emit({
|
|
2931
|
+
type: "notice",
|
|
2932
|
+
level: "warning",
|
|
2933
|
+
text: `未知权限预设「${preset}」,已回落「${target}」`,
|
|
2934
|
+
textEn: `Unknown permission preset "${preset}"; fell back to "${target}"`,
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
try {
|
|
2938
|
+
const res = await this.runtime.setPermission(conv.sessionId, target);
|
|
2939
|
+
if (!res.ok) {
|
|
2940
|
+
this.emit({
|
|
2941
|
+
type: "notice",
|
|
2942
|
+
level: "warning",
|
|
2943
|
+
text: `权限切换失败:${res.message ?? res.code ?? "未知错误"}`,
|
|
2944
|
+
textEn: `Permission switch failed: ${res.message ?? res.code ?? "unknown error"}`,
|
|
2945
|
+
});
|
|
2946
|
+
this.flushSnapshot();
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
conv.permissionPreset = res.currentValue || target;
|
|
2950
|
+
if (Array.isArray(res.options) && res.options.length > 0) {
|
|
2951
|
+
this.permissionOptions = res.options;
|
|
2952
|
+
this.pushPermission();
|
|
2953
|
+
}
|
|
2954
|
+
this.emitConversations();
|
|
2955
|
+
this.flushSnapshot();
|
|
2956
|
+
}
|
|
2957
|
+
catch (err) {
|
|
2958
|
+
this.emit({
|
|
2959
|
+
type: "notice",
|
|
2960
|
+
level: "error",
|
|
2961
|
+
text: `权限切换失败:${err.message}`,
|
|
2962
|
+
textEn: `Permission switch failed: ${err.message}`,
|
|
2963
|
+
});
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2522
2966
|
async savePreset(name) {
|
|
2523
2967
|
const presets = this.stateStore.getPresets(this.clientId);
|
|
2524
2968
|
const existing = presets.find((p) => p.name === name);
|
|
@@ -3187,6 +3631,15 @@ export class DshClientSession {
|
|
|
3187
3631
|
detail: pick(this.getLang(), "DSH 引擎不需要 pi CLI", "The DSH engine does not need the pi CLI", "dsh.engine.no.cli"),
|
|
3188
3632
|
});
|
|
3189
3633
|
}
|
|
3634
|
+
/** DSH 是否就绪:auth.json 的 deepseek key 或环境变量 DEEPSEEK_API_KEY 任一即可。 */
|
|
3635
|
+
isDshConfigured() {
|
|
3636
|
+
return !!loadDeepSeekKey(this.agentDir) || !!process.env.DEEPSEEK_API_KEY;
|
|
3637
|
+
}
|
|
3638
|
+
/** 下拉框里的 deepseek-official 归一到 auth.json 的 deepseek 槽位(loadDeepSeekKey 只读它)。 */
|
|
3639
|
+
normalizeDshProvider(provider) {
|
|
3640
|
+
const pid = provider.trim();
|
|
3641
|
+
return pid === "deepseek-official" ? "deepseek" : pid;
|
|
3642
|
+
}
|
|
3190
3643
|
async setProviderApiKey(provider, apiKey) {
|
|
3191
3644
|
const key = apiKey.trim();
|
|
3192
3645
|
if (!key) {
|
|
@@ -3203,7 +3656,7 @@ export class DshClientSession {
|
|
|
3203
3656
|
catch {
|
|
3204
3657
|
/* new file */
|
|
3205
3658
|
}
|
|
3206
|
-
auth[
|
|
3659
|
+
auth[this.normalizeDshProvider(provider)] = { type: "api_key", key };
|
|
3207
3660
|
mkdirSync(dirname(authPath), { recursive: true });
|
|
3208
3661
|
writeFileSync(authPath, JSON.stringify(auth, null, 2) + "\n");
|
|
3209
3662
|
this.emit({
|
|
@@ -3226,7 +3679,7 @@ export class DshClientSession {
|
|
|
3226
3679
|
}
|
|
3227
3680
|
}
|
|
3228
3681
|
async clearProviderApiKey(provider) {
|
|
3229
|
-
const pid =
|
|
3682
|
+
const pid = this.normalizeDshProvider(provider);
|
|
3230
3683
|
try {
|
|
3231
3684
|
const authPath = join(this.agentDir, "auth.json");
|
|
3232
3685
|
const auth = JSON.parse(readFileSync(authPath, "utf8"));
|
|
@@ -3255,6 +3708,27 @@ export class DshClientSession {
|
|
|
3255
3708
|
});
|
|
3256
3709
|
}
|
|
3257
3710
|
}
|
|
3711
|
+
startProviderOAuth(_provider) {
|
|
3712
|
+
this.emit({
|
|
3713
|
+
type: "notice",
|
|
3714
|
+
level: "warning",
|
|
3715
|
+
text: "当前引擎不支持 OAuth 登录",
|
|
3716
|
+
textEn: "The current engine does not support OAuth login",
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3719
|
+
replyProviderOAuth(_flowId, _promptId, _value) { }
|
|
3720
|
+
cancelProviderOAuth(_flowId) { }
|
|
3721
|
+
listProviderOAuthFlows() {
|
|
3722
|
+
this.emit({ type: "provider_oauth_flows", flows: [] });
|
|
3723
|
+
}
|
|
3724
|
+
async logoutProviderOAuth(provider) {
|
|
3725
|
+
this.emit({
|
|
3726
|
+
type: "provider_oauth_logout_result",
|
|
3727
|
+
provider,
|
|
3728
|
+
ok: false,
|
|
3729
|
+
error: "当前引擎不支持 OAuth 登录",
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3258
3732
|
async listModelsConfig() {
|
|
3259
3733
|
this.emit({ type: "models_config", providers: [] });
|
|
3260
3734
|
}
|
|
@@ -3289,8 +3763,11 @@ export class DshClientSession {
|
|
|
3289
3763
|
{
|
|
3290
3764
|
id: "deepseek-official",
|
|
3291
3765
|
name: pick(this.getLang(), "DeepSeek 官方", "DeepSeek Official", "dsh.provider.deepseek.official"),
|
|
3292
|
-
configured:
|
|
3293
|
-
source: loadDeepSeekKey() ? "stored" : undefined,
|
|
3766
|
+
configured: this.isDshConfigured(),
|
|
3767
|
+
source: loadDeepSeekKey(this.agentDir) ? "stored" : process.env.DEEPSEEK_API_KEY ? "environment" : undefined,
|
|
3768
|
+
supportsApiKey: true,
|
|
3769
|
+
supportsOAuth: false,
|
|
3770
|
+
usingOAuth: false,
|
|
3294
3771
|
},
|
|
3295
3772
|
],
|
|
3296
3773
|
});
|