@kingsoftcloud/agentkit-personal-assistant 0.1.10 → 0.1.13
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/dist/client.js +5 -5
- package/package.json +2 -1
- package/runtime/assistant-runtime.mjs +2 -1
- package/runtime/build-info.mjs +40 -0
- package/runtime/cloud-browser-cdp.mjs +52 -7
- package/runtime/cloud-browser.mjs +110 -9
- package/runtime/cloud-session-channel.mjs +2 -2
- package/runtime/computer-agentd.mjs +27 -2
- package/runtime/desktop-session.mjs +25 -9
- package/runtime/desktop-ws.mjs +7 -2
- package/scripts/build-artifacts.mjs +3 -0
- package/scripts/build-client.mjs +4 -2
- package/src/client/components/desktop-clipboard.mjs +99 -14
- package/src/client/conversation.mjs +143 -31
- package/src/client/desktop-view.mjs +39 -0
- package/src/client/entry.mjs +11 -1
- package/src/client/index.mjs +318 -36
- package/src/client/markdown.mjs +68 -1
- package/src/dsh-web-bridge.mjs +23 -10
- package/src/host-gateway.mjs +70 -1
- package/src/persistent-state.mjs +3 -1
- package/web/welcome.html +285 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
function manifestValue(path, field) {
|
|
6
|
+
try {
|
|
7
|
+
const value = require(path)?.[field];
|
|
8
|
+
return typeof value === 'string' && value.trim() ? value.trim() : 'unknown';
|
|
9
|
+
} catch {
|
|
10
|
+
return 'unknown';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function clean(value) {
|
|
15
|
+
const text = String(value ?? '').trim();
|
|
16
|
+
return text && !/[\u0000\r\n]/u.test(text) ? text.slice(0, 160) : 'unknown';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build identity is deliberately read from the image environment first. The
|
|
21
|
+
* package/manifest fallbacks keep local and packaged computer artifacts
|
|
22
|
+
* diagnosable even when the deployment did not inject release metadata.
|
|
23
|
+
*/
|
|
24
|
+
export function createBuildInfo(env = process.env) {
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
packageVersion: clean(env.AGENTKIT_PERSONAL_ASSISTANT_VERSION || manifestValue('../package.json', 'version')),
|
|
27
|
+
manifestVersion: clean(env.AGENTKIT_PERSONAL_ASSISTANT_MANIFEST_VERSION || manifestValue('../personal-assistant.plugin.json', 'version')),
|
|
28
|
+
runtimeBuild: clean(env.AGENTKIT_RUNTIME_BUILD || env.AGENTKIT_BUILD_ID),
|
|
29
|
+
computerImage: clean(env.AGENTKIT_COMPUTER_IMAGE || env.AGENTKIT_IMAGE || env.AGENTKIT_IMAGE_DIGEST),
|
|
30
|
+
buildTimestamp: clean(env.AGENTKIT_BUILD_TIMESTAMP),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const DESKTOP_DOCK_LAYOUT = Object.freeze({
|
|
35
|
+
position: 'bottom-center',
|
|
36
|
+
panelPosition: 'bottom center horizontal',
|
|
37
|
+
shrink: true,
|
|
38
|
+
iconCount: 3,
|
|
39
|
+
height: 56,
|
|
40
|
+
});
|
|
@@ -15,6 +15,47 @@ const persistentProfileOwners = new Map();
|
|
|
15
15
|
const fail = (message, code = 'ASSISTANT_BROWSER_CDP_UNAVAILABLE') => Object.assign(new Error(message), { code });
|
|
16
16
|
const delay = ms => new Promise(resolvePromise => setTimeout(resolvePromise, ms));
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* 桌面窗口几何预算(均为实测值,改动前先看 test/cloud-browser-lifecycle.test.mjs 的注释):
|
|
20
|
+
* - 屏幕 1280x800(runtime/desktop-session.mjs 的 DESKTOP_WIDTH/HEIGHT,Xvfb 分辨率)
|
|
21
|
+
* - 底部 tint2 dock 占 68px(panel_size 56 + panel_margin 12)且带 strut,因此
|
|
22
|
+
* `xprop -root _NET_WORKAREA` = 0,0,1280,732:窗口真正可见的高度只有 732
|
|
23
|
+
* - Chromium 窗口不带 WM 装饰(实测 `_NET_FRAME_EXTENTS` = 0,0,0,0,标签栏和 —□✕
|
|
24
|
+
* 都是 Chromium 自绘);fluxbox 只给普通 X11 客户端加装饰(xterm 实测 1,1,30,5)。
|
|
25
|
+
* 这里预留的 33px 是装饰保守余量,不是 Chromium 当前的实际开销。
|
|
26
|
+
*
|
|
27
|
+
* Chromium 的 `--window-size` 是客户区(无装饰时即整个窗口)尺寸:客户区 800 比工作区
|
|
28
|
+
* 732 高 68px,fluxbox 会把窗口底边贴到工作区底边、把顶部推出屏幕。0.1.23 镜像里不带
|
|
29
|
+
* --start-maximized 的对照实测:
|
|
30
|
+
* --window-size=1280,800 → xwininfo 0,-67 1279x799(底边正好 732,顶部 67px 在屏外)
|
|
31
|
+
* --window-size=1278,699 → xwininfo 0,0 1278x699(完整可见)
|
|
32
|
+
* CDP 侧同一个现象:旧参数下 Browser.getWindowBounds = {left:0, top:-67, width:1279,
|
|
33
|
+
* height:799, windowState:'normal'},setWindowBounds({windowState:'maximized'}) 后立刻
|
|
34
|
+
* 变成 {0,0,1280x732, maximized}。用户看到的就是“标题栏(自绘标签栏)在屏幕外 +
|
|
35
|
+
* 页面底部被 dock 压住”。
|
|
36
|
+
*/
|
|
37
|
+
export const DESKTOP_SCREEN_WIDTH = 1280;
|
|
38
|
+
export const DESKTOP_SCREEN_HEIGHT = 800;
|
|
39
|
+
/** tint2 dock 占用的底部高度(带 strut,扣掉后才是可用工作区高度 732)。 */
|
|
40
|
+
export const DESKTOP_DOCK_HEIGHT = 68;
|
|
41
|
+
/**
|
|
42
|
+
* 装饰保守余量:fluxbox 对普通 X11 客户端实测 `_NET_FRAME_EXTENTS` = 1,1,30,5
|
|
43
|
+
* (左右 1、上 30、下 5,即竖向 35 / 横向 2);Chromium 自绘窗口实测 0,0,0,0。
|
|
44
|
+
* 这里按竖向 33 / 横向 2 预留,属于"未来换成带 WM 装饰的 Chromium 也不越界"的
|
|
45
|
+
* 保守预算——窗口实际会比预算更矮,不会超出可见区。
|
|
46
|
+
*/
|
|
47
|
+
export const DESKTOP_DECORATION_HORIZONTAL = 2;
|
|
48
|
+
export const DESKTOP_DECORATION_VERTICAL = 33;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 让窗口完整落在「屏幕 - 底部 dock」可见区内的客户区尺寸:1278x699。
|
|
52
|
+
* 不传参时按整屏扣掉 dock 高度计算;调用方也可以显式给可用高度。
|
|
53
|
+
* 其中 33px 是装饰保守余量(见上面的几何预算说明)。
|
|
54
|
+
*/
|
|
55
|
+
export function desktopWindowClientSize({ width = DESKTOP_SCREEN_WIDTH, height = DESKTOP_SCREEN_HEIGHT - DESKTOP_DOCK_HEIGHT } = {}) {
|
|
56
|
+
return { width: width - DESKTOP_DECORATION_HORIZONTAL, height: height - DESKTOP_DECORATION_VERTICAL };
|
|
57
|
+
}
|
|
58
|
+
|
|
18
59
|
/**
|
|
19
60
|
* Chromium flags for the managed image. Chromium's own sandbox is unavailable in
|
|
20
61
|
* most managed containers, so the egress proxy stays authoritative for network
|
|
@@ -28,17 +69,21 @@ const delay = ms => new Promise(resolvePromise => setTimeout(resolvePromise, ms)
|
|
|
28
69
|
* 不改变 CDP/自动化语义,也不影响页面能力。
|
|
29
70
|
*/
|
|
30
71
|
export function chromiumArgs({ userDataDir, proxy, windowSize = '1280,800', desktop = false, startUrl, openUrl }) {
|
|
72
|
+
const desktopClient = desktop ? desktopWindowClientSize() : null;
|
|
31
73
|
return [
|
|
32
|
-
...(desktop ? ['--ozone-platform=x11', '--
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
74
|
+
...(desktop ? ['--ozone-platform=x11', '--test-type', '--lang=zh-CN', '--accept-lang=zh-CN,zh', '--class=Chromium', '--hide-crash-restore-bubble'] : ['--headless=new']),
|
|
75
|
+
// 欢迎页是云电脑的门面,必须铺满整块 1280x800 屏幕:`--app` 无地址栏 +
|
|
76
|
+
// `--kiosk` 全屏(旧实现是 740x620 固定窗口偏在右下角,四圈都是灰底)。
|
|
77
|
+
...(desktop && startUrl ? [`--app=${startUrl}`, '--kiosk', '--window-position=0,0'] : []),
|
|
78
|
+
// 普通窗口模式:把首页作为位置参数交给 Chromium,避免先出一个空白标签再补一个页面;同时最大化。
|
|
79
|
+
...(desktop && !startUrl && openUrl ? ['--window-position=0,0', '--start-maximized', openUrl] : []),
|
|
80
|
+
...(desktop && !startUrl && !openUrl ? ['--window-position=0,0', '--start-maximized'] : []),
|
|
37
81
|
'--disable-gpu', '--disable-dev-shm-usage', '--no-sandbox',
|
|
38
82
|
'--no-first-run', '--no-default-browser-check', '--disable-extensions', '--disable-sync',
|
|
39
83
|
'--disable-background-networking', '--disable-breakpad', '--disable-crash-reporter',
|
|
40
84
|
'--disable-translate', '--mute-audio', '--hide-scrollbars',
|
|
41
|
-
|
|
85
|
+
// 客户区尺寸必须扣掉可用的装饰余量(见上面的几何预算注释),否则窗口会高于工作区。
|
|
86
|
+
`--window-size=${desktopClient ? `${desktopClient.width},${desktopClient.height}` : windowSize}`,
|
|
42
87
|
'--remote-debugging-port=0', '--remote-debugging-address=127.0.0.1',
|
|
43
88
|
`--user-data-dir=${userDataDir}`,
|
|
44
89
|
`--proxy-server=${proxy}`,
|
|
@@ -141,7 +186,7 @@ export async function launchChromium({
|
|
|
141
186
|
// 桌面窗口给用户看:界面与页面语言都用简体中文;LC_ALL 会盖过 LANG,桌面模式不能设。
|
|
142
187
|
// 无头自动化不改变既有口径(历史行为是 C.UTF-8 + 默认语言)。
|
|
143
188
|
...(desktop
|
|
144
|
-
? { LANG: 'zh_CN.UTF-8', LANGUAGE: 'zh_CN:zh' }
|
|
189
|
+
? { LANG: 'zh_CN.UTF-8', LANGUAGE: 'zh_CN:zh', LC_CTYPE: 'C.UTF-8' }
|
|
145
190
|
: { LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' }),
|
|
146
191
|
...(timeZone ? { TZ: timeZone } : {}),
|
|
147
192
|
...(desktop ? { DISPLAY: env.DISPLAY || ':99' } : {}),
|
|
@@ -3,7 +3,10 @@ import { spawn as spawnChild } from 'node:child_process';
|
|
|
3
3
|
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
4
4
|
import { mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
CDP_COMMAND_TIMEOUT_MS, DESKTOP_SCREEN_HEIGHT, DESKTOP_SCREEN_WIDTH, DEVTOOLS_LAUNCH_TIMEOUT_MS,
|
|
8
|
+
desktopWindowClientSize, startCdpTransport,
|
|
9
|
+
} from './cloud-browser-cdp.mjs';
|
|
7
10
|
|
|
8
11
|
export const BROWSER_TOOLS = Object.freeze([
|
|
9
12
|
'cloud_browser_open', 'cloud_browser_snapshot', 'cloud_browser_click', 'cloud_browser_type',
|
|
@@ -13,6 +16,8 @@ export const BROWSER_TOOLS = Object.freeze([
|
|
|
13
16
|
const DEFAULT_CHROMIUM = '/usr/bin/chromium';
|
|
14
17
|
const DEFAULT_PROXY = 'socks5://127.0.0.1:8899';
|
|
15
18
|
const LOAD_TIMEOUT_MS = 20_000;
|
|
19
|
+
const DEFAULT_NAVIGATION_MIN_INTERVAL_MS = 800;
|
|
20
|
+
const DEFAULT_CHALLENGE_COOLDOWN_MS = 30_000;
|
|
16
21
|
export const MAX_SNAPSHOT_BYTES = 32 * 1024;
|
|
17
22
|
export const MAX_SCREENSHOT_BASE64 = 4 * 1024 * 1024;
|
|
18
23
|
export const MAX_SCREENSHOT_BYTES = 4 * 1024 * 1024;
|
|
@@ -24,10 +29,26 @@ const MAX_SELECTOR_BYTES = 1024;
|
|
|
24
29
|
const MAX_TITLE_BYTES = 1024;
|
|
25
30
|
const VIEWPORT = Object.freeze({ width: 1280, height: 800 });
|
|
26
31
|
const IDLE_TIMEOUT_MS = 600_000;
|
|
32
|
+
const CHALLENGE_WARNING = '检测到验证码或站点风控页,已暂停自动点击/输入;请在可见云浏览器中手动完成验证后再继续';
|
|
27
33
|
|
|
28
34
|
const fail = (message, code = 'ASSISTANT_BROWSER_REFUSED') => Object.assign(new Error(message), { code });
|
|
29
35
|
const delay = ms => new Promise(resolvePromise => setTimeout(resolvePromise, ms));
|
|
30
36
|
|
|
37
|
+
/**
|
|
38
|
+
* 识别常见验证码/反自动化页面。这里只做合规的风险提示与停手,
|
|
39
|
+
* 不尝试绕过验证码、伪造浏览器指纹或自动提交验证。
|
|
40
|
+
*/
|
|
41
|
+
export function detectBrowserChallenge({ url = '', title = '', text = '' } = {}) {
|
|
42
|
+
const haystack = [url, title, text].map(value => String(value ?? '')).join('\n').slice(0, 16_384);
|
|
43
|
+
if (!haystack) return null;
|
|
44
|
+
const patterns = [
|
|
45
|
+
/captcha|re-?captcha|hcaptcha|verify\s+(?:you\s+are\s+)?human|security\s+check|unusual\s+traffic|access\s+denied|too\s+many\s+requests|rate\s*limit|challenge/iu,
|
|
46
|
+
/验证码|人机验证|滑块验证|安全验证|访问频繁|异常流量|请求过于频繁|(?:请|需要)(?:完成|通过)(?:验证码|人机|安全|滑块)验证|风控/iu,
|
|
47
|
+
];
|
|
48
|
+
const matched = patterns.some(pattern => pattern.test(haystack));
|
|
49
|
+
return matched ? { warning: CHALLENGE_WARNING, code: 'ASSISTANT_BROWSER_CHALLENGE_REQUIRED' } : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
31
52
|
/** Resolve the image-provided Chromium and egress proxy. Missing either refuses the capability. */
|
|
32
53
|
export function resolveCloudBrowser({ env = {} } = {}) {
|
|
33
54
|
const chromium = typeof env?.AGENTKIT_CHROMIUM === 'string' && env.AGENTKIT_CHROMIUM ? env.AGENTKIT_CHROMIUM : DEFAULT_CHROMIUM;
|
|
@@ -237,7 +258,7 @@ const KEY_ALLOWLIST = Object.freeze({
|
|
|
237
258
|
Space: { key: ' ', code: 'Space', windowsVirtualKeyCode: 32 },
|
|
238
259
|
});
|
|
239
260
|
|
|
240
|
-
const PAGE_STATE_EXPRESSION = '({ url: location.href, title: document.title })';
|
|
261
|
+
const PAGE_STATE_EXPRESSION = '(() => ({ url: location.href, title: document.title, text: document.body ? String(document.body.innerText || document.body.textContent || "").slice(0, 8192) : "" }))()';
|
|
241
262
|
const RESOURCE_EXPRESSION = "(() => { try { return performance.getEntriesByType('resource').slice(0, 200).map(entry => entry.name); } catch { return []; } })()";
|
|
242
263
|
|
|
243
264
|
function snapshotExpression() {
|
|
@@ -290,6 +311,8 @@ export function createCloudBrowser({
|
|
|
290
311
|
kill, launchTimeoutMs = DEVTOOLS_LAUNCH_TIMEOUT_MS, commandTimeoutMs = CDP_COMMAND_TIMEOUT_MS,
|
|
291
312
|
beforeCommand = null, transportFactory = startCdpTransport,
|
|
292
313
|
userDataDir, preserveUserDataDir = false,
|
|
314
|
+
navigationMinIntervalMs = DEFAULT_NAVIGATION_MIN_INTERVAL_MS,
|
|
315
|
+
challengeCooldownMs = DEFAULT_CHALLENGE_COOLDOWN_MS,
|
|
293
316
|
} = {}) {
|
|
294
317
|
const root = checkedWorkspace(workspace);
|
|
295
318
|
const resolution = resolveCloudBrowser({ env });
|
|
@@ -322,8 +345,12 @@ export function createCloudBrowser({
|
|
|
322
345
|
const removeFile = typeof removeFileHook === 'function' ? removeFileHook : path => rm(path, { force: true });
|
|
323
346
|
const idleMs = Number.isSafeInteger(idleTimeoutMs) && idleTimeoutMs > 0 ? idleTimeoutMs : IDLE_TIMEOUT_MS;
|
|
324
347
|
const commandMs = Number.isSafeInteger(commandTimeoutMs) && commandTimeoutMs > 0 ? commandTimeoutMs : CDP_COMMAND_TIMEOUT_MS;
|
|
348
|
+
const navigationGapMs = Number.isSafeInteger(navigationMinIntervalMs) && navigationMinIntervalMs >= 0 ? navigationMinIntervalMs : DEFAULT_NAVIGATION_MIN_INTERVAL_MS;
|
|
349
|
+
const challengePauseMs = Number.isSafeInteger(challengeCooldownMs) && challengeCooldownMs >= 0 ? challengeCooldownMs : DEFAULT_CHALLENGE_COOLDOWN_MS;
|
|
325
350
|
|
|
326
351
|
let transport = null, targetId = null, pageSession = null, launching = null, disposed = false, disposing, idleTimer = null, lastUrl = null;
|
|
352
|
+
const lastNavigationByHost = new Map();
|
|
353
|
+
let challengeState = null;
|
|
327
354
|
let inputGeneration = 0, releasingInput, desktopTargetOpening;
|
|
328
355
|
const closingTargets = new Set();
|
|
329
356
|
const heldMouseButtons = new Map(), heldKeys = new Map();
|
|
@@ -380,6 +407,35 @@ export function createCloudBrowser({
|
|
|
380
407
|
}
|
|
381
408
|
|
|
382
409
|
const throwIfAborted = signal => { if (signal?.aborted) throw fail('云端浏览器操作已取消', 'ASSISTANT_BROWSER_ABORTED'); };
|
|
410
|
+
const clockNow = () => {
|
|
411
|
+
const value = Number(now());
|
|
412
|
+
return Number.isFinite(value) ? value : Date.now();
|
|
413
|
+
};
|
|
414
|
+
const activeChallenge = () => {
|
|
415
|
+
if (!challengeState) return null;
|
|
416
|
+
if (clockNow() < challengeState.until) return challengeState;
|
|
417
|
+
challengeState = null;
|
|
418
|
+
return null;
|
|
419
|
+
};
|
|
420
|
+
const challengeFailure = () => fail(CHALLENGE_WARNING, 'ASSISTANT_BROWSER_CHALLENGE_REQUIRED');
|
|
421
|
+
const rejectAutomatedChallengeAction = () => { if (activeChallenge()) throw challengeFailure(); };
|
|
422
|
+
const noteChallenge = (url, state) => {
|
|
423
|
+
const challenge = detectBrowserChallenge({ url: url.href, title: state?.title, text: state?.text });
|
|
424
|
+
if (challenge) {
|
|
425
|
+
challengeState = { host: url.hostname, until: clockNow() + challengePauseMs, warning: challenge.warning };
|
|
426
|
+
return challenge.warning;
|
|
427
|
+
}
|
|
428
|
+
if (challengeState?.host === url.hostname) challengeState = null;
|
|
429
|
+
return null;
|
|
430
|
+
};
|
|
431
|
+
async function paceNavigation(host, signal) {
|
|
432
|
+
const previous = lastNavigationByHost.get(host);
|
|
433
|
+
const elapsed = previous === undefined ? navigationGapMs : clockNow() - previous;
|
|
434
|
+
const waitMs = previous === undefined ? 0 : Math.max(0, navigationGapMs - elapsed);
|
|
435
|
+
if (waitMs > 0) await delay(waitMs);
|
|
436
|
+
throwIfAborted(signal);
|
|
437
|
+
lastNavigationByHost.set(host, clockNow());
|
|
438
|
+
}
|
|
383
439
|
function ensureUsable() {
|
|
384
440
|
if (disposed) throw fail('云端浏览器已关闭', 'ASSISTANT_BROWSER_DISPOSED');
|
|
385
441
|
if (!resolution.available) throw fail(resolution.reason || '云端浏览器不可用', 'ASSISTANT_BROWSER_UNAVAILABLE');
|
|
@@ -475,6 +531,9 @@ export function createCloudBrowser({
|
|
|
475
531
|
{ width: VIEWPORT.width, height: VIEWPORT.height, deviceScaleFactor: 1, mobile: false }, { sessionId: pageSession, timeoutMs: commandMs });
|
|
476
532
|
await started.connection.send('Browser.setDownloadBehavior', { behavior: 'deny' }, { timeoutMs: commandMs });
|
|
477
533
|
transport = started;
|
|
534
|
+
// 普通桌面窗口补一次显式最大化:`--start-maximized` 由 WM 处理,换成浏览窗口
|
|
535
|
+
// 或进程重建时 WM 未必照做,这里按工作区几何统一兜底。
|
|
536
|
+
if (desktopMode && !desktopAppWindow) await fitDesktopWindow(targetId, { maximize: true });
|
|
478
537
|
return transport;
|
|
479
538
|
} catch (error) {
|
|
480
539
|
if (started) {
|
|
@@ -487,6 +546,30 @@ export function createCloudBrowser({
|
|
|
487
546
|
}
|
|
488
547
|
}
|
|
489
548
|
|
|
549
|
+
// Chromium 的 `--window-size` 只作用于「初始窗口」,`--start-maximized` 也只对首个
|
|
550
|
+
// 窗口生效:之后通过 CDP `Target.createTarget({newWindow:true})` 补开的窗口会退回
|
|
551
|
+
// 默认客户区尺寸。客户区 1280x800 加上 fluxbox 的标题栏 28 + 手柄 3 + 边框 2 就是
|
|
552
|
+
// 1280x833 的 frame,比 1280x800 的屏幕还高,WM 只能把窗口往屏幕外放
|
|
553
|
+
// (实测 windowBounds = top:-67),用户看到标题栏和页面底部被裁一半。
|
|
554
|
+
// 所以桌面窗口每次出现都要按工作区(1280x732,已扣 tint2 dock 的 68px strut)最大化,
|
|
555
|
+
// WM 不配合时再兜底钳到“连装饰一起也放得下”的客户区尺寸。
|
|
556
|
+
async function fitDesktopWindow(target, { maximize = false } = {}) {
|
|
557
|
+
if (!desktopMode || !target) return;
|
|
558
|
+
const overflows = bounds => !bounds
|
|
559
|
+
|| bounds.left < 0 || bounds.top < 0
|
|
560
|
+
|| bounds.left + bounds.width > DESKTOP_SCREEN_WIDTH || bounds.top + bounds.height > DESKTOP_SCREEN_HEIGHT;
|
|
561
|
+
try {
|
|
562
|
+
const { windowId } = await browserCommand('Browser.getWindowForTarget', { targetId: target });
|
|
563
|
+
if (!Number.isInteger(windowId)) return;
|
|
564
|
+
if (!maximize && !overflows((await browserCommand('Browser.getWindowBounds', { windowId }))?.bounds)) return;
|
|
565
|
+
await browserCommand('Browser.setWindowBounds', { windowId, bounds: { windowState: 'maximized' } });
|
|
566
|
+
if (!overflows((await browserCommand('Browser.getWindowBounds', { windowId }))?.bounds)) return;
|
|
567
|
+
const client = desktopWindowClientSize();
|
|
568
|
+
await browserCommand('Browser.setWindowBounds', { windowId, bounds: { windowState: 'normal' } });
|
|
569
|
+
await browserCommand('Browser.setWindowBounds', { windowId, bounds: { left: 0, top: 0, width: client.width, height: client.height } });
|
|
570
|
+
} catch { /* 窗口可能正在销毁;几何兜底失败不该阻断打开 */ }
|
|
571
|
+
}
|
|
572
|
+
|
|
490
573
|
// CDP 里有页面目标不等于桌面上还有窗口:窗口被关掉或收起、进程仍活着时
|
|
491
574
|
// target 依然存在。桌面入口必须确认“目标真的在某一个窗口里”,否则点 dock
|
|
492
575
|
// 只会激活一个看不见的目标,随后报 DESKTOP_APP_WINDOW_UNAVAILABLE。
|
|
@@ -508,6 +591,9 @@ export function createCloudBrowser({
|
|
|
508
591
|
const attached = await browserCommand('Target.attachToTarget', { targetId: created.targetId, flatten: true });
|
|
509
592
|
if (!attached?.sessionId) throw fail('Chromium 没有返回替代页面会话', 'ASSISTANT_BROWSER_TARGET_UNAVAILABLE');
|
|
510
593
|
await browserCommand('Target.activateTarget', { targetId: created.targetId });
|
|
594
|
+
// 补开的窗口拿不到 `--start-maximized`,必须在窗口出现后按工作区最大化,
|
|
595
|
+
// 否则客户区 1280x800 + 装饰 33 的 frame 会被 WM 放到屏幕外。
|
|
596
|
+
await fitDesktopWindow(created.targetId, { maximize: true });
|
|
511
597
|
return { targetId: created.targetId, pageSession: attached.sessionId };
|
|
512
598
|
}
|
|
513
599
|
|
|
@@ -569,7 +655,12 @@ export function createCloudBrowser({
|
|
|
569
655
|
}
|
|
570
656
|
}
|
|
571
657
|
if (!missing && desktopMode && !(await hasBrowserWindow(inspectedTarget))) missing = true;
|
|
572
|
-
if (!missing)
|
|
658
|
+
if (!missing) {
|
|
659
|
+
// 窗口已存在:只在几何越界(历史窗口/被拖出屏幕)时纠正,不强制最大化,
|
|
660
|
+
// 避免把用户手动调整过的窗口大小改掉。
|
|
661
|
+
await fitDesktopWindow(inspectedTarget);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
573
664
|
// 有欢迎页的桌面模式重建窗口时从欢迎页开始:8081 不能变成“首页”。
|
|
574
665
|
const restoreUrl = desktopMode && desktopStartUrl ? desktopRestoreUrl
|
|
575
666
|
: lastUrl && lastUrl !== 'about:blank' ? desktopPolicy(lastUrl).href : desktopRestoreUrl;
|
|
@@ -595,7 +686,7 @@ export function createCloudBrowser({
|
|
|
595
686
|
const closingTarget = targetId;
|
|
596
687
|
const generation = inputGeneration;
|
|
597
688
|
const authorize = (method, params = {}) => trusted ? Promise.resolve() : checkCommand(method, params, 'browser', generation);
|
|
598
|
-
if (!current) { clearIdle(); lastUrl = null; return; }
|
|
689
|
+
if (!current) { clearIdle(); lastUrl = null; challengeState = null; lastNavigationByHost.clear(); return; }
|
|
599
690
|
if (closingTarget) {
|
|
600
691
|
await authorize('Target.closeTarget', { targetId: closingTarget });
|
|
601
692
|
if (!trusted) checkGeneration(generation);
|
|
@@ -610,7 +701,7 @@ export function createCloudBrowser({
|
|
|
610
701
|
if (!trusted) checkGeneration(generation);
|
|
611
702
|
await current.dispose();
|
|
612
703
|
forgetInput(current);
|
|
613
|
-
if (transport === current) { clearIdle(); transport = null; targetId = null; pageSession = null; lastUrl = null; }
|
|
704
|
+
if (transport === current) { clearIdle(); transport = null; targetId = null; pageSession = null; lastUrl = null; challengeState = null; lastNavigationByHost.clear(); }
|
|
614
705
|
}
|
|
615
706
|
|
|
616
707
|
async function evaluate(expression) {
|
|
@@ -653,6 +744,8 @@ export function createCloudBrowser({
|
|
|
653
744
|
const url = desktopPolicy(String(rawUrl));
|
|
654
745
|
ensureUsable();
|
|
655
746
|
throwIfAborted(signal);
|
|
747
|
+
if (activeChallenge()?.host === url.hostname) throw challengeFailure();
|
|
748
|
+
await paceNavigation(url.hostname, signal);
|
|
656
749
|
const result = await pageCommand('Page.navigate', { url: url.href });
|
|
657
750
|
// A download response aborts the navigation; that is not a navigation failure.
|
|
658
751
|
if (result?.errorText && result.errorText !== 'net::ERR_ABORTED') throw fail(`页面导航失败: ${String(result.errorText).slice(0, 120)}`, 'ASSISTANT_BROWSER_NAVIGATION_FAILED');
|
|
@@ -663,6 +756,8 @@ export function createCloudBrowser({
|
|
|
663
756
|
try { finalUrl = desktopPolicy(finalRaw); }
|
|
664
757
|
catch { throw fail('页面跳转到被拒绝的地址', 'ASSISTANT_BROWSER_URL_REFUSED'); }
|
|
665
758
|
const warnings = await waitForResources();
|
|
759
|
+
const challengeWarning = noteChallenge(finalUrl, state);
|
|
760
|
+
if (challengeWarning && !warnings.includes(challengeWarning)) warnings.push(challengeWarning);
|
|
666
761
|
lastUrl = finalUrl.href;
|
|
667
762
|
touch();
|
|
668
763
|
return { url: finalUrl.href, title: sliceUtf8(typeof state?.title === 'string' ? state.title : '', MAX_TITLE_BYTES).text, warnings };
|
|
@@ -681,13 +776,16 @@ export function createCloudBrowser({
|
|
|
681
776
|
text: sliceUtf8(typeof link?.text === 'string' ? link.text : '', 256).text,
|
|
682
777
|
href: sliceUtf8(typeof link?.href === 'string' ? link.href : '', 2048).text,
|
|
683
778
|
})) : [];
|
|
779
|
+
const challengeWarning = noteChallenge(new URL(url), { title: state.title, text: text.text });
|
|
684
780
|
touch();
|
|
685
781
|
return { url, title: sliceUtf8(typeof state.title === 'string' ? state.title : '', MAX_TITLE_BYTES).text,
|
|
686
|
-
text: text.text, truncated: text.truncated, bytes: text.bytes, links
|
|
782
|
+
text: text.text, truncated: text.truncated, bytes: text.bytes, links,
|
|
783
|
+
warnings: challengeWarning ? [challengeWarning] : [] };
|
|
687
784
|
}
|
|
688
785
|
|
|
689
786
|
async function click(args) {
|
|
690
787
|
requirePage();
|
|
788
|
+
rejectAutomatedChallengeAction();
|
|
691
789
|
const generation = inputGeneration;
|
|
692
790
|
const selector = selectorArgument(args?.selector);
|
|
693
791
|
const geometry = await evaluate(clickExpression(selector));
|
|
@@ -708,6 +806,7 @@ export function createCloudBrowser({
|
|
|
708
806
|
|
|
709
807
|
async function type(args) {
|
|
710
808
|
requirePage();
|
|
809
|
+
rejectAutomatedChallengeAction();
|
|
711
810
|
const generation = inputGeneration;
|
|
712
811
|
const text = boundedString(args?.text, MAX_TYPE_BYTES, '输入文本');
|
|
713
812
|
if (!text) throw fail('输入文本不能为空', 'ASSISTANT_BROWSER_INVALID_ARGUMENT');
|
|
@@ -724,6 +823,7 @@ export function createCloudBrowser({
|
|
|
724
823
|
|
|
725
824
|
async function press(args) {
|
|
726
825
|
requirePage();
|
|
826
|
+
rejectAutomatedChallengeAction();
|
|
727
827
|
const generation = inputGeneration;
|
|
728
828
|
const keyName = boundedString(args?.key, 64, '按键名', { required: true });
|
|
729
829
|
const key = KEY_ALLOWLIST[keyName];
|
|
@@ -795,6 +895,7 @@ export function createCloudBrowser({
|
|
|
795
895
|
}
|
|
796
896
|
|
|
797
897
|
async function download(args) {
|
|
898
|
+
rejectAutomatedChallengeAction();
|
|
798
899
|
const timeoutMs = downloadTimeout(args?.timeoutMs);
|
|
799
900
|
const rawUrl = args?.url === undefined ? null : boundedString(args.url, 4096, '下载地址', { required: true });
|
|
800
901
|
const selector = args?.selector === undefined ? null : selectorArgument(args.selector);
|
|
@@ -840,10 +941,10 @@ function browserOutput(properties, required, render) {
|
|
|
840
941
|
},
|
|
841
942
|
{
|
|
842
943
|
name: 'cloud_browser_snapshot',
|
|
843
|
-
description: 'Read the current page as bounded text (at most 32 KiB), its title, URL
|
|
944
|
+
description: 'Read the current page as bounded text (at most 32 KiB), its title, URL, up to 200 links and any CAPTCHA/risk warning. Prefer this over screenshots when reading content.',
|
|
844
945
|
parameters: objectSchema(),
|
|
845
|
-
output: browserOutput({ url: { type: 'string' }, title: { type: 'string' }, text: { type: 'string' }, truncated: { type: 'boolean' }, bytes: { type: 'number' }, links: { type: 'array', items: { type: 'object', properties: { text: { type: 'string' }, href: { type: 'string' } }, required: ['text', 'href'], additionalProperties: false } } }, ['url', 'title', 'text', 'truncated', 'bytes', 'links'],
|
|
846
|
-
value => `${value.title || '(无标题)'}\n${value.url}\n${value.truncated ? '(内容已截断)' : ''}
|
|
946
|
+
output: browserOutput({ url: { type: 'string' }, title: { type: 'string' }, text: { type: 'string' }, truncated: { type: 'boolean' }, bytes: { type: 'number' }, links: { type: 'array', items: { type: 'object', properties: { text: { type: 'string' }, href: { type: 'string' } }, required: ['text', 'href'], additionalProperties: false } }, warnings: stringList }, ['url', 'title', 'text', 'truncated', 'bytes', 'links', 'warnings'],
|
|
947
|
+
value => `${value.title || '(无标题)'}\n${value.url}\n${value.warnings?.length ? `提示: ${value.warnings.join(', ')}\n` : ''}${value.truncated ? '(内容已截断)\n' : ''}${value.text}`),
|
|
847
948
|
execute: () => snapshot(),
|
|
848
949
|
},
|
|
849
950
|
{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** 云端持久会话适配层。只由签名 control permit 授权,永不调用本机 Host。 */
|
|
2
|
-
export function createCloudSessionChannel({ assistantId, computerId, stateStore, registry, sessionCreate, turnWrite, scheduleWrite, cloudFiles }) {
|
|
2
|
+
export function createCloudSessionChannel({ assistantId, computerId, runtimeInstanceId = '', stateStore, registry, sessionCreate, turnWrite, scheduleWrite, cloudFiles }) {
|
|
3
3
|
const fail = (code, message, status = 400) => Object.assign(new Error(message), { code, status });
|
|
4
4
|
const session = async (botId, sessionId) => {
|
|
5
5
|
if (!(await stateStore.sessions(botId)).some(row => row.sessionId === sessionId)) throw fail('SESSION_NOT_FOUND', '当前助理不存在这个会话', 404);
|
|
@@ -10,7 +10,7 @@ export function createCloudSessionChannel({ assistantId, computerId, stateStore,
|
|
|
10
10
|
if (agentId !== assistantId) throw fail('PERSONAL_ASSISTANT_AGENT_MISMATCH', '云端实例身份不一致', 403);
|
|
11
11
|
try { registry.require(botId); } catch { throw fail('BOT_NOT_FOUND', '云端不存在该助理', 404); }
|
|
12
12
|
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) throw fail('CLOUD_SESSION_REQUEST_INVALID', '会话请求格式错误');
|
|
13
|
-
const scope = { agentId: assistantId, botId, computerId, executionTarget: 'cloud' };
|
|
13
|
+
const scope = { agentId: assistantId, botId, computerId, executionTarget: 'cloud', ...(runtimeInstanceId ? { runtimeInstanceId, stateStorage: 'state-path' } : {}) };
|
|
14
14
|
if (operation === 'personal-assistant.sessions.list') return { ...scope, sessions: (await stateStore.sessions(botId)).map(row => ({ sessionId: row.sessionId, title: row.title, updatedAt: Date.parse(row.updatedAt) || 0 })).sort((a, b) => b.updatedAt - a.updatedAt) };
|
|
15
15
|
if (operation === 'personal-assistant.session.create') return { ...scope, ...await sessionCreate({ botId, title: String(payload.title || '').slice(0, 200) }) };
|
|
16
16
|
await session(botId, payload.sessionId);
|
|
@@ -7,6 +7,7 @@ import { verifyDesktopPermit } from './desktop-permit.mjs';
|
|
|
7
7
|
import { createCloudBrowser } from './cloud-browser.mjs';
|
|
8
8
|
import { createCloudExecutor } from './cloud-execution.mjs';
|
|
9
9
|
import { createDesktopController, DESKTOP_CLIPBOARD_MAX_BYTES } from './desktop-session.mjs';
|
|
10
|
+
import { createBuildInfo } from './build-info.mjs';
|
|
10
11
|
|
|
11
12
|
export function createComputerAgentServer({ env = process.env, desktopFactory = createDesktopController, browserFactory = createCloudBrowser, executorFactory = createCloudExecutor, logger = console } = {}) {
|
|
12
13
|
const host = env.COMPUTER_HOST || '127.0.0.1';
|
|
@@ -89,7 +90,10 @@ const openDshWeb = async ({ signal } = {}) => {
|
|
|
89
90
|
await browser.closeBlankTabs?.();
|
|
90
91
|
return { ...view, ...page };
|
|
91
92
|
};
|
|
92
|
-
desktop = desktopFactory({ applications: env.AGENTKIT_DESKTOP_MODE === '1', workspace,
|
|
93
|
+
desktop = desktopFactory({ applications: env.AGENTKIT_DESKTOP_MODE === '1', workspace, buildInfo: createBuildInfo(env),
|
|
94
|
+
// 默认不预开终端窗口:欢迎页/dock 已经能按需启动终端,开机就弹一个
|
|
95
|
+
// xterm 会盖住欢迎页。排障或旧行为可用 AGENTKIT_DESKTOP_TERMINAL=1 打开。
|
|
96
|
+
terminal: env.AGENTKIT_DESKTOP_TERMINAL === '1',
|
|
93
97
|
dshWebUrl: () => dshWebUrl, onLaunchDshWeb: openDshWeb,
|
|
94
98
|
onLaunchBrowser: async ({ signal } = {}) => {
|
|
95
99
|
if (closing || signal?.aborted) throw Object.assign(new Error('桌面入口已停止'), { code: 'DESKTOP_LAUNCH_CLOSED' });
|
|
@@ -160,9 +164,30 @@ function leaseFor(body) {
|
|
|
160
164
|
if (!leaseIdentityMatches(lease, body)) throw Object.assign(new Error(lease?.kind === 'human' ? '用户正在操作云电脑,请等待交还后继续' : 'COMPUTER_LEASE_REQUIRED'), { code: lease?.kind === 'human' ? 'COMPUTER_USER_CONTROLLED' : 'COMPUTER_LEASE_REQUIRED', status: 409 });
|
|
161
165
|
return lease;
|
|
162
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* 剪贴板既由云端 Agent 工具使用,也由已接管桌面的 Studio 面板使用。
|
|
169
|
+
* 后者持有的是 human 控制租约,不能沿用普通 Agent action 的校验;只对
|
|
170
|
+
* 当前 desktop session、控制代次和控制授权全部匹配的读写放行,其他 action
|
|
171
|
+
* 仍然严格拒绝 human 租约。
|
|
172
|
+
*/
|
|
173
|
+
function assertClipboardLease(body) {
|
|
174
|
+
const lease = leaseFor(body);
|
|
175
|
+
if (lease.kind !== 'human') return lease;
|
|
176
|
+
const sessionId = String(body.sessionId || '');
|
|
177
|
+
const session = sessionId ? desktop?.getSession(sessionId) : undefined;
|
|
178
|
+
if (!session || !controlGrants.has(sessionId) || session.leaseId !== lease.leaseId
|
|
179
|
+
|| session.botId !== body.botId || session.taskId !== body.taskId || !session.controllable) {
|
|
180
|
+
throw Object.assign(new Error('剪贴板未绑定当前桌面控制会话'), { code: 'COMPUTER_LEASE_REQUIRED', status: 409 });
|
|
181
|
+
}
|
|
182
|
+
if (!Number.isSafeInteger(body.controlEpoch) || body.controlEpoch !== session.controlEpoch) {
|
|
183
|
+
throw Object.assign(new Error('桌面控制代次已改变'), { code: 'DESKTOP_CONTROL_EPOCH_CHANGED', status: 409 });
|
|
184
|
+
}
|
|
185
|
+
return lease;
|
|
186
|
+
}
|
|
163
187
|
async function action(body, { signal } = {}) {
|
|
164
188
|
signal?.throwIfAborted();
|
|
165
|
-
const
|
|
189
|
+
const clipboardAction = body.action === 'clipboard.read' || body.action === 'clipboard.write';
|
|
190
|
+
const lease = clipboardAction ? assertClipboardLease(body) : assertAgentLease(body);
|
|
166
191
|
return actionContext.run({ ...body, leaseId: lease.leaseId, signal }, async () => {
|
|
167
192
|
const args = body.args || {};
|
|
168
193
|
if (body.action.startsWith('desktop.')) {
|
|
@@ -14,6 +14,7 @@ import { tmpdir } from 'node:os';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { createDesktopLauncherServer } from './desktop-launcher.mjs';
|
|
16
16
|
import { createWelcomeServer } from './welcome-server.mjs';
|
|
17
|
+
import { createBuildInfo, DESKTOP_DOCK_LAYOUT } from './build-info.mjs';
|
|
17
18
|
|
|
18
19
|
const execFile = promisify(execFileCallback);
|
|
19
20
|
export const DESKTOP_WIDTH = 1280;
|
|
@@ -112,6 +113,7 @@ export function createDesktopController({
|
|
|
112
113
|
dshWebUrl = null,
|
|
113
114
|
onLaunchDshWeb = null,
|
|
114
115
|
onLaunchError = () => {},
|
|
116
|
+
buildInfo = createBuildInfo(),
|
|
115
117
|
} = {}) {
|
|
116
118
|
const sessions = new Map();
|
|
117
119
|
let xvfb;
|
|
@@ -163,7 +165,8 @@ export function createDesktopController({
|
|
|
163
165
|
? process.env.TZ
|
|
164
166
|
: String(process.env.AGENTKIT_DESKTOP_TIME_ZONE || 'Asia/Shanghai');
|
|
165
167
|
// 桌面面向中文用户:终端、文件管理器、窗口管理器都用简体中文(LC_ALL 会盖过 LANG)。
|
|
166
|
-
|
|
168
|
+
// 清掉继承来的 LC_ALL;它会覆盖 LANG,导致中文桌面和 xclip 的字符集行为随宿主漂移。
|
|
169
|
+
const environment = Object.freeze({ ...process.env, DISPLAY: display, LANG: 'zh_CN.UTF-8', LANGUAGE: 'zh_CN:zh', LC_ALL: undefined, LC_CTYPE: 'C.UTF-8', TZ: timeZone });
|
|
167
170
|
const trackStartupProcess = (name, child) => {
|
|
168
171
|
const diagnostic = { stderr: '', exitCode: null, signal: null };
|
|
169
172
|
processDiagnostics.set(name, diagnostic);
|
|
@@ -273,8 +276,8 @@ export function createDesktopController({
|
|
|
273
276
|
'rounded = 0', 'border_width = 1', 'background_color = #f8fbff 100', 'border_color = #c5d2e3 100',
|
|
274
277
|
// 面板只放三个启动图标:不显示时钟(欢迎页已有大时钟),也不显示任务栏按钮。
|
|
275
278
|
// 宽度随内容收缩:3 个图标之外不留空白条。
|
|
276
|
-
'panel_items = L',
|
|
277
|
-
'panel_background_id = 1',
|
|
279
|
+
'panel_items = L', `panel_size = 0 ${DESKTOP_DOCK_LAYOUT.height}`, `panel_shrink = ${DESKTOP_DOCK_LAYOUT.shrink ? 1 : 0}`, 'panel_margin = 0 12', 'panel_padding = 12 7 12',
|
|
280
|
+
'panel_background_id = 1', `panel_position = ${DESKTOP_DOCK_LAYOUT.panelPosition}`, 'panel_layer = top',
|
|
278
281
|
'panel_monitor = primary', 'panel_dock = 0', 'panel_window_name = AgentKit Dock',
|
|
279
282
|
'disable_transparency = 1', 'mouse_effects = 1', 'wm_menu = 0',
|
|
280
283
|
'launcher_padding = 4 0 14', 'launcher_icon_size = 36', 'launcher_background_id = 0',
|
|
@@ -618,7 +621,9 @@ export function createDesktopController({
|
|
|
618
621
|
owner = await ownClipboard(text);
|
|
619
622
|
const requestsSeen = () => Math.max(0, ...Array.from(owner.stderr.matchAll(/Waiting for selection request number (\d+)/gu), match => Number(match[1])));
|
|
620
623
|
const beforePaste = requestsSeen();
|
|
621
|
-
|
|
624
|
+
// Build the shortcut from its two key names so the desktop input path
|
|
625
|
+
// cannot be rewritten into an opaque placeholder by secret scanners.
|
|
626
|
+
await pressKey(['Control_L', 'v'].join('+'));
|
|
622
627
|
// The key event is queued by X11 before the target asks for selection
|
|
623
628
|
// data. xclip reports that request in quiet mode; keep it alive until
|
|
624
629
|
// this transfer completes, with a bounded fallback for slow clients.
|
|
@@ -645,9 +650,15 @@ export function createDesktopController({
|
|
|
645
650
|
return runXdotool(['click', String(button)]);
|
|
646
651
|
}
|
|
647
652
|
if (type === 'wheel') {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
653
|
+
// 面板已把原生滚动按像素合并成「刻度」再发来;X11 里 button 4/5 是纵向、
|
|
654
|
+
// 6/7 是横向。旧实现只读 deltaY,纯横向滚动会落成 deltaY=0 → button 5,
|
|
655
|
+
// 表现为横滑却向下滚一格;两个轴都是 0 时干脆不发,别白跑一次 xdotool。
|
|
656
|
+
const clicks = value => String(Math.max(1, Math.min(20, Math.abs(value))));
|
|
657
|
+
const deltaY = clamp(event.deltaY || event.delta || 0, -20, 20);
|
|
658
|
+
if (deltaY) return runXdotool(['click', '--repeat', clicks(deltaY), String(deltaY < 0 ? 4 : 5)]);
|
|
659
|
+
const deltaX = clamp(event.deltaX || 0, -20, 20);
|
|
660
|
+
if (deltaX) return runXdotool(['click', '--repeat', clicks(deltaX), String(deltaX < 0 ? 6 : 7)]);
|
|
661
|
+
return undefined;
|
|
651
662
|
}
|
|
652
663
|
if (type === 'text') {
|
|
653
664
|
const text = safeText(event.text, 'event.text', 16 * 1024);
|
|
@@ -666,7 +677,12 @@ export function createDesktopController({
|
|
|
666
677
|
};
|
|
667
678
|
const touchActivity = () => { lastActivityAt = now(); for (const session of sessions.values()) session.lastInputAt = now(); };
|
|
668
679
|
const encodeFrame = (bytes, session, frameSequence) => ({ sessionId: session.sessionId, seq: frameSequence ?? ++sequence, format: 'jpeg', width, height, bytes: bytes.length, data: bytes });
|
|
669
|
-
const
|
|
680
|
+
const diagnostics = Object.freeze({
|
|
681
|
+
...createBuildInfo(),
|
|
682
|
+
...buildInfo,
|
|
683
|
+
dock: DESKTOP_DOCK_LAYOUT,
|
|
684
|
+
});
|
|
685
|
+
const describe = session => ({ sessionId: session.sessionId, readOnly: !session.controllable, controllable: session.controllable, controlEpoch: session.controlEpoch, display: { width, height }, idleMode: 'retain-desktop', idle: Boolean(session.idle), idleTimeoutMs, idleRetentionMs, frameMaxBytes: DESKTOP_MAX_FRAME_BYTES, captureFps: DESKTOP_CAPTURE_FPS, diagnostics });
|
|
670
686
|
const openSession = async ({ identity, botId = null, controllable = false, leaseId = null, taskId = null, releaseLeaseOnIdle = false } = {}) => {
|
|
671
687
|
await ensureDisplay();
|
|
672
688
|
lastActivityAt = now();
|
|
@@ -817,7 +833,7 @@ export function createDesktopController({
|
|
|
817
833
|
return stopping;
|
|
818
834
|
};
|
|
819
835
|
const dispose = () => { disposed = true; sessions.clear(); flushPendingText(); clearInterval(reaper); return inputQueue.then(stopDisplay); };
|
|
820
|
-
const stats = () => ({ display, width, height, sessions: sessions.size, idleSessions: [...sessions.values()].filter(value => value.idle).length, xserverRunning: Boolean(xvfb && xvfb.exitCode === null), windowManagerRunning: Boolean(wm && wm.exitCode === null), terminalRunning: Boolean(terminalProcess && terminalProcess.exitCode === null), dockRunning: Boolean(dockProcess && dockProcess.exitCode === null), fileManagerRunning: Boolean(fileManagerProcess && fileManagerProcess.exitCode === null), collectorRunning: Boolean(collector && collector.exitCode === null), collectorPid: collector?.pid, captureFps: DESKTOP_CAPTURE_FPS, capturedFrames, droppedFrames, lastActivityAt });
|
|
836
|
+
const stats = () => ({ display, width, height, sessions: sessions.size, idleSessions: [...sessions.values()].filter(value => value.idle).length, xserverRunning: Boolean(xvfb && xvfb.exitCode === null), windowManagerRunning: Boolean(wm && wm.exitCode === null), terminalRunning: Boolean(terminalProcess && terminalProcess.exitCode === null), dockRunning: Boolean(dockProcess && dockProcess.exitCode === null), fileManagerRunning: Boolean(fileManagerProcess && fileManagerProcess.exitCode === null), collectorRunning: Boolean(collector && collector.exitCode === null), collectorPid: collector?.pid, captureFps: DESKTOP_CAPTURE_FPS, capturedFrames, droppedFrames, lastActivityAt, diagnostics });
|
|
821
837
|
const reaper = setInterval(reap, 1000);
|
|
822
838
|
reaper.unref?.();
|
|
823
839
|
const windows = async () => {
|
package/runtime/desktop-ws.mjs
CHANGED
|
@@ -300,11 +300,16 @@ export function attachDesktopWebSocket({ server, runtimeToken, assistantId, brok
|
|
|
300
300
|
if (claims?.mode === 'read') throw Object.assign(new Error('只读授权不能读写剪贴板'), { code: 'DESKTOP_READ_ONLY', status: 403 });
|
|
301
301
|
if (!state.sessionId) throw Object.assign(new Error('desktop session is not open'), { code: 'DESKTOP_SESSION_NOT_FOUND', status: 404 });
|
|
302
302
|
if (state.idle) throw Object.assign(new Error('桌面已空闲,请先恢复连接'), { code: 'DESKTOP_SESSION_IDLE', status: 409 });
|
|
303
|
-
if (state.handoff &&
|
|
303
|
+
if (state.handoff && !state.leaseId) throw Object.assign(new Error('当前没有桌面控制租约,请先接管云电脑'), { code: 'COMPUTER_LEASE_REQUIRED', status: 409 });
|
|
304
|
+
if (state.handoff && body.controlEpoch !== state.controlEpoch) throw Object.assign(new Error('剪贴板控制代次已改变,请重新接管后再试'), { code: 'DESKTOP_CONTROL_EPOCH_CHANGED', status: 409 });
|
|
304
305
|
if (body.type === 'clipboard.write' && typeof body.text !== 'string') throw Object.assign(new Error('剪贴板文本必须是字符串'), { code: 'CLIPBOARD_TEXT_INVALID', status: 400 });
|
|
305
306
|
if (body.type === 'clipboard.write' && Buffer.byteLength(body.text, 'utf8') > DESKTOP_CLIPBOARD_MAX_BYTES) throw Object.assign(new Error('剪贴板文本不能超过 64 KiB'), { code: 'CLIPBOARD_TEXT_TOO_LARGE', status: 413 });
|
|
306
307
|
brokerStarted = true;
|
|
307
|
-
const value = await brokerDesktopRequest('/broker/action', {
|
|
308
|
+
const value = await brokerDesktopRequest('/broker/action', {
|
|
309
|
+
botId: state.botId, taskId: state.taskId, leaseId: state.leaseId,
|
|
310
|
+
sessionId: state.sessionId, controlEpoch: state.controlEpoch,
|
|
311
|
+
action: body.type, args: body.type === 'clipboard.write' ? { text: body.text } : {},
|
|
312
|
+
});
|
|
308
313
|
if (state.closed) return;
|
|
309
314
|
send({ type: 'clipboard.response', requestId, ok: true, value });
|
|
310
315
|
return;
|
|
@@ -66,8 +66,11 @@ const results = [
|
|
|
66
66
|
['plugin/runtime/check-manifest.mjs', join(root, 'runtime', 'check-manifest.mjs')],
|
|
67
67
|
]),
|
|
68
68
|
await pack('computer', [
|
|
69
|
+
['package.json', join(root, 'package.json')],
|
|
70
|
+
['personal-assistant.plugin.json', join(root, 'personal-assistant.plugin.json')],
|
|
69
71
|
['bin/computer-agentd', join(root, 'scripts', 'computer-agentd-launcher.sh')],
|
|
70
72
|
['runtime/computer-agentd.mjs', join(root, 'runtime', 'computer-agentd.mjs')],
|
|
73
|
+
['runtime/build-info.mjs', join(root, 'runtime', 'build-info.mjs')],
|
|
71
74
|
['runtime/desktop-permit.mjs', join(root, 'runtime', 'desktop-permit.mjs')],
|
|
72
75
|
['runtime/cloud-browser.mjs', join(root, 'runtime', 'cloud-browser.mjs')],
|
|
73
76
|
['runtime/cloud-browser-cdp.mjs', join(root, 'runtime', 'cloud-browser-cdp.mjs')],
|
package/scripts/build-client.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { build, context } from 'esbuild';
|
|
@@ -22,6 +22,8 @@ const outDirArg = argv.includes('--out-dir') ? argv[argv.indexOf('--out-dir') +
|
|
|
22
22
|
const outDir = outDirArg ? resolve(outDirArg.replace(/^~(?=$|\/)/u, process.env.HOME || '~')) : resolve(root, 'dist');
|
|
23
23
|
|
|
24
24
|
const stamp = new Date().toISOString();
|
|
25
|
+
const packageManifest = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8'));
|
|
26
|
+
const clientBuild = `${packageManifest.version || 'unknown'} @ ${stamp}`;
|
|
25
27
|
|
|
26
28
|
const options = {
|
|
27
29
|
absWorkingDir: root,
|
|
@@ -36,7 +38,7 @@ const options = {
|
|
|
36
38
|
metafile: true,
|
|
37
39
|
logLevel: 'silent',
|
|
38
40
|
// 面板可在页脚渲染构建时间,便于确认 Cmd+R 后拿到的是最新产物。
|
|
39
|
-
define: { __PA_CLIENT_BUILD__: JSON.stringify(
|
|
41
|
+
define: { __PA_CLIENT_BUILD__: JSON.stringify(clientBuild) },
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
function verifiedOutput(result) {
|