@aipanel/provider-deepseek 1.2.6 → 1.2.8
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/es/api.d.ts +46 -8
- package/es/api.js +216 -28
- package/es/bridge-script.js +37 -0
- package/es/constants.d.ts +8 -5
- package/es/constants.js +2 -5
- package/es/deepseek-web.d.ts +20 -0
- package/es/deepseek-web.js +60 -2
- package/es/profile.d.ts +6 -0
- package/es/profile.js +8 -2
- package/es/provider.d.ts +1 -1
- package/es/provider.js +62 -103
- package/es/system.d.ts +7 -0
- package/es/system.js +41 -0
- package/lib/api.cjs +215 -27
- package/lib/api.d.ts +46 -8
- package/lib/bridge-script.cjs +37 -0
- package/lib/constants.cjs +3 -7
- package/lib/constants.d.ts +8 -5
- package/lib/deepseek-web.cjs +60 -2
- package/lib/deepseek-web.d.ts +20 -0
- package/lib/profile.cjs +7 -2
- package/lib/profile.d.ts +6 -0
- package/lib/provider.cjs +53 -100
- package/lib/provider.d.ts +1 -1
- package/lib/system.cjs +43 -0
- package/lib/system.d.ts +7 -0
- package/package.json +2 -2
package/es/profile.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
AIPANEL_CACHE_DIR,
|
|
5
5
|
MCP_API_PATH,
|
|
6
6
|
CONTEXT_API_PATH,
|
|
7
|
+
HOST_EVENTS_API_PATH,
|
|
7
8
|
createLogger
|
|
8
9
|
} from "@aipanel/core/node";
|
|
9
10
|
const log = createLogger("DeepSeekProfile");
|
|
@@ -14,7 +15,8 @@ function buildDshOverlay(options) {
|
|
|
14
15
|
pluginAvailable = true,
|
|
15
16
|
clientAvailable = true,
|
|
16
17
|
autoDiagnose,
|
|
17
|
-
enableDiagnostics = true
|
|
18
|
+
enableDiagnostics = true,
|
|
19
|
+
eventsToken
|
|
18
20
|
} = options;
|
|
19
21
|
const mcpUrl = `http://127.0.0.1:${vitePort}${MCP_API_PATH}`;
|
|
20
22
|
const rows = [];
|
|
@@ -40,7 +42,11 @@ function buildDshOverlay(options) {
|
|
|
40
42
|
` vitePort: ${vitePort}`,
|
|
41
43
|
` contextApiPath: ${JSON.stringify(CONTEXT_API_PATH)}`,
|
|
42
44
|
` enableDiagnostics: ${enableDiagnostics ? "true" : "false"}`,
|
|
43
|
-
...autoDiagnose !== void 0 ? [` autoDiagnose: ${autoDiagnose ? "true" : "false"}`] : []
|
|
45
|
+
...autoDiagnose !== void 0 ? [` autoDiagnose: ${autoDiagnose ? "true" : "false"}`] : [],
|
|
46
|
+
...eventsToken ? [
|
|
47
|
+
` eventsToken: ${JSON.stringify(eventsToken)}`,
|
|
48
|
+
` eventsPath: ${JSON.stringify(HOST_EVENTS_API_PATH)}`
|
|
49
|
+
] : []
|
|
44
50
|
].join("\n")
|
|
45
51
|
);
|
|
46
52
|
rows.push(
|
package/es/provider.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export declare class DeepSeekWebProvider implements WebProvider {
|
|
|
38
38
|
private buildSettingsToApply;
|
|
39
39
|
stop(): Promise<void>;
|
|
40
40
|
killOrphans(): Promise<number>;
|
|
41
|
-
listSessions(projectDir: string): Promise<ChatSession[]>;
|
|
41
|
+
listSessions(projectDir: string, activeSessionId?: string): Promise<ChatSession[]>;
|
|
42
42
|
createSession(projectDir: string, title?: string): Promise<ChatSession>;
|
|
43
43
|
deleteSession(sessionId: string): Promise<void>;
|
|
44
44
|
buildSessionUrl(projectDir: string, sessionId: string): string;
|
package/es/provider.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
import { sleep } from "@aipanel/core";
|
|
4
5
|
import { createLogger } from "@aipanel/core/node";
|
|
5
6
|
import { DEFAULT_DEEPSEEK_PROVIDER_OPTIONS } from "./constants.js";
|
|
6
|
-
import { DSH_LOOPBACK_HOST
|
|
7
|
+
import { DSH_LOOPBACK_HOST } from "./constants.js";
|
|
7
8
|
import { DeepSeekAPI } from "./api.js";
|
|
8
9
|
import { generateBridgeScript } from "./bridge-script.js";
|
|
9
|
-
import { startDeepSeekWeb } from "./deepseek-web.js";
|
|
10
|
+
import { LaunchToken, startDeepSeekWeb } from "./deepseek-web.js";
|
|
10
11
|
import { buildDshOverlay, writeDshOverlay } from "./profile.js";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
checkDeepSeekInstalled,
|
|
14
|
+
getDeepSeekVersion,
|
|
15
|
+
isDeepSeekVersionAtLeast,
|
|
16
|
+
killOrphanDeepSeekProcesses,
|
|
17
|
+
MIN_DSH_VERSION
|
|
18
|
+
} from "./system.js";
|
|
12
19
|
import {
|
|
13
20
|
DSH_CLIENT_PACKAGE,
|
|
14
21
|
DSH_PLUGIN_PACKAGE,
|
|
@@ -60,6 +67,18 @@ or run without installing:
|
|
|
60
67
|
};
|
|
61
68
|
}
|
|
62
69
|
const version = await getDeepSeekVersion();
|
|
70
|
+
const compatible = version === null ? true : isDeepSeekVersionAtLeast(version);
|
|
71
|
+
if (compatible === false) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
message: `DeepSeek Harness (dsh) ${version} is too old: this provider requires dsh >= ${MIN_DSH_VERSION} (browser-session auth / Remote RPC protocol).
|
|
75
|
+
|
|
76
|
+
Please upgrade:
|
|
77
|
+
|
|
78
|
+
npm install -g @deepseek-ai/dsh@latest
|
|
79
|
+
`
|
|
80
|
+
};
|
|
81
|
+
}
|
|
63
82
|
return { ok: true, version: version ?? void 0 };
|
|
64
83
|
}
|
|
65
84
|
async start(options) {
|
|
@@ -100,18 +119,50 @@ or run without installing:
|
|
|
100
119
|
pluginAvailable,
|
|
101
120
|
clientAvailable,
|
|
102
121
|
autoDiagnose: this.opts.autoDiagnose,
|
|
103
|
-
enableDiagnostics: this.opts.enableDiagnostics
|
|
122
|
+
enableDiagnostics: this.opts.enableDiagnostics,
|
|
123
|
+
// 宿主事件推送令牌(core 每轮启动随机):随 plugin config 注入 dsh-plugin 用于回推鉴权
|
|
124
|
+
eventsToken: options.eventsToken
|
|
104
125
|
});
|
|
105
126
|
const patchPath = writeDshOverlay(options.cwd, overlay);
|
|
127
|
+
const launchToken = new LaunchToken();
|
|
128
|
+
this.api.setLaunchTokenSource(() => launchToken.wait());
|
|
106
129
|
const proc = startDeepSeekWeb({
|
|
107
130
|
port: options.port,
|
|
108
131
|
hostname: DSH_LOOPBACK_HOST,
|
|
109
132
|
cwd: options.cwd,
|
|
110
133
|
patchPath,
|
|
111
134
|
home: this.opts.home,
|
|
112
|
-
verbose: options.verbose
|
|
135
|
+
verbose: options.verbose,
|
|
136
|
+
launchToken
|
|
113
137
|
});
|
|
114
138
|
this.process = proc;
|
|
139
|
+
let webAuthCookie;
|
|
140
|
+
try {
|
|
141
|
+
const token = await launchToken.wait();
|
|
142
|
+
this.api.setLaunchToken(token);
|
|
143
|
+
const maxAuthAttempts = 3;
|
|
144
|
+
for (let attempt = 1; ; attempt++) {
|
|
145
|
+
try {
|
|
146
|
+
await this.api.authenticate();
|
|
147
|
+
break;
|
|
148
|
+
} catch (e) {
|
|
149
|
+
if (attempt >= maxAuthAttempts) throw e;
|
|
150
|
+
log.debug("dsh browser-session auth bootstrap attempt failed, retrying", {
|
|
151
|
+
attempt,
|
|
152
|
+
error: e instanceof Error ? e.message : String(e)
|
|
153
|
+
});
|
|
154
|
+
await sleep(250 * attempt);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
webAuthCookie = this.api.getAuthCookie();
|
|
158
|
+
} catch (e) {
|
|
159
|
+
log.warn(
|
|
160
|
+
"failed to establish dsh web browser-session auth; UI may show authentication required",
|
|
161
|
+
{
|
|
162
|
+
error: e instanceof Error ? e.message : String(e)
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
}
|
|
115
166
|
const settings = this.buildSettingsToApply();
|
|
116
167
|
if (Object.keys(settings).length > 0) {
|
|
117
168
|
void this.api.applySettings(settings).catch((e) => {
|
|
@@ -121,7 +172,7 @@ or run without installing:
|
|
|
121
172
|
});
|
|
122
173
|
});
|
|
123
174
|
}
|
|
124
|
-
return { url: this.api.shellUrl, processHandle: proc };
|
|
175
|
+
return { url: this.api.shellUrl, processHandle: proc, webAuthCookie };
|
|
125
176
|
}
|
|
126
177
|
/** 把 providerOptions 配置映射为 dsh settings 命名空间 patch(仅含用户显式配置项) */
|
|
127
178
|
buildSettingsToApply() {
|
|
@@ -153,8 +204,8 @@ or run without installing:
|
|
|
153
204
|
async killOrphans() {
|
|
154
205
|
return killOrphanDeepSeekProcesses();
|
|
155
206
|
}
|
|
156
|
-
async listSessions(projectDir) {
|
|
157
|
-
const sessions = await this.api.listSessions(projectDir);
|
|
207
|
+
async listSessions(projectDir, activeSessionId) {
|
|
208
|
+
const sessions = await this.api.listSessions(projectDir, activeSessionId);
|
|
158
209
|
const url = this.buildSessionUrl(projectDir, "");
|
|
159
210
|
return sessions.map((s) => toChatSession(s, url));
|
|
160
211
|
}
|
|
@@ -179,104 +230,12 @@ or run without installing:
|
|
|
179
230
|
return `http://${DSH_LOOPBACK_HOST}:${this.deps.getProxyPort()}/`;
|
|
180
231
|
}
|
|
181
232
|
subscribeEvents(handler) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const endpoints = [DSH_MUX_EVENTS_PATH, DSH_HOST_EVENTS_PATH];
|
|
185
|
-
log.debug("Subscribing to dsh event streams (WebSocket)", {
|
|
186
|
-
endpoints: endpoints.map((p) => base + p)
|
|
187
|
-
});
|
|
188
|
-
let aborted = false;
|
|
189
|
-
const sockets = /* @__PURE__ */ new Set();
|
|
190
|
-
const retryTimers = /* @__PURE__ */ new Set();
|
|
191
|
-
const cleanupAll = () => {
|
|
192
|
-
for (const timer of retryTimers) clearTimeout(timer);
|
|
193
|
-
retryTimers.clear();
|
|
194
|
-
for (const ws of sockets) {
|
|
195
|
-
try {
|
|
196
|
-
ws.close();
|
|
197
|
-
} catch {
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
sockets.clear();
|
|
201
|
-
};
|
|
202
|
-
const connect = (path, attempt = 0) => {
|
|
203
|
-
if (aborted) return;
|
|
204
|
-
let socket;
|
|
205
|
-
try {
|
|
206
|
-
socket = new WebSocket(base + path);
|
|
207
|
-
} catch (e) {
|
|
208
|
-
log.warn("dsh event WebSocket create failed", { path, error: String(e) });
|
|
209
|
-
scheduleReconnect(path, attempt + 1);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
sockets.add(socket);
|
|
213
|
-
socket.onmessage = (ev) => {
|
|
214
|
-
try {
|
|
215
|
-
const frame = JSON.parse(String(ev.data));
|
|
216
|
-
const event = mapEvent(frame);
|
|
217
|
-
if (event) handler(event);
|
|
218
|
-
} catch {
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
socket.onclose = () => {
|
|
222
|
-
sockets.delete(socket);
|
|
223
|
-
scheduleReconnect(path, attempt + 1);
|
|
224
|
-
};
|
|
225
|
-
socket.onerror = () => {
|
|
226
|
-
try {
|
|
227
|
-
socket.close();
|
|
228
|
-
} catch {
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
};
|
|
232
|
-
const scheduleReconnect = (path, attempt) => {
|
|
233
|
-
if (aborted) return;
|
|
234
|
-
const delays = [250, 500, 1e3, 2e3, 5e3];
|
|
235
|
-
const after = delays[Math.min(attempt, delays.length - 1)];
|
|
236
|
-
const timer = setTimeout(() => {
|
|
237
|
-
retryTimers.delete(timer);
|
|
238
|
-
connect(path);
|
|
239
|
-
}, after);
|
|
240
|
-
retryTimers.add(timer);
|
|
241
|
-
};
|
|
242
|
-
endpoints.forEach((p) => connect(p));
|
|
233
|
+
void handler;
|
|
234
|
+
log.debug("dsh \u4E8B\u4EF6\u7ECF\u5BBF\u4E3B\u63D2\u4EF6(dsh-plugin)\u4E2D\u7EE7\u63A8\u9001\uFF0Cprovider \u76F4\u8FDE\u4E8B\u4EF6\u901A\u9053\u4E3A\u7A7A");
|
|
243
235
|
return () => {
|
|
244
|
-
aborted = true;
|
|
245
|
-
cleanupAll();
|
|
246
236
|
};
|
|
247
237
|
}
|
|
248
238
|
}
|
|
249
|
-
function mapEvent(frame) {
|
|
250
|
-
if (!frame || typeof frame.method !== "string" || !frame.payload) return null;
|
|
251
|
-
const payload = frame.payload;
|
|
252
|
-
const sessionId = payload.sessionId ?? "";
|
|
253
|
-
switch (frame.method) {
|
|
254
|
-
case "host/session-status": {
|
|
255
|
-
if (!sessionId) return null;
|
|
256
|
-
const running = payload.running === true;
|
|
257
|
-
return { type: "session.status", sessionId, status: running ? "running" : "idle" };
|
|
258
|
-
}
|
|
259
|
-
case "session/event": {
|
|
260
|
-
const event = payload.event;
|
|
261
|
-
const type = event?.type ?? "";
|
|
262
|
-
if (!sessionId || !type) return null;
|
|
263
|
-
switch (type) {
|
|
264
|
-
case "assistant/message":
|
|
265
|
-
case "turn/end":
|
|
266
|
-
return { type: "thinking", sessionId, thinking: false };
|
|
267
|
-
case "assistant/chunk":
|
|
268
|
-
case "thinking/delta":
|
|
269
|
-
case "turn/start":
|
|
270
|
-
case "step/start":
|
|
271
|
-
return { type: "thinking", sessionId, thinking: true };
|
|
272
|
-
default:
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
default:
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
239
|
function toChatSession(s, url) {
|
|
281
240
|
return {
|
|
282
241
|
id: s.sessionId,
|
|
@@ -293,7 +252,7 @@ function resolveDeepSeekOptions(options) {
|
|
|
293
252
|
return {
|
|
294
253
|
...DEFAULT_DEEPSEEK_PROVIDER_OPTIONS,
|
|
295
254
|
home: po.home ?? options.home,
|
|
296
|
-
agentPreset: po.agentPreset ?? options.agentPreset
|
|
255
|
+
agentPreset: po.agentPreset ?? options.agentPreset,
|
|
297
256
|
permissionPreset: po.permissionPreset ?? options.permissionPreset,
|
|
298
257
|
busyEnter: po.busyEnter ?? options.busyEnter,
|
|
299
258
|
autoDiagnose: po.autoDiagnose ?? options.autoDiagnose ?? DEFAULT_DEEPSEEK_PROVIDER_OPTIONS.autoDiagnose,
|
package/es/system.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
export declare function checkDeepSeekInstalled(): Promise<boolean>;
|
|
2
|
+
/** 本 provider 要求的 dsh 最低版本(0.1.2 起:browser-session 认证 + {args} Remote RPC + remote.mux,协议不向下兼容) */
|
|
3
|
+
export declare const MIN_DSH_VERSION = "0.1.2-rc.1";
|
|
4
|
+
/**
|
|
5
|
+
* 判定 version 是否 >= minimum(semver 风格,含 pre-release 比较:0.1.2 > 0.1.2-rc.1)。
|
|
6
|
+
* @returns true/false;任一版本无法解析时返回 null(调用方按"无法确认"放行)。
|
|
7
|
+
*/
|
|
8
|
+
export declare function isDeepSeekVersionAtLeast(version: string, minimum?: string): boolean | null;
|
|
2
9
|
export declare function getDeepSeekVersion(): Promise<string | null>;
|
|
3
10
|
export declare function killOrphanDeepSeekProcesses(): Promise<number>;
|
package/es/system.js
CHANGED
|
@@ -18,6 +18,45 @@ async function checkDeepSeekInstalled() {
|
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
|
+
const MIN_DSH_VERSION = "0.1.2-rc.1";
|
|
22
|
+
function parseDshVersion(version) {
|
|
23
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?/.exec(version.trim());
|
|
24
|
+
if (!match) return null;
|
|
25
|
+
return {
|
|
26
|
+
major: Number(match[1]),
|
|
27
|
+
minor: Number(match[2]),
|
|
28
|
+
patch: Number(match[3]),
|
|
29
|
+
pre: match[4]
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function isDeepSeekVersionAtLeast(version, minimum = MIN_DSH_VERSION) {
|
|
33
|
+
const a = parseDshVersion(version);
|
|
34
|
+
const b = parseDshVersion(minimum);
|
|
35
|
+
if (!a || !b) return null;
|
|
36
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
37
|
+
if (a[key] !== b[key]) return a[key] > b[key];
|
|
38
|
+
}
|
|
39
|
+
if (a.pre === void 0 && b.pre === void 0) return true;
|
|
40
|
+
if (a.pre === void 0) return true;
|
|
41
|
+
if (b.pre === void 0) return false;
|
|
42
|
+
const pa = a.pre.split(".");
|
|
43
|
+
const pb = b.pre.split(".");
|
|
44
|
+
const len = Math.max(pa.length, pb.length);
|
|
45
|
+
for (let i = 0; i < len; i++) {
|
|
46
|
+
const x = pa[i];
|
|
47
|
+
const y = pb[i];
|
|
48
|
+
if (x === void 0) return true;
|
|
49
|
+
if (y === void 0) return false;
|
|
50
|
+
const xn = /^\d+$/.test(x) ? Number(x) : NaN;
|
|
51
|
+
const yn = /^\d+$/.test(y) ? Number(y) : NaN;
|
|
52
|
+
if (!Number.isNaN(xn) && !Number.isNaN(yn)) {
|
|
53
|
+
if (xn !== yn) return xn > yn;
|
|
54
|
+
} else if (x !== y) {
|
|
55
|
+
return x > y;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
21
60
|
function getDeepSeekVersion() {
|
|
22
61
|
return new Promise((resolve) => {
|
|
23
62
|
const proc = spawn("dsh", ["--version"], { stdio: "pipe", shell: true });
|
|
@@ -164,7 +203,9 @@ function killOrphanProcessesOnUnix(resolve, timer) {
|
|
|
164
203
|
});
|
|
165
204
|
}
|
|
166
205
|
export {
|
|
206
|
+
MIN_DSH_VERSION,
|
|
167
207
|
checkDeepSeekInstalled,
|
|
168
208
|
getDeepSeekVersion,
|
|
209
|
+
isDeepSeekVersionAtLeast,
|
|
169
210
|
killOrphanDeepSeekProcesses
|
|
170
211
|
};
|
package/lib/api.cjs
CHANGED
|
@@ -42,26 +42,124 @@ class DeepSeekAPI {
|
|
|
42
42
|
constructor(hostname, getWebPort) {
|
|
43
43
|
__publicField(this, "hostname", hostname);
|
|
44
44
|
__publicField(this, "getWebPort", getWebPort);
|
|
45
|
+
/** dsh 启动打印的 launch token(browser-session 认证),由 provider 在捕获后写入 */
|
|
46
|
+
__publicField(this, "launchToken");
|
|
47
|
+
/** 启动早期等待 token 就绪的源(dsh 进程 stdout 的 LaunchToken.wait),未就绪时勿立即抛错 */
|
|
48
|
+
__publicField(this, "launchTokenSource");
|
|
49
|
+
/** browser-session 签名 Cookie(经 launch token 换取),dsh web 所有 /api 请求必须携带 */
|
|
50
|
+
__publicField(this, "authCookie");
|
|
51
|
+
/** 认证引导的幂等 Promise,避免并发多次换取 */
|
|
52
|
+
__publicField(this, "authPromise");
|
|
45
53
|
}
|
|
46
54
|
/** 应用壳 URL(无 deepLink 能力,所有会话共用) */
|
|
47
55
|
get shellUrl() {
|
|
48
56
|
return `http://${this.hostname}:${this.getWebPort()}`;
|
|
49
57
|
}
|
|
58
|
+
/** 注入 dsh 启动时打印的 launch token(来自 deepseek-web 的 stdout 解析) */
|
|
59
|
+
setLaunchToken(token) {
|
|
60
|
+
this.launchToken = token;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 绑定 launch token 等待源(dsh 进程 stdout 解析的 LaunchToken.wait)。
|
|
64
|
+
* 启动早期 widget 可能在 token 捕获前就发起 /api 请求,此时先等待 token 就绪而非抛错。
|
|
65
|
+
*/
|
|
66
|
+
setLaunchTokenSource(source) {
|
|
67
|
+
this.launchTokenSource = source;
|
|
68
|
+
}
|
|
69
|
+
/** 当前 browser-session Cookie(未认证时 undefined);供代理向转发请求注入同一 Cookie */
|
|
70
|
+
getAuthCookie() {
|
|
71
|
+
return this.authCookie;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 用 launch token 首次访问索引页换取 browser-session Cookie(dsh 0.1.2+ 的 browser-auth 门禁)。
|
|
75
|
+
* 幂等:已认证时直接返回。失败时抛错(调用方按需降级,不阻塞 dsh 启动)。
|
|
76
|
+
*/
|
|
77
|
+
async authenticate() {
|
|
78
|
+
if (this.authCookie) return;
|
|
79
|
+
if (!this.authPromise) {
|
|
80
|
+
this.authPromise = this.bootstrapAuthCookie().then(
|
|
81
|
+
(cookie) => {
|
|
82
|
+
this.authCookie = cookie;
|
|
83
|
+
log.debug("dsh browser-session authenticated");
|
|
84
|
+
},
|
|
85
|
+
(err) => {
|
|
86
|
+
this.authPromise = void 0;
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return this.authPromise;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* GET /?token=<launchToken>,从 303 响应捕获 Set-Cookie(browser-auth 用签名 cookie 换首访权限)。
|
|
95
|
+
* node:http 不自动跟随 303,因此能拿到该次响应的 Set-Cookie。
|
|
96
|
+
*/
|
|
97
|
+
bootstrapAuthCookie() {
|
|
98
|
+
if (!this.launchToken) {
|
|
99
|
+
return Promise.reject(new Error("dsh API authenticate called before setLaunchToken"));
|
|
100
|
+
}
|
|
101
|
+
const token = this.launchToken;
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const req = import_http.default.request(
|
|
104
|
+
{
|
|
105
|
+
hostname: this.hostname,
|
|
106
|
+
port: this.getWebPort(),
|
|
107
|
+
path: `/?token=${encodeURIComponent(token)}`,
|
|
108
|
+
method: "GET",
|
|
109
|
+
headers: { Accept: "text/html" }
|
|
110
|
+
},
|
|
111
|
+
(res) => {
|
|
112
|
+
const setCookie = res.headers["set-cookie"];
|
|
113
|
+
const cookies = Array.isArray(setCookie) ? setCookie : setCookie ? [setCookie] : [];
|
|
114
|
+
res.resume();
|
|
115
|
+
if (cookies.length > 0) {
|
|
116
|
+
resolve(cookies[0].split(";")[0].trim());
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
reject(
|
|
120
|
+
new Error(
|
|
121
|
+
`dsh auth bootstrap failed (status ${res.statusCode}, no Set-Cookie); token may be invalid`
|
|
122
|
+
)
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
req.on("error", (e) => reject(e instanceof Error ? e : new Error(String(e))));
|
|
127
|
+
req.end();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** 确保已认证(未认证时先等待 launch token 就绪;无 token 来源才抛错) */
|
|
131
|
+
async ensureAuthenticated() {
|
|
132
|
+
if (this.authCookie) return;
|
|
133
|
+
if (!this.launchToken) {
|
|
134
|
+
if (this.launchTokenSource) {
|
|
135
|
+
this.launchToken = await this.launchTokenSource();
|
|
136
|
+
}
|
|
137
|
+
if (!this.launchToken) {
|
|
138
|
+
throw new Error("dsh API not authenticated: launch token not available");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
await this.authenticate();
|
|
142
|
+
}
|
|
50
143
|
/** 发起一次 unary RPC,返回 result.value(ok=false 时抛错) */
|
|
51
|
-
async call(method,
|
|
144
|
+
async call(method, args = {}) {
|
|
145
|
+
await this.ensureAuthenticated();
|
|
52
146
|
const message = {
|
|
53
147
|
type: "client-request",
|
|
54
148
|
rpcId: (0, import_node_crypto.randomUUID)(),
|
|
55
149
|
method,
|
|
56
|
-
payload
|
|
150
|
+
payload: { args }
|
|
57
151
|
};
|
|
152
|
+
const headers = { "Content-Type": "application/json" };
|
|
153
|
+
if (this.authCookie) {
|
|
154
|
+
headers.Cookie = this.authCookie;
|
|
155
|
+
}
|
|
58
156
|
const response = await this.createHttpRequest(
|
|
59
157
|
{
|
|
60
158
|
hostname: this.hostname,
|
|
61
159
|
port: this.getWebPort(),
|
|
62
160
|
path: `${import_constants.DSH_API_BASE}/${method}`,
|
|
63
161
|
method: "POST",
|
|
64
|
-
headers
|
|
162
|
+
headers
|
|
65
163
|
},
|
|
66
164
|
JSON.stringify(message)
|
|
67
165
|
);
|
|
@@ -74,24 +172,28 @@ class DeepSeekAPI {
|
|
|
74
172
|
return response.result.value;
|
|
75
173
|
}
|
|
76
174
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
175
|
+
* 列出当前项目目录下的会话(可见性口径与 dsh UI 对齐)。
|
|
176
|
+
* 取 workspace/follow baseline(workspace.list 在 dsh 0.1.2+ 已移除)按 path 匹配工作区,
|
|
177
|
+
* 得 sessionIds 与全局 archivedSessionIds,再结合 session/list 的 cwd 过滤去重;
|
|
178
|
+
* 过滤规则同 dsh-client-ui-workspace 的 sessionVisible:
|
|
179
|
+
* 排除 origin=subagent 的子代理会话(UI 不单列)、排除归档;blank(未开过回合)仅当
|
|
180
|
+
* sessionId === activeSessionId(当前选中会话,对应 UI 的 New Session 占位行)时展示。
|
|
80
181
|
*/
|
|
81
|
-
async listSessions(projectDir, retries = import_core.DEFAULT_RETRIES) {
|
|
82
|
-
const timer = log.timer("listSessions", { projectDir, retries });
|
|
182
|
+
async listSessions(projectDir, activeSessionId, retries = import_core.DEFAULT_RETRIES) {
|
|
183
|
+
const timer = log.timer("listSessions", { projectDir, activeSessionId, retries });
|
|
83
184
|
let lastError = null;
|
|
84
185
|
for (let i = 0; i < retries; i++) {
|
|
85
186
|
try {
|
|
86
|
-
log.debug(`Attempt ${i + 1}/${retries}`, { method: "session
|
|
87
|
-
const workspaces = await this.
|
|
187
|
+
log.debug(`Attempt ${i + 1}/${retries}`, { method: "session/list", projectDir });
|
|
188
|
+
const workspaces = await this.fetchWorkspaceBaseline();
|
|
88
189
|
const matchedWorkspace = workspaces.items.find((w) => w.path === projectDir);
|
|
89
190
|
const ownedByWorkspace = new Set(matchedWorkspace?.sessionIds ?? []);
|
|
90
191
|
const archived = new Set(workspaces.archivedSessionIds);
|
|
91
|
-
const sessions = await this.call("session
|
|
192
|
+
const sessions = await this.call("session/list", { _request: {} });
|
|
92
193
|
const all = sessions.items;
|
|
93
194
|
const filtered = all.filter((s) => {
|
|
94
|
-
if (s.
|
|
195
|
+
if (s.origin === "subagent") return false;
|
|
196
|
+
if (s.blank && s.sessionId !== activeSessionId) return false;
|
|
95
197
|
if (archived.has(s.sessionId)) return false;
|
|
96
198
|
if (ownedByWorkspace.has(s.sessionId)) return true;
|
|
97
199
|
if (s.cwd && s.cwd === projectDir) return true;
|
|
@@ -112,23 +214,22 @@ class DeepSeekAPI {
|
|
|
112
214
|
throw lastError;
|
|
113
215
|
}
|
|
114
216
|
/** 在当前目录下创建会话(dsh 仅返回 { sessionId, agentPreset? },非完整 SessionSummary)。
|
|
115
|
-
*
|
|
116
|
-
* 再用 workspaceId 调 session
|
|
117
|
-
* 新会话会落到侧边栏"未分组"。 */
|
|
217
|
+
* 与旧逻辑一致:先确保 projectDir 对应的 workspace 存在(workspace/create 幂等 get-or-create),
|
|
218
|
+
* 再用 workspaceId 调 session/create,让新会话挂到该 workspace。 */
|
|
118
219
|
async createSession(projectDir, retries = import_core.DEFAULT_RETRIES) {
|
|
119
220
|
const timer = log.timer("createSession", { projectDir, retries });
|
|
120
221
|
let lastError = null;
|
|
121
222
|
for (let i = 0; i < retries; i++) {
|
|
122
223
|
try {
|
|
123
224
|
log.debug(`Attempt ${i + 1}/${retries}`, {
|
|
124
|
-
method: "session
|
|
225
|
+
method: "session/create",
|
|
125
226
|
projectDir
|
|
126
227
|
});
|
|
127
|
-
const { workspace } = await this.call("workspace
|
|
128
|
-
path: projectDir
|
|
228
|
+
const { workspace } = await this.call("workspace/create", {
|
|
229
|
+
request: { path: projectDir }
|
|
129
230
|
});
|
|
130
|
-
const session = await this.call("session
|
|
131
|
-
workspaceId: workspace.workspaceId
|
|
231
|
+
const session = await this.call("session/create", {
|
|
232
|
+
request: { workspaceId: workspace.workspaceId }
|
|
132
233
|
});
|
|
133
234
|
timer.end(`Created session: ${session.sessionId}`);
|
|
134
235
|
return session;
|
|
@@ -144,7 +245,7 @@ class DeepSeekAPI {
|
|
|
144
245
|
throw lastError;
|
|
145
246
|
}
|
|
146
247
|
/**
|
|
147
|
-
* 通过 dsh settings
|
|
248
|
+
* 通过 dsh settings/mutate 应用 providerOptions 指定的用户设置(逐命名空间幂等 patch)。
|
|
148
249
|
* dsh 启动初期 API 未就绪,整体带重试;单个命名空间不存在会导致该次调用失败重试。
|
|
149
250
|
*/
|
|
150
251
|
async applySettings(sections, retries = import_core.DEFAULT_RETRIES) {
|
|
@@ -154,9 +255,14 @@ class DeepSeekAPI {
|
|
|
154
255
|
let lastError = null;
|
|
155
256
|
for (let i = 0; i < retries; i++) {
|
|
156
257
|
try {
|
|
157
|
-
log.debug(`Attempt ${i + 1}/${retries}`, { method: "settings
|
|
258
|
+
log.debug(`Attempt ${i + 1}/${retries}`, { method: "settings/mutate", namespaces });
|
|
158
259
|
for (const [ns, patch] of Object.entries(sections)) {
|
|
159
|
-
|
|
260
|
+
const ops = Object.entries(patch).map(([key, value]) => ({
|
|
261
|
+
op: "set",
|
|
262
|
+
path: [key],
|
|
263
|
+
value
|
|
264
|
+
}));
|
|
265
|
+
await this.call("settings/mutate", { ns, ops });
|
|
160
266
|
}
|
|
161
267
|
timer.end(`Applied settings: ${namespaces.join(", ")}`);
|
|
162
268
|
return;
|
|
@@ -177,8 +283,8 @@ class DeepSeekAPI {
|
|
|
177
283
|
let lastError = null;
|
|
178
284
|
for (let i = 0; i < retries; i++) {
|
|
179
285
|
try {
|
|
180
|
-
log.debug(`Attempt ${i + 1}/${retries}`, { method: "workspace
|
|
181
|
-
await this.call("workspace
|
|
286
|
+
log.debug(`Attempt ${i + 1}/${retries}`, { method: "workspace/archiveSession" });
|
|
287
|
+
await this.call("workspace/archiveSession", { request: { sessionId } });
|
|
182
288
|
timer.end(`Archived session: ${sessionId}`);
|
|
183
289
|
return;
|
|
184
290
|
} catch (e) {
|
|
@@ -194,6 +300,76 @@ class DeepSeekAPI {
|
|
|
194
300
|
timer.end("\u274C All retries exhausted");
|
|
195
301
|
throw lastError;
|
|
196
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* 取 workspace/follow 流的 baseline(等价旧 workspace.list 的快照:{ items, archivedSessionIds })。
|
|
305
|
+
* dsh 0.1.2+ 无 workspace.list RPC,工作区发现走 /api/remote.mux 上的 workspace/follow 流。
|
|
306
|
+
* 直连 dsh web(webPort)并携带 browser-session Cookie(原生 WebSocket 支持自定义请求头),
|
|
307
|
+
* 打开流读到首个 baseline 帧即关闭;启动早期 webPort 已就绪,不存在代理时序竞态。
|
|
308
|
+
*/
|
|
309
|
+
async fetchWorkspaceBaseline() {
|
|
310
|
+
await this.ensureAuthenticated();
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
const cookie = this.authCookie;
|
|
313
|
+
if (!cookie) {
|
|
314
|
+
reject(new Error("dsh workspace baseline unavailable: not authenticated"));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const url = `ws://${this.hostname}:${this.getWebPort()}${import_constants.DSH_REMOTE_MUX_PATH}`;
|
|
318
|
+
const ws = new WebSocket(url, {
|
|
319
|
+
headers: { Cookie: cookie }
|
|
320
|
+
});
|
|
321
|
+
const streamId = (0, import_node_crypto.randomUUID)();
|
|
322
|
+
let settled = false;
|
|
323
|
+
const finish = (err, value) => {
|
|
324
|
+
if (settled) return;
|
|
325
|
+
settled = true;
|
|
326
|
+
clearTimeout(timer);
|
|
327
|
+
try {
|
|
328
|
+
ws.close();
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
if (err) reject(err);
|
|
332
|
+
else resolve(value);
|
|
333
|
+
};
|
|
334
|
+
const timer = setTimeout(
|
|
335
|
+
() => finish(new Error("dsh workspace baseline fetch timed out")),
|
|
336
|
+
15e3
|
|
337
|
+
);
|
|
338
|
+
ws.onopen = () => {
|
|
339
|
+
try {
|
|
340
|
+
ws.send(
|
|
341
|
+
JSON.stringify({
|
|
342
|
+
type: "open",
|
|
343
|
+
streamId,
|
|
344
|
+
endpoint: "workspace/follow",
|
|
345
|
+
payload: { args: {} }
|
|
346
|
+
})
|
|
347
|
+
);
|
|
348
|
+
} catch (e) {
|
|
349
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
ws.onmessage = (ev) => {
|
|
353
|
+
try {
|
|
354
|
+
const msg = JSON.parse(String(ev.data));
|
|
355
|
+
if (!msg || msg.streamId !== streamId) return;
|
|
356
|
+
if (msg.type === "item" && msg.value?.type === "baseline" && msg.value.value) {
|
|
357
|
+
finish(void 0, {
|
|
358
|
+
items: msg.value.value.items ?? [],
|
|
359
|
+
archivedSessionIds: msg.value.value.archivedSessionIds ?? []
|
|
360
|
+
});
|
|
361
|
+
} else if (msg.type === "error") {
|
|
362
|
+
finish(new Error(`dsh workspace/follow failed: ${msg.error?.message ?? "unknown"}`));
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
ws.onerror = () => finish(new Error("dsh workspace baseline WebSocket error"));
|
|
368
|
+
ws.onclose = () => {
|
|
369
|
+
if (!settled) finish(new Error("dsh workspace baseline WebSocket closed before baseline"));
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
}
|
|
197
373
|
createHttpRequest(options, body) {
|
|
198
374
|
const timer = new import_node.PerformanceTimer("HTTP Request", {
|
|
199
375
|
operation: `${options.method || "GET"} ${options.path}`
|
|
@@ -203,13 +379,25 @@ class DeepSeekAPI {
|
|
|
203
379
|
let data = "";
|
|
204
380
|
res.on("data", (chunk) => data += chunk);
|
|
205
381
|
res.on("end", () => {
|
|
382
|
+
const status = res.statusCode ?? 0;
|
|
383
|
+
if (status < 200 || status >= 300) {
|
|
384
|
+
timer.end(`\u274C HTTP ${status}`);
|
|
385
|
+
reject(
|
|
386
|
+
new Error(
|
|
387
|
+
`dsh HTTP ${status} on ${options.method ?? "GET"} ${options.path}: ${(data || "(empty body)").substring(0, 200)}`
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
206
392
|
try {
|
|
207
393
|
const result = JSON.parse(data);
|
|
208
|
-
timer.end(`\u2713 Status: ${
|
|
394
|
+
timer.end(`\u2713 Status: ${status}`);
|
|
209
395
|
resolve(result);
|
|
210
396
|
} catch {
|
|
211
397
|
timer.end("\u274C JSON parse error");
|
|
212
|
-
reject(
|
|
398
|
+
reject(
|
|
399
|
+
new Error(`dsh HTTP ${status}: response is not JSON: ${data.substring(0, 200)}`)
|
|
400
|
+
);
|
|
213
401
|
}
|
|
214
402
|
});
|
|
215
403
|
});
|