dsh-reference-anything 0.3.1 → 0.3.2
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 +22 -22
- package/NOTICE.md +52 -25
- package/README.md +334 -285
- package/cordis.patch.yml +89 -18
- package/lib/client.js +13530 -517
- package/lib/contract.d.ts +155 -3
- package/lib/contract.js +68 -4
- package/lib/download-directory.d.ts +18 -0
- package/lib/download-directory.js +55 -0
- package/lib/errors.d.ts +4 -0
- package/lib/host.d.ts +96 -3
- package/lib/host.js +88 -5
- package/lib/index.d.ts +22 -2
- package/lib/index.js +57 -7
- package/lib/opencli.d.ts +5 -3
- package/lib/opencli.js +13 -6
- package/lib/openlist/host.d.ts +157 -0
- package/lib/openlist/host.js +873 -0
- package/lib/openlist/index.d.ts +84 -0
- package/lib/openlist/index.js +507 -0
- package/lib/render.d.ts +12 -7
- package/lib/render.js +27 -21
- package/lib/sources/cloud-drive/cache.d.ts +60 -0
- package/lib/sources/cloud-drive/cache.js +107 -0
- package/lib/sources/cloud-drive/index.d.ts +190 -0
- package/lib/sources/cloud-drive/index.js +526 -0
- package/lib/sources/cloud-drive/providers/http.d.ts +27 -0
- package/lib/sources/cloud-drive/providers/http.js +73 -0
- package/lib/sources/cloud-drive/providers/openlist.d.ts +59 -0
- package/lib/sources/cloud-drive/providers/openlist.js +535 -0
- package/lib/sources/cloud-drive/registry.d.ts +53 -0
- package/lib/sources/cloud-drive/registry.js +101 -0
- package/lib/sources/cloud-drive/types.d.ts +169 -0
- package/lib/sources/cloud-drive/types.js +17 -0
- package/lib/sources/file.d.ts +2 -0
- package/lib/sources/file.js +2 -0
- package/lib/sources/local-agent/adapters/claude-code.d.ts +16 -0
- package/lib/sources/local-agent/adapters/claude-code.js +171 -0
- package/lib/sources/local-agent/adapters/codex.d.ts +20 -0
- package/lib/sources/local-agent/adapters/codex.js +204 -0
- package/lib/sources/local-agent/adapters/cursor.d.ts +23 -0
- package/lib/sources/local-agent/adapters/cursor.js +121 -0
- package/lib/sources/local-agent/adapters/gemini-cli.d.ts +20 -0
- package/lib/sources/local-agent/adapters/gemini-cli.js +141 -0
- package/lib/sources/local-agent/adapters/grokbuild.d.ts +22 -0
- package/lib/sources/local-agent/adapters/grokbuild.js +76 -0
- package/lib/sources/local-agent/adapters/hermes.d.ts +19 -0
- package/lib/sources/local-agent/adapters/hermes.js +85 -0
- package/lib/sources/local-agent/adapters/kimi.d.ts +28 -0
- package/lib/sources/local-agent/adapters/kimi.js +261 -0
- package/lib/sources/local-agent/adapters/openclaw.d.ts +21 -0
- package/lib/sources/local-agent/adapters/openclaw.js +120 -0
- package/lib/sources/local-agent/adapters/opencode.d.ts +44 -0
- package/lib/sources/local-agent/adapters/opencode.js +236 -0
- package/lib/sources/local-agent/adapters/pi.d.ts +26 -0
- package/lib/sources/local-agent/adapters/pi.js +242 -0
- package/lib/sources/local-agent/adapters/qoder.d.ts +19 -0
- package/lib/sources/local-agent/adapters/qoder.js +93 -0
- package/lib/sources/local-agent/adapters/reasonix.d.ts +23 -0
- package/lib/sources/local-agent/adapters/reasonix.js +90 -0
- package/lib/sources/local-agent/adapters/shared.d.ts +163 -0
- package/lib/sources/local-agent/adapters/shared.js +415 -0
- package/lib/sources/local-agent/adapters/sqlite-shared.d.ts +92 -0
- package/lib/sources/local-agent/adapters/sqlite-shared.js +215 -0
- package/lib/sources/local-agent/adapters/zcode.d.ts +41 -0
- package/lib/sources/local-agent/adapters/zcode.js +159 -0
- package/lib/sources/local-agent/index.d.ts +222 -0
- package/lib/sources/local-agent/index.js +656 -0
- package/lib/sources/local-agent/page.d.ts +55 -0
- package/lib/sources/local-agent/page.js +83 -0
- package/lib/sources/local-agent/path.d.ts +8 -0
- package/lib/sources/local-agent/path.js +43 -0
- package/lib/sources/local-agent/registry.d.ts +68 -0
- package/lib/sources/local-agent/registry.js +116 -0
- package/lib/sources/local-agent/scan.d.ts +175 -0
- package/lib/sources/local-agent/scan.js +472 -0
- package/lib/sources/local-agent/sqlite.d.ts +45 -0
- package/lib/sources/local-agent/sqlite.js +113 -0
- package/lib/sources/local-agent/store.d.ts +93 -0
- package/lib/sources/local-agent/store.js +113 -0
- package/lib/sources/local-agent/types.d.ts +338 -0
- package/lib/sources/local-agent/types.js +20 -0
- package/lib/sources/web-chat/index.d.ts +5 -1
- package/lib/sources/web-chat/index.js +47 -8
- package/lib/store/spec.d.ts +50 -0
- package/lib/store/spec.js +2 -2
- package/lib/sync/index.d.ts +1 -0
- package/lib/sync/index.js +28 -8
- package/lib/tool.d.ts +5 -0
- package/lib/tool.js +418 -45
- package/lib/typert.js +13 -1
- package/lib/types.d.ts +28 -2
- package/lib/update.d.ts +6 -0
- package/lib/update.js +29 -1
- package/lib/web.js +5 -1
- package/lib/wire.d.ts +80 -0
- package/lib/wire.js +30 -1
- package/opencli-plugin/chatgpt.js +30 -27
- package/opencli-plugin/claude.js +12 -8
- package/opencli-plugin/common.js +266 -187
- package/opencli-plugin/deepseek.js +8 -5
- package/opencli-plugin/gemini.js +42 -38
- package/opencli-plugin/grok.js +22 -21
- package/opencli-plugin/kimi.js +34 -31
- package/opencli-plugin/opencli-plugin.json +6 -6
- package/opencli-plugin/package.json +8 -8
- package/package.json +200 -185
package/opencli-plugin/common.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
-
import { createHash } from 'node:crypto'
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
3
|
import { dirname, resolve } from 'node:path'
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry'
|
|
5
5
|
import { sinceGuardSource } from './since-guard.js'
|
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
|
|
13
13
|
export { sinceGuardSource }
|
|
14
14
|
|
|
15
|
-
const HISTORY_COLUMNS = [
|
|
15
|
+
const HISTORY_COLUMNS = [
|
|
16
16
|
'provider', 'accountScope', 'id', 'title', 'url', 'createdAt', 'updatedAt',
|
|
17
17
|
'messageCount', 'cursor', 'partial',
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
const SYNC_INDEX_COLUMNS = ['kind', 'identity', 'sinceApplied', ...HISTORY_COLUMNS]
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
const SYNC_INDEX_COLUMNS = ['kind', 'identity', 'sinceApplied', ...HISTORY_COLUMNS]
|
|
21
21
|
|
|
22
22
|
const DETAIL_COLUMNS = [
|
|
23
23
|
'conversationId', 'ordinal', 'messageId', 'parentId', 'branchId',
|
|
@@ -38,7 +38,24 @@ async function evaluate(page, script, args, operation) {
|
|
|
38
38
|
return parseEvaluate(await page.evaluate(source), operation)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
/** Refuse an identity that does not match the account scope captured by sync. */
|
|
42
|
+
export function verifyAccountScope(provider, site, expectedScope, identity) {
|
|
43
|
+
if (!expectedScope) return
|
|
44
|
+
const actualScope = createHash('sha256').update(`${provider.toLocaleLowerCase()}\0${identity}`).digest('hex')
|
|
45
|
+
if (actualScope !== expectedScope) {
|
|
46
|
+
throw new CommandExecutionError(`DSH_ACCOUNT_SCOPE_MISMATCH: ${site} attachment: attachment belongs to a different logged-in account`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Preserve omission compatibility while rejecting malformed supplied scopes. */
|
|
51
|
+
export function parseExpectedAccountScope(value) {
|
|
52
|
+
if (value === undefined || value === null) return ''
|
|
53
|
+
const scope = String(value).trim()
|
|
54
|
+
if (!/^[a-f0-9]{64}$/.test(scope)) throw new ArgumentError('accountScope must be a 64-character lowercase hexadecimal hash')
|
|
55
|
+
return scope
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertResult(result, domain, operation) {
|
|
42
59
|
if (result?.code === 'AUTH') throw new AuthRequiredError(domain, result.message || `${operation} requires login`)
|
|
43
60
|
if (result?.code === 'RATE_LIMIT') {
|
|
44
61
|
throw new CommandExecutionError(`${operation}: ${result.message || 'provider rate limit reached'}`)
|
|
@@ -46,74 +63,85 @@ function assertResult(result, domain, operation) {
|
|
|
46
63
|
if (result?.ok !== true || !Array.isArray(result.rows)) {
|
|
47
64
|
throw new CommandExecutionError(`${operation}: ${result?.message || 'provider adapter did not return rows'}`)
|
|
48
65
|
}
|
|
49
|
-
return result.rows
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
66
|
+
return result.rows
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertIdentityResult(result, domain, operation) {
|
|
70
|
+
if (result?.code === 'AUTH') throw new AuthRequiredError(domain, result.message)
|
|
71
|
+
if (result?.code === 'RATE_LIMIT') {
|
|
72
|
+
throw new CommandExecutionError(`DSH_PROVIDER_RATE_LIMIT: ${operation}: ${result.message || 'provider rate limit reached'}`)
|
|
73
|
+
}
|
|
74
|
+
if (result?.ok !== true || typeof result.identity !== 'string' || !result.identity) {
|
|
75
|
+
throw new CommandExecutionError(`${operation}: ${result?.message || 'stable account identity unavailable'}`)
|
|
76
|
+
}
|
|
77
|
+
return result.identity
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const PROVIDER_READY_TIMEOUT_MS = 1200
|
|
81
|
+
const PROVIDER_READY_INITIAL_DELAY_MS = 50
|
|
82
|
+
const PROVIDER_READY_MAX_DELAY_MS = 400
|
|
83
|
+
|
|
84
|
+
function terminalProviderResult(result) {
|
|
85
|
+
return result?.ok === true || result?.code === 'AUTH' || result?.code === 'RATE_LIMIT'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function terminalIdentityResult(result) {
|
|
89
|
+
return result?.code === 'AUTH' || result?.code === 'RATE_LIMIT'
|
|
90
|
+
|| (result?.ok === true && typeof result.identity === 'string' && result.identity.length > 0)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run the provider API as the readiness probe. The happy path performs one
|
|
95
|
+
* evaluate immediately after navigation; transient bootstrap failures retry
|
|
96
|
+
* with bounded exponential backoff instead of waiting for the whole SPA DOM
|
|
97
|
+
* to become quiet first.
|
|
98
|
+
*/
|
|
99
|
+
export async function evaluateWhenProviderReady(page, script, args, operation, options = {}) {
|
|
100
|
+
const timeoutMs = options.timeoutMs ?? PROVIDER_READY_TIMEOUT_MS
|
|
101
|
+
const maxDelayMs = options.maxDelayMs ?? PROVIDER_READY_MAX_DELAY_MS
|
|
102
|
+
const accepts = options.accepts ?? terminalProviderResult
|
|
103
|
+
const deadline = Date.now() + timeoutMs
|
|
104
|
+
let delayMs = options.initialDelayMs ?? PROVIDER_READY_INITIAL_DELAY_MS
|
|
105
|
+
let lastResult
|
|
106
|
+
let lastError
|
|
107
|
+
for (;;) {
|
|
108
|
+
try {
|
|
109
|
+
lastResult = await evaluate(page, script, args, operation)
|
|
110
|
+
lastError = undefined
|
|
111
|
+
if (accepts(lastResult)) return lastResult
|
|
112
|
+
} catch (error) {
|
|
113
|
+
lastError = error
|
|
114
|
+
}
|
|
115
|
+
const remaining = deadline - Date.now()
|
|
116
|
+
if (remaining <= 0) {
|
|
117
|
+
if (lastError) throw lastError
|
|
118
|
+
return lastResult
|
|
119
|
+
}
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(delayMs, remaining)))
|
|
121
|
+
delayMs = Math.min(Math.max(delayMs * 2, 1), maxDelayMs)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Each command owns an ephemeral Browser Bridge tab. Close that tab after the
|
|
127
|
+
* command finishes while preserving the provider result or failure.
|
|
128
|
+
*/
|
|
129
|
+
async function inTemporaryTab(page, task) {
|
|
130
|
+
try {
|
|
131
|
+
return await task()
|
|
132
|
+
} finally {
|
|
133
|
+
try {
|
|
134
|
+
await page.closeTab?.()
|
|
135
|
+
} catch {
|
|
136
|
+
// Preserve the provider result/error if the tab or browser closed first.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
115
140
|
|
|
116
|
-
|
|
141
|
+
export function registerProvider(config) {
|
|
142
|
+
const browserSession = SYNC_BROWSER_SESSION
|
|
143
|
+
|
|
144
|
+
cli({
|
|
117
145
|
site: config.site,
|
|
118
146
|
name: 'whoami',
|
|
119
147
|
access: 'read',
|
|
@@ -124,61 +152,54 @@ export function registerProvider(config) {
|
|
|
124
152
|
...browserSession,
|
|
125
153
|
navigateBefore: false,
|
|
126
154
|
args: [],
|
|
127
|
-
columns: ['identity'],
|
|
128
|
-
func: async (page) => inTemporaryTab(page, async () => {
|
|
129
|
-
await page.goto(config.home, { waitUntil: 'none' })
|
|
130
|
-
const result = await evaluateWhenProviderReady(
|
|
131
|
-
page, config.whoamiScript, {}, `${config.site} whoami`, { accepts: terminalIdentityResult },
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
{ kind: 'identity', identity: identity.identity, sinceApplied: effectiveSince },
|
|
176
|
-
...rows.map(row => ({ kind: 'conversation', identity: '', sinceApplied: '', ...row })),
|
|
177
|
-
]
|
|
178
|
-
}),
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
cli({
|
|
155
|
+
columns: ['identity'],
|
|
156
|
+
func: async (page) => inTemporaryTab(page, async () => {
|
|
157
|
+
await page.goto(config.home, { waitUntil: 'none' })
|
|
158
|
+
const result = await evaluateWhenProviderReady(
|
|
159
|
+
page, config.whoamiScript, {}, `${config.site} whoami`, { accepts: terminalIdentityResult },
|
|
160
|
+
)
|
|
161
|
+
return [{ identity: assertIdentityResult(result, config.domain, `${config.site} whoami`) }]
|
|
162
|
+
}),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
cli({
|
|
166
|
+
site: config.site,
|
|
167
|
+
name: 'sync-index',
|
|
168
|
+
access: 'read',
|
|
169
|
+
description: `Resolve the ${config.provider} account and list its conversation history in one browser session`,
|
|
170
|
+
domain: config.domain,
|
|
171
|
+
strategy: Strategy.COOKIE,
|
|
172
|
+
browser: true,
|
|
173
|
+
...browserSession,
|
|
174
|
+
navigateBefore: false,
|
|
175
|
+
args: [{
|
|
176
|
+
name: 'since',
|
|
177
|
+
help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
|
|
178
|
+
}, { name: 'accountScope', help: 'Previously observed hashed account scope; mismatch forces a full listing' }],
|
|
179
|
+
columns: SYNC_INDEX_COLUMNS,
|
|
180
|
+
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
181
|
+
const since = String(kwargs.since || '').trim()
|
|
182
|
+
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
183
|
+
await page.goto(config.home, { waitUntil: 'none' })
|
|
184
|
+
const identity = await evaluateWhenProviderReady(
|
|
185
|
+
page, config.whoamiScript, {}, `${config.site} sync-index identity`, { accepts: terminalIdentityResult },
|
|
186
|
+
)
|
|
187
|
+
const stableIdentity = assertIdentityResult(identity, config.domain, `${config.site} sync-index`)
|
|
188
|
+
const accountScope = createHash('sha256').update(`${config.provider.toLocaleLowerCase()}\0${stableIdentity}`).digest('hex')
|
|
189
|
+
const effectiveSince = String(kwargs.accountScope || '') === accountScope ? since : ''
|
|
190
|
+
const rows = assertResult(
|
|
191
|
+
await evaluate(page, config.historyScript, { since: effectiveSince }, `${config.site} sync-index history`),
|
|
192
|
+
config.domain,
|
|
193
|
+
`${config.site} sync-index`,
|
|
194
|
+
)
|
|
195
|
+
return [
|
|
196
|
+
{ kind: 'identity', identity: stableIdentity, sinceApplied: effectiveSince },
|
|
197
|
+
...rows.map(row => ({ kind: 'conversation', identity: '', sinceApplied: '', ...row })),
|
|
198
|
+
]
|
|
199
|
+
}),
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
cli({
|
|
182
203
|
site: config.site,
|
|
183
204
|
name: 'history-all',
|
|
184
205
|
access: 'read',
|
|
@@ -193,17 +214,17 @@ export function registerProvider(config) {
|
|
|
193
214
|
help: 'ISO 8601 instant; stop paging once the listing predates it. Honoured only while the provider pages newest-first',
|
|
194
215
|
}],
|
|
195
216
|
columns: HISTORY_COLUMNS,
|
|
196
|
-
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
197
|
-
const since = String(kwargs.since || '').trim()
|
|
198
|
-
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
199
|
-
await page.goto(config.home, { waitUntil: 'none' })
|
|
200
|
-
const rows = assertResult(
|
|
201
|
-
await evaluateWhenProviderReady(page, config.historyScript, { since }, `${config.site} history-all`),
|
|
202
|
-
config.domain,
|
|
203
|
-
`${config.site} history-all`,
|
|
204
|
-
)
|
|
205
|
-
return rows
|
|
206
|
-
}),
|
|
217
|
+
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
218
|
+
const since = String(kwargs.since || '').trim()
|
|
219
|
+
if (since && Number.isNaN(Date.parse(since))) throw new ArgumentError('since must be an ISO 8601 instant')
|
|
220
|
+
await page.goto(config.home, { waitUntil: 'none' })
|
|
221
|
+
const rows = assertResult(
|
|
222
|
+
await evaluateWhenProviderReady(page, config.historyScript, { since }, `${config.site} history-all`),
|
|
223
|
+
config.domain,
|
|
224
|
+
`${config.site} history-all`,
|
|
225
|
+
)
|
|
226
|
+
return rows
|
|
227
|
+
}),
|
|
207
228
|
})
|
|
208
229
|
|
|
209
230
|
cli({
|
|
@@ -216,36 +237,33 @@ export function registerProvider(config) {
|
|
|
216
237
|
browser: true,
|
|
217
238
|
...browserSession,
|
|
218
239
|
navigateBefore: false,
|
|
219
|
-
args: [
|
|
220
|
-
{ name: 'id', positional: true, required: true, help: 'Provider conversation id' },
|
|
221
|
-
{ name: 'accountScope', help: 'Expected hashed account scope; mismatch refuses the read' },
|
|
222
|
-
],
|
|
240
|
+
args: [
|
|
241
|
+
{ name: 'id', positional: true, required: true, help: 'Provider conversation id' },
|
|
242
|
+
{ name: 'accountScope', help: 'Expected hashed account scope; mismatch refuses the read' },
|
|
243
|
+
],
|
|
223
244
|
columns: DETAIL_COLUMNS,
|
|
224
|
-
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
225
|
-
const id = String(kwargs.id || '').trim()
|
|
226
|
-
if (!id) throw new ArgumentError('id must be a non-empty conversation id')
|
|
227
|
-
await page.goto(config.home, { settleMs: 600 })
|
|
228
|
-
const expectedScope =
|
|
229
|
-
if (expectedScope) {
|
|
230
|
-
const identity = await evaluate(page, config.whoamiScript, {}, `${config.site} detail identity`)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
let result = await evaluate(page, config.detailScript, { id }, `${config.site} detail`)
|
|
245
|
+
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
246
|
+
const id = String(kwargs.id || '').trim()
|
|
247
|
+
if (!id) throw new ArgumentError('id must be a non-empty conversation id')
|
|
248
|
+
await page.goto(config.home, { settleMs: 600 })
|
|
249
|
+
const expectedScope = parseExpectedAccountScope(kwargs.accountScope)
|
|
250
|
+
if (expectedScope) {
|
|
251
|
+
const identity = await evaluate(page, config.whoamiScript, {}, `${config.site} detail identity`)
|
|
252
|
+
const stableIdentity = assertIdentityResult(identity, config.domain, `${config.site} detail`)
|
|
253
|
+
const actualScope = createHash('sha256').update(`${config.provider.toLocaleLowerCase()}\0${stableIdentity}`).digest('hex')
|
|
254
|
+
if (actualScope !== expectedScope) {
|
|
255
|
+
throw new CommandExecutionError(`DSH_ACCOUNT_SCOPE_MISMATCH: ${config.site} detail: conversation belongs to a different logged-in account`)
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
let result = await evaluate(page, config.detailScript, { id }, `${config.site} detail`)
|
|
241
259
|
if (result?.ok !== true && result?.code !== 'AUTH' && result?.code !== 'RATE_LIMIT' && config.fallbackScript) {
|
|
242
260
|
await page.goto(config.conversationUrl(id), { settleMs: 1800 })
|
|
243
261
|
result = await evaluate(page, config.fallbackScript, { id }, `${config.site} detail fallback`)
|
|
244
262
|
}
|
|
245
|
-
const rows = assertResult(result, config.domain, `${config.site} detail`)
|
|
246
|
-
if (rows.length === 0) throw new EmptyResultError(`${config.site} detail`, `No messages found for ${id}`)
|
|
247
|
-
return rows
|
|
248
|
-
}),
|
|
263
|
+
const rows = assertResult(result, config.domain, `${config.site} detail`)
|
|
264
|
+
if (rows.length === 0) throw new EmptyResultError(`${config.site} detail`, `No messages found for ${id}`)
|
|
265
|
+
return rows
|
|
266
|
+
}),
|
|
249
267
|
})
|
|
250
268
|
|
|
251
269
|
cli({
|
|
@@ -262,10 +280,11 @@ export function registerProvider(config) {
|
|
|
262
280
|
{ name: 'locator', positional: true, required: true, help: 'Stable same-origin attachment locator' },
|
|
263
281
|
{ name: 'output', required: true, help: 'Absolute output file path' },
|
|
264
282
|
{ name: 'maxBytes', type: 'int', default: 26214400, help: 'Maximum decoded bytes' },
|
|
283
|
+
{ name: 'accountScope', help: 'Expected hashed account scope; mismatch refuses the download' },
|
|
265
284
|
],
|
|
266
285
|
columns: ['attachmentId', 'name', 'mimeType', 'size', 'status', 'localPath'],
|
|
267
|
-
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
268
|
-
const locator = String(kwargs.locator || '').trim()
|
|
286
|
+
func: async (page, kwargs) => inTemporaryTab(page, async () => {
|
|
287
|
+
const locator = String(kwargs.locator || '').trim()
|
|
269
288
|
const output = resolve(String(kwargs.output || ''))
|
|
270
289
|
const maxBytes = Number(kwargs.maxBytes ?? 26_214_400)
|
|
271
290
|
if (!locator) throw new ArgumentError('locator must not be empty')
|
|
@@ -273,17 +292,30 @@ export function registerProvider(config) {
|
|
|
273
292
|
throw new ArgumentError('maxBytes must be between 1 and 26214400')
|
|
274
293
|
}
|
|
275
294
|
await page.goto(config.home, { settleMs: 500 })
|
|
295
|
+
const expectedScope = parseExpectedAccountScope(kwargs.accountScope)
|
|
296
|
+
if (expectedScope) {
|
|
297
|
+
const identity = await evaluateWhenProviderReady(
|
|
298
|
+
page, config.whoamiScript, {}, `${config.site} attachment identity`, { accepts: terminalIdentityResult },
|
|
299
|
+
)
|
|
300
|
+
const stableIdentity = assertIdentityResult(identity, config.domain, `${config.site} attachment`)
|
|
301
|
+
verifyAccountScope(config.provider, config.site, expectedScope, stableIdentity)
|
|
302
|
+
}
|
|
276
303
|
const result = await evaluate(page, ATTACHMENT_SCRIPT, {
|
|
277
304
|
locator,
|
|
278
305
|
origin: new URL(config.home).origin,
|
|
279
306
|
maxBytes,
|
|
280
307
|
}, `${config.site} attachment`)
|
|
281
308
|
if (result?.code === 'AUTH') throw new AuthRequiredError(config.domain)
|
|
309
|
+
if (result?.code === 'TOO_LARGE') {
|
|
310
|
+
throw new CommandExecutionError(`DSH_ATTACHMENT_TOO_LARGE: ${config.site} attachment exceeds maxBytes`)
|
|
311
|
+
}
|
|
282
312
|
if (result?.ok !== true || typeof result.base64 !== 'string') {
|
|
283
313
|
throw new CommandExecutionError(`${config.site} attachment: ${result?.message || 'attachment unavailable'}`)
|
|
284
314
|
}
|
|
285
315
|
const bytes = Buffer.from(result.base64, 'base64')
|
|
286
|
-
if (bytes.byteLength > maxBytes)
|
|
316
|
+
if (bytes.byteLength > maxBytes) {
|
|
317
|
+
throw new CommandExecutionError(`DSH_ATTACHMENT_TOO_LARGE: ${config.site} attachment exceeds maxBytes`)
|
|
318
|
+
}
|
|
287
319
|
await mkdir(dirname(output), { recursive: true })
|
|
288
320
|
await writeFile(output, bytes)
|
|
289
321
|
return [{
|
|
@@ -291,30 +323,77 @@ export function registerProvider(config) {
|
|
|
291
323
|
name: result.name || output.split(/[\\/]/).pop() || 'attachment',
|
|
292
324
|
mimeType: result.mimeType || 'application/octet-stream',
|
|
293
325
|
size: bytes.byteLength,
|
|
294
|
-
status: 'available',
|
|
295
|
-
localPath: output,
|
|
296
|
-
}]
|
|
297
|
-
}),
|
|
298
|
-
})
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/** Browser Bridge lifecycle shared by every web-chat command. */
|
|
302
|
-
export const SYNC_BROWSER_SESSION = Object.freeze({
|
|
303
|
-
siteSession: 'ephemeral',
|
|
304
|
-
defaultWindowMode: 'background',
|
|
305
|
-
})
|
|
326
|
+
status: 'available',
|
|
327
|
+
localPath: output,
|
|
328
|
+
}]
|
|
329
|
+
}),
|
|
330
|
+
})
|
|
331
|
+
}
|
|
306
332
|
|
|
307
|
-
|
|
333
|
+
/** Browser Bridge lifecycle shared by every web-chat command. */
|
|
334
|
+
export const SYNC_BROWSER_SESSION = Object.freeze({
|
|
335
|
+
siteSession: 'ephemeral',
|
|
336
|
+
defaultWindowMode: 'background',
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
export const ATTACHMENT_SCRIPT = String.raw`async function (args) {
|
|
308
340
|
try {
|
|
341
|
+
const cancelBody = async body => {
|
|
342
|
+
try { await body?.cancel?.() } catch {}
|
|
343
|
+
}
|
|
309
344
|
const target = new URL(args.locator, args.origin)
|
|
310
345
|
if (target.origin !== args.origin) return JSON.stringify({ ok: false, message: 'cross-origin locator refused' })
|
|
311
|
-
const response = await fetch(target.href, { credentials: 'include' })
|
|
312
|
-
if (response.
|
|
313
|
-
|
|
346
|
+
const response = await fetch(target.href, { credentials: 'include', redirect: 'error' })
|
|
347
|
+
if (response.url && new URL(response.url).origin !== args.origin) {
|
|
348
|
+
await cancelBody(response.body)
|
|
349
|
+
return JSON.stringify({ ok: false, message: 'cross-origin redirect refused' })
|
|
350
|
+
}
|
|
351
|
+
if (response.status === 401 || response.status === 403) {
|
|
352
|
+
await cancelBody(response.body)
|
|
353
|
+
return JSON.stringify({ ok: false, code: 'AUTH' })
|
|
354
|
+
}
|
|
355
|
+
if (!response.ok) {
|
|
356
|
+
await cancelBody(response.body)
|
|
357
|
+
return JSON.stringify({ ok: false, message: 'attachment HTTP ' + response.status })
|
|
358
|
+
}
|
|
314
359
|
const length = Number(response.headers.get('content-length') || 0)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
360
|
+
const declaredLength = Number.isFinite(length) && length > 0 ? length : 0
|
|
361
|
+
if (declaredLength > args.maxBytes) {
|
|
362
|
+
await cancelBody(response.body)
|
|
363
|
+
return JSON.stringify({ ok: false, code: 'TOO_LARGE', message: 'attachment exceeds maxBytes' })
|
|
364
|
+
}
|
|
365
|
+
const chunks = []
|
|
366
|
+
let total = 0
|
|
367
|
+
if (response.body) {
|
|
368
|
+
const reader = response.body.getReader()
|
|
369
|
+
try {
|
|
370
|
+
for (;;) {
|
|
371
|
+
const { done, value } = await reader.read()
|
|
372
|
+
if (done) break
|
|
373
|
+
const chunk = value instanceof Uint8Array ? value : new Uint8Array(value)
|
|
374
|
+
if (total + chunk.byteLength > args.maxBytes) {
|
|
375
|
+
try { await reader.cancel() } catch {}
|
|
376
|
+
return JSON.stringify({ ok: false, code: 'TOO_LARGE', message: 'attachment exceeds maxBytes' })
|
|
377
|
+
}
|
|
378
|
+
chunks.push(chunk)
|
|
379
|
+
total += chunk.byteLength
|
|
380
|
+
}
|
|
381
|
+
} catch (error) {
|
|
382
|
+
try { await reader.cancel() } catch {}
|
|
383
|
+
throw error
|
|
384
|
+
} finally {
|
|
385
|
+
try { reader.releaseLock?.() } catch {}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (declaredLength && total < declaredLength) {
|
|
389
|
+
return JSON.stringify({ ok: false, message: 'attachment download was truncated' })
|
|
390
|
+
}
|
|
391
|
+
const bytes = new Uint8Array(total)
|
|
392
|
+
let offset = 0
|
|
393
|
+
for (const chunk of chunks) {
|
|
394
|
+
bytes.set(chunk, offset)
|
|
395
|
+
offset += chunk.byteLength
|
|
396
|
+
}
|
|
318
397
|
let binary = ''
|
|
319
398
|
for (let at = 0; at < bytes.length; at += 0x8000) {
|
|
320
399
|
binary += String.fromCharCode(...bytes.subarray(at, Math.min(bytes.length, at + 0x8000)))
|
|
@@ -121,10 +121,7 @@ const detailScript = String.raw`async function (args) {
|
|
|
121
121
|
}
|
|
122
122
|
}`
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
site: 'dsh-deepseek', provider: 'DeepSeek', domain: 'chat.deepseek.com',
|
|
126
|
-
home: 'https://chat.deepseek.com/', conversationUrl: id => `https://chat.deepseek.com/a/chat/s/${encodeURIComponent(id)}`,
|
|
127
|
-
whoamiScript: String.raw`async function () {
|
|
124
|
+
export const whoamiScript = String.raw`async function () {
|
|
128
125
|
const headers = (() => {
|
|
129
126
|
try {
|
|
130
127
|
const stored = JSON.parse(localStorage.getItem('userToken') || 'null')
|
|
@@ -135,6 +132,7 @@ registerProvider({
|
|
|
135
132
|
for (const url of ['/api/v0/users/current', '/api/v0/user/profile']) {
|
|
136
133
|
const response = await fetch(url, { credentials: 'include', headers })
|
|
137
134
|
if (response.status === 401 || response.status === 403) return JSON.stringify({ ok: false, code: 'AUTH' })
|
|
135
|
+
if (response.status === 429) return JSON.stringify({ ok: false, code: 'RATE_LIMIT' })
|
|
138
136
|
if (!response.ok) continue
|
|
139
137
|
const contentType = response.headers.get('content-type') || ''
|
|
140
138
|
if (!contentType.includes('application/json')) continue
|
|
@@ -147,6 +145,11 @@ registerProvider({
|
|
|
147
145
|
if (identity) return JSON.stringify({ ok: true, identity })
|
|
148
146
|
}
|
|
149
147
|
return JSON.stringify({ ok: false, code: 'AUTH' })
|
|
150
|
-
}
|
|
148
|
+
}`
|
|
149
|
+
|
|
150
|
+
registerProvider({
|
|
151
|
+
site: 'dsh-deepseek', provider: 'DeepSeek', domain: 'chat.deepseek.com',
|
|
152
|
+
home: 'https://chat.deepseek.com/', conversationUrl: id => `https://chat.deepseek.com/a/chat/s/${encodeURIComponent(id)}`,
|
|
153
|
+
whoamiScript,
|
|
151
154
|
historyScript, detailScript, fallbackScript: domFallbackScript,
|
|
152
155
|
})
|