@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
package/src/client/index.mjs
CHANGED
|
@@ -35,7 +35,8 @@ export function createPersonalAssistantClient({ gateway, profileId = 'default' }
|
|
|
35
35
|
selectedBot: () => selectedBotId,
|
|
36
36
|
session: input => request('personal-assistant.session.create', { ...input, botId: input?.botId ?? selectedBotId }),
|
|
37
37
|
listSessions: botId => request('personal-assistant.sessions.list', { botId: botId ?? selectedBotId }),
|
|
38
|
-
|
|
38
|
+
// 网关公开的是单数 session.create;旧的 sessions.create 从未进入云端会话通道。
|
|
39
|
+
createSession: input => request('personal-assistant.session.create', { ...input, botId: input?.botId ?? selectedBotId }),
|
|
39
40
|
listMessages: sessionId => request('personal-assistant.sessions.messages.list', { sessionId, botId: selectedBotId }),
|
|
40
41
|
appendMessage: (sessionId, input) => request('personal-assistant.sessions.messages.append', { sessionId, ...input, botId: input?.botId ?? selectedBotId }),
|
|
41
42
|
listTurns: sessionId => request('personal-assistant.sessions.turns.list', { sessionId, botId: selectedBotId }),
|
|
@@ -67,9 +68,9 @@ export const inject = Object.freeze(['slots', 'connection', 'sidebarRight']);
|
|
|
67
68
|
export const PERSONAL_ASSISTANT_PANEL_ID = 'personal-assistant';
|
|
68
69
|
export const PERSONAL_ASSISTANT_CLIENT_DEFAULTS = Object.freeze({ enabled: false, autoConnect: false });
|
|
69
70
|
export const PERSONAL_ASSISTANT_COMPUTER_TAB = 'personal-assistant-computer';
|
|
70
|
-
import { mapDisplayPoint, normalizeDisplay } from './desktop-view.mjs';
|
|
71
|
+
import { fitDesktopBox, mapDisplayPoint, mergeWheel, normalizeDisplay } from './desktop-view.mjs';
|
|
71
72
|
import { createDesktopControl } from './desktop-control.mjs';
|
|
72
|
-
import { createClipboardChannel, isPasteShortcut, isCopyShortcut, copyCloudClipboard, pasteToCloud, readLocalClipboard } from './components/desktop-clipboard.mjs';
|
|
73
|
+
import { createClipboardChannel, isPasteShortcut, isCopyShortcut, copyShortcutEvents, copyCloudClipboard, readCloudClipboardAfterCopy, clipboardTextOf, pasteToCloud, readLocalClipboard, writeLocalClipboard } from './components/desktop-clipboard.mjs';
|
|
73
74
|
import { PersonalAssistantConversation } from './conversation.mjs';
|
|
74
75
|
import { createAssistantWorkbench, AssistantNativeRightbar } from './native-workbench.mjs';
|
|
75
76
|
import { publicBotDirectory, resolveAssistantBotId } from './bot-directory.mjs';
|
|
@@ -77,6 +78,14 @@ export const PERSONAL_ASSISTANT_CLIENT_BUILD = typeof __PA_CLIENT_BUILD__ === 's
|
|
|
77
78
|
const FIRST_FRAME_TIMEOUT_MS = 12_000;
|
|
78
79
|
let clientReact;
|
|
79
80
|
export function setClientReactRuntime(runtime) { clientReact = runtime; }
|
|
81
|
+
let clientMarkdown;
|
|
82
|
+
/**
|
|
83
|
+
* 注入宿主 DSH 的 ui-primitives —— 原生 Markdown 渲染器(MarkdownText)所在模块。
|
|
84
|
+
* 老宿主导出的模块里没有 MarkdownText 时传 undefined,对话视图退回自带极简渲染。
|
|
85
|
+
*/
|
|
86
|
+
export function setClientMarkdownRuntime(runtime) {
|
|
87
|
+
clientMarkdown = runtime && typeof runtime.MarkdownText === 'function' ? runtime : undefined;
|
|
88
|
+
}
|
|
80
89
|
const ui = {
|
|
81
90
|
background: 'var(--dsw-alias-bg-layer-1, #fff)', color: 'var(--dsw-alias-label-primary, #17202c)',
|
|
82
91
|
border: '1px solid var(--dsw-alias-border-default, #d8dee8)',
|
|
@@ -84,12 +93,67 @@ const ui = {
|
|
|
84
93
|
const bindingFailure = error => typeof error?.message === 'string' && error.message && !/^(fetch failed|Failed to fetch)$/u.test(error.message) ? error.message.slice(0, 180) : '云电脑连接失败,请稍后重试';
|
|
85
94
|
const failure = error => error?.code === 'PERSONAL_ASSISTANT_DIRECTORY_UNAVAILABLE' && Number(error?.status) === 409 ? '当前云端版本尚不支持助理列表,请更新云端助理后重试' : '连接暂时不可用,请稍后重试';
|
|
86
95
|
const directoryUnsupported = error => Number(error?.status) === 409 && ['PERSONAL_ASSISTANT_DIRECTORY_UNSUPPORTED', 'PERSONAL_ASSISTANT_DIRECTORY_UNAVAILABLE'].includes(error?.code);
|
|
96
|
+
/**
|
|
97
|
+
* 「未开通」引导页的唯一事实来源:云电脑面板、设置页、首页共用同一套判断与动作。
|
|
98
|
+
*
|
|
99
|
+
* reason 由 Studio 给出:needs-create(账号下确实没有实例)、credential-unavailable(有实例但
|
|
100
|
+
* 凭证取不到)、unavailable(连状态都读不到,多为 Studio 未登录云端或网关不可达)。
|
|
101
|
+
* 只有确认没有实例时才给「开通」入口——一个子账号一个个人助理,已开通必须复用,避免重复创建;
|
|
102
|
+
* candidates 非空时一律优先连接已发现的实例。
|
|
103
|
+
*/
|
|
104
|
+
export function unconfiguredGuidance(binding) {
|
|
105
|
+
const reason = typeof binding?.reason === 'string' ? binding.reason : '';
|
|
106
|
+
const candidates = Array.isArray(binding?.candidates) ? binding.candidates.filter(row => typeof row?.agentId === 'string' && row.agentId) : [];
|
|
107
|
+
const check = { id: 'check', label: '检查开通状态' };
|
|
108
|
+
if (candidates.length > 0) return {
|
|
109
|
+
tone: 'reuse', title: '发现已开通的个人助理',
|
|
110
|
+
detail: `账号下已有 ${candidates.length} 个个人助理。一个子账号只需要一个,直接连接即可复用,不会重复创建。`,
|
|
111
|
+
actions: [{ id: 'connect', label: '连接已发现的个人助理', primary: true }, check],
|
|
112
|
+
};
|
|
113
|
+
if (reason === 'needs-create') return {
|
|
114
|
+
tone: 'create', title: '开通个人助理',
|
|
115
|
+
detail: '一个子账号可开通一个个人助理。开通后会得到一位云端持续运行的助理和一台专属云电脑,关掉 Studio 也会继续工作。',
|
|
116
|
+
actions: [{ id: 'create', label: '开通个人助理', primary: true }, check],
|
|
117
|
+
};
|
|
118
|
+
if (reason === 'credential-unavailable') return {
|
|
119
|
+
tone: 'reuse', title: '正在恢复连接凭证',
|
|
120
|
+
detail: '个人助理实例已经在账号下,但云端暂时没有返回连接凭证。重新获取即可复用现有实例,不需要重新开通。',
|
|
121
|
+
actions: [{ id: 'connect', label: '重新获取连接凭证', primary: true }, check],
|
|
122
|
+
};
|
|
123
|
+
const detail = typeof binding?.detail === 'string' && binding.detail ? binding.detail : '';
|
|
124
|
+
return {
|
|
125
|
+
tone: 'retry', title: '暂时读不到开通状态',
|
|
126
|
+
detail: `${detail}请先确认 Studio 已登录云端账号,再重新检查一次;确认账号下确实没有个人助理后再开通,避免重复创建。`,
|
|
127
|
+
actions: [check, { id: 'create', label: '确认未开通后开通', primary: false }],
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const onboardingSteps = tone => tone === 'reuse'
|
|
132
|
+
? ['确认账号下的个人助理实例', '直接连接即可复用,不会重复创建', '需要看画面或动手时,打开「云电脑」接管']
|
|
133
|
+
: ['登录 Studio 云端账号,开通个人助理(约 1–2 分钟)', '连接后即可在「个人助理」页对话,云端 7×24 运行', '需要看画面或动手时,打开「云电脑」接管'];
|
|
134
|
+
|
|
135
|
+
const onboardingStyle = {
|
|
136
|
+
page: { flex: '1 1 auto', minHeight: 0, overflowY: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 14, padding: '32px 20px', textAlign: 'center', boxSizing: 'border-box' },
|
|
137
|
+
eyebrow: { fontSize: 11, letterSpacing: '0.1em', textTransform: 'uppercase', color: 'var(--dsw-alias-label-secondary, #536174)' },
|
|
138
|
+
title: { margin: 0, fontSize: 20, fontWeight: 600, letterSpacing: '-0.01em' },
|
|
139
|
+
detail: { margin: 0, maxWidth: 460, lineHeight: 1.75, color: 'var(--dsw-alias-label-secondary, #536174)' },
|
|
140
|
+
steps: { margin: 0, padding: 0, listStyle: 'none', display: 'flex', flexDirection: 'column', gap: 8, textAlign: 'left', maxWidth: 460 },
|
|
141
|
+
step: { display: 'flex', gap: 10, alignItems: 'flex-start', lineHeight: 1.7 },
|
|
142
|
+
stepIndex: { flex: 'none', width: 20, height: 20, borderRadius: 999, display: 'grid', placeItems: 'center', fontSize: 12, background: 'color-mix(in srgb, currentColor 10%, transparent)' },
|
|
143
|
+
actions: { display: 'flex', gap: 10, flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', marginTop: 4 },
|
|
144
|
+
primary: { ...ui, borderRadius: 8, padding: '8px 16px', cursor: 'pointer', font: 'inherit', fontSize: 13, fontWeight: 500, border: 'none', background: 'var(--dsw-alias-label-primary, #17202c)', color: 'var(--dsw-alias-bg-layer-1, #fff)' },
|
|
145
|
+
ghost: { ...ui, borderRadius: 8, padding: '8px 14px', cursor: 'pointer', font: 'inherit', fontSize: 13, background: 'transparent' },
|
|
146
|
+
notice: { margin: 0, maxWidth: 460, color: 'var(--dsw-alias-label-secondary, #536174)', fontSize: 12, lineHeight: 1.7 },
|
|
147
|
+
diag: { alignSelf: 'center', color: 'var(--dsw-alias-label-tertiary, #76808e)', fontSize: 11, opacity: 0.8, maxWidth: '100%', overflowWrap: 'anywhere' },
|
|
148
|
+
};
|
|
149
|
+
|
|
87
150
|
const keyNames = { Control: 'Control_L', Meta: 'Super_L', Alt: 'Alt_L', Shift: 'Shift_L', Enter: 'Return', Escape: 'Escape', Tab: 'Tab', Backspace: 'BackSpace', Delete: 'Delete', ArrowUp: 'Up', ArrowDown: 'Down', ArrowLeft: 'Left', ArrowRight: 'Right', ' ': 'space' };
|
|
88
151
|
|
|
89
152
|
export function PersonalAssistantPanel({ request, initialBotId = '', openInSidebar = null, variant = 'page', nativeFullscreen = false } = {}) {
|
|
90
153
|
const R = clientReact || globalThis.React;
|
|
91
154
|
if (!R) return '个人助理页面尚未就绪,请重新打开';
|
|
92
155
|
const { createElement: h, useEffect, useRef, useState } = R;
|
|
156
|
+
const useLayoutEffect = R.useLayoutEffect || useEffect;
|
|
93
157
|
const [config, setConfig] = useState(null);
|
|
94
158
|
const [bots, setBots] = useState([]);
|
|
95
159
|
const [directoryError, setDirectoryError] = useState('');
|
|
@@ -105,11 +169,16 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
105
169
|
const [binding, setBinding] = useState(null);
|
|
106
170
|
const [busy, setBusy] = useState('');
|
|
107
171
|
const [clipboardNotice, setClipboardNotice] = useState('');
|
|
172
|
+
const [desktopDiagnostics, setDesktopDiagnostics] = useState(null);
|
|
173
|
+
// 桌面卡片尺寸由「可用宽 × 可用高」共同决定:全屏后高度不够时按高度反推宽度,
|
|
174
|
+
// 保证远端整幅画面(含云浏览器顶部地址/输入条)完整可见,不靠滚动兜底。
|
|
175
|
+
const [desktopDisplay, setDesktopDisplay] = useState(() => normalizeDisplay());
|
|
176
|
+
const [stageBox, setStageBox] = useState(null);
|
|
108
177
|
const controlRef = useRef(null);
|
|
109
178
|
const clipboardRef = useRef(null), clipboardTimerRef = useRef(null);
|
|
110
|
-
const canvasRef = useRef(null), inputRef = useRef(null), viewRef = useRef(null), cursorRef = useRef(null);
|
|
179
|
+
const canvasRef = useRef(null), inputRef = useRef(null), viewRef = useRef(null), cursorRef = useRef(null), stageAreaRef = useRef(null), controlCapsuleRef = useRef(null);
|
|
111
180
|
const fullscreenRef = useRef(nativeFullscreen); fullscreenRef.current = nativeFullscreen;
|
|
112
|
-
const state = useRef({ mounted: true, socket: null, generation: 0, frameEpoch: 0, session: null, suspended: false, resuming: false, pendingInputs: [], display: { width: 1280, height: 800 }, readOnly: true, botId, config: null, reconnect: null, firstFrame: null, receivedSeq: -1, pendingFrame: null, decoding: false, frames: 0, lastMove: 0, pressed: new Set(), buttons: new Set(), composition: false, retries: 0, taskId: '', mode: 'control', frameMetadata: null, reconnectAllowed: false });
|
|
181
|
+
const state = useRef({ mounted: true, socket: null, generation: 0, frameEpoch: 0, session: null, suspended: false, resuming: false, pendingInputs: [], display: { width: 1280, height: 800 }, readOnly: true, botId, config: null, reconnect: null, firstFrame: null, receivedSeq: -1, pendingFrame: null, decoding: false, frames: 0, lastMove: 0, wheelPending: { x: 0, y: 0 }, wheelFrame: 0, pressed: new Set(), buttons: new Set(), composition: false, retries: 0, taskId: '', mode: 'control', frameMetadata: null, reconnectAllowed: false, controlError: '' });
|
|
113
182
|
state.current.botId = botId;
|
|
114
183
|
const rpc = async (operation, payload = {}) => {
|
|
115
184
|
if (typeof request !== 'function') throw new Error('Studio Host 尚未连接');
|
|
@@ -134,11 +203,12 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
134
203
|
send,
|
|
135
204
|
changed: value => {
|
|
136
205
|
const current = state.current; current.controlPhase = value.phase; current.readOnly = value.readOnly; current.controlEpoch = value.controlEpoch;
|
|
206
|
+
if (value.phase === 'controlling') current.controlError = '';
|
|
137
207
|
if (value.readOnly && cursorRef.current) cursorRef.current.style.visibility = 'hidden';
|
|
138
208
|
if (value.phase === 'observe') { current.pressed.clear(); current.buttons.clear(); }
|
|
139
209
|
if (current.mounted) { setReadOnly(value.readOnly); setControlPhase(value.phase); }
|
|
140
210
|
},
|
|
141
|
-
failed: message => { if (state.current.mounted) setNotice(message); },
|
|
211
|
+
failed: message => { state.current.controlError = message || '控制权暂不可用'; if (state.current.mounted) setNotice(message); },
|
|
142
212
|
});
|
|
143
213
|
// 剪贴板请求复用同一条桌面 WS(云端 desktop-ws 的 clipboard.read / clipboard.write),
|
|
144
214
|
// 用 requestId 关联回复;旧版云端不认这两个消息类型,超时后给明确提示而不是转圈。
|
|
@@ -163,6 +233,7 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
163
233
|
try { send({ type: 'close' }); current.socket?.close(1000, 'panel closed'); } catch {}
|
|
164
234
|
controlRef.current.reset({ readOnly: true }); current.lastDrawnFrame = null; current.socket = null; current.session = null; current.pendingFrame = null; current.frameMetadata = null; current.readOnly = true; inputRef.current?.blur();
|
|
165
235
|
current.suspended = false; current.resuming = false; current.pendingInputs = []; current.frameEpoch++;
|
|
236
|
+
if (current.mounted) setDesktopDiagnostics(null);
|
|
166
237
|
if (announce && current.mounted) { setPhase(current.config?.configured ? 'idle' : 'unconfigured'); setReadOnly(true); }
|
|
167
238
|
};
|
|
168
239
|
const ended = value => {
|
|
@@ -201,28 +272,81 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
201
272
|
for (const event of events) sent = input(event) || sent;
|
|
202
273
|
return sent;
|
|
203
274
|
};
|
|
204
|
-
//
|
|
205
|
-
//
|
|
275
|
+
// 剪贴板读写和桌面输入共用 human 控制租约。旁观状态下先申请接管并等待
|
|
276
|
+
// 服务端回执,避免“写入剪贴板”先于 lease 成功而稳定收到 409。
|
|
277
|
+
const ensureClipboardControl = async () => {
|
|
278
|
+
const current = state.current;
|
|
279
|
+
const snapshot = controlRef.current?.snapshot;
|
|
280
|
+
if (!current.session || current.socket?.readyState !== 1) { clipboardNotify('云电脑尚未连接,请先打开桌面'); return false; }
|
|
281
|
+
if (snapshot && snapshot.phase === 'controlling' && snapshot.readOnly === false) return true;
|
|
282
|
+
if (current.suspended || current.resuming) { clipboardNotify('云电脑正在恢复,请稍候再试'); return false; }
|
|
283
|
+
if (!snapshot?.available) { clipboardNotify('当前为只读旁观,请先打开控制模式'); return false; }
|
|
284
|
+
current.controlError = '';
|
|
285
|
+
if (!controlRef.current.acquire()) { clipboardNotify('当前无法接管云电脑,请重试'); return false; }
|
|
286
|
+
const deadline = Date.now() + 8000;
|
|
287
|
+
while (Date.now() < deadline) {
|
|
288
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
289
|
+
const latest = controlRef.current.snapshot;
|
|
290
|
+
if (latest.phase === 'controlling' && latest.readOnly === false) return true;
|
|
291
|
+
if (current.controlError) break;
|
|
292
|
+
}
|
|
293
|
+
clipboardNotify(current.controlError || '接管云电脑超时,请重试');
|
|
294
|
+
return false;
|
|
295
|
+
};
|
|
296
|
+
// ⌘C:先把复制按键交给云电脑,让云端应用自己完成“复制”,再取回云端剪贴板。
|
|
297
|
+
// 云端桌面始终是 Linux,不能把 macOS 的 Meta 映射为 Super_L,否则 Linux 应用不会执行复制。
|
|
206
298
|
const copyCloudClipboardToLocal = async () => {
|
|
207
|
-
|
|
299
|
+
if (!await ensureClipboardControl()) return { ok: false, code: 'CLIPBOARD_CONTROL_REQUIRED' };
|
|
300
|
+
// 先记住旧 selection,再发 Ctrl+C;X11 的 selection owner 在按键后异步更新,
|
|
301
|
+
// 直接 sleep 150ms 仍可能读到旧文本,尤其是中文/浏览器渲染进程较忙时。
|
|
302
|
+
const before = await clipboardRef.current.read().catch(() => ({ text: '' }));
|
|
303
|
+
let sent = false;
|
|
208
304
|
if (!state.current.pressed.has('_copyShortcut')) {
|
|
209
305
|
state.current.pressed.add('_copyShortcut');
|
|
210
|
-
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
306
|
+
try { sent = sendDesktopKeys(copyShortcutEvents()) !== false; }
|
|
307
|
+
finally { state.current.pressed.delete('_copyShortcut'); }
|
|
308
|
+
}
|
|
309
|
+
if (!sent) { clipboardNotify('复制按键未发送,请先接管云电脑后再试'); return { ok: false, code: 'CLIPBOARD_KEY_NOT_SENT' }; }
|
|
310
|
+
let settled;
|
|
311
|
+
try {
|
|
312
|
+
settled = await readCloudClipboardAfterCopy({
|
|
313
|
+
readCloud: () => clipboardRef.current.read(),
|
|
314
|
+
beforeText: clipboardTextOf(before),
|
|
315
|
+
});
|
|
316
|
+
} catch (error) {
|
|
317
|
+
clipboardNotify(error?.message || '读取云电脑剪贴板失败,请重试');
|
|
318
|
+
return { ok: false, code: error?.code || 'CLIPBOARD_READ_FAILED' };
|
|
219
319
|
}
|
|
220
|
-
return copyCloudClipboard({ readCloud: () =>
|
|
320
|
+
return copyCloudClipboard({ readCloud: async () => settled, writeLocal: text => writeHostClipboard(text), notify: clipboardNotify });
|
|
321
|
+
};
|
|
322
|
+
// 本机剪贴板优先走 Host 进程(pbpaste/pbcopy):Studio 渲染器没有 clipboard 权限,
|
|
323
|
+
// navigator.clipboard 在面板里会被拒,⌘C/⌘V 就成了空操作。旧 Host 不支持时再退回浏览器 API。
|
|
324
|
+
const readHostClipboard = async () => {
|
|
325
|
+
try {
|
|
326
|
+
const value = await rpc('personal-assistant.clipboard.read');
|
|
327
|
+
if (typeof value?.text === 'string') return value.text;
|
|
328
|
+
} catch { /* 旧 Host 没有剪贴板 RPC,退回浏览器剪贴板 */ }
|
|
329
|
+
return readLocalClipboard();
|
|
330
|
+
};
|
|
331
|
+
const writeHostClipboard = async text => {
|
|
332
|
+
try { await rpc('personal-assistant.clipboard.write', { text: String(text ?? '') }); return; }
|
|
333
|
+
catch { /* 同上 */ }
|
|
334
|
+
await writeLocalClipboard(String(text ?? ''));
|
|
221
335
|
};
|
|
222
336
|
const pasteLocalClipboardToCloud = () => pasteToCloud({
|
|
223
|
-
readLocal: () =>
|
|
337
|
+
readLocal: () => readHostClipboard(),
|
|
224
338
|
writeCloud: text => clipboardRef.current.write(text),
|
|
225
339
|
sendKeys: sendDesktopKeys,
|
|
340
|
+
ensureControl: ensureClipboardControl,
|
|
341
|
+
notify: clipboardNotify,
|
|
342
|
+
});
|
|
343
|
+
// paste 事件兜底:macOS 上 Electron 的编辑菜单可能先把 ⌘V 吃掉,但浏览器仍会把
|
|
344
|
+
// 剪贴板内容作为 paste 事件交给聚焦的输入框,这里直接把文本写进云电脑剪贴板。
|
|
345
|
+
const pasteTextToCloud = text => pasteToCloud({
|
|
346
|
+
readLocal: async () => String(text ?? ''),
|
|
347
|
+
writeCloud: value => clipboardRef.current.write(value),
|
|
348
|
+
sendKeys: sendDesktopKeys,
|
|
349
|
+
ensureControl: ensureClipboardControl,
|
|
226
350
|
notify: clipboardNotify,
|
|
227
351
|
});
|
|
228
352
|
const drawLatest = async () => {
|
|
@@ -306,7 +430,8 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
306
430
|
let value; try { value = JSON.parse(event.data); } catch { return; }
|
|
307
431
|
if (value.type === 'ready') {
|
|
308
432
|
current.suspended = false; current.resuming = false; current.receivedSeq = -1;
|
|
309
|
-
current.session = value.sessionId; current.readOnly = Boolean(value.readOnly); current.display = normalizeDisplay(value.display);
|
|
433
|
+
current.session = value.sessionId; current.readOnly = Boolean(value.readOnly); current.display = normalizeDisplay(value.display); setDesktopDisplay(current.display);
|
|
434
|
+
setDesktopDiagnostics(value.diagnostics || null);
|
|
310
435
|
current.mode = mode;
|
|
311
436
|
controlRef.current.reset(value);
|
|
312
437
|
const inputs = current.pendingInputs; current.pendingInputs = [];
|
|
@@ -320,6 +445,9 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
320
445
|
if (value.type === 'clipboard.response') { clipboardRef.current?.accept(value); return; }
|
|
321
446
|
if (value.type === 'closed') { if (value.retryable !== true) ended(value); return; }
|
|
322
447
|
if (value.type === 'error') {
|
|
448
|
+
// 旧版 runtime 对 clipboard 消息只回无 requestId 的通用错误帧;
|
|
449
|
+
// 立刻结束在途请求,直接给出“版本较旧”提示,避免再等 5 秒超时。
|
|
450
|
+
if (clipboardRef.current?.accept(value)) return;
|
|
323
451
|
if (Number.isSafeInteger(value.controlEpoch) && value.controlEpoch < current.controlEpoch) return;
|
|
324
452
|
setNotice(value.message || value.code || '云电脑请求失败');
|
|
325
453
|
if (current.handoffSupported && value.recoverable === true) { controlRef.current.reject(value); return; }
|
|
@@ -336,6 +464,9 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
336
464
|
socket.onerror = () => { if (valid()) setNotice('桌面通道暂时断开,正在重连'); };
|
|
337
465
|
socket.onclose = event => {
|
|
338
466
|
if (!valid()) return;
|
|
467
|
+
// 挂起/异常关闭都要立即结束在途请求;否则面板会等到超时才知道
|
|
468
|
+
// 剪贴板操作没有完成,而重连后的响应也不应误认领旧 requestId。
|
|
469
|
+
clipboardRef.current?.rejectAll();
|
|
339
470
|
if (current.suspended && event.code !== 1000) { clearTimeout(current.firstFrame); current.firstFrame = null; current.socket = null; setPhase('dormant'); return; }
|
|
340
471
|
current.session = null; clearTimeout(current.firstFrame); current.firstFrame = null;
|
|
341
472
|
if (!current.reconnectAllowed || event.code === 1000) { ended(); return; }
|
|
@@ -388,7 +519,18 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
388
519
|
} catch (error) {
|
|
389
520
|
if (signal?.aborted || !state.current.mounted) return;
|
|
390
521
|
if (Number.isInteger(error?.closeCode) && error.closeCode >= 1000 && error.closeCode <= 4999) setDirectoryCloseCode(error.closeCode);
|
|
391
|
-
state.current.bots = []; setBots([]); setBotId('');
|
|
522
|
+
state.current.bots = []; setBots([]); setBotId('');
|
|
523
|
+
// 配置接口本身失败(Studio 未登录、网关不可达)时不能直接判死:desktop.status 不抛错,
|
|
524
|
+
// 用它区分「账号下确实没有实例」与「暂时读不到状态」,把开通/复用入口交给引导页。
|
|
525
|
+
const snapshot = retried ? null : await bindingSnapshot(signal).catch(() => null);
|
|
526
|
+
if (signal?.aborted || !state.current.mounted) return;
|
|
527
|
+
if (snapshot?.configured) { setBinding(null); await refreshConnection(signal, true); return; }
|
|
528
|
+
if (snapshot) {
|
|
529
|
+
setBinding(typeof snapshot.reason === 'string' && snapshot.reason ? snapshot : { ...snapshot, reason: 'unavailable', detail: bindingFailure(error) });
|
|
530
|
+
setPhase('unconfigured');
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
setPhase('directory-error');
|
|
392
534
|
setDirectoryError(directoryUnsupported(error) ? '云端助理需要更新,请更新后重新连接。' : failure(error));
|
|
393
535
|
}
|
|
394
536
|
};
|
|
@@ -407,9 +549,28 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
407
549
|
const blur = () => releaseControl();
|
|
408
550
|
const outside = event => { if (!viewRef.current?.contains(event.target)) releaseControl(); };
|
|
409
551
|
document.addEventListener('visibilitychange', visibility); window.addEventListener('blur', blur); document.addEventListener('pointerdown', outside, true);
|
|
410
|
-
return () => { current.mounted = false; observer?.disconnect(); if (current.pointerFrame) cancelAnimationFrame(current.pointerFrame); controller.abort(); close(false); clearInterval(interval); document.removeEventListener('visibilitychange', visibility); window.removeEventListener('blur', blur); document.removeEventListener('pointerdown', outside, true); };
|
|
552
|
+
return () => { current.mounted = false; observer?.disconnect(); if (current.pointerFrame) cancelAnimationFrame(current.pointerFrame); if (current.wheelFrame) cancelAnimationFrame(current.wheelFrame); controller.abort(); close(false); clearInterval(interval); document.removeEventListener('visibilitychange', visibility); window.removeEventListener('blur', blur); document.removeEventListener('pointerdown', outside, true); };
|
|
411
553
|
}, [request]);
|
|
412
554
|
const point = event => mapDisplayPoint({ clientX: event.clientX, clientY: event.clientY, rect: canvasRef.current.getBoundingClientRect(), display: state.current.display });
|
|
555
|
+
// 触控板一次滑动手势会推几十上百个 wheel 事件。以前每个事件都发一条 input,
|
|
556
|
+
// 云端每个都要跑一次 xdotool 且顺带触发一次抓帧,滚起来就明显迟滞;这里按动画帧
|
|
557
|
+
// 合并:像素累计够一格才发一条 wheel(余数跨帧保留),鼠标一格 = 一次滚动,触控板
|
|
558
|
+
// 按累计距离滚动,消息数和远端开销都降一个数量级。
|
|
559
|
+
const wheel = event => {
|
|
560
|
+
event.preventDefault();
|
|
561
|
+
const current = state.current;
|
|
562
|
+
const pending = current.wheelPending || (current.wheelPending = { x: 0, y: 0 });
|
|
563
|
+
current.wheelPending = { x: pending.x + (Number(event.deltaX) || 0), y: pending.y + (Number(event.deltaY) || 0) };
|
|
564
|
+
if (current.wheelFrame) return;
|
|
565
|
+
current.wheelFrame = requestAnimationFrame(() => {
|
|
566
|
+
current.wheelFrame = 0;
|
|
567
|
+
if (!current.mounted) return;
|
|
568
|
+
const merged = mergeWheel(current.wheelPending);
|
|
569
|
+
current.wheelPending = merged.pending;
|
|
570
|
+
if (!merged.clicks.deltaX && !merged.clicks.deltaY) return;
|
|
571
|
+
input({ type: 'wheel', deltaX: merged.clicks.deltaX, deltaY: merged.clicks.deltaY });
|
|
572
|
+
});
|
|
573
|
+
};
|
|
413
574
|
const pointer = event => {
|
|
414
575
|
event.preventDefault(); const current = state.current, button = [1, 2, 3][event.button] || 1;
|
|
415
576
|
if (event.type === 'pointerdown') { inputRef.current?.focus(); canvasRef.current?.setPointerCapture(event.pointerId); if (input({ type: 'button', action: 'down', button, ...point(event) })) current.buttons.add(button); }
|
|
@@ -451,9 +612,56 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
451
612
|
const emptyTitle = phase === 'unconfigured' ? bindingTitle : phase === 'empty' ? '助理正在准备中' : phase === 'directory-error' ? '暂时无法连接云端助理' : '正在检查助理状态';
|
|
452
613
|
const emptyDetail = phase === 'unconfigured' ? bindingDetail : phase === 'empty' ? '云端初始化完成后,可用助理会显示在这里。' : phase === 'directory-error' ? directoryError : '请稍候';
|
|
453
614
|
const computerVariant = variant === 'computer' || variant === 'native-tab';
|
|
615
|
+
// 接管状态紧贴桌面卡片下方;测量时扣掉胶囊本身,避免状态条把画面挤出右栏。
|
|
616
|
+
useLayoutEffect(() => {
|
|
617
|
+
if (!computerVariant) return undefined;
|
|
618
|
+
const node = stageAreaRef.current;
|
|
619
|
+
if (!node) return undefined;
|
|
620
|
+
const measure = () => {
|
|
621
|
+
const rect = node.getBoundingClientRect();
|
|
622
|
+
const measuredCapsuleHeight = controlCapsuleRef.current?.getBoundingClientRect?.().height || 0;
|
|
623
|
+
// Some hosts expose the parent rect for an unlaid-out child during the
|
|
624
|
+
// first pass. Cap the measurement so that transient geometry cannot
|
|
625
|
+
// collapse the desktop card to 0x0; ResizeObserver will refine it later.
|
|
626
|
+
const capsuleHeight = active || selectable ? Math.min(128, Math.max(44, measuredCapsuleHeight)) : 0;
|
|
627
|
+
const availableHeight = Math.max(0, rect.height - (capsuleHeight ? capsuleHeight + 10 : 0));
|
|
628
|
+
const next = fitDesktopBox({ availableWidth: rect.width, availableHeight, display: desktopDisplay, maxWidth: 1280 });
|
|
629
|
+
setStageBox(previous => {
|
|
630
|
+
if (!(next.width > 0 && next.height > 0)) return previous ?? null;
|
|
631
|
+
return previous && Math.abs(previous.width - next.width) < 0.5 && Math.abs(previous.height - next.height) < 0.5 ? previous : next;
|
|
632
|
+
});
|
|
633
|
+
};
|
|
634
|
+
measure();
|
|
635
|
+
const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(measure) : null;
|
|
636
|
+
observer?.observe(node);
|
|
637
|
+
if (controlCapsuleRef.current) observer?.observe(controlCapsuleRef.current);
|
|
638
|
+
return () => observer?.disconnect();
|
|
639
|
+
}, [computerVariant, desktopDisplay, active, selectable, controlPhase, notice, clipboardNotice]);
|
|
640
|
+
const guidance = phase === 'unconfigured' ? unconfiguredGuidance(binding) : null;
|
|
641
|
+
const onboardingClick = action => {
|
|
642
|
+
if (action === 'check') void refreshConnection();
|
|
643
|
+
else void bindAssistant(action);
|
|
644
|
+
};
|
|
645
|
+
const onboarding = guidance ? h('div', { 'data-personal-assistant-onboarding': 'true', style: onboardingStyle.page },
|
|
646
|
+
h('div', { 'data-window-drag': '', style: { display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center' } },
|
|
647
|
+
h('div', { style: onboardingStyle.eyebrow }, '个人助理 · 开通引导'),
|
|
648
|
+
h('h2', { style: onboardingStyle.title }, guidance.title)),
|
|
649
|
+
h('p', { style: onboardingStyle.detail }, guidance.detail),
|
|
650
|
+
h('ol', { style: onboardingStyle.steps },
|
|
651
|
+
onboardingSteps(guidance.tone).map((text, index) => h('li', { key: text, style: onboardingStyle.step },
|
|
652
|
+
h('span', { style: onboardingStyle.stepIndex }, String(index + 1)), h('span', null, text)))),
|
|
653
|
+
h('div', { style: onboardingStyle.actions },
|
|
654
|
+
guidance.actions.map(action => h('button', {
|
|
655
|
+
key: action.id, disabled: Boolean(busy), style: action.primary ? onboardingStyle.primary : onboardingStyle.ghost,
|
|
656
|
+
onClick: () => onboardingClick(action.id),
|
|
657
|
+
}, busy === action.id ? (action.id === 'create' ? '正在开通…' : action.id === 'connect' ? '正在连接…' : '检查中…') : action.label)),
|
|
658
|
+
busy ? h('span', { role: 'status', style: onboardingStyle.notice }, '处理中,请稍候…') : null),
|
|
659
|
+
notice ? h('p', { role: 'status', style: onboardingStyle.notice }, notice) : null,
|
|
660
|
+
h('details', { style: onboardingStyle.diag }, h('summary', { style: { cursor: 'pointer' } }, '诊断'),
|
|
661
|
+
h('small', { 'data-personal-assistant-build': PERSONAL_ASSISTANT_CLIENT_BUILD }, `构建 ${PERSONAL_ASSISTANT_CLIENT_BUILD}`))) : null;
|
|
454
662
|
const stage = h('div', { ref: viewRef, 'data-personal-assistant-desktop-card': computerVariant ? 'true' : undefined, style: {
|
|
455
663
|
position: 'relative', background: 'var(--dsw-alias-bg-layer-2, #f1f3f6)',
|
|
456
|
-
...(computerVariant ? { width: '100%', aspectRatio: `${
|
|
664
|
+
...(computerVariant ? { ...(stageBox && stageBox.width > 0 && stageBox.height > 0 ? { width: `${stageBox.width}px`, height: `${stageBox.height}px` } : { width: '100%', aspectRatio: `${desktopDisplay.width} / ${desktopDisplay.height}` }), flex: 'none', minWidth: 0, borderRadius: 14, overflow: 'hidden', boxShadow: '0 0 0 1px var(--dsw-alias-border-default, #d8dee8), 0 8px 28px #1018280d' } : { flex: 1, minHeight: 240 }),
|
|
457
665
|
} },
|
|
458
666
|
['dormant', 'resuming'].includes(phase) ? h('div', { role: 'status', style: { position: 'absolute', inset: 0, zIndex: 1, pointerEvents: 'none', display: 'grid', placeItems: 'center', padding: 16, textAlign: 'center', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, #fff) 82%, transparent)', color: 'var(--dsw-alias-label-primary, #17202c)' } }, phase === 'dormant' ? '已空闲,操作桌面即可恢复' : '正在恢复桌面…') : null,
|
|
459
667
|
h('canvas', { ref: canvasRef, 'aria-label': '云电脑桌面', tabIndex: active ? 0 : -1, onPointerDown: pointer, onPointerUp: pointer, onPointerCancel: releasePointer, onLostPointerCapture: releasePointer, onPointerLeave: () => { if (cursorRef.current) cursorRef.current.style.visibility = 'hidden'; }, onPointerMove: event => {
|
|
@@ -461,7 +669,7 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
461
669
|
if (cursorRef.current) { const rect = canvasRef.current.getBoundingClientRect(); cursorRef.current.style.transform = `translate(${latest.clientX - rect.left}px, ${latest.clientY - rect.top}px)`; cursorRef.current.style.visibility = current.readOnly ? 'hidden' : 'visible'; }
|
|
462
670
|
current.lastPointer = point(latest);
|
|
463
671
|
if (!current.pointerFrame) current.pointerFrame = requestAnimationFrame(() => { if (current.mounted) flushPointer(); });
|
|
464
|
-
}, onContextMenu: event => event.preventDefault(), onWheel:
|
|
672
|
+
}, onContextMenu: event => event.preventDefault(), onWheel: wheel, onKeyDown: key, onKeyUp: key, style: { ...(computerVariant ? { position: 'absolute', inset: 0 } : {}), width: '100%', height: '100%', display: 'block', visibility: active ? 'visible' : 'hidden', pointerEvents: active ? 'auto' : 'none', touchAction: 'none', cursor: readOnly ? 'default' : 'none' } }),
|
|
465
673
|
h('div', { ref: cursorRef, 'aria-hidden': true, style: { position: 'absolute', top: 0, left: 0, pointerEvents: 'none', visibility: 'hidden', zIndex: 2, width: 18, height: 22 } }, h('svg', { viewBox: '0 0 18 22', width: 18, height: 22 }, h('path', { d: 'M1 1v16l4-4 3 7 3-1-3-7h6z', fill: '#fff', stroke: '#162033', strokeWidth: 1.2 }))),
|
|
466
674
|
!active ? h('div', { style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 10, padding: 16, textAlign: 'center' } },
|
|
467
675
|
h('h2', { style: { margin: 0, fontSize: 18, fontWeight: 500 } }, selectable ? stateText : emptyTitle),
|
|
@@ -471,7 +679,7 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
471
679
|
phase === 'unconfigured' && (bindingReason === 'credential-unavailable' || candidateCount > 0) ? h('button', { style: buttonStyle, disabled: Boolean(busy), onClick: () => void bindAssistant('connect') }, busy === 'connect' ? '正在获取…' : bindingReason === 'credential-unavailable' ? '重新获取连接凭证' : '连接已发现的个人助理') : null,
|
|
472
680
|
h('button', { style: { ...buttonStyle, background: 'transparent' }, disabled: Boolean(busy), onClick: () => void refreshConnection() }, '检查开通状态'),
|
|
473
681
|
busy ? h('span', { role: 'status', style: { color: 'var(--dsw-alias-label-secondary, #536174)', fontSize: 12 } }, '处理中,请稍候…') : null) : null) : null,
|
|
474
|
-
h('input', { ref: inputRef, 'aria-label': '云电脑文本输入', autoComplete: 'off', disabled: !active || (readOnly && !controlRef.current.snapshot.available && !(state.current.mode === 'control' && ['dormant', 'resuming'].includes(phase))), tabIndex: -1, style: { position: 'absolute', width: 1, height: 1, opacity: 0, left: 0, top: 0 }, onKeyDown: key, onKeyUp: key, onCompositionStart: () => { state.current.composition = true; }, onCompositionEnd: event => { state.current.composition = false; const text = event.currentTarget.value; event.currentTarget.value = ''; if (text) input({ type: 'text', text }); }, onInput: event => { if (state.current.composition || event.nativeEvent?.isComposing) return; const text = event.currentTarget.value; event.currentTarget.value = ''; if (text) input({ type: 'text', text }); } }));
|
|
682
|
+
h('input', { ref: inputRef, 'aria-label': '云电脑文本输入', autoComplete: 'off', disabled: !active || (readOnly && !controlRef.current.snapshot.available && !(state.current.mode === 'control' && ['dormant', 'resuming'].includes(phase))), tabIndex: -1, style: { position: 'absolute', width: 1, height: 1, opacity: 0, left: 0, top: 0 }, onKeyDown: key, onKeyUp: key, onPaste: event => { const text = event.clipboardData?.getData?.('text/plain') ?? ''; if (!text) return; event.preventDefault(); void pasteTextToCloud(text); }, onCompositionStart: () => { state.current.composition = true; }, onCompositionEnd: event => { state.current.composition = false; const text = event.currentTarget.value; event.currentTarget.value = ''; if (text) input({ type: 'text', text }); }, onInput: event => { if (state.current.composition || event.nativeEvent?.isComposing) return; const text = event.currentTarget.value; event.currentTarget.value = ''; if (text) input({ type: 'text', text }); } }));
|
|
475
683
|
const secondaryInk = 'var(--dsw-alias-label-secondary, #536174)';
|
|
476
684
|
const ghostStyle = { ...buttonStyle, background: 'transparent', color: 'inherit', border: ui.border };
|
|
477
685
|
const pillStyle = { ...buttonStyle, borderRadius: 999, padding: '7px 16px', border: 'none', fontWeight: 500, background: 'var(--dsw-alias-label-primary, #17202c)', color: 'var(--dsw-alias-bg-layer-1, #fff)' };
|
|
@@ -482,17 +690,29 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
482
690
|
h('span', { role: 'status', 'aria-live': 'polite', style: { color: secondaryInk, fontSize: 12 } }, stateText),
|
|
483
691
|
null) : null,
|
|
484
692
|
readOnly && active && !controlRef.current.snapshot.available ? h('div', { role: 'alert', style: { background: '#fff0d6', color: '#763c00', padding: '8px 12px' } }, '只读旁观:鼠标和键盘不会发送到云电脑') : null,
|
|
485
|
-
h('div', { 'data-personal-assistant-desktop-layout': 'true', style: { flex: 1, minHeight: 0, minWidth: 0, overflow: '
|
|
486
|
-
h('div', { 'data-personal-assistant-desktop-stack': 'true', style: { width: '100%', maxWidth: 1280,
|
|
487
|
-
|
|
693
|
+
h('div', { 'data-personal-assistant-desktop-layout': 'true', style: { flex: 1, minHeight: 0, minWidth: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12, padding: '20px 16px 12px', boxSizing: 'border-box' } },
|
|
694
|
+
h('div', { ref: stageAreaRef, 'data-personal-assistant-desktop-stack': 'true', style: { width: '100%', maxWidth: 1280, minHeight: 0, flex: '1 1 auto', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 10 } },
|
|
695
|
+
onboarding || stage,
|
|
696
|
+
active || selectable ? h('div', { ref: controlCapsuleRef, 'data-personal-assistant-control-capsule': 'true', role: 'status', 'aria-live': 'polite', style: { flex: 'none', display: 'flex', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', gap: 6, width: 'fit-content', maxWidth: '100%', boxSizing: 'border-box', padding: '7px 12px', borderRadius: 999, background: 'var(--dsw-alias-bg-layer-1, #fff)', border: ui.border, boxShadow: '0 1px 4px #10182808', lineHeight: 1.5 } },
|
|
488
697
|
!active && selectable ? h('button', { style: pillStyle, disabled: ['connecting', 'resuming'].includes(phase), onClick: () => void open(state.current.handoffSupported ? 'observe' : 'read') }, phase === 'connecting' ? '正在启动…' : '重新连接') : null,
|
|
489
698
|
active && readOnly ? h('span', { style: { color: secondaryInk, fontSize: 12 } }, controlPhase === 'acquiring' ? '正在接管…' : controlPhase === 'releasing' ? '正在交还…' : '助理正在使用 · 点画面即可接管') : null,
|
|
490
699
|
active && readOnly && controlRef.current.snapshot.available ? h('button', { style: { ...pillStyle, padding: '3px 9px', fontSize: 11 }, onClick: () => controlRef.current.acquire() }, '接管') : null,
|
|
491
700
|
active && !readOnly ? h('span', { style: { color: secondaryInk, fontSize: 12 } }, '您拥有控制权 · 点画面外释放') : null,
|
|
701
|
+
active && !readOnly ? h('button', { style: { ...ghostStyle, padding: '3px 9px', fontSize: 11 }, onClick: releaseControl }, '交还') : null,
|
|
492
702
|
active && notice ? h('span', { style: { color: secondaryInk, fontSize: 12 } }, notice) : null,
|
|
493
703
|
// 剪贴板不再放按钮:面板里只保留 ⌘V 粘贴 / ⌘C 复制与一行状态文字。
|
|
494
704
|
active && clipboardNotice ? h('span', { role: 'status', 'aria-live': 'polite', style: { color: secondaryInk, fontSize: 12 } }, clipboardNotice) : null) : null),
|
|
495
|
-
|
|
705
|
+
),
|
|
706
|
+
h('details', { style: { flex: 'none', alignSelf: 'flex-end', color: 'var(--dsw-alias-label-tertiary, #76808e)', fontSize: 11, opacity: 0.8, maxWidth: '100%', overflowWrap: 'anywhere' } },
|
|
707
|
+
h('summary', { style: { cursor: 'pointer' } }, '诊断'),
|
|
708
|
+
h('div', { style: { paddingTop: 8, lineHeight: 1.7 } },
|
|
709
|
+
h('div', null, `客户端:${PERSONAL_ASSISTANT_CLIENT_BUILD}`),
|
|
710
|
+
desktopDiagnostics ? h('div', null, `云电脑包:${desktopDiagnostics.packageVersion || '未知'} · 清单:${desktopDiagnostics.manifestVersion || '未知'}`) : null,
|
|
711
|
+
desktopDiagnostics?.runtimeBuild && desktopDiagnostics.runtimeBuild !== 'unknown' ? h('div', null, `运行时构建:${desktopDiagnostics.runtimeBuild}`) : null,
|
|
712
|
+
desktopDiagnostics?.computerImage && desktopDiagnostics.computerImage !== 'unknown' ? h('div', null, `电脑镜像:${desktopDiagnostics.computerImage}`) : null,
|
|
713
|
+
desktopDiagnostics?.dock ? h('div', null, `Dock:${desktopDiagnostics.dock.position} · ${desktopDiagnostics.dock.iconCount} 个图标`) : null,
|
|
714
|
+
)),
|
|
715
|
+
);
|
|
496
716
|
}
|
|
497
717
|
return h('section', { 'data-personal-assistant-panel': 'true', style: { ...ui, border: 'none', height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', fontSize: 13 } },
|
|
498
718
|
h('header', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', padding: 12, borderBottom: ui.border } },
|
|
@@ -504,11 +724,17 @@ export function PersonalAssistantPanel({ request, initialBotId = '', openInSideb
|
|
|
504
724
|
active ? h('span', { role: 'status', 'aria-live': 'polite', style: { marginLeft: 'auto', color: 'var(--dsw-alias-label-secondary, #536174)' } }, stateText) : null),
|
|
505
725
|
phase === 'readonly' ? h('div', { role: 'alert', style: { background: '#fff0d6', color: '#763c00', padding: '8px 12px' } }, '当前为只读旁观,鼠标和键盘输入不会发送;可申请控制') : null,
|
|
506
726
|
active && notice ? h('div', { role: 'status', style: { padding: '8px 12px' } }, notice) : null,
|
|
507
|
-
stage,
|
|
727
|
+
onboarding || stage,
|
|
508
728
|
h('footer', { style: { display: 'flex', alignItems: 'start', justifyContent: 'space-between', gap: 12, padding: '7px 12px', color: 'var(--dsw-alias-label-secondary, #536174)', borderTop: ui.border } },
|
|
509
729
|
h('details', { open: diagnosticsOpen, onToggle: event => setDiagnosticsOpen(event.currentTarget.open) }, h('summary', { style: { cursor: 'pointer' } }, '诊断'),
|
|
510
730
|
directoryCloseCode ? h('div', { style: { paddingTop: 8 } }, `目录连接关闭码:${directoryCloseCode}`) : null,
|
|
511
|
-
h('div', { style: { paddingTop: 8, lineHeight: 1.8, overflowWrap: 'anywhere' } },
|
|
731
|
+
h('div', { style: { paddingTop: 8, lineHeight: 1.8, overflowWrap: 'anywhere' } },
|
|
732
|
+
h('div', null, `云端实例:${config?.agentId || '未开通'}`),
|
|
733
|
+
h('div', null, `连接标识:${botId || '未连接'}`),
|
|
734
|
+
h('div', null, `画面帧率:${fps} fps`),
|
|
735
|
+
desktopDiagnostics ? h('div', null, `云电脑包:${desktopDiagnostics.packageVersion || '未知'} · 清单:${desktopDiagnostics.manifestVersion || '未知'}`) : null,
|
|
736
|
+
desktopDiagnostics?.dock ? h('div', null, `Dock:${desktopDiagnostics.dock.position} · ${desktopDiagnostics.dock.iconCount} 个图标`) : null,
|
|
737
|
+
h('div', null, '快捷键会转发到云电脑;Esc 优先退出全屏'))),
|
|
512
738
|
h('small', { 'data-personal-assistant-build': PERSONAL_ASSISTANT_CLIENT_BUILD, style: { opacity: 0.75 } }, `构建 ${PERSONAL_ASSISTANT_CLIENT_BUILD}`)));
|
|
513
739
|
}
|
|
514
740
|
|
|
@@ -520,14 +746,21 @@ export function PersonalAssistantHome({ request, nativeWorkbench } = {}) {
|
|
|
520
746
|
const { createElement: h, useEffect, useState, useCallback } = R;
|
|
521
747
|
const [scope, setScope] = useState(null);
|
|
522
748
|
const [error, setError] = useState('');
|
|
749
|
+
const [binding, setBinding] = useState(null);
|
|
750
|
+
const [bindingBusy, setBindingBusy] = useState('');
|
|
523
751
|
const [attempt, retry] = useState(0);
|
|
524
752
|
useEffect(() => {
|
|
525
753
|
let cancelled = false;
|
|
526
|
-
setScope(null); setError('');
|
|
754
|
+
setScope(null); setError(''); setBinding(null);
|
|
527
755
|
void (async () => {
|
|
528
756
|
try {
|
|
529
757
|
const config = await request('personal-assistant.configuration');
|
|
530
|
-
if (!config.configured)
|
|
758
|
+
if (!config.configured) {
|
|
759
|
+
// 未开通不是错误:读一次不抛错的绑定快照,交给引导页决定「开通」还是「复用」。
|
|
760
|
+
const snapshot = await request('personal-assistant.desktop.status').catch(() => null);
|
|
761
|
+
if (!cancelled) setBinding(snapshot || { configured: false, reason: 'unavailable', candidates: [] });
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
531
764
|
const directory = await request('personal-assistant.bots.list');
|
|
532
765
|
if (directory.directoryAvailable !== true) throw new Error('暂时无法连接云端助理,请稍后重试');
|
|
533
766
|
const botId = resolveAssistantBotId(directory.bots, config.botId || directory.primaryBotId);
|
|
@@ -543,13 +776,38 @@ export function PersonalAssistantHome({ request, nativeWorkbench } = {}) {
|
|
|
543
776
|
return request(operation, { ...payload, ...scope });
|
|
544
777
|
}, [request, scope?.agentId, scope?.botId]);
|
|
545
778
|
useEffect(() => { if (scope && nativeWorkbench?.available) nativeWorkbench.bind(scope, scopedRequest); else if (scope) setError(nativeWorkbench?.unavailableReason || '当前宿主需要更新原生侧栏组件'); }, [nativeWorkbench, scopedRequest, scope?.agentId, scope?.botId]);
|
|
779
|
+
const guidance = binding ? unconfiguredGuidance(binding) : null;
|
|
780
|
+
const runBinding = action => {
|
|
781
|
+
if (bindingBusy) return;
|
|
782
|
+
setBindingBusy(action); setError('');
|
|
783
|
+
void (async () => {
|
|
784
|
+
try { await request(`personal-assistant.desktop.${action}`); retry(value => value + 1); }
|
|
785
|
+
catch (reason) { setError(reason?.message || '操作失败,请稍后重试'); }
|
|
786
|
+
finally { setBindingBusy(''); }
|
|
787
|
+
})();
|
|
788
|
+
};
|
|
789
|
+
if (!scope && guidance) return h('div', { 'data-personal-assistant-onboarding': 'true', style: onboardingStyle.page },
|
|
790
|
+
h('div', { 'data-window-drag': '', style: { display: 'flex', flexDirection: 'column', gap: 4, alignItems: 'center' } },
|
|
791
|
+
h('div', { style: onboardingStyle.eyebrow }, '个人助理 · 开通引导'),
|
|
792
|
+
h('h2', { style: onboardingStyle.title }, guidance.title)),
|
|
793
|
+
h('p', { style: onboardingStyle.detail }, guidance.detail),
|
|
794
|
+
h('ol', { style: onboardingStyle.steps },
|
|
795
|
+
onboardingSteps(guidance.tone).map((text, index) => h('li', { key: text, style: onboardingStyle.step },
|
|
796
|
+
h('span', { style: onboardingStyle.stepIndex }, String(index + 1)), h('span', null, text)))),
|
|
797
|
+
h('div', { style: onboardingStyle.actions },
|
|
798
|
+
guidance.actions.map(action => h('button', {
|
|
799
|
+
key: action.id, disabled: Boolean(bindingBusy),
|
|
800
|
+
style: action.primary ? onboardingStyle.primary : onboardingStyle.ghost,
|
|
801
|
+
onClick: () => action.id === 'check' ? retry(value => value + 1) : runBinding(action.id),
|
|
802
|
+
}, bindingBusy === action.id ? '处理中…' : action.label))),
|
|
803
|
+
error ? h('p', { role: 'alert', style: { ...onboardingStyle.notice, color: '#b42318' } }, error) : null);
|
|
546
804
|
if (!scope) return h('div', { role: 'status', style: { padding: 24 } }, error || '正在连接云端个人助理…', error ? h('button', { onClick: () => retry(value => value + 1) }, '重新连接') : null);
|
|
547
805
|
if (!nativeWorkbench?.available) return h('div', { role: 'alert', style: { padding: 24 } }, '当前宿主尚未提供原生云电脑侧栏,请更新 Studio 后重新打开。');
|
|
548
806
|
return h('div', {
|
|
549
807
|
key: `${scope.agentId}:${scope.botId}`, 'data-personal-assistant-home': 'true',
|
|
550
808
|
'data-agent-id': scope.agentId, 'data-bot-id': scope.botId,
|
|
551
809
|
style: { flex: '1 1 0px', height: '100%', maxHeight: '100%', minHeight: 0, minWidth: 0, overflow: 'hidden', boxSizing: 'border-box', display: 'flex', flexDirection: 'column' },
|
|
552
|
-
}, h(PersonalAssistantConversation, { React: R, request: scopedRequest, scope, onOpenComputer: () => nativeWorkbench.open() }));
|
|
810
|
+
}, h(PersonalAssistantConversation, { React: R, request: scopedRequest, scope, markdown: clientMarkdown, onOpenComputer: () => nativeWorkbench.open() }));
|
|
553
811
|
|
|
554
812
|
}
|
|
555
813
|
|
|
@@ -618,6 +876,8 @@ export function PersonalAssistantSettings({ request, openComputer = null } = {})
|
|
|
618
876
|
const [settingsError, setSettingsError] = useState('');
|
|
619
877
|
const [settingsNotice, setSettingsNotice] = useState('');
|
|
620
878
|
const [settingsBusy, setSettingsBusy] = useState(false);
|
|
879
|
+
const [binding, setBinding] = useState(null);
|
|
880
|
+
const [bindingBusy, setBindingBusy] = useState('');
|
|
621
881
|
|
|
622
882
|
const check = useCallback(async () => {
|
|
623
883
|
setChecking(true); setError('');
|
|
@@ -625,6 +885,8 @@ export function PersonalAssistantSettings({ request, openComputer = null } = {})
|
|
|
625
885
|
try {
|
|
626
886
|
const current = await request('personal-assistant.configuration');
|
|
627
887
|
setConfig(current);
|
|
888
|
+
if (current?.configured) setBinding(null);
|
|
889
|
+
else setBinding(await request('personal-assistant.desktop.status').catch(() => null));
|
|
628
890
|
if (current?.configured) {
|
|
629
891
|
const directory = await request('personal-assistant.bots.list');
|
|
630
892
|
setBots(Array.isArray(directory?.bots) ? directory.bots : []);
|
|
@@ -654,6 +916,14 @@ export function PersonalAssistantSettings({ request, openComputer = null } = {})
|
|
|
654
916
|
} finally { setChecking(false); }
|
|
655
917
|
}, [request]);
|
|
656
918
|
|
|
919
|
+
const runBinding = useCallback(async action => {
|
|
920
|
+
if (bindingBusy) return;
|
|
921
|
+
setBindingBusy(action); setError('');
|
|
922
|
+
try { await request(`personal-assistant.desktop.${action}`); await check(); }
|
|
923
|
+
catch (reason) { setError(reason?.message || '操作失败,请稍后重试'); }
|
|
924
|
+
finally { setBindingBusy(''); }
|
|
925
|
+
}, [bindingBusy, check, request]);
|
|
926
|
+
|
|
657
927
|
const agentId = config?.agentId || '';
|
|
658
928
|
const canConfirm = Boolean(agentId) && confirmText.trim() === agentId;
|
|
659
929
|
const closeAssistant = useCallback(async () => {
|
|
@@ -712,6 +982,17 @@ export function PersonalAssistantSettings({ request, openComputer = null } = {})
|
|
|
712
982
|
const statusStyle = { ...settingsStyle.badge, ...(config === null ? settingsStyle.badgeIdle : configured ? settingsStyle.badgeOk : settingsStyle.badgeIdle) };
|
|
713
983
|
const line = (label, value, first = false) => h('div', { style: first ? settingsStyle.rowFirst : settingsStyle.row },
|
|
714
984
|
h('div', { style: settingsStyle.label }, label), h('div', { style: settingsStyle.value }, value));
|
|
985
|
+
const guidance = configured ? null : unconfiguredGuidance(binding);
|
|
986
|
+
const guidanceCard = guidance ? h('div', { 'data-personal-assistant-onboarding': 'true', style: settingsStyle.card },
|
|
987
|
+
h('div', { style: { ...settingsStyle.rowFirst, flexDirection: 'column', gap: 8, alignItems: 'stretch' } },
|
|
988
|
+
h('div', { style: { fontSize: 15, fontWeight: 600 } }, guidance.title),
|
|
989
|
+
h('div', { style: settingsStyle.note }, guidance.detail),
|
|
990
|
+
h('div', { style: settingsStyle.actions },
|
|
991
|
+
guidance.actions.map(action => h('button', {
|
|
992
|
+
key: action.id, disabled: Boolean(bindingBusy),
|
|
993
|
+
style: action.primary ? settingsStyle.button : { ...settingsStyle.button, background: 'transparent' },
|
|
994
|
+
onClick: () => action.id === 'check' ? void check() : void runBinding(action.id),
|
|
995
|
+
}, bindingBusy === action.id ? '处理中…' : action.label))))) : null;
|
|
715
996
|
|
|
716
997
|
const envSensitiveStyle = { ...settingsStyle.note, display: 'inline-flex', gap: 4, alignItems: 'center' };
|
|
717
998
|
const envActionsStyle = { ...settingsStyle.actions, marginTop: 6 };
|
|
@@ -777,10 +1058,11 @@ export function PersonalAssistantSettings({ request, openComputer = null } = {})
|
|
|
777
1058
|
};
|
|
778
1059
|
|
|
779
1060
|
return h('section', { 'data-personal-assistant-settings': 'true', style: settingsStyle.page },
|
|
780
|
-
h('div',
|
|
1061
|
+
h('div', { 'data-window-drag': '' },
|
|
781
1062
|
h('h1', { style: settingsStyle.title }, '个人助理'),
|
|
782
1063
|
h('p', { style: { ...settingsStyle.lede, marginTop: 6 } }, '云端持续运行的助理,带一台独立云电脑。关掉窗口它也在工作;对话与云电脑在左侧「个人助理」页。')),
|
|
783
1064
|
error ? h('div', { role: 'alert', style: { ...settingsStyle.note, color: '#b42318', background: 'color-mix(in srgb, #b42318 8%, transparent)', border: '1px solid color-mix(in srgb, #b42318 25%, transparent)', borderRadius: 10, padding: '9px 12px' } }, error) : null,
|
|
1065
|
+
guidanceCard,
|
|
784
1066
|
h('div', { style: settingsStyle.card },
|
|
785
1067
|
line('状态', h('span', null, h('span', { style: statusStyle }, status), checkedAt ? h('span', { style: settingsStyle.note }, ` 上次检查 ${checkedAt}`) : null), true),
|
|
786
1068
|
line('云端实例', config?.agentId ? h('span', { style: settingsStyle.mono }, config.agentId) : h('span', { style: settingsStyle.note }, configured ? '未返回实例标识' : '开通后由 Studio 绑定')),
|