@kingsoftcloud/agentkit-personal-assistant 0.0.0-stage → 0.1.7

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.
Files changed (70) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +7 -0
  3. package/README.md +38 -2
  4. package/cordis.patch.yml +6 -0
  5. package/dist/client.js +6 -0
  6. package/package.json +83 -4
  7. package/personal-assistant.plugin.json +31 -0
  8. package/personal-assistant.plugin.schema.json +43 -0
  9. package/runtime/assistant-runtime.mjs +317 -0
  10. package/runtime/check-manifest.mjs +21 -0
  11. package/runtime/cloud-browser-cdp.mjs +313 -0
  12. package/runtime/cloud-browser.mjs +972 -0
  13. package/runtime/cloud-execution.mjs +320 -0
  14. package/runtime/cloud-session-channel.mjs +45 -0
  15. package/runtime/computer-agentd.mjs +488 -0
  16. package/runtime/desktop-clipboard-limit.mjs +2 -0
  17. package/runtime/desktop-launcher.mjs +106 -0
  18. package/runtime/desktop-permit.mjs +42 -0
  19. package/runtime/desktop-session.mjs +840 -0
  20. package/runtime/desktop-ws.mjs +371 -0
  21. package/runtime/prepare-dsh-profile.mjs +17 -0
  22. package/runtime/welcome-server.mjs +144 -0
  23. package/scripts/assistant-runtime-launcher.sh +3 -0
  24. package/scripts/build-artifacts.mjs +81 -0
  25. package/scripts/build-client.mjs +82 -0
  26. package/scripts/computer-agentd-launcher.sh +3 -0
  27. package/scripts/deterministic_tar.py +21 -0
  28. package/scripts/install-local.mjs +262 -0
  29. package/scripts/scan-secrets.mjs +41 -0
  30. package/scripts/sync-public.mjs +52 -0
  31. package/scripts/verification/gateway-desktop-client.mjs +142 -0
  32. package/scripts/verification/verify-desktop-applications-linux.mjs +46 -0
  33. package/scripts/verification/verify-desktop-input-linux.mjs +127 -0
  34. package/scripts/verification/verify-desktop-pod-gateway.mjs +204 -0
  35. package/scripts/verify-cloud-turn.mjs +18 -0
  36. package/scripts/verify-computer-broker.mjs +10 -0
  37. package/src/automation-tools.mjs +18 -0
  38. package/src/bot-private-tools.mjs +23 -0
  39. package/src/bot-registry.mjs +157 -0
  40. package/src/bot-tool-policy.mjs +48 -0
  41. package/src/bot-tool-scope.mjs +40 -0
  42. package/src/client/bot-directory.mjs +23 -0
  43. package/src/client/components/desktop-clipboard.mjs +194 -0
  44. package/src/client/conversation.mjs +558 -0
  45. package/src/client/desktop-control.mjs +70 -0
  46. package/src/client/desktop-view.mjs +68 -0
  47. package/src/client/entry.mjs +8 -0
  48. package/src/client/index.mjs +733 -0
  49. package/src/client/markdown.mjs +130 -0
  50. package/src/client/native-workbench.mjs +62 -0
  51. package/src/cloud-computer-tools.mjs +245 -0
  52. package/src/cloud-files.mjs +57 -0
  53. package/src/cloud-model.mjs +10 -0
  54. package/src/cloud-session-transport.mjs +76 -0
  55. package/src/computer-broker-protocol.mjs +116 -0
  56. package/src/computer-broker.mjs +220 -0
  57. package/src/desktop-tools.mjs +64 -0
  58. package/src/dsh-session-bridge.mjs +207 -0
  59. package/src/dsh-web-bridge.mjs +127 -0
  60. package/src/host-gateway.mjs +281 -0
  61. package/src/host-session-channel.mjs +410 -0
  62. package/src/implementation-capabilities.mjs +10 -0
  63. package/src/index.mjs +148 -0
  64. package/src/persistent-state.mjs +201 -0
  65. package/src/platform-manifest.mjs +24 -0
  66. package/src/plugin-manifest.mjs +82 -0
  67. package/src/schedule-manager.mjs +88 -0
  68. package/src/system-fetch.mjs +72 -0
  69. package/src/workspace-tools.mjs +22 -0
  70. package/src/workspace.mjs +22 -0
