@jaychang1989/dsh-webchat 0.3.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/LICENSE +201 -0
- package/NOTICE +18 -0
- package/README.md +78 -0
- package/README.zh.md +83 -0
- package/cordis.patch.yml +22 -0
- package/lib/client.js +1725 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +3887 -0
- package/lib/types/client/api.d.ts +74 -0
- package/lib/types/client/controller.d.ts +19 -0
- package/lib/types/client/index.d.ts +30 -0
- package/lib/types/client/locales.d.ts +169 -0
- package/lib/types/client/mount.d.ts +23 -0
- package/lib/types/client/panel/Markdown.d.ts +15 -0
- package/lib/types/client/panel/WebChatPanel.d.ts +28 -0
- package/lib/types/client/sidebar-entry-core.d.ts +60 -0
- package/lib/types/client/sidebar-entry.d.ts +22 -0
- package/lib/types/engine/engine.d.ts +247 -0
- package/lib/types/engine/html-md.d.ts +23 -0
- package/lib/types/index.d.ts +63 -0
- package/lib/types/protocol.d.ts +107 -0
- package/lib/types/routes.d.ts +22 -0
- package/lib/types/store.d.ts +69 -0
- package/lib/types/tools.d.ts +27 -0
- package/lib/types/transfer.d.ts +138 -0
- package/package.json +96 -0
- package/src/client/api.ts +90 -0
- package/src/client/controller.ts +43 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/index.ts +75 -0
- package/src/client/locales.ts +172 -0
- package/src/client/mount.tsx +125 -0
- package/src/client/panel/Markdown.tsx +251 -0
- package/src/client/panel/WebChatPanel.tsx +760 -0
- package/src/client/panel/panel.module.css +936 -0
- package/src/client/sidebar-entry-core.ts +207 -0
- package/src/client/sidebar-entry.ts +49 -0
- package/src/engine/engine.ts +1385 -0
- package/src/engine/html-md.ts +126 -0
- package/src/index.ts +212 -0
- package/src/protocol.ts +116 -0
- package/src/routes.ts +307 -0
- package/src/store.ts +217 -0
- package/src/tools.ts +257 -0
- package/src/transfer.ts +602 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal HTML → Markdown converter used to scrape DeepSeek's rendered
|
|
3
|
+
* `.ds-markdown` replies. Covers the shapes DeepSeek renders (headings,
|
|
4
|
+
* paragraphs, code blocks with syntax classes, inline code, bold/italic,
|
|
5
|
+
* links, images, lists, tables, blockquotes, math placeholders). Anything
|
|
6
|
+
* unrecognized falls back to its text content — fidelity loss degrades
|
|
7
|
+
* gracefully instead of dropping text.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Escape text that could be parsed as markdown markup. */
|
|
11
|
+
function escapeText(text: string): string {
|
|
12
|
+
return text
|
|
13
|
+
.replace(/([\\`*_[\]<>])/g, '\\$1')
|
|
14
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface NodeLike {
|
|
18
|
+
readonly tagName?: string
|
|
19
|
+
readonly nodeType?: number
|
|
20
|
+
readonly textContent?: string | null
|
|
21
|
+
readonly children?: readonly NodeLike[]
|
|
22
|
+
readonly attributes?: Record<string, string | undefined>
|
|
23
|
+
readonly innerHTML?: string
|
|
24
|
+
readonly className?: string
|
|
25
|
+
readonly parent?: NodeLike
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const TEXT_NODE = 3
|
|
29
|
+
const ELEMENT_NODE = 1
|
|
30
|
+
|
|
31
|
+
/** Convert one DOM subtree (element or text) to markdown. */
|
|
32
|
+
export function htmlToMarkdown(root: NodeLike): string {
|
|
33
|
+
if (root === null || root === undefined) return ''
|
|
34
|
+
if (root.nodeType === TEXT_NODE) return escapeText(root.textContent ?? '')
|
|
35
|
+
const tag = (root.tagName ?? '').toLowerCase()
|
|
36
|
+
const text = (children: readonly NodeLike[] | undefined): string =>
|
|
37
|
+
(children ?? []).map(htmlToMarkdown).join('')
|
|
38
|
+
|
|
39
|
+
switch (tag) {
|
|
40
|
+
case 'br': return '\n'
|
|
41
|
+
case 'hr': return '\n---\n'
|
|
42
|
+
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': {
|
|
43
|
+
const level = Number(tag[1])
|
|
44
|
+
return `\n${'#'.repeat(level)} ${text(root.children).trim()}\n`
|
|
45
|
+
}
|
|
46
|
+
case 'p': return `\n${text(root.children).trim()}\n`
|
|
47
|
+
case 'strong': case 'b': return `**${text(root.children).trim()}**`
|
|
48
|
+
case 'em': case 'i': return `*${text(root.children).trim()}*`
|
|
49
|
+
case 'code': {
|
|
50
|
+
// Code block vs inline: <pre><code> is a block; bare <code> is inline.
|
|
51
|
+
const parentPre = root.parent?.tagName?.toLowerCase() === 'pre'
|
|
52
|
+
if (parentPre) return text(root.children)
|
|
53
|
+
return `\`${(root.textContent ?? '').replace(/`/g, '\\`')}\``
|
|
54
|
+
}
|
|
55
|
+
case 'pre': {
|
|
56
|
+
const code = root.children?.find(child => (child.tagName ?? '').toLowerCase() === 'code') ?? root
|
|
57
|
+
const raw = code.textContent ?? ''
|
|
58
|
+
const className = code.className ?? ''
|
|
59
|
+
const language = /language-([a-zA-Z0-9_+-]+)/.exec(className)?.[1] ?? ''
|
|
60
|
+
const fence = '```'
|
|
61
|
+
return `\n${fence}${language}\n${raw.replace(/\n$/, '')}\n${fence}\n`
|
|
62
|
+
}
|
|
63
|
+
case 'a': {
|
|
64
|
+
const href = root.attributes?.href
|
|
65
|
+
const label = text(root.children).trim()
|
|
66
|
+
if (href === undefined || href === '' || href.startsWith('javascript:')) return label
|
|
67
|
+
return `[${label || href}](${href})`
|
|
68
|
+
}
|
|
69
|
+
case 'img': {
|
|
70
|
+
const src = root.attributes?.src
|
|
71
|
+
const alt = root.attributes?.alt ?? ''
|
|
72
|
+
return src === undefined ? '' : ``
|
|
73
|
+
}
|
|
74
|
+
case 'ul': return `\n${(root.children ?? []).map(child => {
|
|
75
|
+
if ((child.tagName ?? '').toLowerCase() === 'li') return `- ${text(child.children).trim()}`
|
|
76
|
+
return htmlToMarkdown(child)
|
|
77
|
+
}).join('\n')}\n`
|
|
78
|
+
case 'ol': {
|
|
79
|
+
let index = 1
|
|
80
|
+
return `\n${(root.children ?? []).map(child => {
|
|
81
|
+
if ((child.tagName ?? '').toLowerCase() === 'li') return `${index++}. ${text(child.children).trim()}`
|
|
82
|
+
return htmlToMarkdown(child)
|
|
83
|
+
}).join('\n')}\n`
|
|
84
|
+
}
|
|
85
|
+
case 'li': return text(root.children).trim()
|
|
86
|
+
case 'blockquote': return `\n> ${text(root.children).trim().replace(/\n/g, '\n> ')}\n`
|
|
87
|
+
case 'table': {
|
|
88
|
+
const rows = (root.children ?? []).filter(child => (child.tagName ?? '').toLowerCase() === 'tr')
|
|
89
|
+
if (rows.length === 0) return text(root.children).trim()
|
|
90
|
+
const cellsOf = (row: NodeLike): string[] => (row.children ?? [])
|
|
91
|
+
.filter(child => ['th', 'td'].includes((child.tagName ?? '').toLowerCase()))
|
|
92
|
+
.map(cell => text(cell.children).trim().replace(/\|/g, '\\|'))
|
|
93
|
+
const header = cellsOf(rows[0])
|
|
94
|
+
const body = rows.slice(1).map(cellsOf)
|
|
95
|
+
const width = Math.max(header.length, ...body.map(row => row.length))
|
|
96
|
+
const pad = (cells: string[]): string => {
|
|
97
|
+
const filled = [...cells]
|
|
98
|
+
while (filled.length < width) filled.push('')
|
|
99
|
+
return `| ${filled.join(' | ')} |`
|
|
100
|
+
}
|
|
101
|
+
const lines = [pad(header), `| ${Array.from({ length: width }, () => '---').join(' | ')} |`, ...body.map(pad)]
|
|
102
|
+
return `\n${lines.join('\n')}\n`
|
|
103
|
+
}
|
|
104
|
+
case 'tr': case 'td': case 'th': case 'thead': case 'tbody': case 'tfoot':
|
|
105
|
+
return text(root.children)
|
|
106
|
+
case 'details': return `\n<details>\n${text(root.children)}\n</details>\n`
|
|
107
|
+
case 'summary': return `**${text(root.children).trim()}**`
|
|
108
|
+
case 'input': {
|
|
109
|
+
const checked = root.attributes?.checked !== undefined
|
|
110
|
+
return checked ? '[x] ' : '[ ] '
|
|
111
|
+
}
|
|
112
|
+
case 'math': return `$${root.textContent ?? ''}$`
|
|
113
|
+
case 'svg': case 'button': case 'script': case 'style': return ''
|
|
114
|
+
case 'div': case 'span': case 'section': case 'article': case 'main':
|
|
115
|
+
return text(root.children)
|
|
116
|
+
default: {
|
|
117
|
+
const direct = text(root.children)
|
|
118
|
+
return direct
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Convert serialized DOM (from the in-page scraper) to markdown. */
|
|
124
|
+
export function serializeToMarkdown(root: NodeLike): string {
|
|
125
|
+
return htmlToMarkdown(root).replace(/\n{3,}/g, '\n\n').trim()
|
|
126
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-webchat — host half. Mounts the DeepSeek web engine (a real browser at
|
|
3
|
+
* chat.deepseek.com driven through its own page, persistent login profile),
|
|
4
|
+
* the /api/dsh-webchat route family, the agent tools (webchat_status,
|
|
5
|
+
* webchat_send, webchat_import, webchat_transfer), the harness transfer
|
|
6
|
+
* (seed a new session with a web transcript) and a system-prompt
|
|
7
|
+
* announcement. The browser half (./client) renders the chat panel. All
|
|
8
|
+
* transport rides the official NPM SDK packages — no dsh source changes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
12
|
+
import z from 'schemastery'
|
|
13
|
+
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
14
|
+
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
15
|
+
import type {} from '@deepseek-ai/dsh-tools'
|
|
16
|
+
import { DeepSeekWebEngine } from './engine/engine.ts'
|
|
17
|
+
import { makeRoutes } from './routes.ts'
|
|
18
|
+
import { TranscriptStore } from './store.ts'
|
|
19
|
+
import { webChatImportTool, webChatRecoverTool, webChatSendTool, webChatStatusTool, webChatTransferTool } from './tools.ts'
|
|
20
|
+
import type { WorkspaceRef } from './tools.ts'
|
|
21
|
+
import type { DistillConfig } from './transfer.ts'
|
|
22
|
+
|
|
23
|
+
/** Stable cordis plugin name. */
|
|
24
|
+
export const name = 'webchat'
|
|
25
|
+
|
|
26
|
+
/** Services required before the web-chat surfaces can mount. */
|
|
27
|
+
export const inject = ['webServer', 'tools', 'systemPrompt', 'sessions']
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Label of the web-chat capability. dsh ≥ 0.1.7 addresses settings forms by
|
|
31
|
+
* profile entry id and derives them from this package's exported `Config`, so
|
|
32
|
+
* this value is only the plugin's own label (the browser half spells the same
|
|
33
|
+
* value for its locale namespace) — not a settings-service namespace.
|
|
34
|
+
*/
|
|
35
|
+
export const WEBCHAT_SETTINGS_NAMESPACE = 'dsh-webchat'
|
|
36
|
+
|
|
37
|
+
/** Plugin config, validated by the same-named schemastery schema. */
|
|
38
|
+
export interface Config {
|
|
39
|
+
/** When true (default), a system-prompt section announces the plugin to every agent. */
|
|
40
|
+
announceToAgent?: boolean
|
|
41
|
+
/** Master switch for the plugin (routes, tools, prompt section). */
|
|
42
|
+
enabled?: boolean
|
|
43
|
+
/** Browser channel hint ('chrome' | 'msedge' | 'chromium' | 'auto'). */
|
|
44
|
+
browserChannel?: string
|
|
45
|
+
/** Explicit browser executable path. */
|
|
46
|
+
browserExecutablePath?: string
|
|
47
|
+
/** Proxy mode: 'direct' | 'system' | 'http://host:port'. */
|
|
48
|
+
browserProxy?: string
|
|
49
|
+
/**
|
|
50
|
+
* Run the chat browser headless (invisible). Default true: only the one-time
|
|
51
|
+
* login window is visible, and it auto-closes once logged in. Set false to
|
|
52
|
+
* keep a visible browser window during chatting too.
|
|
53
|
+
*/
|
|
54
|
+
browserHeadless?: boolean
|
|
55
|
+
/** Max ms to wait for a web reply. */
|
|
56
|
+
replyTimeoutMs?: number
|
|
57
|
+
/** Data directory override (tests). */
|
|
58
|
+
dataDir?: string
|
|
59
|
+
/** When true (default), distill the transcript into an executable task brief before transfer. */
|
|
60
|
+
transferDistill?: boolean
|
|
61
|
+
/** Provider route for the transfer distillation call (empty = auto-detect). */
|
|
62
|
+
transferProvider?: string
|
|
63
|
+
/** Model id for the transfer distillation call (empty = auto-detect). */
|
|
64
|
+
transferModel?: string
|
|
65
|
+
/** Output-token cap for the final distillation brief (default 4096). */
|
|
66
|
+
transferMaxTokens?: number
|
|
67
|
+
/** Output-token cap per chunk summary in long-conversation map-reduce (default 1024). */
|
|
68
|
+
transferChunkTokens?: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const Config: z<Config> = z.object({
|
|
72
|
+
announceToAgent: z.boolean().default(true),
|
|
73
|
+
enabled: z.boolean().default(true),
|
|
74
|
+
browserChannel: z.string().default('auto'),
|
|
75
|
+
browserExecutablePath: z.string().default(''),
|
|
76
|
+
browserProxy: z.string().default('direct'),
|
|
77
|
+
browserHeadless: z.boolean().default(true),
|
|
78
|
+
replyTimeoutMs: z.number().default(180_000),
|
|
79
|
+
dataDir: z.string().default(''),
|
|
80
|
+
transferDistill: z.boolean().default(true),
|
|
81
|
+
transferProvider: z.string().default(''),
|
|
82
|
+
transferModel: z.string().default(''),
|
|
83
|
+
transferMaxTokens: z.number().default(4096),
|
|
84
|
+
transferChunkTokens: z.number().default(1024),
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
/** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
|
|
88
|
+
const DEFAULT_ANNOUNCE = true
|
|
89
|
+
|
|
90
|
+
/** Order of the announcement section within the tool-guidance band. */
|
|
91
|
+
const SECTION_ORDER = 155
|
|
92
|
+
|
|
93
|
+
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
94
|
+
export const WEBCHAT_GUIDANCE = '本机已安装 dsh-webchat 插件(Codex ChatGPT 模式 · DeepSeek 网页端聊天):侧边栏「网页聊天」入口;通过真实浏览器驱动 chat.deepseek.com(DeepSeek 网页模型,深度思考/智能搜索开关,网页登录会话,无需 API 额度)。能力:webchat_status 查看登录/会话状态、webchat_send 通过网页端发送消息并流式获取回复(可附带本地图片路径做多模态提问)、webchat_recover 把网页端已有会话同步/恢复到本地、webchat_import 把存储的网页对话导入为 markdown 上下文、webchat_transfer 把网页对话蒸馏成可执行任务简报并创建新 harness 会话(首条消息即任务简报,而非原始聊天记录),或经 targetSessionId 把简报作为新消息追加到已有会话延续同一任务;GUI 面板可将对话随时导出为工作区文件或转入 harness 会话。限制:首次使用需用户在弹出的浏览器窗口完成 DeepSeek 网页登录;网页端受 DeepSeek 官方风控,操作失败或页面改版时返回错误而非崩溃;面板提供「深度思考(R1)」与「智能搜索」开关(网页端无模型选择器)。用户提到「网页聊天 / 网页端 / ChatGPT 模式 / deepseek web / 转移到 harness」时即指本插件,请据此协作。'
|
|
95
|
+
|
|
96
|
+
/** Convert resolved config to engine config. */
|
|
97
|
+
function engineConfigOf(resolve: () => Config): ConstructorParameters<typeof DeepSeekWebEngine>[1] {
|
|
98
|
+
const value = resolve()
|
|
99
|
+
const dataDir = value.dataDir?.trim() ?? ''
|
|
100
|
+
return {
|
|
101
|
+
dataDir: dataDir !== '' ? dataDir : defaultDataDirOf(),
|
|
102
|
+
channel: value.browserChannel === 'auto' ? undefined : (value.browserChannel || undefined),
|
|
103
|
+
executablePath: value.browserExecutablePath !== '' ? value.browserExecutablePath : undefined,
|
|
104
|
+
proxy: value.browserProxy,
|
|
105
|
+
headless: value.browserHeadless,
|
|
106
|
+
replyTimeoutMs: value.replyTimeoutMs,
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Default plugin data dir (mirrors store.defaultDataDir). */
|
|
111
|
+
function defaultDataDirOf(): string {
|
|
112
|
+
const home = process.env.DSH_HOME ?? process.env.HOME ?? '.'
|
|
113
|
+
return `${home}/.dsh/dsh-webchat`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Mount the engine, routes, tools, and announcement.
|
|
118
|
+
* @param ctx - host plugin context carrying webServer/tools/systemPrompt.
|
|
119
|
+
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
120
|
+
*/
|
|
121
|
+
export function apply(ctx: Context, config?: Config): void {
|
|
122
|
+
let current: () => Config = () => config ?? {}
|
|
123
|
+
const resolve = (): Config => ({
|
|
124
|
+
announceToAgent: current().announceToAgent ?? DEFAULT_ANNOUNCE,
|
|
125
|
+
enabled: current().enabled ?? true,
|
|
126
|
+
browserChannel: current().browserChannel ?? 'auto',
|
|
127
|
+
browserExecutablePath: current().browserExecutablePath ?? '',
|
|
128
|
+
browserProxy: current().browserProxy ?? 'direct',
|
|
129
|
+
browserHeadless: current().browserHeadless ?? true,
|
|
130
|
+
replyTimeoutMs: current().replyTimeoutMs ?? 180_000,
|
|
131
|
+
dataDir: current().dataDir ?? '',
|
|
132
|
+
transferDistill: current().transferDistill ?? true,
|
|
133
|
+
transferProvider: current().transferProvider ?? '',
|
|
134
|
+
transferModel: current().transferModel ?? '',
|
|
135
|
+
transferMaxTokens: current().transferMaxTokens ?? 4_096,
|
|
136
|
+
transferChunkTokens: current().transferChunkTokens ?? 1_024,
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const distillConfigOf = (value: Config): DistillConfig => ({
|
|
140
|
+
distill: value.transferDistill ?? true,
|
|
141
|
+
provider: (value.transferProvider ?? '').trim(),
|
|
142
|
+
model: (value.transferModel ?? '').trim(),
|
|
143
|
+
maxTokens: value.transferMaxTokens ?? 4_096,
|
|
144
|
+
chunkTokens: value.transferChunkTokens ?? 1_024,
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const store = new TranscriptStore({ dataDir: resolve().dataDir !== '' ? resolve().dataDir : undefined })
|
|
148
|
+
const engine = new DeepSeekWebEngine(store, engineConfigOf(resolve))
|
|
149
|
+
ctx.effect(() => () => {
|
|
150
|
+
void engine.disposeBrowser()
|
|
151
|
+
}, 'dsh-webchat: engine')
|
|
152
|
+
|
|
153
|
+
const routes = makeRoutes({ ctx, engine, store, distill: distillConfigOf(resolve()) })
|
|
154
|
+
const listWorkspaces = (): WorkspaceRef[] | undefined => {
|
|
155
|
+
const registry = ctx.get('workspaceRegistry') as { list(): Array<{ id: string; path: string; title: string }> } | undefined
|
|
156
|
+
if (registry === undefined) return undefined
|
|
157
|
+
return registry.list().map(ws => ({ id: ws.id, path: ws.path, title: ws.title }))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const tools = [
|
|
161
|
+
webChatStatusTool(engine, store, listWorkspaces),
|
|
162
|
+
webChatSendTool(engine),
|
|
163
|
+
webChatRecoverTool(engine),
|
|
164
|
+
webChatImportTool(store),
|
|
165
|
+
webChatTransferTool(ctx, store, distillConfigOf(resolve())),
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
let disposeSection: (() => void) | undefined
|
|
169
|
+
let disposeRoutes: (() => void) | undefined
|
|
170
|
+
let disposeTools: (() => void) | undefined
|
|
171
|
+
|
|
172
|
+
// Register (or drop) every surface to match the current source. Each group
|
|
173
|
+
// is kept under one disposer: re-registering first tears the old one down
|
|
174
|
+
// so duplicate-name registrations never throw.
|
|
175
|
+
const sync = (): void => {
|
|
176
|
+
if (disposeSection !== undefined) { disposeSection(); disposeSection = undefined }
|
|
177
|
+
if (disposeRoutes !== undefined) { disposeRoutes(); disposeRoutes = undefined }
|
|
178
|
+
if (disposeTools !== undefined) { disposeTools(); disposeTools = undefined }
|
|
179
|
+
const value = resolve()
|
|
180
|
+
if (!value.enabled) return
|
|
181
|
+
if (value.announceToAgent) {
|
|
182
|
+
disposeSection = ctx.systemPrompt.section({
|
|
183
|
+
name: 'plugin:dsh-webchat',
|
|
184
|
+
order: SECTION_ORDER,
|
|
185
|
+
text: WEBCHAT_GUIDANCE,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
disposeRoutes = ctx.effect(
|
|
189
|
+
() => {
|
|
190
|
+
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
191
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
192
|
+
},
|
|
193
|
+
'dsh-webchat: routes',
|
|
194
|
+
)
|
|
195
|
+
disposeTools = ctx.effect(
|
|
196
|
+
() => {
|
|
197
|
+
const disposers = tools.map(tool => ctx.tools.register(tool))
|
|
198
|
+
return () => { for (const dispose of disposers) dispose() }
|
|
199
|
+
},
|
|
200
|
+
'dsh-webchat: tools',
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// dsh ≥ 0.1.7 owns settings forms itself: one per profile entry, addressed by
|
|
205
|
+
// the entry id, schema = this package's exported `Config`, and a form write
|
|
206
|
+
// re-applies the entry (profile `patchReload: live`). The retired
|
|
207
|
+
// installSettingsSection helper no longer exists in @deepseek-ai/dsh-settings
|
|
208
|
+
// (0.2.0 exports only SettingsForms / SettingsConflictError / redactSecrets),
|
|
209
|
+
// so the Loader-provided `config` above is the whole source of truth and one
|
|
210
|
+
// registration pass per apply is enough.
|
|
211
|
+
sync()
|
|
212
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared wire surface of the dsh-webchat plugin: API paths, transcript data
|
|
3
|
+
* model, and engine status views. Imported by both the host routes and the
|
|
4
|
+
* browser panel (the client bundle inlines it — no shared runtime identity).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** One transcript message (markdown text). */
|
|
8
|
+
export interface WebChatMessage {
|
|
9
|
+
/** Local stable id. */
|
|
10
|
+
readonly id: string
|
|
11
|
+
readonly role: 'user' | 'assistant'
|
|
12
|
+
/** Markdown body. */
|
|
13
|
+
readonly content: string
|
|
14
|
+
/** Unix epoch ms. */
|
|
15
|
+
readonly ts: number
|
|
16
|
+
/** True while the engine is still streaming this message. */
|
|
17
|
+
readonly streaming?: boolean
|
|
18
|
+
/** Set when the exchange failed (content may be partial). */
|
|
19
|
+
readonly error?: string
|
|
20
|
+
/** Local absolute paths of images attached to a user message. */
|
|
21
|
+
readonly attachments?: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One web chat session transcript (persisted locally). */
|
|
25
|
+
export interface WebChatTranscript {
|
|
26
|
+
readonly id: string
|
|
27
|
+
title: string
|
|
28
|
+
readonly createdAt: number
|
|
29
|
+
updatedAt: number
|
|
30
|
+
/** Model the chat currently runs on the web (deepseek-chat / deepseek-reasoner). */
|
|
31
|
+
model: string
|
|
32
|
+
readonly messages: WebChatMessage[]
|
|
33
|
+
/** True while an assistant reply is streaming. */
|
|
34
|
+
streaming: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Structured engine error codes so callers (panel / agent tools) can act on a
|
|
39
|
+
* failure instead of only printing it: NEED_LOGIN → prompt the login window;
|
|
40
|
+
* PAGE_CHANGED → the DeepSeek page/stream protocol moved and the plugin likely
|
|
41
|
+
* needs an upgrade; TIMEOUT → generation exceeded the reply window; NETWORK →
|
|
42
|
+
* browser launch / HTTP / connectivity failure.
|
|
43
|
+
*/
|
|
44
|
+
export type WebChatErrorCode = 'NEED_LOGIN' | 'PAGE_CHANGED' | 'TIMEOUT' | 'NETWORK'
|
|
45
|
+
|
|
46
|
+
/** Coarse engine state for the panel. */
|
|
47
|
+
export type EngineState = 'stopped' | 'launching' | 'ready' | 'error'
|
|
48
|
+
|
|
49
|
+
/** Snapshot the panel polls. */
|
|
50
|
+
export interface WebChatState {
|
|
51
|
+
readonly engine: EngineState
|
|
52
|
+
readonly engineError?: string
|
|
53
|
+
/** null = unknown yet; false = browser up but not logged in; true = chat page ready. */
|
|
54
|
+
readonly loggedIn: boolean | null
|
|
55
|
+
readonly pageUrl?: string
|
|
56
|
+
/** Deep-think (R1) toggle state read from the page. */
|
|
57
|
+
readonly deepThink: boolean
|
|
58
|
+
/** Internet-search toggle state read from the page. */
|
|
59
|
+
readonly search: boolean
|
|
60
|
+
readonly activeChatId?: string
|
|
61
|
+
readonly chats: WebChatTranscript[]
|
|
62
|
+
/** True while a message is being sent / replied (serialized engine busy). */
|
|
63
|
+
readonly busy: boolean
|
|
64
|
+
/** Last send error, transient for the panel to display. */
|
|
65
|
+
readonly lastError?: string
|
|
66
|
+
/** Structured code for the last error (so the panel can show a targeted action). */
|
|
67
|
+
readonly lastErrorCode?: WebChatErrorCode
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Engine-level operation results surfaced to agent tools. */
|
|
71
|
+
export interface SendResult {
|
|
72
|
+
readonly ok: boolean
|
|
73
|
+
readonly chatId?: string
|
|
74
|
+
readonly reply?: string
|
|
75
|
+
readonly error?: string
|
|
76
|
+
/** Structured error code (when ok is false) for actionable handling. */
|
|
77
|
+
readonly code?: WebChatErrorCode
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Result of transferring a transcript into harness mode. */
|
|
81
|
+
export interface TransferResult {
|
|
82
|
+
readonly ok: boolean
|
|
83
|
+
readonly sessionId?: string
|
|
84
|
+
readonly filePath?: string
|
|
85
|
+
/** True when the session was attached to a workspace (grouped); false = ungrouped. */
|
|
86
|
+
readonly attached?: boolean
|
|
87
|
+
/** Workspace id the session landed in, when attached. */
|
|
88
|
+
readonly workspaceId?: string
|
|
89
|
+
readonly error?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* How a transfer seeds the new harness session: 'distill' condenses the web
|
|
94
|
+
* conversation into an executable task brief via the harness LLM; 'raw'
|
|
95
|
+
* replays the full transcript verbatim. The web panel lets the user choose.
|
|
96
|
+
*/
|
|
97
|
+
export type TransferMode = 'distill' | 'raw'
|
|
98
|
+
|
|
99
|
+
/** API path constants shared by host routes and the browser panel. */
|
|
100
|
+
export const WEBChat_API = {
|
|
101
|
+
state: '/api/dsh-webchat/state',
|
|
102
|
+
openLogin: '/api/dsh-webchat/open-login',
|
|
103
|
+
closeBrowser: '/api/dsh-webchat/close-browser',
|
|
104
|
+
newChat: '/api/dsh-webchat/new-chat',
|
|
105
|
+
send: '/api/dsh-webchat/send',
|
|
106
|
+
stop: '/api/dsh-webchat/stop',
|
|
107
|
+
deepThink: '/api/dsh-webchat/deep-think',
|
|
108
|
+
search: '/api/dsh-webchat/search',
|
|
109
|
+
transfer: '/api/dsh-webchat/transfer',
|
|
110
|
+
exportFile: '/api/dsh-webchat/export',
|
|
111
|
+
renameChat: '/api/dsh-webchat/rename',
|
|
112
|
+
deleteChat: '/api/dsh-webchat/delete',
|
|
113
|
+
clearChats: '/api/dsh-webchat/clear',
|
|
114
|
+
webChats: '/api/dsh-webchat/web-chats',
|
|
115
|
+
recover: '/api/dsh-webchat/recover',
|
|
116
|
+
} as const
|