dsh-mac-computer-use 0.0.0-stage → 1.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/client.js +340 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +7 -0
- package/index.js +679 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +43 -4
- package/panel.js +140 -0
- package/setup.js +110 -0
- package/README.md +0 -3
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mac-computer-use",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.3.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Vision-first Mac computer use for DSH: window list, screenshots, OCR text targeting, numbered elements, background clicks and typing. Works without the accessibility tree.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "OrderG-X",
|
|
8
|
+
"homepage": "https://github.com/OrderG-X/dsh-mac-computer-use",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/OrderG-X/dsh-mac-computer-use.git",
|
|
12
|
+
"directory": "plugin"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": "./index.js",
|
|
16
|
+
"./client": "./client.js",
|
|
17
|
+
"./package.json": "./package.json",
|
|
18
|
+
"./icon.svg": "./icon.svg",
|
|
19
|
+
"./locale/*.json": "./locale/*.json"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js",
|
|
23
|
+
"setup.js",
|
|
24
|
+
"panel.js",
|
|
25
|
+
"client.js",
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"locale",
|
|
28
|
+
"icon.svg",
|
|
29
|
+
"package.json"
|
|
30
|
+
],
|
|
31
|
+
"icon": "./icon.svg",
|
|
32
|
+
"dsh": {
|
|
33
|
+
"bundle": {
|
|
34
|
+
"patch": "./cordis.patch.yml"
|
|
35
|
+
},
|
|
36
|
+
"client": {
|
|
37
|
+
"platform": "web",
|
|
38
|
+
"inject": [
|
|
39
|
+
"@deepseek-ai/dsh-client-locale",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager"
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
package/panel.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH 插件页里那张设置卡的后端:一条 HTTP 路由,挂在 DSH 自己的 webServer 上。
|
|
3
|
+
*
|
|
4
|
+
* GET /dsh-mac-computer-use/panel → 状态(命令行工具、光标浮层、两项系统权限、看得到几个窗口、光标开关)
|
|
5
|
+
* POST /dsh-mac-computer-use/panel {action} → cursor / copyInstall / copyUpdate / openSettings,回新状态
|
|
6
|
+
*
|
|
7
|
+
* 页面(client.js)跑在 DSH 的网页端,只能走 HTTP。webServer 本身不做鉴权,
|
|
8
|
+
* 而浏览器里随便哪个网页都摸得到 127.0.0.1,所以自己把门:只收本机连接、Host 必须是本机名、
|
|
9
|
+
* 跨站的 Origin 一律拒、POST 必须是 JSON(跨站发不出来,会先被预检挡住)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFile, execFileSync } from 'node:child_process'
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import {
|
|
15
|
+
findBin, overlayState, stopOverlay, permissions, readConfig, writeConfig, INSTALL_CMD, UPDATE_CMD, ENGINE_FEATURES, SETTINGS_PANES,
|
|
16
|
+
} from './setup.js'
|
|
17
|
+
|
|
18
|
+
export const PANEL_ROUTE = '/dsh-mac-computer-use/panel'
|
|
19
|
+
|
|
20
|
+
const PKG_VERSION = (() => {
|
|
21
|
+
try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return '' }
|
|
22
|
+
})()
|
|
23
|
+
|
|
24
|
+
const LOOPBACK = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1'])
|
|
25
|
+
const LOOPBACK_HOST = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/i
|
|
26
|
+
|
|
27
|
+
/** 能看到几个窗口:证明命令行工具真的跑得起来。跑不起来回 null。 */
|
|
28
|
+
function countWindows(bin) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
execFile(bin, ['winlist'], { timeout: 5000 }, (err, out) => {
|
|
31
|
+
resolve(err ? null : String(out).split('\n').filter(Boolean).length)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 引擎支持的增强(dsh-cu features,一行一个)。旧引擎没这个命令,回空集合 */
|
|
37
|
+
function engineFeatures(bin) {
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
execFile(bin, ['features'], { timeout: 5000 }, (err, out) => {
|
|
40
|
+
resolve(new Set(err ? [] : String(out).split('\n').map((l) => l.trim()).filter(Boolean)))
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function panelState() {
|
|
46
|
+
const base = { plugin: PKG_VERSION, platform: process.platform, showCursor: readConfig().showCursor !== false, installCmd: INSTALL_CMD, updateCmd: UPDATE_CMD }
|
|
47
|
+
if (process.platform !== 'darwin') return base
|
|
48
|
+
const bin = findBin()
|
|
49
|
+
const [perms, windows, features] = await Promise.all([
|
|
50
|
+
permissions(), bin ? countWindows(bin.path) : null, bin ? engineFeatures(bin.path) : new Set(),
|
|
51
|
+
])
|
|
52
|
+
const outdated = ENGINE_FEATURES.some((f) => !features.has(f))
|
|
53
|
+
return { ...base, bin: bin && { source: bin.source, outdated }, overlay: overlayState(), perms, windows }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 跨站请求一律拒:没带 Origin(同源 GET、curl)放行;带了就必须是 DSH 自己。 */
|
|
57
|
+
function allowed(req, origin) {
|
|
58
|
+
if (!LOOPBACK.has(req.socket.remoteAddress)) return false
|
|
59
|
+
if (!LOOPBACK_HOST.test(req.headers.host || '')) return false
|
|
60
|
+
const from = req.headers.origin
|
|
61
|
+
if (!from) return true
|
|
62
|
+
if (from === 'null') return false
|
|
63
|
+
let own
|
|
64
|
+
try { own = origin() } catch { own = '' }
|
|
65
|
+
if (from === own) return true
|
|
66
|
+
try {
|
|
67
|
+
const a = new URL(from), b = new URL(own)
|
|
68
|
+
return LOOPBACK_HOST.test(a.host) && a.port === b.port && a.protocol === b.protocol
|
|
69
|
+
} catch {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readJson(req, limit = 4096) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
let size = 0
|
|
77
|
+
const chunks = []
|
|
78
|
+
req.on('data', (c) => {
|
|
79
|
+
size += c.length
|
|
80
|
+
if (size > limit) { reject(new Error('请求太大')); req.destroy() } else chunks.push(c)
|
|
81
|
+
})
|
|
82
|
+
req.on('end', () => {
|
|
83
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')) } catch { reject(new Error('不是 JSON')) }
|
|
84
|
+
})
|
|
85
|
+
req.on('error', reject)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param deps.origin - DSH 网页端自己的 origin(webServer.getOrigin)
|
|
91
|
+
* @param deps.trace - 日志
|
|
92
|
+
*/
|
|
93
|
+
export function panelHandler({ origin, trace = () => {} }) {
|
|
94
|
+
return async (req, res) => {
|
|
95
|
+
const send = (code, body) => {
|
|
96
|
+
res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
|
|
97
|
+
res.end(JSON.stringify(body))
|
|
98
|
+
}
|
|
99
|
+
if (!allowed(req, origin)) {
|
|
100
|
+
trace(`设置页路由拒绝了一个请求:${req.socket.remoteAddress} host=${req.headers.host} origin=${req.headers.origin}`)
|
|
101
|
+
return send(403, { error: 'forbidden' })
|
|
102
|
+
}
|
|
103
|
+
if (req.method === 'GET') return send(200, await panelState())
|
|
104
|
+
if (req.method !== 'POST') return send(405, { error: 'method not allowed' })
|
|
105
|
+
if (!/^application\/json\b/i.test(req.headers['content-type'] || '')) return send(415, { error: 'expect application/json' })
|
|
106
|
+
|
|
107
|
+
let body
|
|
108
|
+
try { body = await readJson(req) } catch (e) { return send(400, { error: e.message }) }
|
|
109
|
+
const extra = {}
|
|
110
|
+
try {
|
|
111
|
+
switch (body.action) {
|
|
112
|
+
case 'cursor':
|
|
113
|
+
writeConfig({ showCursor: !!body.on })
|
|
114
|
+
if (!body.on) stopOverlay()
|
|
115
|
+
trace(`设置页:桌面光标 ${body.on ? '开' : '关'}`)
|
|
116
|
+
break
|
|
117
|
+
case 'copyInstall':
|
|
118
|
+
execFileSync('/usr/bin/pbcopy', [], { input: INSTALL_CMD })
|
|
119
|
+
extra.copied = true
|
|
120
|
+
break
|
|
121
|
+
case 'copyUpdate':
|
|
122
|
+
execFileSync('/usr/bin/pbcopy', [], { input: UPDATE_CMD })
|
|
123
|
+
extra.copied = true
|
|
124
|
+
break
|
|
125
|
+
case 'openSettings': {
|
|
126
|
+
const url = SETTINGS_PANES[body.pane]
|
|
127
|
+
if (!url) return send(400, { error: `不认识的 pane: ${body.pane}` })
|
|
128
|
+
execFileSync('/usr/bin/open', [url], { stdio: 'ignore' })
|
|
129
|
+
extra.opened = body.pane
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
default:
|
|
133
|
+
return send(400, { error: `不认识的 action: ${body.action}` })
|
|
134
|
+
}
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return send(200, { ...(await panelState()), actionError: String((e && e.message) || e) })
|
|
137
|
+
}
|
|
138
|
+
return send(200, { ...(await panelState()), ...extra })
|
|
139
|
+
}
|
|
140
|
+
}
|
package/setup.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本机环境:命令行引擎 dsh-cu 在哪、光标浮层在不在、DSH 有没有系统权限、用户设置。
|
|
3
|
+
* 工具(index.js)和 DSH 设置页(panel.js)共用。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execFile, execFileSync } from 'node:child_process'
|
|
7
|
+
import fs from 'node:fs'
|
|
8
|
+
import os from 'node:os'
|
|
9
|
+
import path from 'node:path'
|
|
10
|
+
|
|
11
|
+
// 找 dsh-cu 的位置:先看环境变量,再看待安装位置,最后是开发目录(本机调试用)
|
|
12
|
+
const CANDIDATES = [
|
|
13
|
+
['env', process.env.DSH_CU_BIN],
|
|
14
|
+
['installed', path.join(os.homedir(), '.dsh/bin/dsh-cu')],
|
|
15
|
+
['dev', path.join(os.homedir(), 'Projects/dsh-cua-patched/tools/dsh-cu')],
|
|
16
|
+
].filter(([, p]) => p)
|
|
17
|
+
|
|
18
|
+
export const INSTALL_CMD = 'git clone https://github.com/OrderG-X/dsh-mac-computer-use.git && cd dsh-mac-computer-use && ./install.sh'
|
|
19
|
+
|
|
20
|
+
/** 引擎更新:在家目录拉(或更新)一份源码再重装。之前在别处 clone 过也没关系,装的都是 ~/.dsh/bin */
|
|
21
|
+
export const UPDATE_CMD = 'cd ~ && { [ -d dsh-mac-computer-use ] || git clone https://github.com/OrderG-X/dsh-mac-computer-use.git; } && cd dsh-mac-computer-use && git pull --ff-only && ./install.sh'
|
|
22
|
+
|
|
23
|
+
/** 协议号不变的引擎增强(dsh-cu features)。缺了不拦着用,只在设置页提示「引擎可以更新」 */
|
|
24
|
+
export const ENGINE_FEATURES = ['window-shot']
|
|
25
|
+
|
|
26
|
+
/** 找不到二进制时的报错:必须自带公开安装步骤,不能只有本机才看得懂的路径提示。 */
|
|
27
|
+
export const INSTALL_HINT = [
|
|
28
|
+
'dsh-cu 还没装(一次性前置,装完不用再管):',
|
|
29
|
+
'1. 装 Xcode 命令行工具(提供 swiftc):xcode-select --install',
|
|
30
|
+
'2. 拉源码并编译:',
|
|
31
|
+
` ${INSTALL_CMD}`,
|
|
32
|
+
' 它会用 swiftc 编译 dsh-cu 与光标浮层,装到 ~/.dsh/bin/',
|
|
33
|
+
'3. 回到这里重试即可(想看装没装好:ls ~/.dsh/bin/dsh-cu)',
|
|
34
|
+
'二进制在别处的话,用 DSH_CU_BIN=/path/to/dsh-cu 指过去。',
|
|
35
|
+
].join('\n')
|
|
36
|
+
|
|
37
|
+
/** @returns {{ path: string, source: 'env'|'installed'|'dev' } | null} */
|
|
38
|
+
export function findBin() {
|
|
39
|
+
for (const [source, p] of CANDIDATES) {
|
|
40
|
+
try { fs.accessSync(p, fs.constants.X_OK); return { path: p, source } } catch {}
|
|
41
|
+
}
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const OVERLAY = process.env.DSH_CU_OVERLAY || path.join(os.homedir(), '.dsh/bin/dsh-cursor-overlay')
|
|
46
|
+
|
|
47
|
+
// 只认当前账号的浮层:别的账号开着的不算、也不去关
|
|
48
|
+
// Windows / Linux 上没有 getuid:模块顶层直接调会让整个插件导入失败(DSH 里显示「没启动」)
|
|
49
|
+
const UID = typeof process.getuid === 'function' ? String(process.getuid()) : ''
|
|
50
|
+
const MINE = ['-U', UID, '-f', 'dsh-cursor-overlay']
|
|
51
|
+
|
|
52
|
+
export function overlayState() {
|
|
53
|
+
let installed = false, running = false
|
|
54
|
+
try { fs.accessSync(OVERLAY, fs.constants.X_OK); installed = true } catch {}
|
|
55
|
+
try { execFileSync('/usr/bin/pgrep', MINE, { stdio: 'ignore' }); running = true } catch {}
|
|
56
|
+
return { installed, running }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function stopOverlay() {
|
|
60
|
+
try { execFileSync('/usr/bin/pkill', MINE, { stdio: 'ignore' }) } catch { /* 本来就没跑 */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── 系统权限 ────────────────────────────────────────────────────────────────
|
|
64
|
+
// osascript 是 DSH 派生出来的,系统按「负责的应用」算权限,所以这里查到的就是 DSH 自己有没有。
|
|
65
|
+
// CGPreflightScreenCaptureAccess 不在 JXA 的默认桥里,得手动 bind。
|
|
66
|
+
|
|
67
|
+
const PERM_JXA = [
|
|
68
|
+
'ObjC.import("ApplicationServices")',
|
|
69
|
+
'ObjC.bindFunction("CGPreflightScreenCaptureAccess", ["bool", []])',
|
|
70
|
+
'JSON.stringify({ screen: $.CGPreflightScreenCaptureAccess(), ax: $.AXIsProcessTrusted() })',
|
|
71
|
+
].join(';')
|
|
72
|
+
|
|
73
|
+
let permCache = { at: 0, value: null }
|
|
74
|
+
|
|
75
|
+
/** @returns {Promise<{ screen: boolean|null, ax: boolean|null }>} null = 查不出来 */
|
|
76
|
+
export function permissions(maxAgeMs = 5000) {
|
|
77
|
+
if (permCache.value && Date.now() - permCache.at < maxAgeMs) return Promise.resolve(permCache.value)
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
execFile('/usr/bin/osascript', ['-l', 'JavaScript', '-e', PERM_JXA], { timeout: 5000 }, (err, out) => {
|
|
80
|
+
let value = { screen: null, ax: null }
|
|
81
|
+
if (!err) {
|
|
82
|
+
try { const p = JSON.parse(String(out).trim()); value = { screen: !!p.screen, ax: !!p.ax } } catch {}
|
|
83
|
+
}
|
|
84
|
+
permCache = { at: Date.now(), value }
|
|
85
|
+
resolve(value)
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const SETTINGS_PANES = {
|
|
91
|
+
screen: 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture',
|
|
92
|
+
ax: 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility',
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── 用户设置 ────────────────────────────────────────────────────────────────
|
|
96
|
+
// 存在 ~/.dsh/dsh-mac-computer-use.json。每次现读(文件很小),设置页一改马上生效,不用重启 DSH。
|
|
97
|
+
|
|
98
|
+
export const CONFIG_FILE = process.env.DSH_CU_CONFIG || path.join(os.homedir(), '.dsh/dsh-mac-computer-use.json')
|
|
99
|
+
const DEFAULTS = { showCursor: true }
|
|
100
|
+
|
|
101
|
+
export function readConfig() {
|
|
102
|
+
try { return { ...DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) } } catch { return { ...DEFAULTS } }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function writeConfig(patch) {
|
|
106
|
+
const next = { ...readConfig(), ...patch }
|
|
107
|
+
fs.mkdirSync(path.dirname(CONFIG_FILE), { recursive: true })
|
|
108
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2) + '\n')
|
|
109
|
+
return next
|
|
110
|
+
}
|
package/README.md
DELETED