dsh-webseek 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +106 -2
- package/cordis.patch.yml +14 -0
- package/docs/images/import-dialog.png +0 -0
- package/docs/images/sidebar-entry.png +0 -0
- package/lib/client.js +924 -0
- package/lib/index.js +688 -0
- package/lib/share.js +161 -0
- package/package.json +67 -4
package/lib/share.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-webseek — share-link import, pure half.
|
|
3
|
+
*
|
|
4
|
+
* The web app can share a conversation as a public link
|
|
5
|
+
* (`https://chat.deepseek.com/share/<id>`). The page behind that link is a
|
|
6
|
+
* normal SPA that reads the conversation from one API:
|
|
7
|
+
*
|
|
8
|
+
* GET https://chat.deepseek.com/api/v0/share/content?share_id=<id>
|
|
9
|
+
* → { data: { biz_code, biz_data: { title, model_type, messages[] } } }
|
|
10
|
+
*
|
|
11
|
+
* The API needs no login — a shared conversation is public — but it does need
|
|
12
|
+
* a browser-shaped User-Agent (the edge answers a bare client with its own
|
|
13
|
+
* error page instead of JSON).
|
|
14
|
+
*
|
|
15
|
+
* Everything here is pure so it can be unit-tested offline: the id check, the
|
|
16
|
+
* main-line walk (a shared conversation is a *tree* of regenerated branches,
|
|
17
|
+
* and the web app shows only the first-child path), and the mapping onto DSH
|
|
18
|
+
* messages.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The share content endpoint the web app uses. */
|
|
22
|
+
export const SHARE_CONTENT_URL = 'https://chat.deepseek.com/api/v0/share/content'
|
|
23
|
+
|
|
24
|
+
/** The host that hosts share links; nothing else is accepted as a source. */
|
|
25
|
+
const SHARE_HOST = 'chat.deepseek.com'
|
|
26
|
+
|
|
27
|
+
/** What a share id may look like (the path segment of a share URL). */
|
|
28
|
+
const SHARE_ID_PATTERN = /^[A-Za-z0-9_-]{6,64}$/
|
|
29
|
+
|
|
30
|
+
/** Upper bound on a fetched share body; a shared conversation is far smaller. */
|
|
31
|
+
export const SHARE_BODY_LIMIT = 2 * 1024 * 1024
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pull the share id out of what the user pasted.
|
|
35
|
+
*
|
|
36
|
+
* Only two shapes are accepted: a bare id, or a `chat.deepseek.com/share/<id>`
|
|
37
|
+
* URL. The host and path of a pasted URL are never used to build the request
|
|
38
|
+
* — the request URL is always assembled from the fixed endpoint above, so a
|
|
39
|
+
* pasted link can never point the fetch somewhere else.
|
|
40
|
+
* @param {unknown} input - a share URL or a bare id.
|
|
41
|
+
* @returns {string|null} the share id, or null when the input is not a share link.
|
|
42
|
+
*/
|
|
43
|
+
export function parseShareId(input) {
|
|
44
|
+
if (typeof input !== 'string') return null
|
|
45
|
+
const text = input.trim()
|
|
46
|
+
if (text === '') return null
|
|
47
|
+
if (SHARE_ID_PATTERN.test(text)) return text
|
|
48
|
+
let url
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(text)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
if (url.hostname.toLowerCase() !== SHARE_HOST) return null
|
|
55
|
+
const parts = url.pathname.split('/').filter((part) => part !== '')
|
|
56
|
+
if (parts.length !== 2 || parts[0] !== 'share') return null
|
|
57
|
+
let id
|
|
58
|
+
try {
|
|
59
|
+
id = decodeURIComponent(parts[1])
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return null // a malformed escape is not a share link
|
|
62
|
+
}
|
|
63
|
+
return SHARE_ID_PATTERN.test(id) ? id : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The main line of a shared conversation.
|
|
68
|
+
*
|
|
69
|
+
* Shared messages form a tree: a regenerated answer is a *sibling* of the
|
|
70
|
+
* answer it replaced, linked through `parent_id`. The web app keeps the first
|
|
71
|
+
* child of every node (`currentChildIndex: 0`), so that is the line a reader
|
|
72
|
+
* sees — and the line worth importing. Walking it here keeps abandoned
|
|
73
|
+
* branches out and preserves the original order.
|
|
74
|
+
* @param {unknown[]} messages - `biz_data.messages`.
|
|
75
|
+
* @returns {object[]} the messages on the main line, in order.
|
|
76
|
+
*/
|
|
77
|
+
export function pickMainLine(messages) {
|
|
78
|
+
if (!Array.isArray(messages)) return []
|
|
79
|
+
const nodes = messages.filter((message) => message !== null && typeof message === 'object')
|
|
80
|
+
const children = new Map()
|
|
81
|
+
for (const message of nodes) {
|
|
82
|
+
const parent = message.parent_id === undefined || message.parent_id === null ? null : String(message.parent_id)
|
|
83
|
+
const key = parent === null ? '' : parent
|
|
84
|
+
const bucket = children.get(key)
|
|
85
|
+
if (bucket === undefined) children.set(key, [message])
|
|
86
|
+
else bucket.push(message)
|
|
87
|
+
}
|
|
88
|
+
const roots = children.get('') ?? []
|
|
89
|
+
if (roots.length === 0) return []
|
|
90
|
+
const line = []
|
|
91
|
+
const seen = new Set()
|
|
92
|
+
let current = roots[0]
|
|
93
|
+
while (current !== undefined && current !== null) {
|
|
94
|
+
const id = String(current.message_id)
|
|
95
|
+
if (seen.has(id)) break // a malformed share must not spin
|
|
96
|
+
seen.add(id)
|
|
97
|
+
line.push(current)
|
|
98
|
+
const bucket = children.get(id)
|
|
99
|
+
current = bucket === undefined ? undefined : bucket[0]
|
|
100
|
+
}
|
|
101
|
+
return line
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The text of one fragment type inside a message.
|
|
106
|
+
* @param {object} message - a share message.
|
|
107
|
+
* @param {string} type - the fragment type to collect.
|
|
108
|
+
* @returns {string} the joined text, trimmed.
|
|
109
|
+
*/
|
|
110
|
+
function fragmentText(message, type) {
|
|
111
|
+
const fragments = Array.isArray(message.fragments) ? message.fragments : []
|
|
112
|
+
return fragments
|
|
113
|
+
.filter((fragment) => fragment !== null && typeof fragment === 'object' && String(fragment.type).toUpperCase() === type)
|
|
114
|
+
.map((fragment) => (typeof fragment.content === 'string' ? fragment.content : ''))
|
|
115
|
+
.map((text) => text.trim())
|
|
116
|
+
.filter((text) => text !== '')
|
|
117
|
+
.join('\n\n')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Map the main line of a shared conversation onto DSH messages.
|
|
122
|
+
*
|
|
123
|
+
* A user turn is a `REQUEST` fragment, an assistant turn a `RESPONSE`
|
|
124
|
+
* fragment, and a `THINK` fragment is the model's reasoning — kept in front of
|
|
125
|
+
* the answer so the imported transcript still reads like the web one. Every
|
|
126
|
+
* other fragment type (files, tool calls, templates) has no DSH counterpart
|
|
127
|
+
* and is dropped.
|
|
128
|
+
* @param {unknown[]} messages - `biz_data.messages`.
|
|
129
|
+
* @returns {{role: 'user'|'assistant', text: string}[]} the importable messages.
|
|
130
|
+
*/
|
|
131
|
+
export function mapShareMessages(messages) {
|
|
132
|
+
const line = pickMainLine(messages)
|
|
133
|
+
const mapped = []
|
|
134
|
+
for (const message of line) {
|
|
135
|
+
const role = String(message.role ?? '').toUpperCase()
|
|
136
|
+
if (role === 'USER') {
|
|
137
|
+
const text = fragmentText(message, 'REQUEST')
|
|
138
|
+
if (text !== '') mapped.push({ role: 'user', text })
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
if (role === 'ASSISTANT') {
|
|
142
|
+
const thinking = fragmentText(message, 'THINK')
|
|
143
|
+
const answer = fragmentText(message, 'RESPONSE')
|
|
144
|
+
if (answer === '' && thinking === '') continue
|
|
145
|
+
const text = thinking === '' ? answer : answer === '' ? '[思考] ' + thinking : '[思考] ' + thinking + '\n\n' + answer
|
|
146
|
+
mapped.push({ role: 'assistant', text })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return mapped
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The title of a shared conversation, with a fallback.
|
|
154
|
+
* @param {unknown} title - `biz_data.title`.
|
|
155
|
+
* @returns {string} the title to give the imported session.
|
|
156
|
+
*/
|
|
157
|
+
export function shareTitle(title) {
|
|
158
|
+
if (typeof title !== 'string') return '导入的网页会话'
|
|
159
|
+
const trimmed = title.trim()
|
|
160
|
+
return trimmed === '' ? '导入的网页会话' : trimmed
|
|
161
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-webseek",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Imports a public DeepSeek share link as a real DSH session: one icon in the left sidebar, a dialog with the link and the target workspace, and a cold session with the same title.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"deepseek-harness",
|
|
8
|
+
"deepseek",
|
|
9
|
+
"share",
|
|
10
|
+
"import",
|
|
11
|
+
"sidebar",
|
|
12
|
+
"harness-plugin",
|
|
13
|
+
"plugin"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/HelloQingTao/dsh-webseek.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/HelloQingTao/dsh-webseek#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/HelloQingTao/dsh-webseek/issues"
|
|
22
|
+
},
|
|
23
|
+
"author": "HelloQingTao",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"registry": "https://registry.npmjs.org/"
|
|
28
|
+
},
|
|
29
|
+
"type": "module",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22",
|
|
32
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
33
|
+
},
|
|
34
|
+
"main": "lib/index.js",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"default": "./lib/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./client": {
|
|
40
|
+
"default": "./lib/client.js"
|
|
41
|
+
},
|
|
42
|
+
"./share": {
|
|
43
|
+
"default": "./lib/share.js"
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json"
|
|
46
|
+
},
|
|
47
|
+
"dsh": {
|
|
48
|
+
"bundle": {
|
|
49
|
+
"patch": "./cordis.patch.yml"
|
|
50
|
+
},
|
|
51
|
+
"client": {
|
|
52
|
+
"platform": "web",
|
|
53
|
+
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-workspace",
|
|
56
|
+
"@deepseek-ai/dsh-client-locale"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"lib/index.js",
|
|
62
|
+
"lib/client.js",
|
|
63
|
+
"lib/share.js",
|
|
64
|
+
"cordis.patch.yml",
|
|
65
|
+
"README.md",
|
|
66
|
+
"LICENSE",
|
|
67
|
+
"docs/images"
|
|
68
|
+
]
|
|
69
|
+
}
|