@ponydev/dsh-phone 0.0.0-stage → 0.2.1
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/AGENTS.md +77 -0
- package/CHANGELOG.md +52 -0
- package/CONTRIBUTING.md +53 -0
- package/LICENSE +21 -0
- package/README.md +153 -2
- package/RELEASE.md +65 -0
- package/cordis.patch.yml +9 -0
- package/docs/DESIGN.md +248 -0
- package/docs/mockups/device.png +0 -0
- package/docs/mockups/list.png +0 -0
- package/docs/mockups/mockup.html +384 -0
- package/docs/screenshots/01-devices.png +0 -0
- package/docs/screenshots/02-preview.png +0 -0
- package/docs/screenshots/03-overview.png +0 -0
- package/lib/adb.js +400 -0
- package/lib/client.js +1652 -0
- package/lib/index.js +184 -0
- package/lib/stream.js +111 -0
- package/lib/targets.js +55 -0
- package/lib/transport.js +55 -0
- package/package.json +65 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// dsh-phone · Host 半区
|
|
2
|
+
// - 挂 /phone 前缀路由(webServer.register + connection.admit 认证围栏)
|
|
3
|
+
// - 纯 JSON 协议:POST /phone/<endpoint> → { ok: true, value } | { ok: false, error }
|
|
4
|
+
// - 截图例外:成功时直接回 image/png 原始字节
|
|
5
|
+
// - 不暴露任意 adb shell;serial 必须在最新 adb devices 列表里
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { homedir } from 'node:os'
|
|
8
|
+
import { createTransports } from './transport.js'
|
|
9
|
+
import { createTargetStore } from './targets.js'
|
|
10
|
+
import { STREAM_QUALITY, createStreamer, streamSizeFor } from './stream.js'
|
|
11
|
+
|
|
12
|
+
export const name = 'dsh-phone'
|
|
13
|
+
|
|
14
|
+
const DEFAULTS = {
|
|
15
|
+
adbPath: 'adb',
|
|
16
|
+
pollMs: 700,
|
|
17
|
+
streamTimeLimit: 180,
|
|
18
|
+
streamBitRate: 6_000_000,
|
|
19
|
+
streamSize: '540x1200',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function messageOf(error) {
|
|
23
|
+
const text = error && error.message ? String(error.message) : '未知错误'
|
|
24
|
+
return text.length > 400 ? text.slice(0, 400) : text
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function failOf(error) {
|
|
28
|
+
const code = error && typeof error.code === 'string' ? error.code : 'phone/error'
|
|
29
|
+
return { ok: false, error: { code, message: messageOf(error) } }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readBody(req, cap = 1024 * 1024) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const chunks = []
|
|
35
|
+
let size = 0
|
|
36
|
+
req.on('data', (chunk) => {
|
|
37
|
+
size += chunk.length
|
|
38
|
+
if (size > cap) {
|
|
39
|
+
reject(new Error('request body too large'))
|
|
40
|
+
req.destroy()
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
chunks.push(chunk)
|
|
44
|
+
})
|
|
45
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')))
|
|
46
|
+
req.on('error', reject)
|
|
47
|
+
})
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function json(res, code, payload) {
|
|
51
|
+
res.writeHead(code, {
|
|
52
|
+
'content-type': 'application/json; charset=utf-8',
|
|
53
|
+
'cache-control': 'no-store',
|
|
54
|
+
})
|
|
55
|
+
res.end(JSON.stringify(payload))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function apply(ctx, rawConfig) {
|
|
59
|
+
const config = { ...DEFAULTS, ...(rawConfig || {}) }
|
|
60
|
+
const { adb } = createTransports({ adbPath: config.adbPath })
|
|
61
|
+
const home = process.env.DSH_HOME && process.env.DSH_HOME.trim() ? process.env.DSH_HOME.trim() : join(homedir(), '.dsh')
|
|
62
|
+
const targets = createTargetStore(join(home, 'phone', 'targets.json'))
|
|
63
|
+
const streamer = createStreamer(adb, {
|
|
64
|
+
timeLimit: config.streamTimeLimit,
|
|
65
|
+
bitRate: config.streamBitRate,
|
|
66
|
+
size: config.streamSize,
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const ok = (value) => ({ ok: true, value })
|
|
70
|
+
|
|
71
|
+
async function handle(endpoint, payload) {
|
|
72
|
+
switch (endpoint) {
|
|
73
|
+
case 'health': {
|
|
74
|
+
try {
|
|
75
|
+
const info = await adb.resolve()
|
|
76
|
+
const { devices } = await adb.devices()
|
|
77
|
+
return ok({ adb: info, devices: devices.length })
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return ok({ adb: null, devices: 0, error: messageOf(error) })
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
case 'devices': {
|
|
83
|
+
const { devices, at } = await adb.devices({ fresh: true })
|
|
84
|
+
return ok({ devices, at })
|
|
85
|
+
}
|
|
86
|
+
case 'connect': {
|
|
87
|
+
const result = await adb.connect(String(payload.address || ''))
|
|
88
|
+
await targets.record(result.address)
|
|
89
|
+
return ok(result)
|
|
90
|
+
}
|
|
91
|
+
case 'targets':
|
|
92
|
+
return ok({ targets: await targets.list() })
|
|
93
|
+
case 'targets/remove':
|
|
94
|
+
return ok({ removed: await targets.remove(String(payload.address || '')) })
|
|
95
|
+
case 'screen-info':
|
|
96
|
+
return ok(await adb.screenInfo(String(payload.serial || '')))
|
|
97
|
+
case 'disconnect':
|
|
98
|
+
return ok(await adb.disconnect(String(payload.serial || '')))
|
|
99
|
+
case 'input':
|
|
100
|
+
return ok(await adb.input(String(payload.serial || ''), payload.event || {}))
|
|
101
|
+
default:
|
|
102
|
+
return { ok: false, error: { code: 'phone/not-found', message: `未知接口: ${endpoint || '(empty)'}` } }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
ctx.inject(['connection', 'webServer'], (scope) => {
|
|
107
|
+
scope.effect(
|
|
108
|
+
() =>
|
|
109
|
+
scope.webServer.register({
|
|
110
|
+
kind: 'prefix',
|
|
111
|
+
path: '/phone',
|
|
112
|
+
handler: async (req, res) => {
|
|
113
|
+
const admission = scope.connection.admit(req)
|
|
114
|
+
if ('rejection' in admission) {
|
|
115
|
+
res.writeHead(admission.rejection)
|
|
116
|
+
res.end(admission.rejection === 401 ? 'unauthorized' : 'forbidden')
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
const pathname = String(req.url || '/').split('?')[0]
|
|
120
|
+
const endpoint = pathname.replace(/^\/phone\/?/, '').replace(/\/+$/, '')
|
|
121
|
+
if (req.method !== 'POST') {
|
|
122
|
+
json(res, 405, { ok: false, error: { code: 'phone/method', message: '只接受 POST' } })
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
let payload = {}
|
|
126
|
+
try {
|
|
127
|
+
payload = JSON.parse((await readBody(req)) || '{}')
|
|
128
|
+
} catch {
|
|
129
|
+
json(res, 400, { ok: false, error: { code: 'phone/bad-body', message: '请求体不是 JSON' } })
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
if (endpoint === 'stream') {
|
|
133
|
+
const serial = String(payload.serial || '')
|
|
134
|
+
try {
|
|
135
|
+
await adb.assertReady(serial)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
json(res, 200, failOf(error))
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
res.writeHead(200, {
|
|
141
|
+
'content-type': 'video/h264',
|
|
142
|
+
'cache-control': 'no-store',
|
|
143
|
+
'x-content-type-options': 'nosniff',
|
|
144
|
+
'x-accel-buffering': 'no',
|
|
145
|
+
})
|
|
146
|
+
if (typeof res.flushHeaders === 'function') res.flushHeaders()
|
|
147
|
+
const preset = STREAM_QUALITY[String(payload.quality || '')]
|
|
148
|
+
let openOptions = {}
|
|
149
|
+
if (preset) {
|
|
150
|
+
openOptions = { bitRate: preset.bitRate }
|
|
151
|
+
try {
|
|
152
|
+
const info = await adb.screenInfo(serial)
|
|
153
|
+
const dims = streamSizeFor(info, preset.shortSide)
|
|
154
|
+
if (dims) openOptions.size = dims.width + 'x' + dims.height
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
streamer.open(serial, res, openOptions)
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
if (endpoint === 'shot') {
|
|
161
|
+
try {
|
|
162
|
+
const buffer = await adb.shot(String(payload.serial || ''))
|
|
163
|
+
res.writeHead(200, {
|
|
164
|
+
'content-type': 'image/png',
|
|
165
|
+
'content-length': buffer.length,
|
|
166
|
+
'cache-control': 'no-store',
|
|
167
|
+
})
|
|
168
|
+
res.end(buffer)
|
|
169
|
+
} catch (error) {
|
|
170
|
+
json(res, 200, failOf(error))
|
|
171
|
+
}
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
json(res, 200, await handle(endpoint, payload))
|
|
176
|
+
} catch (error) {
|
|
177
|
+
json(res, 200, failOf(error))
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
'dsh-phone: route',
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
}
|
package/lib/stream.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// dsh-phone · H.264 流(P1)
|
|
2
|
+
// `adb exec-out screenrecord --output-format=h264 -` 是 Annex-B 裸流,
|
|
3
|
+
// 这里只负责把子进程 stdout 原样 pipe 给 HTTP 响应,并管理生命周期:
|
|
4
|
+
// - 客户端断开 → kill 子进程
|
|
5
|
+
// - screenrecord 单次上限(默认 180s)→ 自动重启,客户端在下个 IDR 恢复
|
|
6
|
+
// - res.write 背压 → pause/resume 子进程 stdout
|
|
7
|
+
import { spawn } from 'node:child_process'
|
|
8
|
+
|
|
9
|
+
/** 清晰度预设:按设备实际显示的短边缩放,保持屏幕比例,不改手机分辨率。 */
|
|
10
|
+
export const STREAM_QUALITY = Object.freeze({
|
|
11
|
+
low: { shortSide: 360, bitRate: 2_000_000 },
|
|
12
|
+
medium: { shortSide: 540, bitRate: 6_000_000 },
|
|
13
|
+
high: { shortSide: 720, bitRate: 10_000_000 },
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 按设备分辨率算出 screenrecord 的输出尺寸:等比缩放到目标短边、宽高取偶数。
|
|
18
|
+
* 例如 Pixel 1080x2400 + 标准(540) => 540x1200;720x1280 => 540x960。
|
|
19
|
+
*/
|
|
20
|
+
export function streamSizeFor(info, shortSide) {
|
|
21
|
+
const width = Number(info && info.width) || 0
|
|
22
|
+
const height = Number(info && info.height) || 0
|
|
23
|
+
if (width < 2 || height < 2) return null
|
|
24
|
+
const short = Math.min(width, height)
|
|
25
|
+
const scale = Math.min(1, shortSide / short)
|
|
26
|
+
const even = (value, max) => {
|
|
27
|
+
const rounded = Math.max(2, Math.round((value * scale) / 2) * 2)
|
|
28
|
+
return Math.min(Math.floor(max / 2) * 2, rounded)
|
|
29
|
+
}
|
|
30
|
+
return { width: even(width, width), height: even(height, height) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULTS = {
|
|
34
|
+
timeLimit: 180,
|
|
35
|
+
bitRate: 6_000_000,
|
|
36
|
+
size: '540x1200',
|
|
37
|
+
restartDelayMs: 250,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createStreamer(adb, options = {}) {
|
|
41
|
+
const config = { ...DEFAULTS, ...options }
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
/**
|
|
45
|
+
* 把某台设备的 H.264 流接到一个 HTTP 响应上。
|
|
46
|
+
* 调用前必须先 adb.assertReady(serial),这里不再重复校验。
|
|
47
|
+
*/
|
|
48
|
+
open(serial, res, overrides = {}) {
|
|
49
|
+
const size = overrides.size || config.size
|
|
50
|
+
const bitRate = overrides.bitRate || config.bitRate
|
|
51
|
+
let closed = false
|
|
52
|
+
let child = null
|
|
53
|
+
let restartTimer = null
|
|
54
|
+
|
|
55
|
+
const stop = () => {
|
|
56
|
+
if (closed) return
|
|
57
|
+
closed = true
|
|
58
|
+
if (restartTimer) clearTimeout(restartTimer)
|
|
59
|
+
if (child) {
|
|
60
|
+
try {
|
|
61
|
+
child.kill('SIGTERM')
|
|
62
|
+
} catch {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const start = () => {
|
|
67
|
+
if (closed) return
|
|
68
|
+
const args = [
|
|
69
|
+
'-s',
|
|
70
|
+
serial,
|
|
71
|
+
'exec-out',
|
|
72
|
+
'screenrecord',
|
|
73
|
+
'--output-format=h264',
|
|
74
|
+
'--time-limit=' + String(config.timeLimit),
|
|
75
|
+
'--bit-rate=' + String(bitRate),
|
|
76
|
+
'--size=' + size,
|
|
77
|
+
'-',
|
|
78
|
+
]
|
|
79
|
+
child = spawn(adb.bin, args, { stdio: ['ignore', 'pipe', 'ignore'] })
|
|
80
|
+
if (!child.stdout) {
|
|
81
|
+
stop()
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
child.stdout.on('data', (chunk) => {
|
|
85
|
+
if (closed) return
|
|
86
|
+
const ok = res.write(chunk)
|
|
87
|
+
if (!ok) {
|
|
88
|
+
child.stdout.pause()
|
|
89
|
+
res.once('drain', () => {
|
|
90
|
+
if (!closed && child && child.stdout) child.stdout.resume()
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
child.stdout.on('error', stop)
|
|
95
|
+
child.on('error', () => {
|
|
96
|
+
if (closed) return
|
|
97
|
+
restartTimer = setTimeout(start, 1000)
|
|
98
|
+
})
|
|
99
|
+
child.on('exit', () => {
|
|
100
|
+
if (closed) return
|
|
101
|
+
restartTimer = setTimeout(start, config.restartDelayMs)
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
res.on('close', stop)
|
|
106
|
+
res.on('error', stop)
|
|
107
|
+
start()
|
|
108
|
+
return stop
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
package/lib/targets.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// dsh-phone · 连接目标收藏($DSH_HOME/phone/targets.json)
|
|
2
|
+
// 只存地址和最近使用时间,不存任何凭据;文件权限 600。
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
4
|
+
import { dirname } from 'node:path'
|
|
5
|
+
import { ADDRESS_RE } from './adb.js'
|
|
6
|
+
|
|
7
|
+
const LIMIT = 12
|
|
8
|
+
|
|
9
|
+
export function createTargetStore(file) {
|
|
10
|
+
let cache = null
|
|
11
|
+
|
|
12
|
+
async function load() {
|
|
13
|
+
if (cache !== null) return cache
|
|
14
|
+
try {
|
|
15
|
+
const parsed = JSON.parse(await readFile(file, 'utf8'))
|
|
16
|
+
const raw = Array.isArray(parsed) ? parsed : parsed && Array.isArray(parsed.targets) ? parsed.targets : []
|
|
17
|
+
cache = raw.filter((item) => item && typeof item.address === 'string' && ADDRESS_RE.test(item.address))
|
|
18
|
+
} catch {
|
|
19
|
+
cache = []
|
|
20
|
+
}
|
|
21
|
+
return cache
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function persist(list) {
|
|
25
|
+
cache = list.slice(0, LIMIT)
|
|
26
|
+
await mkdir(dirname(file), { recursive: true })
|
|
27
|
+
await writeFile(file, JSON.stringify({ version: 1, targets: cache }, null, 2) + '\n', { mode: 0o600 })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
async list() {
|
|
32
|
+
return [...(await load())]
|
|
33
|
+
},
|
|
34
|
+
/** 连接成功后记录最近使用(去重、置顶、最多 12 条)。 */
|
|
35
|
+
async record(address) {
|
|
36
|
+
if (typeof address !== 'string' || !ADDRESS_RE.test(address)) return null
|
|
37
|
+
const list = await load()
|
|
38
|
+
const index = list.findIndex((item) => item.address === address)
|
|
39
|
+
const entry = {
|
|
40
|
+
address,
|
|
41
|
+
lastSeenAt: Date.now(),
|
|
42
|
+
pinned: index >= 0 ? list[index].pinned === true : false,
|
|
43
|
+
}
|
|
44
|
+
if (index >= 0) list.splice(index, 1)
|
|
45
|
+
list.unshift(entry)
|
|
46
|
+
await persist(list)
|
|
47
|
+
return entry
|
|
48
|
+
},
|
|
49
|
+
async remove(address) {
|
|
50
|
+
const list = await load()
|
|
51
|
+
await persist(list.filter((item) => item.address !== address))
|
|
52
|
+
return true
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
}
|
package/lib/transport.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// dsh-phone · transport 层
|
|
2
|
+
// 把「设备从哪来、画面怎么取、输入怎么发」抽成一个接口。
|
|
3
|
+
// 现在只有 adb;以后加 hdc(鸿蒙)或 iOS(idb / WDA)时,各实现一个 transport,UI 不变。
|
|
4
|
+
//
|
|
5
|
+
// interface PhoneTransport {
|
|
6
|
+
// id: 'adb' | 'hdc' | 'ios'
|
|
7
|
+
// label: string
|
|
8
|
+
// resolve(): Promise<unknown>
|
|
9
|
+
// list(): Promise<Device[]>
|
|
10
|
+
// connect(target: string): Promise<unknown>
|
|
11
|
+
// disconnect(serial: string): Promise<unknown>
|
|
12
|
+
// shot(serial: string): Promise<Buffer>
|
|
13
|
+
// input(serial: string, event: object): Promise<unknown>
|
|
14
|
+
// }
|
|
15
|
+
import { Adb } from './adb.js'
|
|
16
|
+
|
|
17
|
+
export class AdbTransport {
|
|
18
|
+
constructor(adb) {
|
|
19
|
+
this.id = 'adb'
|
|
20
|
+
this.label = 'adb'
|
|
21
|
+
this.adb = adb
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
resolve() {
|
|
25
|
+
return this.adb.resolve()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async list() {
|
|
29
|
+
const { devices } = await this.adb.devices()
|
|
30
|
+
return devices.map((device) => ({ ...device, transport: this.id }))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
connect(address) {
|
|
34
|
+
return this.adb.connect(address)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
disconnect(serial) {
|
|
38
|
+
return this.adb.disconnect(serial)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
shot(serial) {
|
|
42
|
+
return this.adb.shot(serial)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
input(serial, event) {
|
|
46
|
+
return this.adb.input(serial, event)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createTransports(options = {}) {
|
|
51
|
+
const adb = new Adb({ path: options.adbPath })
|
|
52
|
+
const transports = new Map()
|
|
53
|
+
transports.set('adb', new AdbTransport(adb))
|
|
54
|
+
return { adb, transports }
|
|
55
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,67 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ponydev/dsh-phone",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "DSH Web 插件:右侧栏预览并点按控制已连接的 Android 设备(adb)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dsh",
|
|
7
|
+
"dsh-plugin",
|
|
8
|
+
"deepseek-harness",
|
|
9
|
+
"android",
|
|
10
|
+
"adb",
|
|
11
|
+
"phone",
|
|
12
|
+
"mirror",
|
|
13
|
+
"scrcpy"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Pony <205503242+jmaubay7@users.noreply.github.com>",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/PonyDevAI/dsh-phone.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/PonyDevAI/dsh-phone/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/PonyDevAI/dsh-phone#readme",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"files": [
|
|
27
|
+
"lib",
|
|
28
|
+
"docs",
|
|
29
|
+
"cordis.patch.yml",
|
|
30
|
+
"README.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"AGENTS.md",
|
|
34
|
+
"CONTRIBUTING.md",
|
|
35
|
+
"RELEASE.md"
|
|
36
|
+
],
|
|
37
|
+
"exports": {
|
|
38
|
+
".": "./lib/index.js",
|
|
39
|
+
"./client": "./lib/client.js"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0",
|
|
43
|
+
"node": ">=22.5"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"check": "node --check lib/adb.js && node --check lib/targets.js && node --check lib/stream.js && node --check lib/transport.js && node --check lib/index.js && node --check lib/client.js && node scripts/check-i18n.mjs",
|
|
47
|
+
"smoke": "node scripts/smoke.mjs",
|
|
48
|
+
"check-i18n": "node scripts/check-i18n.mjs",
|
|
49
|
+
"release-notes": "node scripts/release-notes.mjs",
|
|
50
|
+
"test-ui": "node scripts/render-test.mjs"
|
|
51
|
+
},
|
|
52
|
+
"dsh": {
|
|
53
|
+
"manifestVersion": 1,
|
|
54
|
+
"bundle": {
|
|
55
|
+
"patch": "./cordis.patch.yml"
|
|
56
|
+
},
|
|
57
|
+
"client": {
|
|
58
|
+
"platform": "web",
|
|
59
|
+
"immediately": true,
|
|
60
|
+
"inject": [
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
63
|
+
"@deepseek-ai/dsh-client-locale"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|