@@ -0,0 +1,733 @@
1
+ /** Browser-safe Personal Assistant client and Cloud Computer panel. */
2
+
3
+ export class PersonalAssistantGatewayUnavailableError extends Error {
4
+ constructor() {
5
+ super('Personal Assistant gateway is not connected');
6
+ this.name = 'PersonalAssistantGatewayUnavailableError';
7
+ this.code = 'PERSONAL_ASSISTANT_GATEWAY_UNAVAILABLE';
8
+ }
9
+ }
10
+
11
+ function gatewayRequest(gateway, operation, payload) {
12
+ if (!gateway || typeof gateway.request !== 'function') throw new PersonalAssistantGatewayUnavailableError();
13
+ return gateway.request(operation, payload);
14
+ }
15
+
16
+ export function createPersonalAssistantClient({ gateway, profileId = 'default' } = {}) {
17
+ let selectedBotId = null;
18
+ const request = (operation, payload = {}) => gatewayRequest(gateway, operation, { ...payload, profileId });
19
+ const desktop = {
20
+ open: input => request('personal-assistant.desktop.session.open', { ...input, botId: input?.botId ?? selectedBotId }),
21
+ ticket: input => request('personal-assistant.desktop.ticket', { ...input, botId: input?.botId ?? selectedBotId }),
22
+ frame: input => request('personal-assistant.desktop.frame', input),
23
+ input: input => request('personal-assistant.desktop.input', input),
24
+ close: input => request('personal-assistant.desktop.session.close', input),
25
+ };
26
+ return Object.freeze({
27
+ configuration: () => request('personal-assistant.configuration'),
28
+ listBots: () => request('personal-assistant.bots.list'),
29
+ getBot: botId => request('personal-assistant.bots.get', { botId }),
30
+ selectBot(botId) {
31
+ selectedBotId = String(botId ?? '').trim();
32
+ if (!selectedBotId) throw new TypeError('botId must be a non-empty string');
33
+ return selectedBotId;
34
+ },
35
+ selectedBot: () => selectedBotId,
36
+ session: input => request('personal-assistant.session.create', { ...input, botId: input?.botId ?? selectedBotId }),
37
+ listSessions: botId => request('personal-assistant.sessions.list', { botId: botId ?? selectedBotId }),
38
+ createSession: input => request('personal-assistant.sessions.create', { ...input, botId: input?.botId ?? selectedBotId }),
39
+ listMessages: sessionId => request('personal-assistant.sessions.messages.list', { sessionId, botId: selectedBotId }),
40
+ appendMessage: (sessionId, input) => request('personal-assistant.sessions.messages.append', { sessionId, ...input, botId: input?.botId ?? selectedBotId }),
41
+ listTurns: sessionId => request('personal-assistant.sessions.turns.list', { sessionId, botId: selectedBotId }),
42
+ submitTurn: (sessionId, input) => request('personal-assistant.sessions.turns.submit', { sessionId, ...input, botId: input?.botId ?? selectedBotId }),
43
+ searchMemory: query => request('personal-assistant.memory.search', { query, botId: selectedBotId }),
44
+ remember: input => request('personal-assistant.memory.upsert', { ...input, botId: input?.botId ?? selectedBotId }),
45
+ listSchedules: botId => request('personal-assistant.schedules.list', { botId: botId ?? selectedBotId }),
46
+ createSchedule: input => request('personal-assistant.schedules.create', { ...input, botId: input?.botId ?? selectedBotId }),
47
+ deleteSchedule: scheduleId => request('personal-assistant.schedules.delete', { scheduleId, botId: selectedBotId }),
48
+ computerStatus: computerId => request('personal-assistant.computer.status', { computerId, botId: selectedBotId }),
49
+ computerAction: input => request('personal-assistant.computer.action', { ...input, botId: input?.botId ?? selectedBotId }),
50
+ browserOpen: url => request('personal-assistant.computer.action', { action: 'open', args: { url }, botId: selectedBotId }),
51
+ browserSnapshot: () => request('personal-assistant.computer.action', { action: 'snapshot', args: {}, botId: selectedBotId }),
52
+ browserClick: selector => request('personal-assistant.computer.action', { action: 'click', args: { selector }, botId: selectedBotId }),
53
+ browserType: (text, selector) => request('personal-assistant.computer.action', { action: 'type', args: { text, ...(selector === undefined ? {} : { selector }) }, botId: selectedBotId }),
54
+ browserPress: key => request('personal-assistant.computer.action', { action: 'press', args: { key }, botId: selectedBotId }),
55
+ browserScreenshot: () => request('personal-assistant.computer.action', { action: 'screenshot', args: {}, botId: selectedBotId }),
56
+ browserDownload: args => request('personal-assistant.computer.action', { action: 'download', args: args || {}, botId: selectedBotId }),
57
+ browserClose: () => request('personal-assistant.computer.action', { action: 'stop', args: {}, botId: selectedBotId }),
58
+ cloudBash: (command, timeoutMs) => request('personal-assistant.computer.action', { action: 'terminal.exec', args: { language: 'shell', command, ...(timeoutMs === undefined ? {} : { timeoutMs }) }, botId: selectedBotId }),
59
+ cloudRunCode: (language, code, timeoutMs) => request('personal-assistant.computer.action', { action: 'terminal.exec', args: { language, code, ...(timeoutMs === undefined ? {} : { timeoutMs }) }, botId: selectedBotId }),
60
+ clipboardRead: () => request('personal-assistant.computer.action', { action: 'clipboard.read', args: {}, botId: selectedBotId }),
61
+ clipboardWrite: text => request('personal-assistant.computer.action', { action: 'clipboard.write', args: { text }, botId: selectedBotId }),
62
+ desktop,
63
+ });
64
+ }
65
+
66
+ export const inject = Object.freeze(['slots', 'connection', 'sidebarRight']);
67
+ export const PERSONAL_ASSISTANT_PANEL_ID = 'personal-assistant';
68
+ export const PERSONAL_ASSISTANT_CLIENT_DEFAULTS = Object.freeze({ enabled: false, autoConnect: false });
69
+ export const PERSONAL_ASSISTANT_COMPUTER_TAB = 'personal-assistant-computer';
70
+ import { mapDisplayPoint, normalizeDisplay } from './desktop-view.mjs';
71
+ import { createDesktopControl } from './desktop-control.mjs';
72
+ import { createClipboardChannel, isPasteShortcut, isCopyShortcut, copyCloudClipboard, pasteToCloud, readLocalClipboard } from './components/desktop-clipboard.mjs';
73
+ import { PersonalAssistantConversation } from './conversation.mjs';
74
+ import { createAssistantWorkbench, AssistantNativeRightbar } from './native-workbench.mjs';
75
+ import { publicBotDirectory, resolveAssistantBotId } from './bot-directory.mjs';
76
+ export const PERSONAL_ASSISTANT_CLIENT_BUILD = typeof __PA_CLIENT_BUILD__ === 'string' ? __PA_CLIENT_BUILD__ : '开发模式';
77
+ const FIRST_FRAME_TIMEOUT_MS = 12_000;
78
+ let clientReact;
79
+ export function setClientReactRuntime(runtime) { clientReact = runtime; }
80
+ const ui = {
81
+ background: 'var(--dsw-alias-bg-layer-1, #fff)', color: 'var(--dsw-alias-label-primary, #17202c)',
82
+ border: '1px solid var(--dsw-alias-border-default, #d8dee8)',
83
+ };
84
+ const bindingFailure = error => typeof error?.message === 'string' && error.message && !/^(fetch failed|Failed to fetch)$/u.test(error.message) ? error.message.slice(0, 180) : '云电脑连接失败,请稍后重试';
85
+ const failure = error => error?.code === 'PERSONAL_ASSISTANT_DIRECTORY_UNAVAILABLE' && Number(error?.status) === 409 ? '当前云端版本尚不支持助理列表,请更新云端助理后重试' : '连接暂时不可用,请稍后重试';
86
+ const directoryUnsupported = error => Number(error?.status) === 409 && ['PERSONAL_ASSISTANT_DIRECTORY_UNSUPPORTED', 'PERSONAL_ASSISTANT_DIRECTORY_UNAVAILABLE'].includes(error?.code);
87
+ 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
+
89
+ export function PersonalAssistantPanel({ request, initialBotId = '', openInSidebar = null, variant = 'page', nativeFullscreen = false } = {}) {
90
+ const R = clientReact || globalThis.React;
91
+ if (!R) return '个人助理页面尚未就绪,请重新打开';
92
+ const { createElement: h, useEffect, useRef, useState } = R;
93
+ const [config, setConfig] = useState(null);
94
+ const [bots, setBots] = useState([]);
95
+ const [directoryError, setDirectoryError] = useState('');
96
+ const [directoryCloseCode, setDirectoryCloseCode] = useState(null);
97
+ const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
98
+ const [botId, setBotId] = useState(initialBotId);
99
+ const [phase, setPhase] = useState('loading');
100
+ const [notice, setNotice] = useState('');
101
+ const [readOnly, setReadOnly] = useState(true);
102
+ const [attempt, setAttempt] = useState(0);
103
+ const [fps, setFps] = useState(0);
104
+ const [controlPhase, setControlPhase] = useState('observe');
105
+ const [binding, setBinding] = useState(null);
106
+ const [busy, setBusy] = useState('');
107
+ const [clipboardNotice, setClipboardNotice] = useState('');
108
+ const controlRef = useRef(null);
109
+ const clipboardRef = useRef(null), clipboardTimerRef = useRef(null);
110
+ const canvasRef = useRef(null), inputRef = useRef(null), viewRef = useRef(null), cursorRef = useRef(null);
111
+ 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 });
113
+ state.current.botId = botId;
114
+ const rpc = async (operation, payload = {}) => {
115
+ if (typeof request !== 'function') throw new Error('Studio Host 尚未连接');
116
+ return request(operation, payload);
117
+ };
118
+ const send = body => {
119
+ const current = state.current;
120
+ if (current.socket?.readyState !== 1) return false;
121
+ current.socket.send(JSON.stringify(body)); return true;
122
+ };
123
+ const releaseInputs = () => {
124
+ const current = state.current;
125
+ for (const key of current.pressed) controlRef.current?.input({ type: 'key', action: 'up', key });
126
+ for (const button of current.buttons) controlRef.current?.input({ type: 'button', action: 'up', button });
127
+ current.pressed.clear(); current.buttons.clear();
128
+ };
129
+ const releasePointer = () => {
130
+ for (const button of state.current.buttons) controlRef.current?.input({ type: 'button', action: 'up', button });
131
+ state.current.buttons.clear();
132
+ };
133
+ if (!controlRef.current) controlRef.current = createDesktopControl({
134
+ send,
135
+ changed: value => {
136
+ const current = state.current; current.controlPhase = value.phase; current.readOnly = value.readOnly; current.controlEpoch = value.controlEpoch;
137
+ if (value.readOnly && cursorRef.current) cursorRef.current.style.visibility = 'hidden';
138
+ if (value.phase === 'observe') { current.pressed.clear(); current.buttons.clear(); }
139
+ if (current.mounted) { setReadOnly(value.readOnly); setControlPhase(value.phase); }
140
+ },
141
+ failed: message => { if (state.current.mounted) setNotice(message); },
142
+ });
143
+ // 剪贴板请求复用同一条桌面 WS(云端 desktop-ws 的 clipboard.read / clipboard.write),
144
+ // 用 requestId 关联回复;旧版云端不认这两个消息类型,超时后给明确提示而不是转圈。
145
+ if (!clipboardRef.current) clipboardRef.current = createClipboardChannel({ send, controlEpoch: () => controlRef.current?.snapshot?.controlEpoch });
146
+ const releaseControl = () => { releaseInputs(); controlRef.current.release(); state.current.pressed.clear(); state.current.buttons.clear(); };
147
+ const flushPointer = () => {
148
+ const current = state.current;
149
+ if (current.pointerFrame) cancelAnimationFrame(current.pointerFrame);
150
+ current.pointerFrame = null;
151
+ const next = current.lastPointer; current.lastPointer = null;
152
+ if (next && (!current.readOnly || current.controlPhase === 'acquiring') && (!current.sentPointer || next.x !== current.sentPointer.x || next.y !== current.sentPointer.y)) {
153
+ controlRef.current.input({ type: 'pointermove', ...next }); current.sentPointer = next;
154
+ }
155
+ };
156
+ const clearTimers = () => {
157
+ clearTimeout(state.current.reconnect); clearTimeout(state.current.firstFrame);
158
+ state.current.reconnect = null; state.current.firstFrame = null;
159
+ };
160
+ const close = (announce = true) => {
161
+ const current = state.current; current.reconnectAllowed = false; clearTimers(); releaseControl(); current.generation += 1;
162
+ clipboardRef.current?.rejectAll();
163
+ try { send({ type: 'close' }); current.socket?.close(1000, 'panel closed'); } catch {}
164
+ 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
+ current.suspended = false; current.resuming = false; current.pendingInputs = []; current.frameEpoch++;
166
+ if (announce && current.mounted) { setPhase(current.config?.configured ? 'idle' : 'unconfigured'); setReadOnly(true); }
167
+ };
168
+ const ended = value => {
169
+ close(false); setReadOnly(true); setPhase('closed');
170
+ setNotice(value?.reason === 'lease-lost' || Number(value?.status) === 409
171
+ ? '控制租约已失效,桌面连接已关闭;请重新连接或选择只读旁观'
172
+ : value?.code === 'DESKTOP_SESSION_NOT_FOUND' || Number(value?.status) === 404
173
+ ? '桌面会话已结束(可能因空闲超时),请重新连接'
174
+ : '桌面连接已结束,可重新连接云电脑');
175
+ };
176
+ const input = event => {
177
+ const current = state.current;
178
+ if (event.type !== 'pointermove') flushPointer();
179
+ if (current.suspended || current.resuming) {
180
+ if (event.type === 'pointermove') return false;
181
+ const controlling = current.mode === 'control';
182
+ if (controlling) {
183
+ if (current.pendingInputs.length >= 32) { close(); setNotice('恢复期间输入过快,请重新连接'); return false; }
184
+ current.pendingInputs.push(event);
185
+ }
186
+ if (!current.resuming) resumeIdle();
187
+ return controlling;
188
+ }
189
+ if (!current.session || current.socket?.readyState !== 1) { setNotice('云电脑尚未连接,输入未发送'); return false; }
190
+ return controlRef.current.input(event);
191
+ };
192
+ const clipboardNotify = message => {
193
+ if (!state.current.mounted) return;
194
+ setClipboardNotice(message);
195
+ if (clipboardTimerRef.current) clearTimeout(clipboardTimerRef.current);
196
+ clipboardTimerRef.current = setTimeout(() => { if (state.current.mounted) setClipboardNotice(''); }, 4000);
197
+ };
198
+ // 合成按键走现有桌面输入通道:只读旁观时 input() 会拒发并返回 false,由粘贴流程提示先接管。
199
+ const sendDesktopKeys = events => {
200
+ let sent = false;
201
+ for (const event of events) sent = input(event) || sent;
202
+ return sent;
203
+ };
204
+ // ⌘C:先把按键原样交给云电脑,让云端应用自己完成“复制”,再取回云端剪贴板。
205
+ // 之前只读剪贴板不转发按键,用户按 ⌘C 等于没复制,还会在输入框里反复触发按键。
206
+ const copyCloudClipboardToLocal = async () => {
207
+ const modifier = /Mac|iPhone|iPad/u.test(String(globalThis.navigator?.platform || '')) ? 'Meta' : 'Control';
208
+ if (!state.current.pressed.has('_copyShortcut')) {
209
+ state.current.pressed.add('_copyShortcut');
210
+ try {
211
+ sendDesktopKeys([
212
+ { type: 'key', action: 'down', key: modifier },
213
+ { type: 'key', action: 'down', key: 'c' },
214
+ { type: 'key', action: 'up', key: 'c' },
215
+ { type: 'key', action: 'up', key: modifier },
216
+ ]);
217
+ await new Promise(resolve => setTimeout(resolve, 150));
218
+ } finally { state.current.pressed.delete('_copyShortcut'); }
219
+ }
220
+ return copyCloudClipboard({ readCloud: () => clipboardRef.current.read(), notify: clipboardNotify });
221
+ };
222
+ const pasteLocalClipboardToCloud = () => pasteToCloud({
223
+ readLocal: () => readLocalClipboard(),
224
+ writeCloud: text => clipboardRef.current.write(text),
225
+ sendKeys: sendDesktopKeys,
226
+ notify: clipboardNotify,
227
+ });
228
+ const drawLatest = async () => {
229
+ const current = state.current;
230
+ if (current.decoding) return;
231
+ current.decoding = true;
232
+ try {
233
+ while (current.pendingFrame) {
234
+ const job = current.pendingFrame; current.pendingFrame = null;
235
+ let bitmap;
236
+ try {
237
+ bitmap = await createImageBitmap(new Blob([job.bytes], { type: 'image/jpeg' }));
238
+ if (!current.mounted || current.suspended || job.frameEpoch !== current.frameEpoch || job.generation !== current.generation || job.seq < current.receivedSeq || !canvasRef.current) continue;
239
+ current.lastDrawnFrame = job;
240
+ const canvas = canvasRef.current, rect = canvas.getBoundingClientRect();
241
+ const dpr = Math.max(1, globalThis.devicePixelRatio || 1);
242
+ canvas.width = Math.max(1, Math.round(rect.width * dpr)); canvas.height = Math.max(1, Math.round(rect.height * dpr));
243
+ const context = canvas.getContext('2d'), display = current.display;
244
+ const scale = Math.min(canvas.width / display.width, canvas.height / display.height);
245
+ const width = display.width * scale, height = display.height * scale;
246
+ context.clearRect(0, 0, canvas.width, canvas.height);
247
+ context.drawImage(bitmap, (canvas.width - width) / 2, (canvas.height - height) / 2, width, height);
248
+ const waitingForFirstFrame = current.firstFrame !== null;
249
+ current.frames += 1; clearTimeout(current.firstFrame); current.firstFrame = null; current.retries = 0;
250
+ if (waitingForFirstFrame) setNotice('');
251
+ setPhase(current.readOnly ? 'readonly' : 'ready'); setAttempt(0);
252
+ } catch { if (current.mounted) setNotice('桌面帧无法解码,请重试'); }
253
+ finally { bitmap?.close(); }
254
+ }
255
+ } finally { current.decoding = false; }
256
+ };
257
+ const markIdle = value => {
258
+ const current = state.current;
259
+ clearTimeout(current.firstFrame); current.firstFrame = null;
260
+ current.suspended = true; current.resuming = false; current.readOnly = true; current.pendingInputs = []; current.frameEpoch++;
261
+ current.pendingFrame = null; current.frameMetadata = null; current.pressed.clear(); current.buttons.clear(); current.frames = 0;
262
+ setFps(0); setReadOnly(true); setPhase('dormant'); setNotice(value?.leaseReleased === false ? '已空闲,画面已暂停;控制权仍由外部任务管理' : '已空闲,控制权已释放;操作桌面即可恢复');
263
+ };
264
+ const resumeIdle = () => {
265
+ const current = state.current;
266
+ if (current.resuming || !current.suspended) return;
267
+ if (current.socket?.readyState !== 1) { void open(current.mode, false, current.pendingInputs); return; }
268
+ current.resuming = true; setPhase('resuming'); setNotice('正在恢复桌面并检查控制权,请稍候。');
269
+ send({ type: 'resume', mode: current.mode, activity: true });
270
+ const generation = current.generation, socket = current.socket;
271
+ current.firstFrame = setTimeout(() => { if (current.mounted && current.generation === generation && current.firstFrame !== null) socket.close(4000, 'resume frame timeout'); }, FIRST_FRAME_TIMEOUT_MS);
272
+ };
273
+ const open = async (mode = 'read', reconnecting = false, bufferedInputs = []) => {
274
+ const current = state.current;
275
+ const selected = String(current.botId || '').trim();
276
+ if (!current.config?.configured) { setPhase('unconfigured'); return; }
277
+ if (!selected || !current.bots?.some(bot => bot.botId === selected)) { setNotice('云端助理尚未就绪,请稍后重试'); return; }
278
+ close(false);
279
+ current.pendingInputs = bufferedInputs; current.resuming = bufferedInputs.length > 0;
280
+ const generation = ++current.generation;
281
+ current.reconnectAllowed = true;
282
+ if (!reconnecting) current.retries = 0;
283
+ current.mode = mode; current.taskId ||= `studio-desktop-${globalThis.crypto.randomUUID()}`;
284
+ current.receivedSeq = -1; current.frameMetadata = null;
285
+ setPhase(reconnecting ? 'reconnecting' : 'connecting'); setNotice('首次或休眠后连接可能需要数秒,请稍候。');
286
+ try {
287
+ const ticket = await rpc('personal-assistant.desktop.ticket', { agentId: current.config.agentId, botId: selected, mode: mode === 'observe' ? 'control' : mode });
288
+ if (!current.mounted || generation !== current.generation) return;
289
+ const socket = new WebSocket(ticket.wsUrl, ['agentkit.personal-assistant.desktop.v1', `agentkit.desktop.ticket.${ticket.ticket}`]);
290
+ socket.binaryType = 'arraybuffer'; current.socket = socket;
291
+ const valid = () => current.mounted && current.generation === generation;
292
+ socket.onopen = () => {
293
+ if (!valid()) { socket.close(); return; }
294
+ send({ type: 'open', botId: selected, taskId: current.taskId, mode, ...(ticket.leaseId ? { leaseId: ticket.leaseId } : {}) });
295
+ current.firstFrame = setTimeout(() => { if (valid()) { setNotice('云电脑响应较慢,正在重新连接'); socket.close(4000, 'first frame timeout'); } }, FIRST_FRAME_TIMEOUT_MS);
296
+ };
297
+ socket.onmessage = event => {
298
+ if (!valid()) return;
299
+ if (typeof event.data !== 'string') {
300
+ const meta = current.frameMetadata; current.frameMetadata = null;
301
+ const bytes = event.data;
302
+ if (!meta || !Number.isSafeInteger(meta.seq) || meta.seq <= current.receivedSeq || !(bytes instanceof ArrayBuffer) || bytes.byteLength === 0 || bytes.byteLength > 307200) return;
303
+ if (current.suspended) return;
304
+ current.receivedSeq = meta.seq; current.pendingFrame = { bytes, seq: meta.seq, generation, frameEpoch: current.frameEpoch }; void drawLatest(); return;
305
+ }
306
+ let value; try { value = JSON.parse(event.data); } catch { return; }
307
+ if (value.type === 'ready') {
308
+ current.suspended = false; current.resuming = false; current.receivedSeq = -1;
309
+ current.session = value.sessionId; current.readOnly = Boolean(value.readOnly); current.display = normalizeDisplay(value.display);
310
+ current.mode = mode;
311
+ controlRef.current.reset(value);
312
+ const inputs = current.pendingInputs; current.pendingInputs = [];
313
+ if (!current.readOnly) for (const event of inputs) controlRef.current.input(event);
314
+ else { current.pressed.clear(); current.buttons.clear(); }
315
+ setReadOnly(current.readOnly); setNotice('云电脑已连接,正在准备桌面画面'); return;
316
+ }
317
+ if (value.type === 'control') { controlRef.current.accept(value); setNotice(''); return; }
318
+ if (value.type === 'idle') { markIdle(value); return; }
319
+ if (value.type === 'frame') { if (!value.unchanged) current.frameMetadata = value; return; }
320
+ if (value.type === 'clipboard.response') { clipboardRef.current?.accept(value); return; }
321
+ if (value.type === 'closed') { if (value.retryable !== true) ended(value); return; }
322
+ if (value.type === 'error') {
323
+ if (Number.isSafeInteger(value.controlEpoch) && value.controlEpoch < current.controlEpoch) return;
324
+ setNotice(value.message || value.code || '云电脑请求失败');
325
+ if (current.handoffSupported && value.recoverable === true) { controlRef.current.reject(value); return; }
326
+ if (value.recoverable === true && Number(value.status) === 409) {
327
+ if (value.code === 'DESKTOP_SESSION_IDLE') markIdle();
328
+ else { current.pendingInputs = []; current.readOnly = true; current.pressed.clear(); current.buttons.clear(); setReadOnly(true); setPhase('readonly'); }
329
+ return;
330
+ }
331
+ if (Number(value.status) === 409 && mode === 'control' && !current.session) { void open(state.current.handoffSupported ? 'observe' : 'read'); return; }
332
+ if ([404, 409].includes(Number(value.status)) || value.code === 'DESKTOP_SESSION_NOT_FOUND') ended(value);
333
+ else if (!current.session) { setPhase('error'); close(false); }
334
+ }
335
+ };
336
+ socket.onerror = () => { if (valid()) setNotice('桌面通道暂时断开,正在重连'); };
337
+ socket.onclose = event => {
338
+ if (!valid()) return;
339
+ if (current.suspended && event.code !== 1000) { clearTimeout(current.firstFrame); current.firstFrame = null; current.socket = null; setPhase('dormant'); return; }
340
+ current.session = null; clearTimeout(current.firstFrame); current.firstFrame = null;
341
+ if (!current.reconnectAllowed || event.code === 1000) { ended(); return; }
342
+ if (document.hidden) { setPhase('hidden'); return; }
343
+ const count = ++current.retries; setAttempt(count);
344
+ if (count > 5) { current.reconnectAllowed = false; setPhase('error'); setNotice('无法连接云电脑,请检查实例状态或网络后重试'); return; }
345
+ setPhase('reconnecting'); current.reconnect = setTimeout(() => { void open(mode, true, current.pendingInputs); }, Math.min(8000, 1000 * 2 ** (count - 1)));
346
+ };
347
+ } catch (error) { if (current.mounted && generation === current.generation) { setPhase('error'); setNotice(failure(error)); } }
348
+ };
349
+ // Studio 负责发现实例、恢复/生成凭证;页面只读取公开状态,绝不接触凭据。
350
+ const bindingSnapshot = async signal => {
351
+ if (signal?.aborted) return null;
352
+ try { const value = await rpc('personal-assistant.desktop.status'); return value && typeof value === 'object' ? value : null; }
353
+ catch (error) {
354
+ if (error?.code === 'PERSONAL_ASSISTANT_OPERATION_UNAVAILABLE' || error?.status === 404) return null;
355
+ return { configured: false, reason: 'unavailable', detail: bindingFailure(error), candidates: [] };
356
+ }
357
+ };
358
+ const bindAssistant = async action => {
359
+ if (busy) return;
360
+ setBusy(action);
361
+ setNotice(action === 'create' ? '正在创建个人助理,可能需要几分钟…' : '正在获取云端连接凭证…');
362
+ try { await rpc(`personal-assistant.desktop.${action}`); setNotice(''); await refreshConnection(); }
363
+ catch (error) { setNotice(bindingFailure(error)); setBinding(await bindingSnapshot().catch(() => null)); }
364
+ finally { setBusy(''); }
365
+ };
366
+ const refreshConnection = async (signal, retried = false) => {
367
+ close(false); setPhase('loading'); setNotice(''); setDirectoryError(''); setDirectoryCloseCode(null);
368
+ try {
369
+ const value = await rpc('personal-assistant.configuration');
370
+ if (signal?.aborted || !state.current.mounted) return;
371
+ state.current.config = value; setConfig(value);
372
+ if (!value.configured) {
373
+ const snapshot = await bindingSnapshot(signal);
374
+ if (signal?.aborted || !state.current.mounted) return;
375
+ state.current.bots = []; setBots([]); setBotId(''); setBinding(snapshot); setPhase('unconfigured');
376
+ if (snapshot?.configured && !retried) { await refreshConnection(signal, true); return; }
377
+ return;
378
+ }
379
+ setBinding(null);
380
+ const result = await rpc('personal-assistant.bots.list');
381
+ if (signal?.aborted || !state.current.mounted) return;
382
+ if (result.directoryAvailable !== true) throw Object.assign(new Error('directory unavailable'), { code: 'PERSONAL_ASSISTANT_DIRECTORY_UNAVAILABLE', status: 409 });
383
+ const available = publicBotDirectory(result.bots);
384
+ state.current.bots = available; state.current.handoffSupported = result.features?.includes('desktop-handoff/v1') === true; setBots(available);
385
+ const selected = available.length ? resolveAssistantBotId(available, initialBotId || value.botId) : '';
386
+ state.current.botId = selected; setBotId(selected); setPhase(available.length ? 'idle' : 'empty');
387
+ if (selected && ['computer', 'native-tab'].includes(variant)) void open(state.current.handoffSupported ? 'observe' : 'read');
388
+ } catch (error) {
389
+ if (signal?.aborted || !state.current.mounted) return;
390
+ if (Number.isInteger(error?.closeCode) && error.closeCode >= 1000 && error.closeCode <= 4999) setDirectoryCloseCode(error.closeCode);
391
+ state.current.bots = []; setBots([]); setBotId(''); setPhase('directory-error');
392
+ setDirectoryError(directoryUnsupported(error) ? '云端助理需要更新,请更新后重新连接。' : failure(error));
393
+ }
394
+ };
395
+ useEffect(() => {
396
+ const current = state.current; current.mounted = true;
397
+ const controller = new AbortController();
398
+ void refreshConnection(controller.signal);
399
+ const interval = setInterval(() => { if (current.mounted) setFps(current.frames); current.frames = 0; }, 1000);
400
+ const visibility = () => {
401
+ if (document.hidden) { releaseControl(); send({ type: 'pause' }); }
402
+ else if (current.socket?.readyState === 1) send({ type: 'resume' });
403
+ else if (current.reconnectAllowed && current.config?.configured && current.session === null && current.socket) void open(current.mode, true);
404
+ };
405
+ const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(() => { if (current.lastDrawnFrame) { current.pendingFrame = current.lastDrawnFrame; void drawLatest(); } }) : null;
406
+ if (canvasRef.current) observer?.observe(canvasRef.current);
407
+ const blur = () => releaseControl();
408
+ const outside = event => { if (!viewRef.current?.contains(event.target)) releaseControl(); };
409
+ 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); };
411
+ }, [request]);
412
+ const point = event => mapDisplayPoint({ clientX: event.clientX, clientY: event.clientY, rect: canvasRef.current.getBoundingClientRect(), display: state.current.display });
413
+ const pointer = event => {
414
+ event.preventDefault(); const current = state.current, button = [1, 2, 3][event.button] || 1;
415
+ 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); }
416
+ else { input({ type: 'button', action: 'up', button, ...point(event) }); current.buttons.delete(button); if (canvasRef.current?.hasPointerCapture(event.pointerId)) canvasRef.current.releasePointerCapture(event.pointerId); }
417
+ };
418
+ const key = event => {
419
+ if (state.current.composition || event.isComposing || event.nativeEvent?.isComposing || event.key === 'Process' || event.key === 'Unidentified') return;
420
+ // 画布上的 ⌘V/Ctrl+V 是"把本地剪贴板贴进云电脑",不能把裸按键转给 Linux 桌面(它要的是 Ctrl+V)。
421
+ if (isPasteShortcut(event)) { event.preventDefault(); void pasteLocalClipboardToCloud(); return; }
422
+ if (isCopyShortcut(event)) {
423
+ event.preventDefault();
424
+ // 长按/重复的 keydown 不再触发第二次复制。
425
+ if (event.type === 'keydown' && !event.repeat) void copyCloudClipboardToLocal();
426
+ return;
427
+ }
428
+ const modified = event.ctrlKey || event.metaKey || event.altKey;
429
+ // 普通文本只走 input/compositionend,避免同一按键被发送两遍。
430
+ if (event.key.length === 1 && !modified) return;
431
+ if (event.key === 'Escape' && fullscreenRef.current) return;
432
+ event.preventDefault();
433
+ const name = keyNames[event.key] || event.key;
434
+ if (!/^[A-Za-z0-9_]+$/u.test(name)) return;
435
+ if (event.type === 'keydown') { if (!event.repeat && input({ type: 'key', action: 'down', key: name })) state.current.pressed.add(name); }
436
+ else { input({ type: 'key', action: 'up', key: name }); state.current.pressed.delete(name); }
437
+ };
438
+ const stateText = { loading: '正在检查助理状态', idle: '准备连接', dormant: '已空闲', resuming: '正在恢复桌面', unconfigured: '个人助理尚未开通', empty: '暂时没有可用助理', 'directory-error': '暂时无法读取助理列表', connecting: '正在启动云电脑', readonly: '只读旁观', ready: '正在控制云电脑', reconnecting: '正在恢复连接', hidden: '画面已暂停', closed: '桌面连接已结束', error: '连接暂时不可用' }[phase];
439
+ const buttonStyle = { ...ui, borderRadius: 6, padding: '7px 12px', cursor: 'pointer', font: 'inherit' };
440
+ const active = ['ready', 'readonly', 'hidden', 'reconnecting', 'dormant', 'resuming'].includes(phase);
441
+ const selectable = bots.length > 0;
442
+ const bindingReason = binding?.reason || '';
443
+ const candidateCount = Array.isArray(binding?.candidates) ? binding.candidates.length : 0;
444
+ const bindingTitle = bindingReason === 'needs-create' ? '还没有创建个人助理'
445
+ : bindingReason === 'credential-unavailable' ? '正在恢复个人助理的连接凭证'
446
+ : bindingReason === 'unavailable' ? '暂时无法自动连接个人助理' : '个人助理尚未开通';
447
+ const bindingDetail = bindingReason === 'needs-create' ? '云账号下还没有个人助理实例,创建后即可在这里连接专属云电脑。'
448
+ : bindingReason === 'credential-unavailable' ? '云端暂时没有返回连接凭证,可以重新获取。'
449
+ : bindingReason === 'unavailable' ? (binding?.detail || '请检查云端登录状态后重试。')
450
+ : '开通完成后,即可在这里连接专属云电脑。';
451
+ const emptyTitle = phase === 'unconfigured' ? bindingTitle : phase === 'empty' ? '助理正在准备中' : phase === 'directory-error' ? '暂时无法连接云端助理' : '正在检查助理状态';
452
+ const emptyDetail = phase === 'unconfigured' ? bindingDetail : phase === 'empty' ? '云端初始化完成后,可用助理会显示在这里。' : phase === 'directory-error' ? directoryError : '请稍候';
453
+ const computerVariant = variant === 'computer' || variant === 'native-tab';
454
+ const stage = h('div', { ref: viewRef, 'data-personal-assistant-desktop-card': computerVariant ? 'true' : undefined, style: {
455
+ position: 'relative', background: 'var(--dsw-alias-bg-layer-2, #f1f3f6)',
456
+ ...(computerVariant ? { width: '100%', aspectRatio: `${state.current.display.width} / ${state.current.display.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
+ } },
458
+ ['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
+ 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 => {
460
+ const current = state.current, latest = event.nativeEvent?.getCoalescedEvents?.().at(-1) || event;
461
+ 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
+ current.lastPointer = point(latest);
463
+ if (!current.pointerFrame) current.pointerFrame = requestAnimationFrame(() => { if (current.mounted) flushPointer(); });
464
+ }, onContextMenu: event => event.preventDefault(), onWheel: event => { event.preventDefault(); input({ type: 'wheel', deltaY: event.deltaY / 100, deltaX: event.deltaX / 100 }); }, 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
+ 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
+ !active ? h('div', { style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 10, padding: 16, textAlign: 'center' } },
467
+ h('h2', { style: { margin: 0, fontSize: 18, fontWeight: 500 } }, selectable ? stateText : emptyTitle),
468
+ h('p', { style: { margin: 0, maxWidth: 380, lineHeight: 1.7, color: 'var(--dsw-alias-label-secondary, #536174)' } }, selectable ? notice || '连接后即可使用云端桌面,关闭面板会释放本次控制。' : emptyDetail),
469
+ !selectable && phase !== 'loading' ? h('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center' } },
470
+ phase === 'unconfigured' && bindingReason === 'needs-create' ? h('button', { style: buttonStyle, disabled: Boolean(busy), onClick: () => void bindAssistant('create') }, busy === 'create' ? '正在创建…' : '创建个人助理') : null,
471
+ 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
+ h('button', { style: { ...buttonStyle, background: 'transparent' }, disabled: Boolean(busy), onClick: () => void refreshConnection() }, '检查开通状态'),
473
+ 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 }); } }));
475
+ const secondaryInk = 'var(--dsw-alias-label-secondary, #536174)';
476
+ const ghostStyle = { ...buttonStyle, background: 'transparent', color: 'inherit', border: ui.border };
477
+ 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)' };
478
+ if (computerVariant) {
479
+ return h('section', { 'data-personal-assistant-computer': 'true', style: { ...ui, border: 'none', height: '100%', minHeight: 0, minWidth: 0, display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base, #fff)', color: 'inherit', fontSize: 13 } },
480
+ variant !== 'native-tab' ? h('div', { style: { display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', borderBottom: ui.border } },
481
+ h('strong', { style: { fontWeight: 500 } }, '云电脑'),
482
+ h('span', { role: 'status', 'aria-live': 'polite', style: { color: secondaryInk, fontSize: 12 } }, stateText),
483
+ null) : null,
484
+ 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: 'auto', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12, padding: '20px 16px 12px', boxSizing: 'border-box' } },
486
+ h('div', { 'data-personal-assistant-desktop-stack': 'true', style: { width: '100%', maxWidth: 1280, margin: 'auto 0', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12, flex: 'none' } }, stage,
487
+ active || selectable ? h('div', { 'data-personal-assistant-control-capsule': 'true', role: 'status', 'aria-live': 'polite', style: { 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
+ !active && selectable ? h('button', { style: pillStyle, disabled: ['connecting', 'resuming'].includes(phase), onClick: () => void open(state.current.handoffSupported ? 'observe' : 'read') }, phase === 'connecting' ? '正在启动…' : '重新连接') : null,
489
+ active && readOnly ? h('span', { style: { color: secondaryInk, fontSize: 12 } }, controlPhase === 'acquiring' ? '正在接管…' : controlPhase === 'releasing' ? '正在交还…' : '助理正在使用 · 点画面即可接管') : null,
490
+ active && readOnly && controlRef.current.snapshot.available ? h('button', { style: { ...pillStyle, padding: '3px 9px', fontSize: 11 }, onClick: () => controlRef.current.acquire() }, '接管') : null,
491
+ active && !readOnly ? h('span', { style: { color: secondaryInk, fontSize: 12 } }, '您拥有控制权 · 点画面外释放') : null,
492
+ active && notice ? h('span', { style: { color: secondaryInk, fontSize: 12 } }, notice) : null,
493
+ // 剪贴板不再放按钮:面板里只保留 ⌘V 粘贴 / ⌘C 复制与一行状态文字。
494
+ active && clipboardNotice ? h('span', { role: 'status', 'aria-live': 'polite', style: { color: secondaryInk, fontSize: 12 } }, clipboardNotice) : null) : null),
495
+ h('details', { style: { alignSelf: 'flex-end', color: 'var(--dsw-alias-label-tertiary, #76808e)', fontSize: 11, opacity: 0.8, maxWidth: '100%', overflowWrap: 'anywhere' } }, h('summary', { style: { cursor: 'pointer' } }, '诊断'), h('small', { 'data-personal-assistant-build': PERSONAL_ASSISTANT_CLIENT_BUILD }, `构建 ${PERSONAL_ASSISTANT_CLIENT_BUILD}`))));
496
+ }
497
+ return h('section', { 'data-personal-assistant-panel': 'true', style: { ...ui, border: 'none', height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', fontSize: 13 } },
498
+ h('header', { style: { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', padding: 12, borderBottom: ui.border } },
499
+ h('strong', { style: { fontWeight: 500, marginRight: 8 } }, '个人助理'),
500
+ selectable && ['idle', 'closed', 'error'].includes(phase) ? h('button', { style: buttonStyle, onClick: () => void refreshConnection() }, '刷新助理列表') : null,
501
+ typeof openInSidebar === 'function' ? h('button', { style: buttonStyle, onClick: openInSidebar }, '放到右侧栏') : null,
502
+ selectable ? h('button', { style: buttonStyle, disabled: ['connecting', 'resuming'].includes(phase), onClick: () => phase === 'dormant' ? resumeIdle() : void open(state.current.handoffSupported ? 'observe' : 'control') }, phase === 'dormant' ? '恢复画面' : readOnly && phase === 'readonly' ? '申请控制' : active ? '重新连接' : '连接云电脑') : null,
503
+ selectable ? h('button', { style: buttonStyle, disabled: phase === 'connecting', onClick: () => void open('read') }, '只读旁观') : null,
504
+ active ? h('span', { role: 'status', 'aria-live': 'polite', style: { marginLeft: 'auto', color: 'var(--dsw-alias-label-secondary, #536174)' } }, stateText) : null),
505
+ phase === 'readonly' ? h('div', { role: 'alert', style: { background: '#fff0d6', color: '#763c00', padding: '8px 12px' } }, '当前为只读旁观,鼠标和键盘输入不会发送;可申请控制') : null,
506
+ active && notice ? h('div', { role: 'status', style: { padding: '8px 12px' } }, notice) : null,
507
+ stage,
508
+ 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
+ h('details', { open: diagnosticsOpen, onToggle: event => setDiagnosticsOpen(event.currentTarget.open) }, h('summary', { style: { cursor: 'pointer' } }, '诊断'),
510
+ directoryCloseCode ? h('div', { style: { paddingTop: 8 } }, `目录连接关闭码:${directoryCloseCode}`) : null,
511
+ h('div', { style: { paddingTop: 8, lineHeight: 1.8, overflowWrap: 'anywhere' } }, h('div', null, `云端实例:${config?.agentId || '未开通'}`), h('div', null, `连接标识:${botId || '未连接'}`), h('div', null, `画面帧率:${fps} fps`), h('div', null, '快捷键会转发到云电脑;Esc 优先退出全屏'))),
512
+ h('small', { 'data-personal-assistant-build': PERSONAL_ASSISTANT_CLIENT_BUILD, style: { opacity: 0.75 } }, `构建 ${PERSONAL_ASSISTANT_CLIENT_BUILD}`)));
513
+ }
514
+
515
+
516
+ /** 主面板:对话式入口,云电脑在右侧栏。 */
517
+ export function PersonalAssistantHome({ request, nativeWorkbench } = {}) {
518
+ const R = clientReact || globalThis.React;
519
+ if (!R) return '个人助理页面尚未就绪,请重新打开';
520
+ const { createElement: h, useEffect, useState, useCallback } = R;
521
+ const [scope, setScope] = useState(null);
522
+ const [error, setError] = useState('');
523
+ const [attempt, retry] = useState(0);
524
+ useEffect(() => {
525
+ let cancelled = false;
526
+ setScope(null); setError('');
527
+ void (async () => {
528
+ try {
529
+ const config = await request('personal-assistant.configuration');
530
+ if (!config.configured) throw new Error('个人助理尚未开通,请先在设置中连接云端实例');
531
+ const directory = await request('personal-assistant.bots.list');
532
+ if (directory.directoryAvailable !== true) throw new Error('暂时无法连接云端助理,请稍后重试');
533
+ const botId = resolveAssistantBotId(directory.bots, config.botId || directory.primaryBotId);
534
+ if (!cancelled) setScope({ agentId: config.agentId, botId });
535
+ } catch (failure) { if (!cancelled) setError(failure.message || '云端助理暂不可用'); }
536
+ })();
537
+ return () => { cancelled = true; };
538
+ }, [request, attempt]);
539
+ const scopedRequest = useCallback((operation, payload = {}) => {
540
+ if (!scope) throw new Error('云端助理尚未连接');
541
+ if (payload.agentId && payload.agentId !== scope.agentId) throw new Error('云端实例不一致');
542
+ if (operation !== 'personal-assistant.bots.list' && payload.botId && payload.botId !== scope.botId) throw new Error('云端助理不一致');
543
+ return request(operation, { ...payload, ...scope });
544
+ }, [request, scope?.agentId, scope?.botId]);
545
+ useEffect(() => { if (scope && nativeWorkbench?.available) nativeWorkbench.bind(scope, scopedRequest); else if (scope) setError(nativeWorkbench?.unavailableReason || '当前宿主需要更新原生侧栏组件'); }, [nativeWorkbench, scopedRequest, scope?.agentId, scope?.botId]);
546
+ if (!scope) return h('div', { role: 'status', style: { padding: 24 } }, error || '正在连接云端个人助理…', error ? h('button', { onClick: () => retry(value => value + 1) }, '重新连接') : null);
547
+ if (!nativeWorkbench?.available) return h('div', { role: 'alert', style: { padding: 24 } }, '当前宿主尚未提供原生云电脑侧栏,请更新 Studio 后重新打开。');
548
+ return h('div', {
549
+ key: `${scope.agentId}:${scope.botId}`, 'data-personal-assistant-home': 'true',
550
+ 'data-agent-id': scope.agentId, 'data-bot-id': scope.botId,
551
+ 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() }));
553
+
554
+ }
555
+
556
+
557
+ const settingsStyle = {
558
+ page: { padding: '2px 2px 28px', display: 'flex', flexDirection: 'column', gap: 18, maxWidth: 700, fontSize: 13, color: 'var(--dsw-alias-label-primary, #17202c)' },
559
+ title: { margin: 0, fontSize: 18, fontWeight: 600, letterSpacing: '-0.01em' },
560
+ lede: { margin: 0, color: 'var(--dsw-alias-label-secondary, #536174)', fontSize: 13, lineHeight: 1.7 },
561
+ card: { border: ui.border, borderRadius: 12, overflow: 'hidden' },
562
+ row: { display: 'flex', gap: 16, padding: '12px 14px', borderTop: '1px solid var(--dsw-alias-border-default, #d8dee8)', alignItems: 'flex-start' },
563
+ rowFirst: { display: 'flex', gap: 16, padding: '12px 14px', alignItems: 'flex-start' },
564
+ label: { width: 116, flex: 'none', color: 'var(--dsw-alias-label-secondary, #536174)', fontSize: 13, lineHeight: 1.7 },
565
+ value: { flex: 1, minWidth: 0, fontSize: 13, lineHeight: 1.7, overflowWrap: 'anywhere' },
566
+ mono: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12 },
567
+ actions: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
568
+ button: { ...ui, borderRadius: 8, padding: '6px 11px', cursor: 'pointer', font: 'inherit', fontSize: 13 },
569
+ note: { color: 'var(--dsw-alias-label-secondary, #536174)', fontSize: 12, lineHeight: 1.7 },
570
+ badge: { display: 'inline-block', borderRadius: 999, padding: '1px 8px', fontSize: 12, lineHeight: 1.7 },
571
+ badgeOk: { background: 'color-mix(in srgb, #067647 12%, transparent)', color: '#067647' },
572
+ badgeIdle: { background: 'color-mix(in srgb, #946200 14%, transparent)', color: '#946200' },
573
+ badgeError: { background: 'color-mix(in srgb, #b42318 12%, transparent)', color: '#b42318' },
574
+ };
575
+
576
+ /** 设置页里的「个人助理」一节:状态、连接自检、能力与记忆位置。 */
577
+ export function PersonalAssistantSettings({ request, openComputer = null } = {}) {
578
+ const R = clientReact || globalThis.React;
579
+ if (!R) return '设置页尚未就绪,请重新打开 Studio';
580
+ const { createElement: h, useEffect, useState, useCallback } = R;
581
+ const [config, setConfig] = useState(null);
582
+ const [bots, setBots] = useState(null);
583
+ const [error, setError] = useState('');
584
+ const [checking, setChecking] = useState(false);
585
+ const [checkedAt, setCheckedAt] = useState('');
586
+
587
+ const check = useCallback(async () => {
588
+ setChecking(true); setError('');
589
+ const started = Date.now();
590
+ try {
591
+ const current = await request('personal-assistant.configuration');
592
+ setConfig(current);
593
+ if (current?.configured) {
594
+ const directory = await request('personal-assistant.bots.list');
595
+ setBots(Array.isArray(directory?.bots) ? directory.bots : []);
596
+ } else { setBots([]); }
597
+ setCheckedAt(`${new Date().toLocaleTimeString('zh-CN', { hour12: false })} · ${Date.now() - started} ms`);
598
+ } catch (reason) {
599
+ setError(reason?.message || '连接检查失败');
600
+ setCheckedAt(`${new Date().toLocaleTimeString('zh-CN', { hour12: false })} · 失败`);
601
+ } finally { setChecking(false); }
602
+ }, [request]);
603
+
604
+ useEffect(() => { void check(); }, [check]);
605
+
606
+ const configured = config?.configured === true;
607
+ const status = config === null ? '检查中' : configured ? '已开通' : '尚未开通';
608
+ const statusStyle = { ...settingsStyle.badge, ...(config === null ? settingsStyle.badgeIdle : configured ? settingsStyle.badgeOk : settingsStyle.badgeIdle) };
609
+ const line = (label, value, first = false) => h('div', { style: first ? settingsStyle.rowFirst : settingsStyle.row },
610
+ h('div', { style: settingsStyle.label }, label), h('div', { style: settingsStyle.value }, value));
611
+
612
+ return h('section', { 'data-personal-assistant-settings': 'true', style: settingsStyle.page },
613
+ h('div', null,
614
+ h('h1', { style: settingsStyle.title }, '个人助理'),
615
+ h('p', { style: { ...settingsStyle.lede, marginTop: 6 } }, '云端持续运行的助理,带一台独立云电脑。关掉窗口它也在工作;对话与云电脑在左侧「个人助理」页。')),
616
+ 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,
617
+ h('div', { style: settingsStyle.card },
618
+ line('状态', h('span', null, h('span', { style: statusStyle }, status), checkedAt ? h('span', { style: settingsStyle.note }, ` 上次检查 ${checkedAt}`) : null), true),
619
+ line('云端实例', config?.agentId ? h('span', { style: settingsStyle.mono }, config.agentId) : h('span', { style: settingsStyle.note }, configured ? '未返回实例标识' : '开通后由 Studio 绑定')),
620
+ line('网关', config?.gatewayUrl ? h('span', { style: settingsStyle.mono }, config.gatewayUrl) : h('span', { style: settingsStyle.note }, '由 Studio Host 配置')),
621
+ line('助理连接', h('span', { style: settingsStyle.note }, bots === null ? '检查中' : bots.length ? '已连接你的助理' : '助理尚未就绪')),
622
+ line('记忆', h('div', null, h('div', { style: settingsStyle.mono }, '/workspace/assistant-state'),
623
+ h('div', { style: settingsStyle.note }, '记忆、会话与定时任务随云电脑持久保存,换设备也在'))),
624
+ line('云端能力', h('div', null,
625
+ h('div', null, '默认开启:云端命令执行、云端浏览器'),
626
+ h('div', { style: settingsStyle.note }, '都跑在隔离工作区:读不到本机文件与凭证,不能访问内网与本机地址;敏感操作会先请你确认'))),
627
+ line('插件版本', h('span', { style: settingsStyle.note }, `构建 ${PERSONAL_ASSISTANT_CLIENT_BUILD}`))),
628
+ h('div', { style: settingsStyle.actions },
629
+ h('button', { style: settingsStyle.button, disabled: checking, onClick: () => void check() }, checking ? '正在检查…' : '检查连接'),
630
+ h('span', { style: settingsStyle.note }, '检查会读取开通状态并拉取一次助理列表,不会启动云电脑')));
631
+ }
632
+
633
+ export function PersonalAssistantPanelIcon({ size } = {}) {
634
+ const R = clientReact;
635
+ const edge = Number(size) > 0 ? Number(size) : 16;
636
+ if (!R?.createElement) return '助';
637
+ return R.createElement('svg', {
638
+ width: edge, height: edge, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4,
639
+ strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', focusable: 'false', style: { display: 'block' },
640
+ },
641
+ R.createElement('path', { d: 'M3.4 3.2h9.2a1.3 1.3 0 0 1 1.3 1.3H7.7l-2.9 2.3v-2.3H3.4a1.3 1.3 0 0 1-1.3-1.3V4.5a1.3 1.3 0 0 1 1.3-1.3z' }),
642
+ R.createElement('path', { d: 'M8.1 5.6l.62 1.46 1.46.62-1.46.62L8.1 9.76l-.62-1.46-1.46-.62 1.46-.62z' }));
643
+ }
644
+ export function PersonalAssistantComputerTabGlyph({ size } = {}) {
645
+ const R = clientReact;
646
+ const edge = Number(size) > 0 ? Number(size) : 16;
647
+ if (!R?.createElement) return null;
648
+ return R.createElement('svg', {
649
+ width: edge, height: edge, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4,
650
+ strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', focusable: 'false',
651
+ style: { display: 'block', flex: '0 0 auto' },
652
+ },
653
+ R.createElement('rect', { x: 2.2, y: 3.4, width: 11.6, height: 7.4, rx: 1.4 }),
654
+ R.createElement('path', { d: 'M6.6 13.4h2.8' }),
655
+ R.createElement('path', { d: 'M8 10.8v2.6' }));
656
+ }
657
+
658
+ export function PersonalAssistantComputerTabTitle() {
659
+ const R = clientReact;
660
+ const glyph = PersonalAssistantComputerTabGlyph({ size: 15 });
661
+ if (!R?.createElement || !glyph) return '云电脑';
662
+ return R.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: '6px', lineHeight: 1 } },
663
+ glyph, R.createElement('span', null, '云电脑'));
664
+ }
665
+ export function apply(ctx) {
666
+ if (!ctx?.slots?.inject || !ctx.slots.register) throw new TypeError('Personal Assistant client requires the public DSH slots service');
667
+ const request = async (operation, payload = {}) => {
668
+ if (!ctx.connection?.rpc?.call) throw new PersonalAssistantGatewayUnavailableError();
669
+ const result = await ctx.connection.rpc.call('/api', 'personal-assistant', { operation, payload });
670
+ if (!result?.ok) throw Object.assign(new Error(result?.error?.message || 'Studio Host 请求失败'), { code: result?.error?.code, status: result?.error?.details?.status, closeCode: result?.error?.details?.closeCode });
671
+ return result.value;
672
+ };
673
+ // 注册一律走作用域助手:任一条注册失败只降级该入口,绝不让插件激活整体失败(激活失败会导致 Studio 重置 profile)。
674
+ const scope = (factory, label) => {
675
+ try {
676
+ const run = () => factory();
677
+ if (typeof ctx.effect === 'function') return ctx.effect(run, label);
678
+ return run();
679
+ } catch (error) {
680
+ try { console.warn(`[personal-assistant] ${label} 注册失败`, error); } catch {}
681
+ return undefined;
682
+ }
683
+ };
684
+ const nativeWorkbench = createAssistantWorkbench({ sidebarRight: ctx.sidebarRight, slots: ctx.slots });
685
+ if (nativeWorkbench) {
686
+ nativeWorkbench.render = props => AssistantNativeRightbar({ ...props, React: clientReact || globalThis.React, workbench: nativeWorkbench, Panel: PersonalAssistantPanel });
687
+ scope(() => () => nativeWorkbench.dispose(), 'personal-assistant: native workbench');
688
+ }
689
+ scope(() => ctx.slots.inject('main', () => ctx.slots.register({
690
+ name: 'main', key: PERSONAL_ASSISTANT_PANEL_ID, inject: () => ({ request, nativeWorkbench }),
691
+ }, PersonalAssistantHome)), 'personal-assistant: main panel');
692
+ scope(() => ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
693
+ name: 'sidebar.panellist', id: PERSONAL_ASSISTANT_PANEL_ID, order: 90, label: '个人助理',
694
+ }, PersonalAssistantPanelIcon)), 'personal-assistant: sidebar row');
695
+ // Studio 设置页里的「个人助理」一节(原产品页被去重后留下的位置)。
696
+ scope(() => ctx.slots.inject('settings.section', () => ctx.slots.register({
697
+ name: 'settings.section', id: `${PERSONAL_ASSISTANT_PANEL_ID}-settings`, order: 196, label: '个人助理',
698
+ inject: () => ({ request }),
699
+ }, PersonalAssistantSettings)), 'personal-assistant: settings section');
700
+ // 普通会话标签页仍可注册;全局个人助理页面使用原生 panel surface,不创建本地会话。
701
+ const registerComputerTab = () => {
702
+ if (typeof ctx.inject !== 'function') return;
703
+ scope(() => ctx.inject(['sidebarRightTabs'], (sub) => {
704
+ const cleanups = [];
705
+ const add = (dispose) => { if (typeof dispose === 'function') cleanups.push(dispose); };
706
+ try {
707
+ const tabs = sub.sidebarRightTabs;
708
+ if (tabs === undefined || typeof tabs.register !== 'function') return undefined;
709
+ add(tabs.register({
710
+ id: PERSONAL_ASSISTANT_COMPUTER_TAB,
711
+ kind: PERSONAL_ASSISTANT_COMPUTER_TAB,
712
+ title: () => '云电脑',
713
+ guide: [{
714
+ order: 20,
715
+ title: () => '云电脑',
716
+ description: () => '个人助理的云端电脑画面,可获取控制权',
717
+ }],
718
+ }));
719
+ add(sub.slots.inject('sidebar.right.pane.tab', () => sub.slots.register({
720
+ name: 'sidebar.right.pane.tab', key: PERSONAL_ASSISTANT_COMPUTER_TAB, inject: () => ({ request }),
721
+ }, props => PersonalAssistantPanel({ ...props, variant: 'computer' }))));
722
+ add(sub.slots.inject('sidebar.right.pane.tab.title', () => sub.slots.register({
723
+ name: 'sidebar.right.pane.tab.title', key: PERSONAL_ASSISTANT_COMPUTER_TAB,
724
+ }, PersonalAssistantComputerTabTitle)));
725
+ } catch (error) {
726
+ try { console.warn('[personal-assistant] 右侧栏注册失败', error); } catch {}
727
+ }
728
+ return () => { for (const dispose of cleanups.reverse()) dispose(); };
729
+ }), 'personal-assistant: right tab');
730
+ };
731
+ registerComputerTab();
732
+ }
733
+ export const createClientPlugin = createPersonalAssistantClient;