@aipanel/provider-deepseek 1.2.7 → 1.2.9
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 +45 -12
- package/es/api.js +208 -53
- package/es/constants.d.ts +8 -14
- package/es/constants.js +2 -14
- package/es/deepseek-web.d.ts +20 -0
- package/es/deepseek-web.js +60 -2
- package/es/index.d.ts +1 -2
- package/es/index.js +0 -2
- package/es/profile.d.ts +19 -3
- package/es/profile.js +20 -3
- package/es/provider.d.ts +7 -8
- package/es/provider.js +70 -142
- package/es/system.d.ts +7 -0
- package/es/system.js +41 -0
- package/lib/api.cjs +207 -52
- package/lib/api.d.ts +45 -12
- package/lib/constants.cjs +3 -17
- package/lib/constants.d.ts +8 -14
- package/lib/deepseek-web.cjs +60 -2
- package/lib/deepseek-web.d.ts +20 -0
- package/lib/index.cjs +0 -3
- package/lib/index.d.ts +1 -2
- package/lib/profile.cjs +19 -3
- package/lib/profile.d.ts +19 -3
- package/lib/provider.cjs +61 -139
- package/lib/provider.d.ts +7 -8
- package/lib/system.cjs +43 -0
- package/lib/system.d.ts +7 -0
- package/package.json +2 -2
- package/es/bridge-script.d.ts +0 -13
- package/es/bridge-script.js +0 -158
- package/lib/bridge-script.cjs +0 -181
- package/lib/bridge-script.d.ts +0 -13
package/es/api.d.ts
CHANGED
|
@@ -7,30 +7,63 @@ import type { SessionSummary } from "./types";
|
|
|
7
7
|
export declare class DeepSeekAPI {
|
|
8
8
|
private hostname;
|
|
9
9
|
private getWebPort;
|
|
10
|
+
/** dsh 启动打印的 launch token(browser-session 认证),由 provider 在捕获后写入 */
|
|
11
|
+
private launchToken?;
|
|
12
|
+
/** 启动早期等待 token 就绪的源(dsh 进程 stdout 的 LaunchToken.wait),未就绪时勿立即抛错 */
|
|
13
|
+
private launchTokenSource?;
|
|
14
|
+
/** browser-session 签名 Cookie(经 launch token 换取),dsh web 所有 /api 请求必须携带 */
|
|
15
|
+
private authCookie?;
|
|
16
|
+
/** 认证引导的幂等 Promise,避免并发多次换取 */
|
|
17
|
+
private authPromise?;
|
|
10
18
|
constructor(hostname: string, getWebPort: () => number);
|
|
11
19
|
/** 应用壳 URL(无 deepLink 能力,所有会话共用) */
|
|
12
20
|
get shellUrl(): string;
|
|
21
|
+
/** 注入 dsh 启动时打印的 launch token(来自 deepseek-web 的 stdout 解析) */
|
|
22
|
+
setLaunchToken(token: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* 绑定 launch token 等待源(dsh 进程 stdout 解析的 LaunchToken.wait)。
|
|
25
|
+
* 启动早期 widget 可能在 token 捕获前就发起 /api 请求,此时先等待 token 就绪而非抛错。
|
|
26
|
+
*/
|
|
27
|
+
setLaunchTokenSource(source: () => Promise<string>): void;
|
|
28
|
+
/** 当前 browser-session Cookie(未认证时 undefined);供代理向转发请求注入同一 Cookie */
|
|
29
|
+
getAuthCookie(): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* 用 launch token 首次访问索引页换取 browser-session Cookie(dsh 0.1.2+ 的 browser-auth 门禁)。
|
|
32
|
+
* 幂等:已认证时直接返回。失败时抛错(调用方按需降级,不阻塞 dsh 启动)。
|
|
33
|
+
*/
|
|
34
|
+
authenticate(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* GET /?token=<launchToken>,从 303 响应捕获 Set-Cookie(browser-auth 用签名 cookie 换首访权限)。
|
|
37
|
+
* node:http 不自动跟随 303,因此能拿到该次响应的 Set-Cookie。
|
|
38
|
+
*/
|
|
39
|
+
private bootstrapAuthCookie;
|
|
40
|
+
/** 确保已认证(未认证时先等待 launch token 就绪;无 token 来源才抛错) */
|
|
41
|
+
private ensureAuthenticated;
|
|
13
42
|
/** 发起一次 unary RPC,返回 result.value(ok=false 时抛错) */
|
|
14
43
|
private call;
|
|
15
44
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
45
|
+
* 列出当前项目目录下的会话(可见性口径与 dsh UI 对齐)。
|
|
46
|
+
* 取 workspace/follow baseline(workspace.list 在 dsh 0.1.2+ 已移除)按 path 匹配工作区,
|
|
47
|
+
* 得 sessionIds 与全局 archivedSessionIds,再结合 session/list 的 cwd 过滤去重;
|
|
48
|
+
* 过滤规则同 dsh-client-ui-workspace 的 sessionVisible:
|
|
49
|
+
* 排除 origin=subagent 的子代理会话(UI 不单列)、排除归档;blank(未开过回合)仅当
|
|
50
|
+
* sessionId === activeSessionId(当前选中会话,对应 UI 的 New Session 占位行)时展示。
|
|
19
51
|
*/
|
|
20
|
-
listSessions(projectDir: string, retries?: number): Promise<SessionSummary[]>;
|
|
52
|
+
listSessions(projectDir: string, activeSessionId?: string, retries?: number): Promise<SessionSummary[]>;
|
|
21
53
|
/** 在当前目录下创建会话(dsh 仅返回 { sessionId, agentPreset? },非完整 SessionSummary)。
|
|
22
|
-
*
|
|
23
|
-
* 再用 workspaceId 调 session
|
|
24
|
-
* 新会话会落到侧边栏"未分组"。 */
|
|
54
|
+
* 与旧逻辑一致:先确保 projectDir 对应的 workspace 存在(workspace/create 幂等 get-or-create),
|
|
55
|
+
* 再用 workspaceId 调 session/create,让新会话挂到该 workspace。 */
|
|
25
56
|
createSession(projectDir: string, retries?: number): Promise<{
|
|
26
57
|
sessionId: string;
|
|
27
58
|
}>;
|
|
28
|
-
/**
|
|
29
|
-
* 通过 dsh settings.update 应用 providerOptions 指定的用户设置(逐命名空间幂等 patch)。
|
|
30
|
-
* dsh 启动初期 API 未就绪,整体带重试;单个命名空间不存在会导致该次调用失败重试。
|
|
31
|
-
*/
|
|
32
|
-
applySettings(sections: Record<string, Record<string, unknown>>, retries?: number): Promise<void>;
|
|
33
59
|
/** 归档会话(dsh 无硬删除,仅归档;幂等) */
|
|
34
60
|
archiveSession(sessionId: string, retries?: number): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* 取 workspace/follow 流的 baseline(等价旧 workspace.list 的快照:{ items, archivedSessionIds })。
|
|
63
|
+
* dsh 0.1.2+ 无 workspace.list RPC,工作区发现走 /api/remote.mux 上的 workspace/follow 流。
|
|
64
|
+
* 直连 dsh web(webPort)并携带 browser-session Cookie(原生 WebSocket 支持自定义请求头),
|
|
65
|
+
* 打开流读到首个 baseline 帧即关闭;启动早期 webPort 已就绪,不存在代理时序竞态。
|
|
66
|
+
*/
|
|
67
|
+
private fetchWorkspaceBaseline;
|
|
35
68
|
private createHttpRequest;
|
|
36
69
|
}
|
package/es/api.js
CHANGED
|
@@ -5,32 +5,130 @@ import http from "http";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { DEFAULT_RETRIES, RETRY_DELAY, sleep } from "@aipanel/core";
|
|
7
7
|
import { PerformanceTimer, createLogger } from "@aipanel/core/node";
|
|
8
|
-
import { DSH_API_BASE } from "./constants.js";
|
|
8
|
+
import { DSH_API_BASE, DSH_REMOTE_MUX_PATH } from "./constants.js";
|
|
9
9
|
const log = createLogger("DeepSeekAPI");
|
|
10
10
|
class DeepSeekAPI {
|
|
11
11
|
constructor(hostname, getWebPort) {
|
|
12
12
|
__publicField(this, "hostname", hostname);
|
|
13
13
|
__publicField(this, "getWebPort", getWebPort);
|
|
14
|
+
/** dsh 启动打印的 launch token(browser-session 认证),由 provider 在捕获后写入 */
|
|
15
|
+
__publicField(this, "launchToken");
|
|
16
|
+
/** 启动早期等待 token 就绪的源(dsh 进程 stdout 的 LaunchToken.wait),未就绪时勿立即抛错 */
|
|
17
|
+
__publicField(this, "launchTokenSource");
|
|
18
|
+
/** browser-session 签名 Cookie(经 launch token 换取),dsh web 所有 /api 请求必须携带 */
|
|
19
|
+
__publicField(this, "authCookie");
|
|
20
|
+
/** 认证引导的幂等 Promise,避免并发多次换取 */
|
|
21
|
+
__publicField(this, "authPromise");
|
|
14
22
|
}
|
|
15
23
|
/** 应用壳 URL(无 deepLink 能力,所有会话共用) */
|
|
16
24
|
get shellUrl() {
|
|
17
25
|
return `http://${this.hostname}:${this.getWebPort()}`;
|
|
18
26
|
}
|
|
27
|
+
/** 注入 dsh 启动时打印的 launch token(来自 deepseek-web 的 stdout 解析) */
|
|
28
|
+
setLaunchToken(token) {
|
|
29
|
+
this.launchToken = token;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 绑定 launch token 等待源(dsh 进程 stdout 解析的 LaunchToken.wait)。
|
|
33
|
+
* 启动早期 widget 可能在 token 捕获前就发起 /api 请求,此时先等待 token 就绪而非抛错。
|
|
34
|
+
*/
|
|
35
|
+
setLaunchTokenSource(source) {
|
|
36
|
+
this.launchTokenSource = source;
|
|
37
|
+
}
|
|
38
|
+
/** 当前 browser-session Cookie(未认证时 undefined);供代理向转发请求注入同一 Cookie */
|
|
39
|
+
getAuthCookie() {
|
|
40
|
+
return this.authCookie;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 用 launch token 首次访问索引页换取 browser-session Cookie(dsh 0.1.2+ 的 browser-auth 门禁)。
|
|
44
|
+
* 幂等:已认证时直接返回。失败时抛错(调用方按需降级,不阻塞 dsh 启动)。
|
|
45
|
+
*/
|
|
46
|
+
async authenticate() {
|
|
47
|
+
if (this.authCookie) return;
|
|
48
|
+
if (!this.authPromise) {
|
|
49
|
+
this.authPromise = this.bootstrapAuthCookie().then(
|
|
50
|
+
(cookie) => {
|
|
51
|
+
this.authCookie = cookie;
|
|
52
|
+
log.debug("dsh browser-session authenticated");
|
|
53
|
+
},
|
|
54
|
+
(err) => {
|
|
55
|
+
this.authPromise = void 0;
|
|
56
|
+
throw err;
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return this.authPromise;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* GET /?token=<launchToken>,从 303 响应捕获 Set-Cookie(browser-auth 用签名 cookie 换首访权限)。
|
|
64
|
+
* node:http 不自动跟随 303,因此能拿到该次响应的 Set-Cookie。
|
|
65
|
+
*/
|
|
66
|
+
bootstrapAuthCookie() {
|
|
67
|
+
if (!this.launchToken) {
|
|
68
|
+
return Promise.reject(new Error("dsh API authenticate called before setLaunchToken"));
|
|
69
|
+
}
|
|
70
|
+
const token = this.launchToken;
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const req = http.request(
|
|
73
|
+
{
|
|
74
|
+
hostname: this.hostname,
|
|
75
|
+
port: this.getWebPort(),
|
|
76
|
+
path: `/?token=${encodeURIComponent(token)}`,
|
|
77
|
+
method: "GET",
|
|
78
|
+
headers: { Accept: "text/html" }
|
|
79
|
+
},
|
|
80
|
+
(res) => {
|
|
81
|
+
const setCookie = res.headers["set-cookie"];
|
|
82
|
+
const cookies = Array.isArray(setCookie) ? setCookie : setCookie ? [setCookie] : [];
|
|
83
|
+
res.resume();
|
|
84
|
+
if (cookies.length > 0) {
|
|
85
|
+
resolve(cookies[0].split(";")[0].trim());
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
reject(
|
|
89
|
+
new Error(
|
|
90
|
+
`dsh auth bootstrap failed (status ${res.statusCode}, no Set-Cookie); token may be invalid`
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
req.on("error", (e) => reject(e instanceof Error ? e : new Error(String(e))));
|
|
96
|
+
req.end();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/** 确保已认证(未认证时先等待 launch token 就绪;无 token 来源才抛错) */
|
|
100
|
+
async ensureAuthenticated() {
|
|
101
|
+
if (this.authCookie) return;
|
|
102
|
+
if (!this.launchToken) {
|
|
103
|
+
if (this.launchTokenSource) {
|
|
104
|
+
this.launchToken = await this.launchTokenSource();
|
|
105
|
+
}
|
|
106
|
+
if (!this.launchToken) {
|
|
107
|
+
throw new Error("dsh API not authenticated: launch token not available");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await this.authenticate();
|
|
111
|
+
}
|
|
19
112
|
/** 发起一次 unary RPC,返回 result.value(ok=false 时抛错) */
|
|
20
|
-
async call(method,
|
|
113
|
+
async call(method, args = {}) {
|
|
114
|
+
await this.ensureAuthenticated();
|
|
21
115
|
const message = {
|
|
22
116
|
type: "client-request",
|
|
23
117
|
rpcId: randomUUID(),
|
|
24
118
|
method,
|
|
25
|
-
payload
|
|
119
|
+
payload: { args }
|
|
26
120
|
};
|
|
121
|
+
const headers = { "Content-Type": "application/json" };
|
|
122
|
+
if (this.authCookie) {
|
|
123
|
+
headers.Cookie = this.authCookie;
|
|
124
|
+
}
|
|
27
125
|
const response = await this.createHttpRequest(
|
|
28
126
|
{
|
|
29
127
|
hostname: this.hostname,
|
|
30
128
|
port: this.getWebPort(),
|
|
31
129
|
path: `${DSH_API_BASE}/${method}`,
|
|
32
130
|
method: "POST",
|
|
33
|
-
headers
|
|
131
|
+
headers
|
|
34
132
|
},
|
|
35
133
|
JSON.stringify(message)
|
|
36
134
|
);
|
|
@@ -43,24 +141,28 @@ class DeepSeekAPI {
|
|
|
43
141
|
return response.result.value;
|
|
44
142
|
}
|
|
45
143
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
144
|
+
* 列出当前项目目录下的会话(可见性口径与 dsh UI 对齐)。
|
|
145
|
+
* 取 workspace/follow baseline(workspace.list 在 dsh 0.1.2+ 已移除)按 path 匹配工作区,
|
|
146
|
+
* 得 sessionIds 与全局 archivedSessionIds,再结合 session/list 的 cwd 过滤去重;
|
|
147
|
+
* 过滤规则同 dsh-client-ui-workspace 的 sessionVisible:
|
|
148
|
+
* 排除 origin=subagent 的子代理会话(UI 不单列)、排除归档;blank(未开过回合)仅当
|
|
149
|
+
* sessionId === activeSessionId(当前选中会话,对应 UI 的 New Session 占位行)时展示。
|
|
49
150
|
*/
|
|
50
|
-
async listSessions(projectDir, retries = DEFAULT_RETRIES) {
|
|
51
|
-
const timer = log.timer("listSessions", { projectDir, retries });
|
|
151
|
+
async listSessions(projectDir, activeSessionId, retries = DEFAULT_RETRIES) {
|
|
152
|
+
const timer = log.timer("listSessions", { projectDir, activeSessionId, retries });
|
|
52
153
|
let lastError = null;
|
|
53
154
|
for (let i = 0; i < retries; i++) {
|
|
54
155
|
try {
|
|
55
|
-
log.debug(`Attempt ${i + 1}/${retries}`, { method: "session
|
|
56
|
-
const workspaces = await this.
|
|
156
|
+
log.debug(`Attempt ${i + 1}/${retries}`, { method: "session/list", projectDir });
|
|
157
|
+
const workspaces = await this.fetchWorkspaceBaseline();
|
|
57
158
|
const matchedWorkspace = workspaces.items.find((w) => w.path === projectDir);
|
|
58
159
|
const ownedByWorkspace = new Set(matchedWorkspace?.sessionIds ?? []);
|
|
59
160
|
const archived = new Set(workspaces.archivedSessionIds);
|
|
60
|
-
const sessions = await this.call("session
|
|
161
|
+
const sessions = await this.call("session/list", { _request: {} });
|
|
61
162
|
const all = sessions.items;
|
|
62
163
|
const filtered = all.filter((s) => {
|
|
63
|
-
if (s.
|
|
164
|
+
if (s.origin === "subagent") return false;
|
|
165
|
+
if (s.blank && s.sessionId !== activeSessionId) return false;
|
|
64
166
|
if (archived.has(s.sessionId)) return false;
|
|
65
167
|
if (ownedByWorkspace.has(s.sessionId)) return true;
|
|
66
168
|
if (s.cwd && s.cwd === projectDir) return true;
|
|
@@ -81,23 +183,22 @@ class DeepSeekAPI {
|
|
|
81
183
|
throw lastError;
|
|
82
184
|
}
|
|
83
185
|
/** 在当前目录下创建会话(dsh 仅返回 { sessionId, agentPreset? },非完整 SessionSummary)。
|
|
84
|
-
*
|
|
85
|
-
* 再用 workspaceId 调 session
|
|
86
|
-
* 新会话会落到侧边栏"未分组"。 */
|
|
186
|
+
* 与旧逻辑一致:先确保 projectDir 对应的 workspace 存在(workspace/create 幂等 get-or-create),
|
|
187
|
+
* 再用 workspaceId 调 session/create,让新会话挂到该 workspace。 */
|
|
87
188
|
async createSession(projectDir, retries = DEFAULT_RETRIES) {
|
|
88
189
|
const timer = log.timer("createSession", { projectDir, retries });
|
|
89
190
|
let lastError = null;
|
|
90
191
|
for (let i = 0; i < retries; i++) {
|
|
91
192
|
try {
|
|
92
193
|
log.debug(`Attempt ${i + 1}/${retries}`, {
|
|
93
|
-
method: "session
|
|
194
|
+
method: "session/create",
|
|
94
195
|
projectDir
|
|
95
196
|
});
|
|
96
|
-
const { workspace } = await this.call("workspace
|
|
97
|
-
path: projectDir
|
|
197
|
+
const { workspace } = await this.call("workspace/create", {
|
|
198
|
+
request: { path: projectDir }
|
|
98
199
|
});
|
|
99
|
-
const session = await this.call("session
|
|
100
|
-
workspaceId: workspace.workspaceId
|
|
200
|
+
const session = await this.call("session/create", {
|
|
201
|
+
request: { workspaceId: workspace.workspaceId }
|
|
101
202
|
});
|
|
102
203
|
timer.end(`Created session: ${session.sessionId}`);
|
|
103
204
|
return session;
|
|
@@ -112,42 +213,14 @@ class DeepSeekAPI {
|
|
|
112
213
|
timer.end("\u274C All retries exhausted");
|
|
113
214
|
throw lastError;
|
|
114
215
|
}
|
|
115
|
-
/**
|
|
116
|
-
* 通过 dsh settings.update 应用 providerOptions 指定的用户设置(逐命名空间幂等 patch)。
|
|
117
|
-
* dsh 启动初期 API 未就绪,整体带重试;单个命名空间不存在会导致该次调用失败重试。
|
|
118
|
-
*/
|
|
119
|
-
async applySettings(sections, retries = DEFAULT_RETRIES) {
|
|
120
|
-
const namespaces = Object.keys(sections);
|
|
121
|
-
if (namespaces.length === 0) return;
|
|
122
|
-
const timer = log.timer("applySettings", { namespaces });
|
|
123
|
-
let lastError = null;
|
|
124
|
-
for (let i = 0; i < retries; i++) {
|
|
125
|
-
try {
|
|
126
|
-
log.debug(`Attempt ${i + 1}/${retries}`, { method: "settings.update", namespaces });
|
|
127
|
-
for (const [ns, patch] of Object.entries(sections)) {
|
|
128
|
-
await this.call("settings.update", { ns, patch });
|
|
129
|
-
}
|
|
130
|
-
timer.end(`Applied settings: ${namespaces.join(", ")}`);
|
|
131
|
-
return;
|
|
132
|
-
} catch (e) {
|
|
133
|
-
lastError = e instanceof Error ? e : new Error(String(e));
|
|
134
|
-
log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, { method: "applySettings" });
|
|
135
|
-
if (i < retries - 1) {
|
|
136
|
-
await sleep(RETRY_DELAY);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
timer.end("\u274C All retries exhausted");
|
|
141
|
-
throw lastError;
|
|
142
|
-
}
|
|
143
216
|
/** 归档会话(dsh 无硬删除,仅归档;幂等) */
|
|
144
217
|
async archiveSession(sessionId, retries = DEFAULT_RETRIES) {
|
|
145
218
|
const timer = log.timer("archiveSession", { sessionId, retries });
|
|
146
219
|
let lastError = null;
|
|
147
220
|
for (let i = 0; i < retries; i++) {
|
|
148
221
|
try {
|
|
149
|
-
log.debug(`Attempt ${i + 1}/${retries}`, { method: "workspace
|
|
150
|
-
await this.call("workspace
|
|
222
|
+
log.debug(`Attempt ${i + 1}/${retries}`, { method: "workspace/archiveSession" });
|
|
223
|
+
await this.call("workspace/archiveSession", { request: { sessionId } });
|
|
151
224
|
timer.end(`Archived session: ${sessionId}`);
|
|
152
225
|
return;
|
|
153
226
|
} catch (e) {
|
|
@@ -163,6 +236,76 @@ class DeepSeekAPI {
|
|
|
163
236
|
timer.end("\u274C All retries exhausted");
|
|
164
237
|
throw lastError;
|
|
165
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* 取 workspace/follow 流的 baseline(等价旧 workspace.list 的快照:{ items, archivedSessionIds })。
|
|
241
|
+
* dsh 0.1.2+ 无 workspace.list RPC,工作区发现走 /api/remote.mux 上的 workspace/follow 流。
|
|
242
|
+
* 直连 dsh web(webPort)并携带 browser-session Cookie(原生 WebSocket 支持自定义请求头),
|
|
243
|
+
* 打开流读到首个 baseline 帧即关闭;启动早期 webPort 已就绪,不存在代理时序竞态。
|
|
244
|
+
*/
|
|
245
|
+
async fetchWorkspaceBaseline() {
|
|
246
|
+
await this.ensureAuthenticated();
|
|
247
|
+
return new Promise((resolve, reject) => {
|
|
248
|
+
const cookie = this.authCookie;
|
|
249
|
+
if (!cookie) {
|
|
250
|
+
reject(new Error("dsh workspace baseline unavailable: not authenticated"));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const url = `ws://${this.hostname}:${this.getWebPort()}${DSH_REMOTE_MUX_PATH}`;
|
|
254
|
+
const ws = new WebSocket(url, {
|
|
255
|
+
headers: { Cookie: cookie }
|
|
256
|
+
});
|
|
257
|
+
const streamId = randomUUID();
|
|
258
|
+
let settled = false;
|
|
259
|
+
const finish = (err, value) => {
|
|
260
|
+
if (settled) return;
|
|
261
|
+
settled = true;
|
|
262
|
+
clearTimeout(timer);
|
|
263
|
+
try {
|
|
264
|
+
ws.close();
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
if (err) reject(err);
|
|
268
|
+
else resolve(value);
|
|
269
|
+
};
|
|
270
|
+
const timer = setTimeout(
|
|
271
|
+
() => finish(new Error("dsh workspace baseline fetch timed out")),
|
|
272
|
+
15e3
|
|
273
|
+
);
|
|
274
|
+
ws.onopen = () => {
|
|
275
|
+
try {
|
|
276
|
+
ws.send(
|
|
277
|
+
JSON.stringify({
|
|
278
|
+
type: "open",
|
|
279
|
+
streamId,
|
|
280
|
+
endpoint: "workspace/follow",
|
|
281
|
+
payload: { args: {} }
|
|
282
|
+
})
|
|
283
|
+
);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
finish(e instanceof Error ? e : new Error(String(e)));
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
ws.onmessage = (ev) => {
|
|
289
|
+
try {
|
|
290
|
+
const msg = JSON.parse(String(ev.data));
|
|
291
|
+
if (!msg || msg.streamId !== streamId) return;
|
|
292
|
+
if (msg.type === "item" && msg.value?.type === "baseline" && msg.value.value) {
|
|
293
|
+
finish(void 0, {
|
|
294
|
+
items: msg.value.value.items ?? [],
|
|
295
|
+
archivedSessionIds: msg.value.value.archivedSessionIds ?? []
|
|
296
|
+
});
|
|
297
|
+
} else if (msg.type === "error") {
|
|
298
|
+
finish(new Error(`dsh workspace/follow failed: ${msg.error?.message ?? "unknown"}`));
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
ws.onerror = () => finish(new Error("dsh workspace baseline WebSocket error"));
|
|
304
|
+
ws.onclose = () => {
|
|
305
|
+
if (!settled) finish(new Error("dsh workspace baseline WebSocket closed before baseline"));
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
}
|
|
166
309
|
createHttpRequest(options, body) {
|
|
167
310
|
const timer = new PerformanceTimer("HTTP Request", {
|
|
168
311
|
operation: `${options.method || "GET"} ${options.path}`
|
|
@@ -172,13 +315,25 @@ class DeepSeekAPI {
|
|
|
172
315
|
let data = "";
|
|
173
316
|
res.on("data", (chunk) => data += chunk);
|
|
174
317
|
res.on("end", () => {
|
|
318
|
+
const status = res.statusCode ?? 0;
|
|
319
|
+
if (status < 200 || status >= 300) {
|
|
320
|
+
timer.end(`\u274C HTTP ${status}`);
|
|
321
|
+
reject(
|
|
322
|
+
new Error(
|
|
323
|
+
`dsh HTTP ${status} on ${options.method ?? "GET"} ${options.path}: ${(data || "(empty body)").substring(0, 200)}`
|
|
324
|
+
)
|
|
325
|
+
);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
175
328
|
try {
|
|
176
329
|
const result = JSON.parse(data);
|
|
177
|
-
timer.end(`\u2713 Status: ${
|
|
330
|
+
timer.end(`\u2713 Status: ${status}`);
|
|
178
331
|
resolve(result);
|
|
179
332
|
} catch {
|
|
180
333
|
timer.end("\u274C JSON parse error");
|
|
181
|
-
reject(
|
|
334
|
+
reject(
|
|
335
|
+
new Error(`dsh HTTP ${status}: response is not JSON: ${data.substring(0, 200)}`)
|
|
336
|
+
);
|
|
182
337
|
}
|
|
183
338
|
});
|
|
184
339
|
});
|
package/es/constants.d.ts
CHANGED
|
@@ -4,24 +4,18 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { DeepSeekProviderOptions } from "./types";
|
|
6
6
|
/** ==================== dsh API ==================== */
|
|
7
|
-
/** dsh 所有 RPC 路径前缀(POST /api/<
|
|
7
|
+
/** dsh 所有 RPC 路径前缀(POST /api/<endpoint>) */
|
|
8
8
|
export declare const DSH_API_BASE = "/api";
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Remote mux WebSocket 端点(dsh 0.1.2+)。
|
|
11
|
+
* 旧版 /api/events.mux、/api/events.host 与 workspace.list RPC 已移除:
|
|
12
|
+
* 会话/工作区能力改经此 mux 以流方式订阅(workspace/follow baseline、session/follow、
|
|
13
|
+
* 转发事件 $events 等),unary RPC 则保持 POST /api/<endpoint>。
|
|
14
|
+
*/
|
|
15
|
+
export declare const DSH_REMOTE_MUX_PATH = "/api/remote.mux";
|
|
13
16
|
/** dsh 唯一允许的绑定主机字面量(服务 schema 只接受 127.0.0.1 / 0.0.0.0) */
|
|
14
17
|
export declare const DSH_LOOPBACK_HOST = "127.0.0.1";
|
|
15
18
|
/** dsh web 默认端口(未显式指定时) */
|
|
16
19
|
export declare const DSH_DEFAULT_PORT = 3080;
|
|
17
|
-
/** ==================== dsh localStorage 键 ==================== */
|
|
18
|
-
export declare const DSH_STORAGE_KEYS: {
|
|
19
|
-
/** 当前选中会话(SPA 启动时据此恢复选中,无 URL 深链) */
|
|
20
|
-
readonly CURRENT_SESSION: "dsh.sessions.current";
|
|
21
|
-
/** 选中的页面元素(bridge 写入,dsh-client 的 @aipanel source 读取) */
|
|
22
|
-
readonly SELECTION: "dsh.bridge.selection";
|
|
23
|
-
/** 诊断功能开关标记(bridge 按 provider 配置写入,dsh-client 据此决定是否注册诊断视图) */
|
|
24
|
-
readonly DIAGNOSTICS_ENABLED: "dsh.bridge.diagnostics.enabled";
|
|
25
|
-
};
|
|
26
20
|
/** ==================== Provider 专属配置默认值 ==================== */
|
|
27
21
|
export declare const DEFAULT_DEEPSEEK_PROVIDER_OPTIONS: DeepSeekProviderOptions;
|
package/es/constants.js
CHANGED
|
@@ -1,18 +1,8 @@
|
|
|
1
1
|
const DSH_API_BASE = "/api";
|
|
2
|
-
const
|
|
3
|
-
const DSH_HOST_EVENTS_PATH = "/api/events.host";
|
|
2
|
+
const DSH_REMOTE_MUX_PATH = "/api/remote.mux";
|
|
4
3
|
const DSH_LOOPBACK_HOST = "127.0.0.1";
|
|
5
4
|
const DSH_DEFAULT_PORT = 3080;
|
|
6
|
-
const DSH_STORAGE_KEYS = {
|
|
7
|
-
/** 当前选中会话(SPA 启动时据此恢复选中,无 URL 深链) */
|
|
8
|
-
CURRENT_SESSION: "dsh.sessions.current",
|
|
9
|
-
/** 选中的页面元素(bridge 写入,dsh-client 的 @aipanel source 读取) */
|
|
10
|
-
SELECTION: "dsh.bridge.selection",
|
|
11
|
-
/** 诊断功能开关标记(bridge 按 provider 配置写入,dsh-client 据此决定是否注册诊断视图) */
|
|
12
|
-
DIAGNOSTICS_ENABLED: "dsh.bridge.diagnostics.enabled"
|
|
13
|
-
};
|
|
14
5
|
const DEFAULT_DEEPSEEK_PROVIDER_OPTIONS = {
|
|
15
|
-
agentPreset: "code",
|
|
16
6
|
// 对齐 opencode 的 enableLsp(默认 true):诊断功能默认开启
|
|
17
7
|
enableDiagnostics: true,
|
|
18
8
|
// 对齐 opencode:自动诊断默认开启
|
|
@@ -22,8 +12,6 @@ export {
|
|
|
22
12
|
DEFAULT_DEEPSEEK_PROVIDER_OPTIONS,
|
|
23
13
|
DSH_API_BASE,
|
|
24
14
|
DSH_DEFAULT_PORT,
|
|
25
|
-
DSH_HOST_EVENTS_PATH,
|
|
26
15
|
DSH_LOOPBACK_HOST,
|
|
27
|
-
|
|
28
|
-
DSH_STORAGE_KEYS
|
|
16
|
+
DSH_REMOTE_MUX_PATH
|
|
29
17
|
};
|
package/es/deepseek-web.d.ts
CHANGED
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
import { type ResultPromise } from "execa";
|
|
2
|
+
/**
|
|
3
|
+
* dsh 启动时打印的 launch token 捕获器。
|
|
4
|
+
* dsh web 升级后在索引页与 /api 走 browser-session 认证:启动成功会在 stdout 打印
|
|
5
|
+
* `dsh web: http://127.0.0.1:3080/?token=<launchToken>`,首次访问需携带该 token 换取签名 cookie。
|
|
6
|
+
* 本类在子进程 stdout 就该 URL 打洞解析 token,供 DeepSeekAPI 与代理做认证。
|
|
7
|
+
*/
|
|
8
|
+
export declare class LaunchToken {
|
|
9
|
+
private token?;
|
|
10
|
+
/** 首次超时后缓存失败:本启动已不可能再打印 token,后续 wait 直接快速失败 */
|
|
11
|
+
private failure?;
|
|
12
|
+
private waiters;
|
|
13
|
+
/** 从子进程输出写入已解析的 token(幂等:只接受第一个) */
|
|
14
|
+
set(token: string): void;
|
|
15
|
+
/** 已解析的 token(未就绪时 undefined) */
|
|
16
|
+
get(): string | undefined;
|
|
17
|
+
/** 等待 token 就绪(默认 20s 超时;未打印则抛错并快速失败,调用方降级处理) */
|
|
18
|
+
wait(timeoutMs?: number): Promise<string>;
|
|
19
|
+
}
|
|
2
20
|
export interface DeepSeekWebOptions {
|
|
3
21
|
/** 服务端口 */
|
|
4
22
|
port: number;
|
|
@@ -12,6 +30,8 @@ export interface DeepSeekWebOptions {
|
|
|
12
30
|
home?: string;
|
|
13
31
|
/** 启用 verbose 模式 */
|
|
14
32
|
verbose?: boolean;
|
|
33
|
+
/** launch token 捕获器:进程 stdout 解析出 ?token= 后写入,供认证使用 */
|
|
34
|
+
launchToken?: LaunchToken;
|
|
15
35
|
}
|
|
16
36
|
/**
|
|
17
37
|
* 启动 dsh web 服务。
|
package/es/deepseek-web.js
CHANGED
|
@@ -1,8 +1,54 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
1
4
|
import { execa } from "execa";
|
|
2
5
|
import { createLogger, getProcessLogBuffer } from "@aipanel/core/node";
|
|
3
6
|
const log = createLogger("DeepSeekWeb");
|
|
7
|
+
class LaunchToken {
|
|
8
|
+
constructor() {
|
|
9
|
+
__publicField(this, "token");
|
|
10
|
+
/** 首次超时后缓存失败:本启动已不可能再打印 token,后续 wait 直接快速失败 */
|
|
11
|
+
__publicField(this, "failure");
|
|
12
|
+
__publicField(this, "waiters", []);
|
|
13
|
+
}
|
|
14
|
+
/** 从子进程输出写入已解析的 token(幂等:只接受第一个) */
|
|
15
|
+
set(token) {
|
|
16
|
+
if (this.token !== void 0) return;
|
|
17
|
+
this.token = token;
|
|
18
|
+
this.failure = void 0;
|
|
19
|
+
for (const w of this.waiters) {
|
|
20
|
+
clearTimeout(w.timer);
|
|
21
|
+
w.resolve(token);
|
|
22
|
+
}
|
|
23
|
+
this.waiters = [];
|
|
24
|
+
}
|
|
25
|
+
/** 已解析的 token(未就绪时 undefined) */
|
|
26
|
+
get() {
|
|
27
|
+
return this.token;
|
|
28
|
+
}
|
|
29
|
+
/** 等待 token 就绪(默认 20s 超时;未打印则抛错并快速失败,调用方降级处理) */
|
|
30
|
+
wait(timeoutMs = 2e4) {
|
|
31
|
+
if (this.token !== void 0) return Promise.resolve(this.token);
|
|
32
|
+
if (this.failure) return Promise.reject(this.failure);
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
const err = new Error(
|
|
36
|
+
`dsh launch token was not captured from stdout within ${timeoutMs}ms (dsh >= 0.1.2 should print the "dsh web: http://127.0.0.1:<port>/?token=..." URL)`
|
|
37
|
+
);
|
|
38
|
+
this.failure = err;
|
|
39
|
+
for (const w of this.waiters) {
|
|
40
|
+
clearTimeout(w.timer);
|
|
41
|
+
w.reject(err);
|
|
42
|
+
}
|
|
43
|
+
this.waiters = [];
|
|
44
|
+
reject(err);
|
|
45
|
+
}, timeoutMs);
|
|
46
|
+
this.waiters.push({ resolve, reject, timer });
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
4
50
|
function startDeepSeekWeb(options) {
|
|
5
|
-
const { port, hostname, cwd, patchPath, home, verbose } = options;
|
|
51
|
+
const { port, hostname, cwd, patchPath, home, verbose, launchToken } = options;
|
|
6
52
|
const args = ["--profile", "web"];
|
|
7
53
|
if (patchPath) {
|
|
8
54
|
args.push("--patch", patchPath);
|
|
@@ -41,8 +87,19 @@ function startDeepSeekWeb(options) {
|
|
|
41
87
|
}).catch((e) => {
|
|
42
88
|
log.error("[dsh spawn failed]", { error: e instanceof Error ? e.message : String(e) });
|
|
43
89
|
});
|
|
90
|
+
let stdoutBuffer = "";
|
|
44
91
|
proc.stdout?.on("data", (data) => {
|
|
45
|
-
const
|
|
92
|
+
const chunk = data.toString();
|
|
93
|
+
stdoutBuffer += chunk;
|
|
94
|
+
if (stdoutBuffer.length > 4096) stdoutBuffer = stdoutBuffer.slice(-4096);
|
|
95
|
+
if (launchToken && !launchToken.get()) {
|
|
96
|
+
const match = stdoutBuffer.match(/[?&]token=([A-Za-z0-9_-]+)/);
|
|
97
|
+
if (match) {
|
|
98
|
+
launchToken.set(match[1]);
|
|
99
|
+
log.debug("[dsh] captured launch token");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const output = chunk.trim();
|
|
46
103
|
if (output) {
|
|
47
104
|
log.debug("[dsh stdout]", { output });
|
|
48
105
|
getProcessLogBuffer().addProviderStdout(output);
|
|
@@ -58,5 +115,6 @@ function startDeepSeekWeb(options) {
|
|
|
58
115
|
return proc;
|
|
59
116
|
}
|
|
60
117
|
export {
|
|
118
|
+
LaunchToken,
|
|
61
119
|
startDeepSeekWeb
|
|
62
120
|
};
|
package/es/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DeepSeek Harness Web Provider
|
|
3
|
-
* 实现 WebProvider 契约:进程管理、RPC 会话 API
|
|
3
|
+
* 实现 WebProvider 契约:进程管理、RPC 会话 API、dsh 侧插件编排、CLI 环境检查。
|
|
4
4
|
* 所有 dsh 专属类型与常量自包含于此包。
|
|
5
5
|
*/
|
|
6
6
|
import type { ProviderInitContext, WebProvider } from "@aipanel/core";
|
|
@@ -10,7 +10,6 @@ export { DeepSeekAPI } from "./api";
|
|
|
10
10
|
export type { DeepSeekWebProviderConfig, DeepSeekWebProviderDeps } from "./provider";
|
|
11
11
|
export { startDeepSeekWeb, type DeepSeekWebOptions } from "./deepseek-web";
|
|
12
12
|
export { buildDshOverlay, writeDshOverlay } from "./profile";
|
|
13
|
-
export { generateBridgeScript, type BridgeScriptOptions } from "./bridge-script";
|
|
14
13
|
export { checkDeepSeekInstalled, getDeepSeekVersion, killOrphanDeepSeekProcesses } from "./system";
|
|
15
14
|
export { DEFAULT_DEEPSEEK_PROVIDER_OPTIONS, DSH_LOOPBACK_HOST, DSH_DEFAULT_PORT } from "./constants";
|
|
16
15
|
export type { DeepSeekProviderOptions, DeepSeekPermissionPreset, DeepSeekBusyEnter, SessionSummary, WorkspaceView, SessionStreamEvent, ServerRequest, ServerResponse, ClientRequest, } from "./types";
|
package/es/index.js
CHANGED
|
@@ -10,7 +10,6 @@ function createProvider(ctx) {
|
|
|
10
10
|
import { DeepSeekAPI } from "./api.js";
|
|
11
11
|
import { startDeepSeekWeb } from "./deepseek-web.js";
|
|
12
12
|
import { buildDshOverlay, writeDshOverlay } from "./profile.js";
|
|
13
|
-
import { generateBridgeScript } from "./bridge-script.js";
|
|
14
13
|
import { checkDeepSeekInstalled, getDeepSeekVersion, killOrphanDeepSeekProcesses } from "./system.js";
|
|
15
14
|
import { DEFAULT_DEEPSEEK_PROVIDER_OPTIONS, DSH_LOOPBACK_HOST as DSH_LOOPBACK_HOST2, DSH_DEFAULT_PORT } from "./constants.js";
|
|
16
15
|
export {
|
|
@@ -21,7 +20,6 @@ export {
|
|
|
21
20
|
buildDshOverlay,
|
|
22
21
|
checkDeepSeekInstalled,
|
|
23
22
|
createProvider,
|
|
24
|
-
generateBridgeScript,
|
|
25
23
|
getDeepSeekVersion,
|
|
26
24
|
killOrphanDeepSeekProcesses,
|
|
27
25
|
startDeepSeekWeb,
|