@aipanel/provider-deepseek 1.2.0-beta.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/es/api.d.ts +36 -0
- package/es/api.js +196 -0
- package/es/bridge-script.d.ts +11 -0
- package/es/bridge-script.js +142 -0
- package/es/constants.d.ts +25 -0
- package/es/constants.js +23 -0
- package/es/deepseek-web.d.ts +21 -0
- package/es/deepseek-web.js +62 -0
- package/es/dsh-install.d.ts +16 -0
- package/es/dsh-install.js +58 -0
- package/es/index.d.ts +16 -0
- package/es/index.js +29 -0
- package/es/profile.d.ts +11 -0
- package/es/profile.js +57 -0
- package/es/provider.d.ts +49 -0
- package/es/provider.js +290 -0
- package/es/system.d.ts +3 -0
- package/es/system.js +170 -0
- package/es/types.d.ts +127 -0
- package/es/types.js +19 -0
- package/lib/api.cjs +228 -0
- package/lib/api.d.ts +36 -0
- package/lib/bridge-script.cjs +165 -0
- package/lib/bridge-script.d.ts +11 -0
- package/lib/constants.cjs +52 -0
- package/lib/constants.d.ts +25 -0
- package/lib/deepseek-web.cjs +85 -0
- package/lib/deepseek-web.d.ts +21 -0
- package/lib/dsh-install.cjs +95 -0
- package/lib/dsh-install.d.ts +16 -0
- package/lib/index.cjs +63 -0
- package/lib/index.d.ts +16 -0
- package/lib/profile.cjs +91 -0
- package/lib/profile.d.ts +11 -0
- package/lib/provider.cjs +308 -0
- package/lib/provider.d.ts +49 -0
- package/lib/system.cjs +195 -0
- package/lib/system.d.ts +3 -0
- package/lib/types.cjs +42 -0
- package/lib/types.d.ts +127 -0
- package/package.json +34 -0
package/es/profile.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pathToFileURL } from "url";
|
|
4
|
+
import { MCP_API_PATH, createLogger } from "@aipanel/core/node";
|
|
5
|
+
const log = createLogger("DeepSeekProfile");
|
|
6
|
+
function buildDshOverlay(options) {
|
|
7
|
+
const { vitePort, cwd, pluginDistPath, clientAvailable = true } = options;
|
|
8
|
+
const mcpUrl = `http://127.0.0.1:${vitePort}${MCP_API_PATH}`;
|
|
9
|
+
const rows = [];
|
|
10
|
+
rows.push(
|
|
11
|
+
[
|
|
12
|
+
" - id: aipanel-mcp",
|
|
13
|
+
" name: '@deepseek-ai/dsh-mcp-client'",
|
|
14
|
+
" config:",
|
|
15
|
+
" serverName: aipanel",
|
|
16
|
+
" transport: streamable-http",
|
|
17
|
+
` url: ${mcpUrl}`,
|
|
18
|
+
" headers: {}"
|
|
19
|
+
].join("\n")
|
|
20
|
+
);
|
|
21
|
+
if (pluginDistPath && fs.existsSync(pluginDistPath)) {
|
|
22
|
+
const pluginSpec = pathToFileURL(pluginDistPath).href;
|
|
23
|
+
log.debug("injecting aipanel host plugin row", { pluginDistPath, pluginSpec });
|
|
24
|
+
rows.push(
|
|
25
|
+
[
|
|
26
|
+
" - id: aipanel",
|
|
27
|
+
` name: '${pluginSpec}'`,
|
|
28
|
+
" inject: [tools, subprocess]",
|
|
29
|
+
" config:",
|
|
30
|
+
` cwd: ${JSON.stringify(cwd)}`,
|
|
31
|
+
" autoDiagnose: true"
|
|
32
|
+
].join("\n")
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
log.debug("aipanel dsh-plugin dist not found, skipping host plugin row", { pluginDistPath });
|
|
36
|
+
}
|
|
37
|
+
rows.push(
|
|
38
|
+
[
|
|
39
|
+
" - id: aipanel-client",
|
|
40
|
+
" name: '@aipanel/dsh-client'",
|
|
41
|
+
...clientAvailable ? [] : [" disabled: true"]
|
|
42
|
+
].join("\n")
|
|
43
|
+
);
|
|
44
|
+
return ["- insert:", rows.join("\n"), ""].join("\n");
|
|
45
|
+
}
|
|
46
|
+
function writeDshOverlay(workspaceCwd, overlay) {
|
|
47
|
+
const dir = path.join(workspaceCwd, "node_modules", ".cache", "aipanel");
|
|
48
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
49
|
+
const file = path.join(dir, "dsh-overlay.cordis.yml");
|
|
50
|
+
fs.writeFileSync(file, overlay, "utf-8");
|
|
51
|
+
log.debug("Wrote dsh overlay", { file });
|
|
52
|
+
return file;
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
buildDshOverlay,
|
|
56
|
+
writeDshOverlay
|
|
57
|
+
};
|
package/es/provider.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ChatSession, ProviderConfig, ProviderEnvironmentInfo, ProviderEvent, ProviderStartOptions, ProviderStartResult, WebProvider } from "@aipanel/core";
|
|
2
|
+
/** DeepSeekWebProvider 构造配置(核心层传入的运行时参数) */
|
|
3
|
+
export interface DeepSeekWebProviderConfig {
|
|
4
|
+
/** 服务主机名 */
|
|
5
|
+
hostname: string;
|
|
6
|
+
}
|
|
7
|
+
/** DeepSeekWebProvider 构造依赖(端口等运行时状态由编排层提供) */
|
|
8
|
+
export interface DeepSeekWebProviderDeps {
|
|
9
|
+
/** 实际 Web 端口读取器 */
|
|
10
|
+
getWebPort: () => number;
|
|
11
|
+
/** 实际代理端口读取器 */
|
|
12
|
+
getProxyPort: () => number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* DeepSeek Harness Web Provider
|
|
16
|
+
* 组合 CLI 进程管理、RPC 会话 API、桥接脚本,向核心层暴露 WebProvider 契约。
|
|
17
|
+
*/
|
|
18
|
+
export declare class DeepSeekWebProvider implements WebProvider {
|
|
19
|
+
readonly id = "deepseek";
|
|
20
|
+
readonly displayName = "DeepSeek Harness";
|
|
21
|
+
/** SPA 型无 URL 深链:iframe 保持应用壳,切会话通过 FOCUS_SESSION 消息完成 */
|
|
22
|
+
readonly capabilities: {
|
|
23
|
+
readonly deepLink: false;
|
|
24
|
+
};
|
|
25
|
+
private readonly api;
|
|
26
|
+
private deps;
|
|
27
|
+
private process;
|
|
28
|
+
private bridgeOptions;
|
|
29
|
+
private readonly opts;
|
|
30
|
+
constructor(config: DeepSeekWebProviderConfig, deps: DeepSeekWebProviderDeps, options?: Record<string, unknown>);
|
|
31
|
+
/** 代理注入到 HTML 的桥接脚本(Provider 资产) */
|
|
32
|
+
get bridgeScript(): string | undefined;
|
|
33
|
+
/** 初始化桥接配置(主题等) */
|
|
34
|
+
applyConfig(config: ProviderConfig): void;
|
|
35
|
+
checkEnvironment(): Promise<ProviderEnvironmentInfo>;
|
|
36
|
+
start(options: ProviderStartOptions): Promise<ProviderStartResult>;
|
|
37
|
+
/** 把 providerOptions 配置映射为 dsh settings 命名空间 patch(仅含用户显式配置项) */
|
|
38
|
+
private buildSettingsToApply;
|
|
39
|
+
/** 解析 aipanel dsh-plugin 的构建产物路径(存在才在 overlay 里注入插件行)。
|
|
40
|
+
* 注意用 fileURLToPath:URL.pathname 是 percent-encoded,中文路径下 existsSync 会误判不存在。 */
|
|
41
|
+
private resolvePluginDist;
|
|
42
|
+
stop(): Promise<void>;
|
|
43
|
+
killOrphans(): Promise<number>;
|
|
44
|
+
listSessions(projectDir: string): Promise<ChatSession[]>;
|
|
45
|
+
createSession(projectDir: string, title?: string): Promise<ChatSession>;
|
|
46
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
47
|
+
buildSessionUrl(projectDir: string, sessionId: string): string;
|
|
48
|
+
subscribeEvents(handler: (e: ProviderEvent) => void): () => void;
|
|
49
|
+
}
|
package/es/provider.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
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);
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { createLogger } from "@aipanel/core/node";
|
|
6
|
+
import { DEFAULT_DEEPSEEK_PROVIDER_OPTIONS } from "./constants.js";
|
|
7
|
+
import { DSH_LOOPBACK_HOST, DSH_MUX_EVENTS_PATH, DSH_HOST_EVENTS_PATH } from "./constants.js";
|
|
8
|
+
import { DeepSeekAPI } from "./api.js";
|
|
9
|
+
import { generateBridgeScript } from "./bridge-script.js";
|
|
10
|
+
import { startDeepSeekWeb } from "./deepseek-web.js";
|
|
11
|
+
import { buildDshOverlay, writeDshOverlay } from "./profile.js";
|
|
12
|
+
import { checkDeepSeekInstalled, getDeepSeekVersion, killOrphanDeepSeekProcesses } from "./system.js";
|
|
13
|
+
import {
|
|
14
|
+
dshProfileDir,
|
|
15
|
+
ensureDshClient,
|
|
16
|
+
resolveDevDshClientSource,
|
|
17
|
+
DSH_CLIENT_PACKAGE
|
|
18
|
+
} from "./dsh-install.js";
|
|
19
|
+
const log = createLogger("DeepSeekWebProvider");
|
|
20
|
+
class DeepSeekWebProvider {
|
|
21
|
+
constructor(config, deps, options) {
|
|
22
|
+
__publicField(this, "id", "deepseek");
|
|
23
|
+
__publicField(this, "displayName", "DeepSeek Harness");
|
|
24
|
+
/** SPA 型无 URL 深链:iframe 保持应用壳,切会话通过 FOCUS_SESSION 消息完成 */
|
|
25
|
+
__publicField(this, "capabilities", { deepLink: false });
|
|
26
|
+
__publicField(this, "api");
|
|
27
|
+
__publicField(this, "deps");
|
|
28
|
+
__publicField(this, "process", null);
|
|
29
|
+
__publicField(this, "bridgeOptions", {});
|
|
30
|
+
__publicField(this, "opts");
|
|
31
|
+
this.deps = deps;
|
|
32
|
+
this.opts = resolveDeepSeekOptions(options);
|
|
33
|
+
this.api = new DeepSeekAPI(DSH_LOOPBACK_HOST, deps.getWebPort);
|
|
34
|
+
}
|
|
35
|
+
/** 代理注入到 HTML 的桥接脚本(Provider 资产) */
|
|
36
|
+
get bridgeScript() {
|
|
37
|
+
return generateBridgeScript(this.bridgeOptions);
|
|
38
|
+
}
|
|
39
|
+
/** 初始化桥接配置(主题等) */
|
|
40
|
+
applyConfig(config) {
|
|
41
|
+
this.bridgeOptions = { theme: config.theme ?? "auto" };
|
|
42
|
+
}
|
|
43
|
+
async checkEnvironment() {
|
|
44
|
+
if (!await checkDeepSeekInstalled()) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
message: `DeepSeek Harness (dsh) is not installed!
|
|
48
|
+
|
|
49
|
+
Please install dsh first:
|
|
50
|
+
|
|
51
|
+
npm install -g @deepseek-ai/dsh
|
|
52
|
+
|
|
53
|
+
or run without installing:
|
|
54
|
+
|
|
55
|
+
npx @deepseek-ai/dsh web
|
|
56
|
+
`
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const version = await getDeepSeekVersion();
|
|
60
|
+
return { ok: true, version: version ?? void 0 };
|
|
61
|
+
}
|
|
62
|
+
async start(options) {
|
|
63
|
+
log.debug("Starting dsh web process", {
|
|
64
|
+
port: options.port,
|
|
65
|
+
hostname: DSH_LOOPBACK_HOST,
|
|
66
|
+
cwd: options.cwd,
|
|
67
|
+
vitePort: options.vitePort
|
|
68
|
+
});
|
|
69
|
+
const devClientDir = resolveDevDshClientSource(import.meta.url);
|
|
70
|
+
const clientAvailable = await ensureDshClient(
|
|
71
|
+
dshProfileDir(this.opts.home),
|
|
72
|
+
devClientDir ?? DSH_CLIENT_PACKAGE,
|
|
73
|
+
this.opts.home
|
|
74
|
+
);
|
|
75
|
+
if (!clientAvailable) {
|
|
76
|
+
log.warn("@aipanel/dsh-client unavailable; @ menu chip highlight disabled", {
|
|
77
|
+
metaUrl: import.meta.url
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const overlay = buildDshOverlay({
|
|
81
|
+
vitePort: options.vitePort,
|
|
82
|
+
cwd: options.cwd,
|
|
83
|
+
pluginDistPath: this.resolvePluginDist(),
|
|
84
|
+
clientAvailable
|
|
85
|
+
});
|
|
86
|
+
const patchPath = writeDshOverlay(options.cwd, overlay);
|
|
87
|
+
const proc = startDeepSeekWeb({
|
|
88
|
+
port: options.port,
|
|
89
|
+
hostname: DSH_LOOPBACK_HOST,
|
|
90
|
+
cwd: options.cwd,
|
|
91
|
+
patchPath,
|
|
92
|
+
home: this.opts.home,
|
|
93
|
+
verbose: options.verbose
|
|
94
|
+
});
|
|
95
|
+
this.process = proc;
|
|
96
|
+
const settings = this.buildSettingsToApply();
|
|
97
|
+
if (Object.keys(settings).length > 0) {
|
|
98
|
+
void this.api.applySettings(settings).catch((e) => {
|
|
99
|
+
log.warn("failed to apply provider settings to dsh", {
|
|
100
|
+
settings,
|
|
101
|
+
error: e instanceof Error ? e.message : String(e)
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return { url: this.api.shellUrl, processHandle: proc };
|
|
106
|
+
}
|
|
107
|
+
/** 把 providerOptions 配置映射为 dsh settings 命名空间 patch(仅含用户显式配置项) */
|
|
108
|
+
buildSettingsToApply() {
|
|
109
|
+
const sections = {};
|
|
110
|
+
if (this.opts.agentPreset !== void 0) {
|
|
111
|
+
sections["agent-presets"] = { ...sections["agent-presets"], default: this.opts.agentPreset };
|
|
112
|
+
}
|
|
113
|
+
if (this.opts.permissionPreset !== void 0) {
|
|
114
|
+
sections.permission = {
|
|
115
|
+
...sections.permission,
|
|
116
|
+
defaultPreset: this.opts.permissionPreset
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (this.opts.busyEnter !== void 0) {
|
|
120
|
+
sections["ui-conversation"] = {
|
|
121
|
+
...sections["ui-conversation"],
|
|
122
|
+
busyEnter: this.opts.busyEnter
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return sections;
|
|
126
|
+
}
|
|
127
|
+
/** 解析 aipanel dsh-plugin 的构建产物路径(存在才在 overlay 里注入插件行)。
|
|
128
|
+
* 注意用 fileURLToPath:URL.pathname 是 percent-encoded,中文路径下 existsSync 会误判不存在。 */
|
|
129
|
+
resolvePluginDist() {
|
|
130
|
+
const fromHere = new URL("../dsh-plugin/dist/index.js", import.meta.url);
|
|
131
|
+
return fileURLToPath(fromHere);
|
|
132
|
+
}
|
|
133
|
+
async stop() {
|
|
134
|
+
if (this.process) {
|
|
135
|
+
log.debug("Killing dsh web process", { pid: this.process.pid });
|
|
136
|
+
this.process.kill("SIGTERM");
|
|
137
|
+
this.process = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async killOrphans() {
|
|
141
|
+
return killOrphanDeepSeekProcesses();
|
|
142
|
+
}
|
|
143
|
+
async listSessions(projectDir) {
|
|
144
|
+
const sessions = await this.api.listSessions(projectDir);
|
|
145
|
+
const url = this.buildSessionUrl(projectDir, "");
|
|
146
|
+
return sessions.map((s) => toChatSession(s, url));
|
|
147
|
+
}
|
|
148
|
+
async createSession(projectDir, title) {
|
|
149
|
+
const url = this.buildSessionUrl(projectDir, "");
|
|
150
|
+
const created = await this.api.createSession(projectDir);
|
|
151
|
+
return {
|
|
152
|
+
id: created.sessionId,
|
|
153
|
+
title: title ?? "",
|
|
154
|
+
updatedAt: Date.now(),
|
|
155
|
+
url
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
// 删除走归档:dsh 无硬删除语义(workspace.archiveSession),
|
|
159
|
+
// 归档后 listSessions 按 archivedSessionIds 过滤即从列表消失。
|
|
160
|
+
async deleteSession(sessionId) {
|
|
161
|
+
await this.api.archiveSession(sessionId);
|
|
162
|
+
}
|
|
163
|
+
buildSessionUrl(projectDir, sessionId) {
|
|
164
|
+
void projectDir;
|
|
165
|
+
void sessionId;
|
|
166
|
+
return `http://${DSH_LOOPBACK_HOST}:${this.deps.getProxyPort()}/`;
|
|
167
|
+
}
|
|
168
|
+
subscribeEvents(handler) {
|
|
169
|
+
const port = this.deps.getWebPort();
|
|
170
|
+
const base = `ws://${DSH_LOOPBACK_HOST}:${port}`;
|
|
171
|
+
const endpoints = [DSH_MUX_EVENTS_PATH, DSH_HOST_EVENTS_PATH];
|
|
172
|
+
log.debug("Subscribing to dsh event streams (WebSocket)", {
|
|
173
|
+
endpoints: endpoints.map((p) => base + p)
|
|
174
|
+
});
|
|
175
|
+
let aborted = false;
|
|
176
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
177
|
+
const retryTimers = /* @__PURE__ */ new Set();
|
|
178
|
+
const cleanupAll = () => {
|
|
179
|
+
for (const timer of retryTimers) clearTimeout(timer);
|
|
180
|
+
retryTimers.clear();
|
|
181
|
+
for (const ws of sockets) {
|
|
182
|
+
try {
|
|
183
|
+
ws.close();
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
sockets.clear();
|
|
188
|
+
};
|
|
189
|
+
const connect = (path, attempt = 0) => {
|
|
190
|
+
if (aborted) return;
|
|
191
|
+
let socket;
|
|
192
|
+
try {
|
|
193
|
+
socket = new WebSocket(base + path);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
log.warn("dsh event WebSocket create failed", { path, error: String(e) });
|
|
196
|
+
scheduleReconnect(path, attempt + 1);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
sockets.add(socket);
|
|
200
|
+
socket.onmessage = (ev) => {
|
|
201
|
+
try {
|
|
202
|
+
const frame = JSON.parse(String(ev.data));
|
|
203
|
+
const event = mapEvent(frame);
|
|
204
|
+
if (event) handler(event);
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
socket.onclose = () => {
|
|
209
|
+
sockets.delete(socket);
|
|
210
|
+
scheduleReconnect(path, attempt + 1);
|
|
211
|
+
};
|
|
212
|
+
socket.onerror = () => {
|
|
213
|
+
try {
|
|
214
|
+
socket.close();
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
const scheduleReconnect = (path, attempt) => {
|
|
220
|
+
if (aborted) return;
|
|
221
|
+
const delays = [250, 500, 1e3, 2e3, 5e3];
|
|
222
|
+
const after = delays[Math.min(attempt, delays.length - 1)];
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
retryTimers.delete(timer);
|
|
225
|
+
connect(path);
|
|
226
|
+
}, after);
|
|
227
|
+
retryTimers.add(timer);
|
|
228
|
+
};
|
|
229
|
+
endpoints.forEach((p) => connect(p));
|
|
230
|
+
return () => {
|
|
231
|
+
aborted = true;
|
|
232
|
+
cleanupAll();
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function mapEvent(frame) {
|
|
237
|
+
if (!frame || typeof frame.method !== "string" || !frame.payload) return null;
|
|
238
|
+
const payload = frame.payload;
|
|
239
|
+
const sessionId = payload.sessionId ?? "";
|
|
240
|
+
switch (frame.method) {
|
|
241
|
+
case "host/session-status": {
|
|
242
|
+
if (!sessionId) return null;
|
|
243
|
+
const running = payload.running === true;
|
|
244
|
+
return { type: "session.status", sessionId, status: running ? "running" : "idle" };
|
|
245
|
+
}
|
|
246
|
+
case "session/event": {
|
|
247
|
+
const event = payload.event;
|
|
248
|
+
const type = event?.type ?? "";
|
|
249
|
+
if (!sessionId || !type) return null;
|
|
250
|
+
switch (type) {
|
|
251
|
+
case "assistant/message":
|
|
252
|
+
case "turn/end":
|
|
253
|
+
return { type: "thinking", sessionId, thinking: false };
|
|
254
|
+
case "assistant/chunk":
|
|
255
|
+
case "thinking/delta":
|
|
256
|
+
case "turn/start":
|
|
257
|
+
case "step/start":
|
|
258
|
+
return { type: "thinking", sessionId, thinking: true };
|
|
259
|
+
default:
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
default:
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function toChatSession(s, url) {
|
|
268
|
+
return {
|
|
269
|
+
id: s.sessionId,
|
|
270
|
+
// dsh 标题在 projections.values.title,不在顶层
|
|
271
|
+
title: s.projections?.values?.title ?? "",
|
|
272
|
+
updatedAt: s.updatedAt,
|
|
273
|
+
parentId: s.parentSessionId,
|
|
274
|
+
url
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function resolveDeepSeekOptions(options) {
|
|
278
|
+
if (!options) return { ...DEFAULT_DEEPSEEK_PROVIDER_OPTIONS };
|
|
279
|
+
const po = options.providerOptions ?? {};
|
|
280
|
+
return {
|
|
281
|
+
...DEFAULT_DEEPSEEK_PROVIDER_OPTIONS,
|
|
282
|
+
home: po.home ?? options.home,
|
|
283
|
+
agentPreset: po.agentPreset ?? options.agentPreset,
|
|
284
|
+
permissionPreset: po.permissionPreset ?? options.permissionPreset,
|
|
285
|
+
busyEnter: po.busyEnter ?? options.busyEnter
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export {
|
|
289
|
+
DeepSeekWebProvider
|
|
290
|
+
};
|
package/es/system.d.ts
ADDED
package/es/system.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { createLogger } from "@aipanel/core/node";
|
|
3
|
+
const log = createLogger("DeepSeekSystem");
|
|
4
|
+
async function checkDeepSeekInstalled() {
|
|
5
|
+
const timer = log.timer("checkDeepSeekInstalled");
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
log.debug("Checking if dsh is installed...");
|
|
8
|
+
const proc = spawn("dsh", ["--version"], { stdio: "ignore", shell: true });
|
|
9
|
+
proc.on("close", (code) => {
|
|
10
|
+
const installed = code === 0;
|
|
11
|
+
timer.end(installed ? "\u2713 dsh is installed" : "\u274C dsh not found");
|
|
12
|
+
resolve(installed);
|
|
13
|
+
});
|
|
14
|
+
proc.on("error", (err) => {
|
|
15
|
+
log.debug("Failed to check dsh installation", { error: err.message });
|
|
16
|
+
timer.end("\u274C Check failed");
|
|
17
|
+
resolve(false);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function getDeepSeekVersion() {
|
|
22
|
+
return new Promise((resolve) => {
|
|
23
|
+
const proc = spawn("dsh", ["--version"], { stdio: "pipe", shell: true });
|
|
24
|
+
let output = "";
|
|
25
|
+
proc.stdout?.on("data", (data) => {
|
|
26
|
+
output += data.toString();
|
|
27
|
+
});
|
|
28
|
+
proc.on("close", (code) => {
|
|
29
|
+
if (code === 0 && output.trim()) {
|
|
30
|
+
resolve(output.trim());
|
|
31
|
+
} else {
|
|
32
|
+
resolve(null);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
proc.on("error", () => {
|
|
36
|
+
resolve(null);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const KILL_ORPHAN_TIMEOUT = 5e3;
|
|
41
|
+
async function killOrphanDeepSeekProcesses() {
|
|
42
|
+
const timer = log.timer("killOrphanDeepSeekProcesses");
|
|
43
|
+
log.debug("Looking for orphan dsh processes (PPID=1)");
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
let settled = false;
|
|
46
|
+
const done = (count) => {
|
|
47
|
+
if (settled) return;
|
|
48
|
+
settled = true;
|
|
49
|
+
resolve(count);
|
|
50
|
+
};
|
|
51
|
+
const timeout = setTimeout(() => {
|
|
52
|
+
log.warn("Kill orphan processes timed out, skipping");
|
|
53
|
+
timer.end("\u26A0 Timeout, skipped");
|
|
54
|
+
done(0);
|
|
55
|
+
}, KILL_ORPHAN_TIMEOUT);
|
|
56
|
+
const wrappedResolve = (count) => {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
done(count);
|
|
59
|
+
};
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
killOrphanProcessesOnWindows(wrappedResolve, timer);
|
|
62
|
+
} else {
|
|
63
|
+
killOrphanProcessesOnUnix(wrappedResolve, timer);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function killOrphanProcessesOnWindows(resolve, timer) {
|
|
68
|
+
log.debug("Using Windows method to find orphan processes");
|
|
69
|
+
const proc = spawn(
|
|
70
|
+
"wmic",
|
|
71
|
+
["process", "where", 'name="node.exe"', "get", "processid,parentprocessid,commandline"],
|
|
72
|
+
{ stdio: "pipe" }
|
|
73
|
+
);
|
|
74
|
+
let output = "";
|
|
75
|
+
proc.stdout?.on("data", (data) => {
|
|
76
|
+
output += data.toString();
|
|
77
|
+
});
|
|
78
|
+
proc.on("close", () => {
|
|
79
|
+
const pidsToKill = [];
|
|
80
|
+
output.split("\n").forEach((rawLine) => {
|
|
81
|
+
const line = rawLine.trim();
|
|
82
|
+
if (!line.includes("dsh")) return;
|
|
83
|
+
const parts = line.trim().split(/\s+/);
|
|
84
|
+
if (parts.length >= 2) {
|
|
85
|
+
const ppid = parts[0];
|
|
86
|
+
const pid = parts[1];
|
|
87
|
+
if (ppid === "1" && pid && !isNaN(Number(pid))) {
|
|
88
|
+
pidsToKill.push(pid);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
if (pidsToKill.length > 0) {
|
|
93
|
+
log.debug(`Found ${pidsToKill.length} orphan processes`, { pids: pidsToKill });
|
|
94
|
+
let killedCount = 0;
|
|
95
|
+
let completedCount = 0;
|
|
96
|
+
pidsToKill.forEach((pid) => {
|
|
97
|
+
const killProc = spawn("taskkill", ["/F", "/PID", pid], { stdio: "ignore" });
|
|
98
|
+
killProc.on("close", (code) => {
|
|
99
|
+
completedCount++;
|
|
100
|
+
if (code === 0) killedCount++;
|
|
101
|
+
if (completedCount === pidsToKill.length) {
|
|
102
|
+
timer.end(`\u2713 Killed ${killedCount} orphan processes`);
|
|
103
|
+
resolve(killedCount);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
log.debug("No orphan processes found");
|
|
109
|
+
timer.end("No orphan processes found");
|
|
110
|
+
resolve(0);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
proc.on("error", (err) => {
|
|
114
|
+
log.debug("Failed to find orphan processes", { error: err.message });
|
|
115
|
+
timer.end("\u274C Failed to find orphan processes");
|
|
116
|
+
resolve(0);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function killOrphanProcessesOnUnix(resolve, timer) {
|
|
120
|
+
log.debug("Using Unix method to find orphan processes");
|
|
121
|
+
const proc = spawn("ps", ["-e", "-o", "pid,ppid,args"], { stdio: "pipe" });
|
|
122
|
+
let output = "";
|
|
123
|
+
proc.stdout?.on("data", (data) => {
|
|
124
|
+
output += data.toString();
|
|
125
|
+
});
|
|
126
|
+
proc.on("close", () => {
|
|
127
|
+
const lines = output.split("\n");
|
|
128
|
+
const pidsToKill = [];
|
|
129
|
+
lines.forEach((line) => {
|
|
130
|
+
if (!line.includes("dsh")) return;
|
|
131
|
+
const parts = line.trim().split(/\s+/);
|
|
132
|
+
if (parts.length >= 3) {
|
|
133
|
+
const pid = parts[0];
|
|
134
|
+
const ppid = parts[1];
|
|
135
|
+
if (ppid === "1") {
|
|
136
|
+
pidsToKill.push(pid);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
if (pidsToKill.length > 0) {
|
|
141
|
+
log.debug(`Found ${pidsToKill.length} orphan processes`, { pids: pidsToKill });
|
|
142
|
+
const killProc = spawn("kill", ["-9", ...pidsToKill], { stdio: "ignore" });
|
|
143
|
+
killProc.on("close", (code) => {
|
|
144
|
+
const killedCount = code === 0 ? pidsToKill.length : 0;
|
|
145
|
+
timer.end(
|
|
146
|
+
killedCount > 0 ? `\u2713 Killed ${killedCount} orphan processes` : "\u274C Failed to kill processes"
|
|
147
|
+
);
|
|
148
|
+
resolve(killedCount);
|
|
149
|
+
});
|
|
150
|
+
killProc.on("error", () => {
|
|
151
|
+
timer.end("\u274C Failed to kill processes");
|
|
152
|
+
resolve(0);
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
log.debug("No orphan processes found");
|
|
156
|
+
timer.end("No orphan processes found");
|
|
157
|
+
resolve(0);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
proc.on("error", (err) => {
|
|
161
|
+
log.debug("Failed to find orphan processes", { error: err.message });
|
|
162
|
+
timer.end("\u274C Failed to find orphan processes");
|
|
163
|
+
resolve(0);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
checkDeepSeekInstalled,
|
|
168
|
+
getDeepSeekVersion,
|
|
169
|
+
killOrphanDeepSeekProcesses
|
|
170
|
+
};
|
package/es/types.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness Provider 专属类型
|
|
3
|
+
* 所有与 DeepSeek Harness (dsh) 绑定的类型自包含于此,核心层不感知。
|
|
4
|
+
*/
|
|
5
|
+
/** 权限预设(对应 dsh settings permission.defaultPreset) */
|
|
6
|
+
export type DeepSeekPermissionPreset = "read-only" | "workspace-write" | "danger-full-access";
|
|
7
|
+
/** 繁忙时 Enter 键行为(对应 dsh settings ui-conversation.busyEnter) */
|
|
8
|
+
export type DeepSeekBusyEnter = "queue" | "steer";
|
|
9
|
+
/**
|
|
10
|
+
* DeepSeek Provider 专属配置(对应插件配置的 providerOptions 段)
|
|
11
|
+
* 保留字符串索引签名,以赋给 PluginOptions 的 Record<string, unknown> 泛型约束。
|
|
12
|
+
*/
|
|
13
|
+
export type DeepSeekProviderOptions = {
|
|
14
|
+
/** dsh 数据目录($DSH_HOME),默认跟随系统(~/.dsh) */
|
|
15
|
+
home?: string;
|
|
16
|
+
/** 默认 Agent 预设(dsh settings agent-presets.default,如 code/standard;新建会话的默认模式) */
|
|
17
|
+
agentPreset?: string;
|
|
18
|
+
/** 默认权限预设(dsh settings permission.defaultPreset) */
|
|
19
|
+
permissionPreset?: DeepSeekPermissionPreset;
|
|
20
|
+
/** 繁忙时 Enter 键行为(dsh settings ui-conversation.busyEnter) */
|
|
21
|
+
busyEnter?: DeepSeekBusyEnter;
|
|
22
|
+
/**
|
|
23
|
+
* Provider 允许自定义扩展字段
|
|
24
|
+
* [key: string]: unknown;
|
|
25
|
+
*/
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* dsh API 四象限 RPC envelope(客户端请求 / 服务端响应)。
|
|
30
|
+
* 字段为 type/rpcId/method/payload + result:{ok,value|error},不是标准 JSON-RPC。
|
|
31
|
+
*/
|
|
32
|
+
/** 客户端 → 服务端 请求体(POST /api/<method>) */
|
|
33
|
+
export interface ClientRequest {
|
|
34
|
+
type: "client-request";
|
|
35
|
+
rpcId: string;
|
|
36
|
+
method: string;
|
|
37
|
+
payload: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
/** 服务端 → 客户端 响应体 */
|
|
40
|
+
export interface ServerResponse {
|
|
41
|
+
type: "server-response";
|
|
42
|
+
rpcId: string;
|
|
43
|
+
result: {
|
|
44
|
+
ok: true;
|
|
45
|
+
value: unknown;
|
|
46
|
+
} | {
|
|
47
|
+
ok: false;
|
|
48
|
+
error: {
|
|
49
|
+
code?: string;
|
|
50
|
+
message?: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** 服务端推送帧(SSE data 载荷) */
|
|
55
|
+
export interface ServerRequest {
|
|
56
|
+
type: "server-request";
|
|
57
|
+
rpcId: string;
|
|
58
|
+
method: string;
|
|
59
|
+
payload: Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
/** 会话摘要(POST /api/session.list 的 items 项) */
|
|
62
|
+
export interface SessionSummary {
|
|
63
|
+
sessionId: string;
|
|
64
|
+
updatedAt: number;
|
|
65
|
+
running: boolean;
|
|
66
|
+
blank: boolean;
|
|
67
|
+
parentSessionId?: string;
|
|
68
|
+
origin?: "subagent";
|
|
69
|
+
cwd?: string;
|
|
70
|
+
agentPreset?: string;
|
|
71
|
+
/** 会话投影(标题等派生值存在 projections.values.title) */
|
|
72
|
+
projections?: {
|
|
73
|
+
values?: {
|
|
74
|
+
title?: string | null;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
};
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** session.list 的 value:容器对象(不是数组),会话在 items 下 */
|
|
81
|
+
export interface SessionListResult {
|
|
82
|
+
items: SessionSummary[];
|
|
83
|
+
}
|
|
84
|
+
/** 工作区视图(POST /api/workspace.list 的 items 项,用于按目录匹配会话) */
|
|
85
|
+
export interface WorkspaceView {
|
|
86
|
+
workspaceId: string;
|
|
87
|
+
path: string;
|
|
88
|
+
title: string;
|
|
89
|
+
sessionIds: string[];
|
|
90
|
+
createdAt: string;
|
|
91
|
+
updatedAt: string;
|
|
92
|
+
}
|
|
93
|
+
/** workspace.list 的 value:容器对象(不是数组),工作区在 items 下,archivedSessionIds 为已归档会话 */
|
|
94
|
+
export interface WorkspaceListResult {
|
|
95
|
+
items: WorkspaceView[];
|
|
96
|
+
archivedSessionIds: string[];
|
|
97
|
+
}
|
|
98
|
+
/** workspace.create 的 value(幂等:已存在则返回既有 workspace,created=false) */
|
|
99
|
+
export interface WorkspaceCreateResult {
|
|
100
|
+
workspace: WorkspaceView;
|
|
101
|
+
created: boolean;
|
|
102
|
+
}
|
|
103
|
+
/** 会话流式事件(session/event 的 event 字段) */
|
|
104
|
+
export interface SessionStreamEvent {
|
|
105
|
+
type: string;
|
|
106
|
+
seq: number;
|
|
107
|
+
time?: number;
|
|
108
|
+
data?: Record<string, unknown>;
|
|
109
|
+
}
|
|
110
|
+
/** dsh 会话日志事件类型(用于 thinking/status 推导的稳定子集) */
|
|
111
|
+
export declare const SESSION_EVENT_TYPES: {
|
|
112
|
+
/** 回合开始 → 视为 streaming */
|
|
113
|
+
readonly TURN_START: "turn/start";
|
|
114
|
+
/** 回合结束(稳定停等) */
|
|
115
|
+
readonly TURN_END: "turn/end";
|
|
116
|
+
/** 步骤开始 */
|
|
117
|
+
readonly STEP_START: "step/start";
|
|
118
|
+
/** 步骤结束 */
|
|
119
|
+
readonly STEP_END: "step/end";
|
|
120
|
+
/** 助手输出完成 */
|
|
121
|
+
readonly ASSISTANT_MESSAGE: "assistant/message";
|
|
122
|
+
/** 助手输出增量分片 */
|
|
123
|
+
readonly ASSISTANT_CHUNK: "assistant/chunk";
|
|
124
|
+
/** 思考增量分片 */
|
|
125
|
+
readonly THINKING_DELTA: "thinking/delta";
|
|
126
|
+
};
|
|
127
|
+
export type SessionEventType = (typeof SESSION_EVENT_TYPES)[keyof typeof SESSION_EVENT_TYPES];
|