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/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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }