@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 ADDED
@@ -0,0 +1,36 @@
1
+ import type { SessionSummary } from "./types";
2
+ /**
3
+ * dsh 会话 API 客户端。
4
+ * dsh 使用自定义四象限 RPC envelope({type:'client-request',rpcId,method,payload} →
5
+ * {type:'server-response',rpcId,result:{ok,value|error}}),本节封装该协议。
6
+ */
7
+ export declare class DeepSeekAPI {
8
+ private hostname;
9
+ private getWebPort;
10
+ constructor(hostname: string, getWebPort: () => number);
11
+ /** 应用壳 URL(无 deepLink 能力,所有会话共用) */
12
+ get shellUrl(): string;
13
+ /** 发起一次 unary RPC,返回 result.value(ok=false 时抛错) */
14
+ private call;
15
+ /**
16
+ * 列出当前项目目录下的会话。
17
+ * dsh 的 session.list 不提供按目录过滤,故结合 workspace.list(path→sessionIds)
18
+ * 与各会话的 cwd 字段,在本端合并去重出属于 projectDir 的会话集。
19
+ */
20
+ listSessions(projectDir: string, retries?: number): Promise<SessionSummary[]>;
21
+ /** 在当前目录下创建会话(dsh 仅返回 { sessionId, agentPreset? },非完整 SessionSummary)。
22
+ * 关键:必须先确保 projectDir 对应的 workspace 存在(workspace.create 幂等 get-or-create),
23
+ * 再用 workspaceId 调 session.create。若只传 cwd,dsh 侧不会把会话挂到任何 workspace,
24
+ * 新会话会落到侧边栏"未分组"。 */
25
+ createSession(projectDir: string, retries?: number): Promise<{
26
+ sessionId: string;
27
+ }>;
28
+ /**
29
+ * 通过 dsh settings.update 应用 providerOptions 指定的用户设置(逐命名空间幂等 patch)。
30
+ * dsh 启动初期 API 未就绪,整体带重试;单个命名空间不存在会导致该次调用失败重试。
31
+ */
32
+ applySettings(sections: Record<string, Record<string, unknown>>, retries?: number): Promise<void>;
33
+ /** 归档会话(dsh 无硬删除,仅归档;幂等) */
34
+ archiveSession(sessionId: string, retries?: number): Promise<void>;
35
+ private createHttpRequest;
36
+ }
package/es/api.js ADDED
@@ -0,0 +1,196 @@
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 http from "http";
5
+ import { randomUUID } from "node:crypto";
6
+ import { DEFAULT_RETRIES, RETRY_DELAY, sleep } from "@aipanel/core";
7
+ import { PerformanceTimer, createLogger } from "@aipanel/core/node";
8
+ import { DSH_API_BASE } from "./constants.js";
9
+ const log = createLogger("DeepSeekAPI");
10
+ class DeepSeekAPI {
11
+ constructor(hostname, getWebPort) {
12
+ __publicField(this, "hostname", hostname);
13
+ __publicField(this, "getWebPort", getWebPort);
14
+ }
15
+ /** 应用壳 URL(无 deepLink 能力,所有会话共用) */
16
+ get shellUrl() {
17
+ return `http://${this.hostname}:${this.getWebPort()}`;
18
+ }
19
+ /** 发起一次 unary RPC,返回 result.value(ok=false 时抛错) */
20
+ async call(method, payload = {}) {
21
+ const message = {
22
+ type: "client-request",
23
+ rpcId: randomUUID(),
24
+ method,
25
+ payload
26
+ };
27
+ const response = await this.createHttpRequest(
28
+ {
29
+ hostname: this.hostname,
30
+ port: this.getWebPort(),
31
+ path: `${DSH_API_BASE}/${method}`,
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" }
34
+ },
35
+ JSON.stringify(message)
36
+ );
37
+ if (response?.result?.ok !== true) {
38
+ const err = response?.result && "error" in response.result ? response.result.error : void 0;
39
+ throw new Error(
40
+ `dsh RPC ${method} failed: ${err?.message ?? JSON.stringify(response?.result ?? response)}`
41
+ );
42
+ }
43
+ return response.result.value;
44
+ }
45
+ /**
46
+ * 列出当前项目目录下的会话。
47
+ * dsh 的 session.list 不提供按目录过滤,故结合 workspace.list(path→sessionIds)
48
+ * 与各会话的 cwd 字段,在本端合并去重出属于 projectDir 的会话集。
49
+ */
50
+ async listSessions(projectDir, retries = DEFAULT_RETRIES) {
51
+ const timer = log.timer("listSessions", { projectDir, retries });
52
+ let lastError = null;
53
+ for (let i = 0; i < retries; i++) {
54
+ try {
55
+ log.debug(`Attempt ${i + 1}/${retries}`, { method: "session.list", projectDir });
56
+ const workspaces = await this.call("workspace.list");
57
+ const matchedWorkspace = workspaces.items.find((w) => w.path === projectDir);
58
+ const ownedByWorkspace = new Set(matchedWorkspace?.sessionIds ?? []);
59
+ const archived = new Set(workspaces.archivedSessionIds);
60
+ const sessions = await this.call("session.list");
61
+ const all = sessions.items;
62
+ const filtered = all.filter((s) => {
63
+ if (s.blank) return false;
64
+ if (archived.has(s.sessionId)) return false;
65
+ if (ownedByWorkspace.has(s.sessionId)) return true;
66
+ if (s.cwd && s.cwd === projectDir) return true;
67
+ return false;
68
+ });
69
+ const result = filtered.sort((a, b) => b.updatedAt - a.updatedAt);
70
+ timer.end(`Found ${result.length} sessions`);
71
+ return result;
72
+ } catch (e) {
73
+ lastError = e instanceof Error ? e : new Error(String(e));
74
+ log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, { method: "listSessions" });
75
+ if (i < retries - 1) {
76
+ await sleep(RETRY_DELAY);
77
+ }
78
+ }
79
+ }
80
+ timer.end("\u274C All retries exhausted");
81
+ throw lastError;
82
+ }
83
+ /** 在当前目录下创建会话(dsh 仅返回 { sessionId, agentPreset? },非完整 SessionSummary)。
84
+ * 关键:必须先确保 projectDir 对应的 workspace 存在(workspace.create 幂等 get-or-create),
85
+ * 再用 workspaceId 调 session.create。若只传 cwd,dsh 侧不会把会话挂到任何 workspace,
86
+ * 新会话会落到侧边栏"未分组"。 */
87
+ async createSession(projectDir, retries = DEFAULT_RETRIES) {
88
+ const timer = log.timer("createSession", { projectDir, retries });
89
+ let lastError = null;
90
+ for (let i = 0; i < retries; i++) {
91
+ try {
92
+ log.debug(`Attempt ${i + 1}/${retries}`, {
93
+ method: "session.create",
94
+ projectDir
95
+ });
96
+ const { workspace } = await this.call("workspace.create", {
97
+ path: projectDir
98
+ });
99
+ const session = await this.call("session.create", {
100
+ workspaceId: workspace.workspaceId
101
+ });
102
+ timer.end(`Created session: ${session.sessionId}`);
103
+ return session;
104
+ } catch (e) {
105
+ lastError = e instanceof Error ? e : new Error(String(e));
106
+ log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, { method: "createSession" });
107
+ if (i < retries - 1) {
108
+ await sleep(RETRY_DELAY);
109
+ }
110
+ }
111
+ }
112
+ timer.end("\u274C All retries exhausted");
113
+ throw lastError;
114
+ }
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
+ /** 归档会话(dsh 无硬删除,仅归档;幂等) */
144
+ async archiveSession(sessionId, retries = DEFAULT_RETRIES) {
145
+ const timer = log.timer("archiveSession", { sessionId, retries });
146
+ let lastError = null;
147
+ for (let i = 0; i < retries; i++) {
148
+ try {
149
+ log.debug(`Attempt ${i + 1}/${retries}`, { method: "workspace.archiveSession" });
150
+ await this.call("workspace.archiveSession", { sessionId });
151
+ timer.end(`Archived session: ${sessionId}`);
152
+ return;
153
+ } catch (e) {
154
+ lastError = e instanceof Error ? e : new Error(String(e));
155
+ log.debug(`Attempt ${i + 1} failed: ${lastError.message}`, {
156
+ method: "archiveSession"
157
+ });
158
+ if (i < retries - 1) {
159
+ await sleep(RETRY_DELAY);
160
+ }
161
+ }
162
+ }
163
+ timer.end("\u274C All retries exhausted");
164
+ throw lastError;
165
+ }
166
+ createHttpRequest(options, body) {
167
+ const timer = new PerformanceTimer("HTTP Request", {
168
+ operation: `${options.method || "GET"} ${options.path}`
169
+ });
170
+ return new Promise((resolve, reject) => {
171
+ const req = http.request(options, (res) => {
172
+ let data = "";
173
+ res.on("data", (chunk) => data += chunk);
174
+ res.on("end", () => {
175
+ try {
176
+ const result = JSON.parse(data);
177
+ timer.end(`\u2713 Status: ${res.statusCode}`);
178
+ resolve(result);
179
+ } catch {
180
+ timer.end("\u274C JSON parse error");
181
+ reject(new Error(`JSON parse error: ${data.substring(0, 100)}`));
182
+ }
183
+ });
184
+ });
185
+ req.on("error", (e) => {
186
+ timer.end("\u274C Request failed");
187
+ reject(e);
188
+ });
189
+ if (body) req.write(body);
190
+ req.end();
191
+ });
192
+ }
193
+ }
194
+ export {
195
+ DeepSeekAPI
196
+ };
@@ -0,0 +1,11 @@
1
+ export interface BridgeScriptOptions {
2
+ /** 主题模式 */
3
+ theme?: "light" | "dark" | "auto";
4
+ }
5
+ /**
6
+ * 生成 PostMessage Bridge 脚本。
7
+ * dsh 无正式"切换会话"URL 钩子,选中态持久化在 localStorage(CURRENT_SESSION),
8
+ * 且 SPA 启动时会据此恢复选中——因此桥接采用"写入选中 + 刷新 iframe"的策略。
9
+ * 若 dsh 后续暴露正式切会话接口,可替换以下 FOCUS_SESSION 分支。
10
+ */
11
+ export declare function generateBridgeScript(options?: BridgeScriptOptions): string;
@@ -0,0 +1,142 @@
1
+ import { WIDGET_MSG } from "@aipanel/core";
2
+ import { DSH_STORAGE_KEYS } from "./constants.js";
3
+ function generateBridgeScript(options = {}) {
4
+ const { theme = "auto" } = options;
5
+ return `
6
+ (function() {
7
+ const CURRENT_SESSION_KEY = ${JSON.stringify(DSH_STORAGE_KEYS.CURRENT_SESSION)};
8
+ const SELECTION_KEY = ${JSON.stringify(DSH_STORAGE_KEYS.SELECTION)};
9
+ const THEME = ${JSON.stringify(theme)};
10
+
11
+ // \u6865\u63A5\u5C42\u901A\u77E5 dsh-client \u7684\u9009\u4E2D\u5143\u7D20\u63D2\u5165\u4E8B\u4EF6\uFF08\u4E0E opencode \u4E00\u81F4\uFF1A\u9009\u4E2D\u5373\u8FFD\u52A0\u5230\u5BF9\u8BDD\u6846\uFF09
12
+ const INSERT_ELEMENT_EVENT = "aipanel:insert-element";
13
+
14
+ // === \u8BB0\u5F55\u6700\u8FD1\u9009\u4E2D\u5143\u7D20 ===
15
+ // \u4F9B dsh-client \u7684 @aipanel source \u4F5C\u4E3A\u5019\u9009\u8BFB\u53D6\uFF08\u53EF\u591A\u9009\uFF0C\u6700\u591A\u4FDD\u7559 20 \u4E2A\uFF09\uFF0C\u5E76\u6D3E\u53D1
16
+ // aipanel:insert-element \u8BA9 dsh-client \u7ACB\u5373\u628A\u5143\u7D20\u8FFD\u52A0\u5230\u5F53\u524D\u4F1A\u8BDD\u8F93\u5165\u6846\u3002
17
+ function pushSelection(element) {
18
+ if (!element) return;
19
+ try {
20
+ var list = [];
21
+ var raw = localStorage.getItem(SELECTION_KEY);
22
+ if (raw) list = JSON.parse(raw) || [];
23
+ if (!Array.isArray(list)) list = [];
24
+ var exists = list.some(function (e) {
25
+ return e && e.filePath === element.filePath && e.line === element.line;
26
+ });
27
+ if (!exists) list.unshift(element);
28
+ if (list.length > 20) list.length = 20;
29
+ localStorage.setItem(SELECTION_KEY, JSON.stringify(list));
30
+ // \u901A\u77E5 dsh-client \u8FFD\u52A0\u5230\u5BF9\u8BDD\u6846\uFF08\u9009\u4E2D\u5373\u63D2\u5165\uFF0C\u65E0\u9700\u518D\u8F93\u5165 @\uFF09
31
+ window.dispatchEvent(new CustomEvent(INSERT_ELEMENT_EVENT, {
32
+ detail: { element: element }
33
+ }));
34
+ } catch (e) { /* ignore */ }
35
+ }
36
+
37
+ // === \u4E3B\u9898\u540C\u6B65\uFF08UI \u5448\u73B0\uFF1B\u6301\u4E45\u5316\u4EA4\u7531 dsh settings\u300Cui-theme.preference\u300D\uFF0C\u4E0D\u5728\u6B64\u5199 localStorage\uFF09===
38
+ // dsh \u7684\u6697\u8272\u7531 body[data-ds-dark-theme] + colorScheme \u5448\u73B0\u3002
39
+ // \u6CE8\u610F\uFF1Abridge \u53EF\u80FD\u88AB\u6CE8\u5165\u5230 <head>\uFF0C\u6B64\u65F6 document.body \u5C1A\u4E0D\u5B58\u5728\uFF0C\u987B\u5728 DOM ready \u540E\u518D\u6267\u884C\u3002
40
+ // theme \u53C2\u6570\uFF1A"light" | "dark" | "auto"\uFF08auto \u4E0D\u5E72\u9884\uFF0C\u4EA4\u7ED9 dsh \u539F\u751F\u4E3B\u9898\u5904\u7406\uFF09\u3002
41
+ function applyTheme(theme) {
42
+ if (!document.body) return;
43
+ if (theme === "dark") {
44
+ document.body.setAttribute("data-ds-dark-theme", "");
45
+ document.documentElement.style.colorScheme = "dark";
46
+ } else if (theme === "light") {
47
+ document.body.removeAttribute("data-ds-dark-theme");
48
+ document.documentElement.style.colorScheme = "light";
49
+ }
50
+ }
51
+
52
+ // === \u5E03\u5C40\u8986\u76D6 ===
53
+ // \u9690\u85CF dsh \u7684\u4FA7\u8FB9\u680F\u5217\uFF0C\u907F\u514D\u4E0E AIPanel \u81EA\u5E26\u4F1A\u8BDD\u5217\u8868\u91CD\u590D\u3002
54
+ // \u7A33\u5B9A\u951A\u70B9\uFF1AAppFrame \u6839\u7F51\u683C\u5728\u4FA7\u8FB9\u680F\u6298\u53E0\u65F6\u5E26 data-sidebar-collapsed\uFF08AIPanel \u7A84 iframe \u4E0B\u6052\u5B58\u5728\uFF09\uFF0C
55
+ // \u4FA7\u8FB9\u680F\u5217\u662F\u6839\u7F51\u683C\u7684\u9996\u4E2A\u5B50\u5143\u7D20\u3002
56
+ // \u6CE8\u610F\u4E0D\u80FD\u53EA display:none \u5143\u7D20\uFF1A\u663E\u5F0F gridTemplateColumns\uFF08\u5185\u8054\u6837\u5F0F\uFF09\u8F68\u9053\u4E0D\u4F1A\u56E0\u5B50\u9879
57
+ // \u9690\u85CF\u800C\u6D88\u5931\uFF0C\u4F1A\u7559\u4E00\u6761\u7A7A\u767D\u5217\uFF0C\u987B\u628A\u9996\u5217\u8F68\u9053\u6539\u6210 auto\uFF08\u5B50\u9879\u9690\u85CF\u540E\u574D\u7F29\u4E3A 0\uFF09\u3002
58
+ function applyLayoutOverrides() {
59
+ try {
60
+ if (document.getElementById("aipanel-layout-overrides")) return;
61
+ var style = document.createElement("style");
62
+ style.id = "aipanel-layout-overrides";
63
+ style.textContent = [
64
+ "[data-sidebar-collapsed] {",
65
+ " grid-template-columns: auto !important;",
66
+ "}",
67
+ "[data-sidebar-collapsed] > :first-child {",
68
+ " display: none !important;",
69
+ "}",
70
+ ].join("\\n");
71
+ document.head.appendChild(style);
72
+ } catch (e) { /* ignore */ }
73
+ }
74
+
75
+ // === \u6D88\u606F\u76D1\u542C ===
76
+ window.addEventListener("message", function(event) {
77
+ if (!event.data) return;
78
+
79
+ // \u6838\u5FC3\u5C42\u901A\u77E5\uFF1A\u5207\u6362\u4E3B\u9898\uFF08\u5DE5\u5177\u680F\u4E3B\u9898\u6309\u94AE/\u8DDF\u968F\u7CFB\u7EDF\u53D8\u5316\uFF09\u3002\u52A8\u6001\u5E94\u7528\uFF0C\u65E0\u9700\u91CD\u8F7D\u3002
80
+ if (event.data.type === ${JSON.stringify(WIDGET_MSG.SET_THEME)}) {
81
+ applyTheme(event.data.theme);
82
+ }
83
+
84
+ // \u6838\u5FC3\u5C42\u901A\u77E5\uFF1A\u805A\u7126\u6307\u5B9A\u4F1A\u8BDD\uFF08\u65E0 deepLink \u6A21\u5F0F\uFF09\u3002
85
+ // dsh \u7684\u9009\u4E2D\u6001\u6301\u4E45\u5316\u4E3A JSON.stringify({ sessionId, subagentAddress? })\uFF0CSPA \u542F\u52A8\u65F6\u636E\u6B64\u6062\u590D\u3002
86
+ if (event.data.type === ${JSON.stringify(WIDGET_MSG.FOCUS_SESSION)}) {
87
+ const sessionId = event.data.sessionId;
88
+ if (!sessionId) return;
89
+ try {
90
+ localStorage.setItem(CURRENT_SESSION_KEY, JSON.stringify({ sessionId }));
91
+ } catch (e) {
92
+ console.warn("dsh bridge: failed to persist session selection", e);
93
+ }
94
+ // dsh SPA \u4ECE\u6301\u4E45\u5316\u9009\u4E2D\u6062\u590D\uFF0C\u5237\u65B0\u4EE5\u5207\u6362\u5230\u76EE\u6807\u4F1A\u8BDD
95
+ location.reload();
96
+ }
97
+
98
+ // \u6838\u5FC3\u5C42\u901A\u77E5\uFF1A\u9875\u9762\u9009\u4E2D\u5143\u7D20 \u2192 \u8BB0\u5F55\uFF0C\u4F9B @aipanel \u83DC\u5355\u7B5B\u9009\u540E\u4EE5 reference \u63D2\u5165
99
+ if (event.data.type === ${JSON.stringify(WIDGET_MSG.INSERT_FILE_PART)}) {
100
+ pushSelection(event.data.element);
101
+ }
102
+ });
103
+
104
+ // === \u5C31\u7EEA\u901A\u77E5 ===
105
+ // \u4E0D\u518D\u7528 MutationObserver \u6BCF\u5E27\u53D1 READY\uFF08\u90A3\u4F1A\u5728 dsh \u521A\u6E32\u67D3\u3001\u76EE\u6807\u4F1A\u8BDD\u5C1A\u672A\u6FC0\u6D3B\u65F6\u5C31
106
+ // \u63D0\u524D\u653E\u884C loading\uFF09\u3002\u6539\u4E3A\u76D1\u542C dsh-client \u63D2\u4EF6\u5728"\u76EE\u6807\u4F1A\u8BDD\u6FC0\u6D3B\u4E14\u6E32\u67D3\u7A33\u5B9A"\u540E\u6D3E\u53D1\u7684
107
+ // aipanel:session-ready \u4E8B\u4EF6\uFF0C\u6536\u5230\u540E\u4E0A\u62A5 SESSION_READY{sessionId}\u3002
108
+ // \u53EA\u53D1 SESSION_READY \u800C\u4E0D\u53D1 READY\uFF1A\u5BA2\u6237\u7AEF\u5DF2\u80FD\u51ED sessionId \u5339\u914D\u5F53\u524D\u4F1A\u8BDD\u51B3\u5B9A\u4F55\u65F6
109
+ // \u653E\u884C loading / \u8865\u53D1\u805A\u7126\uFF1B\u907F\u514D\u4E0E"FOCUS_SESSION \u2192 reload \u2192 \u518D\u5C31\u7EEA"\u6784\u6210\u5237\u65B0\u6B7B\u5FAA\u73AF\u3002
110
+ const SESSION_READY_EVENT = "aipanel:session-ready";
111
+
112
+ function sendSessionReady(sessionId) {
113
+ if (window.parent !== window) {
114
+ window.parent.postMessage({
115
+ type: ${JSON.stringify(WIDGET_MSG.SESSION_READY)},
116
+ sessionId
117
+ }, "*");
118
+ }
119
+ }
120
+
121
+ window.addEventListener(SESSION_READY_EVENT, function(e) {
122
+ const sessionId = e && e.detail && e.detail.sessionId;
123
+ if (!sessionId) return;
124
+ sendSessionReady(sessionId);
125
+ });
126
+
127
+ // \u4E3B\u9898\u540C\u6B65/\u5E03\u5C40\u8986\u76D6\u4ECD\u987B\u5728\u6BCF\u6B21\u9875\u9762\u52A0\u8F7D\u65F6\u751F\u6548\uFF08\u4E0E\u5C31\u7EEA\u5224\u5B9A\u89E3\u8026\uFF0C\u4E0D\u53C2\u4E0E loading \u653E\u884C\uFF09
128
+ function init() {
129
+ applyTheme(THEME);
130
+ applyLayoutOverrides();
131
+ }
132
+ if (document.readyState === "loading") {
133
+ document.addEventListener("DOMContentLoaded", init);
134
+ } else {
135
+ init();
136
+ }
137
+ })();
138
+ `;
139
+ }
140
+ export {
141
+ generateBridgeScript
142
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * DeepSeek Harness Provider 专属常量
3
+ * 与 dsh web 绑定的常量自包含于此,核心层不感知。
4
+ */
5
+ import type { DeepSeekProviderOptions } from "./types";
6
+ /** ==================== dsh API ==================== */
7
+ /** dsh 所有 RPC 路径前缀(POST /api/<method>、GET /api/events.mux) */
8
+ export declare const DSH_API_BASE = "/api";
9
+ /** mux 事件流端点(会话级聚合流,含 session/event 可推导 thinking/streaming) */
10
+ export declare const DSH_MUX_EVENTS_PATH = "/api/events.mux";
11
+ /** host 事件流端点(host 级,含 host/session-status.running 运行态开关) */
12
+ export declare const DSH_HOST_EVENTS_PATH = "/api/events.host";
13
+ /** dsh 唯一允许的绑定主机字面量(服务 schema 只接受 127.0.0.1 / 0.0.0.0) */
14
+ export declare const DSH_LOOPBACK_HOST = "127.0.0.1";
15
+ /** dsh web 默认端口(未显式指定时) */
16
+ 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
+ };
24
+ /** ==================== Provider 专属配置默认值 ==================== */
25
+ export declare const DEFAULT_DEEPSEEK_PROVIDER_OPTIONS: DeepSeekProviderOptions;
@@ -0,0 +1,23 @@
1
+ const DSH_API_BASE = "/api";
2
+ const DSH_MUX_EVENTS_PATH = "/api/events.mux";
3
+ const DSH_HOST_EVENTS_PATH = "/api/events.host";
4
+ const DSH_LOOPBACK_HOST = "127.0.0.1";
5
+ 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
+ };
12
+ const DEFAULT_DEEPSEEK_PROVIDER_OPTIONS = {
13
+ agentPreset: "code"
14
+ };
15
+ export {
16
+ DEFAULT_DEEPSEEK_PROVIDER_OPTIONS,
17
+ DSH_API_BASE,
18
+ DSH_DEFAULT_PORT,
19
+ DSH_HOST_EVENTS_PATH,
20
+ DSH_LOOPBACK_HOST,
21
+ DSH_MUX_EVENTS_PATH,
22
+ DSH_STORAGE_KEYS
23
+ };
@@ -0,0 +1,21 @@
1
+ import { type ResultPromise } from "execa";
2
+ export interface DeepSeekWebOptions {
3
+ /** 服务端口 */
4
+ port: number;
5
+ /** 服务主机(dsh 只接受 127.0.0.1,见 DSH_LOOPBACK_HOST) */
6
+ hostname: string;
7
+ /** 工作目录 */
8
+ cwd: string;
9
+ /** cordis overlay 文件路径(--patch 注入 MCP client / aipanel 插件) */
10
+ patchPath?: string;
11
+ /** dsh 数据目录(透传 DSH_HOME;缺省跟随 $DSH_HOME / ~/.dsh) */
12
+ home?: string;
13
+ /** 启用 verbose 模式 */
14
+ verbose?: boolean;
15
+ }
16
+ /**
17
+ * 启动 dsh web 服务。
18
+ * dsh web 是自包含单进程(web server + agent runtime 同进程),无需额外配置/插件注入。
19
+ * 也无需 API key 即可启动 UI 壳。
20
+ */
21
+ export declare function startDeepSeekWeb(options: DeepSeekWebOptions): ResultPromise;
@@ -0,0 +1,62 @@
1
+ import { execa } from "execa";
2
+ import { createLogger, getProcessLogBuffer } from "@aipanel/core/node";
3
+ const log = createLogger("DeepSeekWeb");
4
+ function startDeepSeekWeb(options) {
5
+ const { port, hostname, cwd, patchPath, home, verbose } = options;
6
+ const args = ["--profile", "web"];
7
+ if (patchPath) {
8
+ args.push("--patch", patchPath);
9
+ }
10
+ args.push(
11
+ "--port",
12
+ String(port),
13
+ "--host",
14
+ hostname,
15
+ // 由插件内嵌 iframe 展示,禁止 dsh 自动打开默认浏览器
16
+ "--no-open"
17
+ );
18
+ log.debug("Spawning dsh web process", {
19
+ command: "dsh",
20
+ args: args.join(" "),
21
+ cwd,
22
+ home: home ?? process.env.DSH_HOME
23
+ });
24
+ const proc = execa("dsh", args, {
25
+ cwd,
26
+ reject: false,
27
+ cleanup: true,
28
+ shell: true,
29
+ env: {
30
+ ...process.env,
31
+ ...home ? { DSH_HOME: home } : {},
32
+ ...verbose ? { VERBOSE: "1" } : {}
33
+ }
34
+ });
35
+ proc.then((result) => {
36
+ if (result.exitCode !== 0) {
37
+ log.warn("[dsh exited]", { exitCode: result.exitCode, signal: result.signal });
38
+ } else {
39
+ log.debug("[dsh exited]", { exitCode: result.exitCode });
40
+ }
41
+ }).catch((e) => {
42
+ log.error("[dsh spawn failed]", { error: e instanceof Error ? e.message : String(e) });
43
+ });
44
+ proc.stdout?.on("data", (data) => {
45
+ const output = data.toString().trim();
46
+ if (output) {
47
+ log.debug("[dsh stdout]", { output });
48
+ getProcessLogBuffer().addProviderStdout(output);
49
+ }
50
+ });
51
+ proc.stderr?.on("data", (data) => {
52
+ const output = data.toString().trim();
53
+ if (output) {
54
+ log.warn("[dsh stderr]", { output });
55
+ getProcessLogBuffer().addProviderStderr(output);
56
+ }
57
+ });
58
+ return proc;
59
+ }
60
+ export {
61
+ startDeepSeekWeb
62
+ };
@@ -0,0 +1,16 @@
1
+ export declare const DSH_CLIENT_PACKAGE = "@aipanel/dsh-client";
2
+ /**
3
+ * 是否为 dev workspace 且 dsh-client 产物就绪(存在 package.json 与 lib/client.js)。
4
+ * 生产安装的 provider 在 node_modules/@aipanel/provider-deepseek/ 下,上一级不是 dsh-client,返回 null。
5
+ */
6
+ export declare function resolveDevDshClientSource(metaUrl: string): string | null;
7
+ /** dsh web profile 目录(固定 --profile web);home 未指定时回退 $DSH_HOME / ~/.dsh */
8
+ export declare function dshProfileDir(home?: string): string;
9
+ /** @aipanel/dsh-client 在 profile 的 node_modules 中是否可解析 */
10
+ export declare function isDshClientInstalled(profileDir: string): boolean;
11
+ /**
12
+ * 确保 @aipanel/dsh-client 已安装且为最新(官方命令 dsh plugin add)。
13
+ * 每次启动都执行(不跳过已安装):dev 本地目录每次重装保证改代码生效,
14
+ * 生产 npm 包每次检查 registry 拉取最新版本。安装失败返回 false(不阻塞启动,仅 chip 高亮不可用)。
15
+ */
16
+ export declare function ensureDshClient(profileDir: string, target: string, home?: string): Promise<boolean>;
@@ -0,0 +1,58 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { execa } from "execa";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createLogger } from "@aipanel/core/node";
7
+ const log = createLogger("DshInstall");
8
+ const DSH_CLIENT_PACKAGE = "@aipanel/dsh-client";
9
+ function resolveDevDshClientSource(metaUrl) {
10
+ const here = path.dirname(fileURLToPath(metaUrl));
11
+ const devDir = path.resolve(here, "../dsh-client");
12
+ if (fs.existsSync(path.join(devDir, "package.json")) && fs.existsSync(path.join(devDir, "lib/client.js"))) {
13
+ return devDir;
14
+ }
15
+ return null;
16
+ }
17
+ function dshProfileDir(home) {
18
+ const resolved = home || process.env.DSH_HOME || path.join(os.homedir(), ".dsh");
19
+ return path.join(resolved, "profiles", "web");
20
+ }
21
+ function isDshClientInstalled(profileDir) {
22
+ return fs.existsSync(
23
+ path.join(profileDir, "node_modules", "@aipanel", "dsh-client", "package.json")
24
+ );
25
+ }
26
+ async function ensureDshClient(profileDir, target, home) {
27
+ try {
28
+ log.debug(`installing ${target} into dsh profile via dsh plugin add`);
29
+ await execa("dsh", ["plugin", "--profile", "web", "add", target], {
30
+ reject: true,
31
+ shell: true,
32
+ env: {
33
+ ...process.env,
34
+ ...home ? { DSH_HOME: home } : {}
35
+ }
36
+ });
37
+ if (!isDshClientInstalled(profileDir)) {
38
+ log.warn(`dsh plugin add finished but ${DSH_CLIENT_PACKAGE} is not resolvable`, {
39
+ profileDir
40
+ });
41
+ return false;
42
+ }
43
+ return true;
44
+ } catch (e) {
45
+ log.warn(`failed to install ${DSH_CLIENT_PACKAGE} via dsh plugin add`, {
46
+ target,
47
+ error: e instanceof Error ? e.message : String(e)
48
+ });
49
+ return false;
50
+ }
51
+ }
52
+ export {
53
+ DSH_CLIENT_PACKAGE,
54
+ dshProfileDir,
55
+ ensureDshClient,
56
+ isDshClientInstalled,
57
+ resolveDevDshClientSource
58
+ };
package/es/index.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * DeepSeek Harness Web Provider
3
+ * 实现 WebProvider 契约:进程管理、RPC 会话 API、桥接脚本、CLI 环境检查。
4
+ * 所有 dsh 专属类型与常量自包含于此包。
5
+ */
6
+ import type { ProviderInitContext, WebProvider } from "@aipanel/core";
7
+ /** 约定工厂:核心层动态加载本包后调用,初始化动作完全由 Provider 定义 */
8
+ export declare function createProvider(ctx: ProviderInitContext): WebProvider;
9
+ export { DeepSeekAPI } from "./api";
10
+ export type { DeepSeekWebProviderConfig, DeepSeekWebProviderDeps } from "./provider";
11
+ export { startDeepSeekWeb, type DeepSeekWebOptions } from "./deepseek-web";
12
+ export { buildDshOverlay, writeDshOverlay } from "./profile";
13
+ export { generateBridgeScript, type BridgeScriptOptions } from "./bridge-script";
14
+ export { checkDeepSeekInstalled, getDeepSeekVersion, killOrphanDeepSeekProcesses } from "./system";
15
+ export { DEFAULT_DEEPSEEK_PROVIDER_OPTIONS, DSH_LOOPBACK_HOST, DSH_DEFAULT_PORT } from "./constants";
16
+ export type { DeepSeekProviderOptions, DeepSeekPermissionPreset, DeepSeekBusyEnter, SessionSummary, WorkspaceView, SessionStreamEvent, ServerRequest, ServerResponse, ClientRequest, } from "./types";
package/es/index.js ADDED
@@ -0,0 +1,29 @@
1
+ import { DeepSeekWebProvider } from "./provider.js";
2
+ import { DSH_LOOPBACK_HOST } from "./constants.js";
3
+ function createProvider(ctx) {
4
+ return new DeepSeekWebProvider(
5
+ { hostname: DSH_LOOPBACK_HOST },
6
+ { getWebPort: ctx.getWebPort, getProxyPort: ctx.getProxyPort },
7
+ ctx.options
8
+ );
9
+ }
10
+ import { DeepSeekAPI } from "./api.js";
11
+ import { startDeepSeekWeb } from "./deepseek-web.js";
12
+ import { buildDshOverlay, writeDshOverlay } from "./profile.js";
13
+ import { generateBridgeScript } from "./bridge-script.js";
14
+ import { checkDeepSeekInstalled, getDeepSeekVersion, killOrphanDeepSeekProcesses } from "./system.js";
15
+ import { DEFAULT_DEEPSEEK_PROVIDER_OPTIONS, DSH_LOOPBACK_HOST as DSH_LOOPBACK_HOST2, DSH_DEFAULT_PORT } from "./constants.js";
16
+ export {
17
+ DEFAULT_DEEPSEEK_PROVIDER_OPTIONS,
18
+ DSH_DEFAULT_PORT,
19
+ DSH_LOOPBACK_HOST2 as DSH_LOOPBACK_HOST,
20
+ DeepSeekAPI,
21
+ buildDshOverlay,
22
+ checkDeepSeekInstalled,
23
+ createProvider,
24
+ generateBridgeScript,
25
+ getDeepSeekVersion,
26
+ killOrphanDeepSeekProcesses,
27
+ startDeepSeekWeb,
28
+ writeDshOverlay
29
+ };
@@ -0,0 +1,11 @@
1
+ /** 组装 overlay YAML */
2
+ export declare function buildDshOverlay(options: {
3
+ vitePort: number;
4
+ cwd: string;
5
+ pluginDistPath?: string;
6
+ /** client 插件是否可被 dsh 解析(provider 已同步到 dsh profile);false 时停用该行,避免 fail-loud */
7
+ clientAvailable?: boolean;
8
+ }): string;
9
+ /** 将 overlay 写入项目缓存目录(node_modules/.cache/aipanel,参照 opencode 的
10
+ * node_modules/.cache/opencode 惯例),不污染用户项目根目录。 */
11
+ export declare function writeDshOverlay(workspaceCwd: string, overlay: string): string;