@lsamien/yaovi 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build-info.json +23 -3
- package/desktop/main.mjs +18 -9
- package/dist/assets/{AboutDialog-CBKv505Y.js → AboutDialog-CJ9tCjSs.js} +2 -2
- package/dist/assets/AboutDialog-CJ9tCjSs.js.map +1 -0
- package/dist/assets/{BotAutomationsView-S3aHciek.js → BotAutomationsView-CsoI3js3.js} +2 -2
- package/dist/assets/{BotAutomationsView-S3aHciek.js.map → BotAutomationsView-CsoI3js3.js.map} +1 -1
- package/dist/assets/{BotControlSettingsPanel-DJF4oA6g.js → BotControlSettingsPanel-DIuBZlLu.js} +2 -2
- package/dist/assets/{BotControlSettingsPanel-DJF4oA6g.js.map → BotControlSettingsPanel-DIuBZlLu.js.map} +1 -1
- package/dist/assets/{BotPluginsDialog-BVOQ3DTp.js → BotPluginsDialog-BsGszDMH.js} +2 -2
- package/dist/assets/{BotPluginsDialog-BVOQ3DTp.js.map → BotPluginsDialog-BsGszDMH.js.map} +1 -1
- package/dist/assets/{BotSettingsPanel-CEH3KSAO.js → BotSettingsPanel-j3ThYgt2.js} +3 -3
- package/dist/assets/{BotSettingsPanel-CEH3KSAO.js.map → BotSettingsPanel-j3ThYgt2.js.map} +1 -1
- package/dist/assets/{ComputerPanel-CC-xkAxr.js → ComputerPanel-DdlgOm-B.js} +2 -2
- package/dist/assets/{ComputerPanel-CC-xkAxr.js.map → ComputerPanel-DdlgOm-B.js.map} +1 -1
- package/dist/assets/{ComputerViewerView-CEQrOmy7.js → ComputerViewerView-I3adPI6O.js} +2 -2
- package/dist/assets/{ComputerViewerView-CEQrOmy7.js.map → ComputerViewerView-I3adPI6O.js.map} +1 -1
- package/dist/assets/ConversationsView-CBhzg-9w.css +1 -0
- package/dist/assets/{ConversationsView-CzFWn-79.js → ConversationsView-CxVFk5sQ.js} +73 -73
- package/dist/assets/ConversationsView-CxVFk5sQ.js.map +1 -0
- package/dist/assets/{DevicePairingDialog-ByYk05DC.js → DevicePairingDialog-C307ARt-.js} +2 -2
- package/dist/assets/{DevicePairingDialog-ByYk05DC.js.map → DevicePairingDialog-C307ARt-.js.map} +1 -1
- package/dist/assets/{LocalVmSettingsPanel-DBkqouAU.js → LocalVmSettingsPanel-Zs7CiXdf.js} +2 -2
- package/dist/assets/{LocalVmSettingsPanel-DBkqouAU.js.map → LocalVmSettingsPanel-Zs7CiXdf.js.map} +1 -1
- package/dist/assets/{ProviderLoginDialog-DdPS62uM.js → ProviderLoginDialog-D3mqdAlo.js} +2 -2
- package/dist/assets/{ProviderLoginDialog-DdPS62uM.js.map → ProviderLoginDialog-D3mqdAlo.js.map} +1 -1
- package/dist/assets/{RunnerSettingsPanel-gzA2xzOg.js → RunnerSettingsPanel-DjMGnXGP.js} +2 -2
- package/dist/assets/{RunnerSettingsPanel-gzA2xzOg.js.map → RunnerSettingsPanel-DjMGnXGP.js.map} +1 -1
- package/dist/assets/{StandaloneDialog-B9HCNEFb.js → StandaloneDialog-BXCK22Dx.js} +2 -2
- package/dist/assets/{StandaloneDialog-B9HCNEFb.js.map → StandaloneDialog-BXCK22Dx.js.map} +1 -1
- package/dist/assets/{UpdateCheckDialog-djkXTQgD.js → UpdateCheckDialog-DAY0VEQm.js} +2 -2
- package/dist/assets/{UpdateCheckDialog-djkXTQgD.js.map → UpdateCheckDialog-DAY0VEQm.js.map} +1 -1
- package/dist/assets/{UsageView-DFaWBB40.js → UsageView-DNwG8LAx.js} +2 -2
- package/dist/assets/{UsageView-DFaWBB40.js.map → UsageView-DNwG8LAx.js.map} +1 -1
- package/dist/assets/{WorkspaceKnowledgePanel-In2a_s_z.js → WorkspaceKnowledgePanel-hy_IkIJI.js} +2 -2
- package/dist/assets/{WorkspaceKnowledgePanel-In2a_s_z.js.map → WorkspaceKnowledgePanel-hy_IkIJI.js.map} +1 -1
- package/dist/assets/{WorkspaceRoutinesPanel-zOxFoqML.js → WorkspaceRoutinesPanel-B7PkBP7x.js} +2 -2
- package/dist/assets/{WorkspaceRoutinesPanel-zOxFoqML.js.map → WorkspaceRoutinesPanel-B7PkBP7x.js.map} +1 -1
- package/dist/assets/{WorkstreamsView-HvlivC3b.js → WorkstreamsView-BeE2TRXK.js} +2 -2
- package/dist/assets/{WorkstreamsView-HvlivC3b.js.map → WorkstreamsView-BeE2TRXK.js.map} +1 -1
- package/dist/assets/{index-BPE9bBla.js → index-D11cgXI9.js} +3 -3
- package/dist/assets/{index-BPE9bBla.js.map → index-D11cgXI9.js.map} +1 -1
- package/dist/assets/{pdf-Q2a37aK5.js → pdf-BjTcV6As.js} +2 -2
- package/dist/assets/{pdf-Q2a37aK5.js.map → pdf-BjTcV6As.js.map} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/server/botControl.js +1 -1
- package/dist-server/server/desktopEnvironments.js +10 -9
- package/dist-server/server/desktopHostProtocol.js +1 -1
- package/dist-server/server/workspacePrompt.js +2 -2
- package/dist-server/shared/botEnvironment.js +10 -4
- package/dist-server/shared/fileTransferEndpoint.mjs +5 -5
- package/dist-server/shared/hostPaths.mjs +14 -4
- package/package.json +3 -2
- package/release.json +3 -3
- package/dist/assets/AboutDialog-CBKv505Y.js.map +0 -1
- package/dist/assets/ConversationsView-B6JNGLt_.css +0 -1
- package/dist/assets/ConversationsView-CzFWn-79.js.map +0 -1
package/dist/index.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
|
|
10
10
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
11
11
|
<title>Yaovi</title>
|
|
12
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
+
<script type="module" crossorigin src="/assets/index-D11cgXI9.js"></script>
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Dd_uD5pT.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/_plugin-vue_export-helper-BumXM028.js">
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-BQLAALTS.css">
|
|
@@ -245,7 +245,7 @@ export class BotControl {
|
|
|
245
245
|
disabled('global_disabled', '请在服务器设置中开放对应电脑环境。');
|
|
246
246
|
else {
|
|
247
247
|
row.reason = target === 'local' ? 'message_device_required' : matches.length > 1 ? 'ambiguous_device' : 'device_not_observed';
|
|
248
|
-
row.suggestion = target === 'local' ? '
|
|
248
|
+
row.suggestion = target === 'local' ? '“本机”“这台设备”“这台电脑”由本轮消息来源决定;已识别的客户端无需另选设备,身份缺失时检查客户端连接,不能按服务器推断。' : '尚无该资源的实时状态,请连接设备或使用明确设备编号。';
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
251
|
return row;
|
|
@@ -8,7 +8,7 @@ import { readHostTools } from './hostToolSettings.js';
|
|
|
8
8
|
import { readComputerNames } from './computerNames.js';
|
|
9
9
|
import { requireTeamToolBridge } from './workspaceToolLease.js';
|
|
10
10
|
import { copyBetweenEndpoints } from './fileTransfer.js';
|
|
11
|
-
import { deviceSourceText, deviceInventoryText } from '../shared/botEnvironment.js';
|
|
11
|
+
import { deviceSourceText, deviceInventoryText, isCurrentDeviceAlias } from '../shared/botEnvironment.js';
|
|
12
12
|
import { desktopHostExchangeSchema } from './desktopHostProtocol.js';
|
|
13
13
|
const digest = (s) => createHash('sha256').update(s).digest('hex');
|
|
14
14
|
const inputSchema = z.discriminatedUnion('kind', [
|
|
@@ -60,12 +60,12 @@ const shellSchema = z.object({ command: z.string().min(1).max(8000), cwd: z.stri
|
|
|
60
60
|
const copySchema = z.object({ sourceHost: z.string().trim().min(1).max(128), sourcePath: z.string().min(1).max(2048), targetHost: z.string().trim().min(1).max(128), targetPath: z.string().min(1).max(2048), overwrite: z.boolean().default(false) }).strict();
|
|
61
61
|
const newHostState = (key, remote) => ({ key, remote, seen: 0, pending: new Map(), queue: new Map(), manual: new Map(), paused: new Set(), frames: new Map(), generation: new Map(), notes: new Map(), activity: new Map() });
|
|
62
62
|
export const DESKTOP_ENVIRONMENT_TOOLS = [
|
|
63
|
-
{ id: 'desktop_environment_view', name: 'desktop_environment_view', description: '查看电脑真实桌面截图,返回截图尺寸、环境类型和电脑名称。仅在需要看窗口、图标布局或做点击输入时使用;列目录、读文件、写文件、跑命令不要先截图。可用 host
|
|
63
|
+
{ id: 'desktop_environment_view', name: 'desktop_environment_view', description: '查看电脑真实桌面截图,返回截图尺寸、环境类型和电脑名称。仅在需要看窗口、图标布局或做点击输入时使用;列目录、读文件、写文件、跑命令不要先截图。可用 host 指定目标电脑;省略或写「本机」「这台设备」「这台电脑」时只使用本轮消息来源电脑;身份缺失时提示修复客户端连接,离线时说明原因,不要求重复选择设备,禁止回退。', inputSchema: { type: 'object', properties: { host: { type: 'string', description: '可选,目标电脑的名称或编号;缺省用本轮消息来源电脑;server 明确指服务器' } }, additionalProperties: false } },
|
|
64
64
|
{ id: 'desktop_environment_action', name: 'desktop_environment_action', description: '操作电脑桌面(click、text、key、scroll、drag)。做这些鼠标键盘操作前必须先截图,坐标用刚截到的像素;页面变化后重新截图。列目录和跑命令请用文件/shell 工具,不要用本工具。可用 host 指定目标电脑。', inputSchema: { type: 'object', properties: { host: { type: 'string' }, kind: { enum: ['click', 'drag', 'text', 'key', 'scroll'] }, x: { type: 'integer' }, y: { type: 'integer' }, fromX: { type: 'integer' }, fromY: { type: 'integer' }, toX: { type: 'integer' }, toY: { type: 'integer' }, button: { enum: ['left', 'right', 'middle'] }, count: { type: 'integer' }, text: { type: 'string' }, key: { type: 'string' }, modifiers: { type: 'array', items: { enum: ['ctrl', 'alt', 'shift', 'super'] } }, direction: { enum: ['up', 'down', 'left', 'right'] }, amount: { type: 'integer' } }, required: ['kind'], additionalProperties: false } },
|
|
65
|
-
{ id: 'desktop_file_list', name: 'desktop_file_list', description: '
|
|
65
|
+
{ id: 'desktop_file_list', name: 'desktop_file_list', description: '列出当前电脑允许范围内的目录内容,返回名称、类型、大小与修改时间。可用绝对路径;相对路径基于用户主目录,例如 Desktop、Documents 或 ~/Desktop。范围以该电脑上报的文件授权为准。可用 host 指定目标电脑。', inputSchema: { type: 'object', properties: { path: { type: 'string' }, host: { type: 'string' } }, required: ['path'], additionalProperties: false } },
|
|
66
66
|
{ id: 'desktop_file_read', name: 'desktop_file_read', description: '读取当前电脑允许范围内的文件。文本请传 offsetLine(从0起)或 limitLines(默认100、最多400)按行读取,按 nextOffsetLine 继续;长行完整保留并可由结果引用读取。二进制传送用 desktop_file_copy。省略分页参数保留旧 base64 行为。可用 host 指定目标电脑。', inputSchema: { type: 'object', properties: { path: { type: 'string' }, host: { type: 'string' }, offsetLine: { type: 'integer', minimum: 0 }, limitLines: { type: 'integer', minimum: 1, maximum: 400 } }, required: ['path'], additionalProperties: false } },
|
|
67
|
-
{ id: 'desktop_file_write', name: 'desktop_file_write', description: '把 base64
|
|
68
|
-
{ id: 'desktop_file_copy', name: 'desktop_file_copy', description: '把文件从一台电脑直接复制到另一台电脑,支持电脑之间、电脑与服务器之间、电脑与当前 Bot 虚拟机之间双向传输。填写来源和目标电脑的名称或编号(服务器用 server,当前 Bot 的虚拟机用 vm
|
|
67
|
+
{ id: 'desktop_file_write', name: 'desktop_file_write', description: '把 base64 内容写入当前电脑上的文件(覆盖已存在文件)。可用绝对路径或相对用户主目录的路径;实际范围以该电脑的文件授权为准,访问受系统权限限制。可用 host 指定目标电脑。', inputSchema: { type: 'object', properties: { path: { type: 'string' }, data: { type: 'string' }, host: { type: 'string' } }, required: ['path', 'data'], additionalProperties: false } },
|
|
68
|
+
{ id: 'desktop_file_copy', name: 'desktop_file_copy', description: '把文件从一台电脑直接复制到另一台电脑,支持电脑之间、电脑与服务器之间、电脑与当前 Bot 虚拟机之间双向传输。填写来源和目标电脑的名称或编号(服务器用 server,当前 Bot 的虚拟机用 vm;本机/这台设备/这台电脑均指本轮消息来源电脑),以及各自电脑允许范围内的绝对路径或相对用户主目录的路径,例如 Desktop/a.txt;实际范围以目标设备的授权为准。保留来源文件,默认拒绝覆盖目标同名文件;只有用户要求覆盖时才设 overwrite=true。最大 25 MiB,支持二进制文件,写入并校验后返回结果。文件数据由服务器中转,不经过模型或聊天附件。', inputSchema: { type: 'object', properties: { sourceHost: { type: 'string' }, sourcePath: { type: 'string' }, targetHost: { type: 'string' }, targetPath: { type: 'string' }, overwrite: { type: 'boolean', default: false } }, required: ['sourceHost', 'sourcePath', 'targetHost', 'targetPath'], additionalProperties: false } },
|
|
69
69
|
{ id: 'desktop_shell', name: 'desktop_shell', description: '在这台电脑的用户主目录执行一条 shell 命令,返回 stdout、stderr 与退出码。长任务会超时终止;不要用它代替文件工具读写大文件。可用 host 指定目标电脑。', inputSchema: { type: 'object', properties: { command: { type: 'string' }, cwd: { type: 'string' }, timeoutMs: { type: 'integer' }, host: { type: 'string' } }, required: ['command'], additionalProperties: false } },
|
|
70
70
|
];
|
|
71
71
|
export const DESKTOP_CUA_TOOL_IDS = new Set(['desktop_window_list', 'desktop_ax_tree', 'desktop_window_action']);
|
|
@@ -81,7 +81,7 @@ DESKTOP_ENVIRONMENT_TOOLS.push({ id: 'desktop_window_list', name: 'desktop_windo
|
|
|
81
81
|
export const DESKTOP_FILE_TOOL_IDS = new Set(['desktop_file_list', 'desktop_file_read', 'desktop_file_write', 'desktop_file_copy', 'desktop_shell']);
|
|
82
82
|
export const DESKTOP_FILE_TRANSFER_RULES = '电脑间传文件用 desktop_file_copy,一次指定 sourceHost、sourcePath、targetHost、targetPath;服务器桌面代理用 server,当前 Bot 的虚拟机用 vm,设备名用设置里的名字,「本机」只指本轮消息来源电脑。例:把 mac1 桌面的 a.txt 放到 mac2 桌面,调用 {sourceHost:"mac1",sourcePath:"Desktop/a.txt",targetHost:"mac2",targetPath:"Desktop/a.txt"}。这是复制,保留原文件;只有用户明确要求覆盖时才传 overwrite:true。无需先截图,不要读取 base64 给模型、上传聊天附件或假设不同电脑共享文件路径。最大 25 MiB;工具返回已校验的成功结果后才报告完成。';
|
|
83
83
|
/** Macs connected to the server as computers. */
|
|
84
|
-
export const DESKTOP_ENVIRONMENT_RULES = '操作连接服务器的电脑时,使用本轮提供的 desktop_*
|
|
84
|
+
export const DESKTOP_ENVIRONMENT_RULES = '操作连接服务器的电脑时,使用本轮提供的 desktop_* 工具;「本机」「这台设备」「这台电脑」直接使用本轮已识别的来源电脑,无需用户再次选择,可省略 host 或填写该电脑编号。用户点名其他电脑时用 host 指定其名称或编号。Yaovi 已授权终端和文件工具不能代替该电脑上的操作。';
|
|
85
85
|
/** Yaovi host Mac — same look-then-act pattern as the cloud computer. */
|
|
86
86
|
export const SERVER_COMPUTER_RULES = '操作客户端提供的服务器桌面代理时,使用本轮提供的 desktop_* 工具并明确指定 host="server"。处理 Yaovi 已授权工具实际控制的运行环境时,优先使用对应原生工具;处理服务器桌面代理用户主目录内的文件时,使用已授权的 desktop_file_* 或 desktop_shell,不假定两者共享路径或能力。';
|
|
87
87
|
/** The loopback native transport is admitted only by serviceInstance's
|
|
@@ -160,7 +160,7 @@ export class DesktopEnvironments {
|
|
|
160
160
|
}
|
|
161
161
|
/** Tool aliases are relative to the triggering message, never to a Bot binding. */
|
|
162
162
|
namedHost(host, deviceHost, owner) {
|
|
163
|
-
if (host
|
|
163
|
+
if (isCurrentDeviceAlias(host) || host === 'local')
|
|
164
164
|
return this.messageHost(deviceHost);
|
|
165
165
|
if (host === 'server' || host === '服务器')
|
|
166
166
|
return this.requireHost(this.localSession(), owner);
|
|
@@ -186,7 +186,7 @@ export class DesktopEnvironments {
|
|
|
186
186
|
}
|
|
187
187
|
messageHost(deviceHost) {
|
|
188
188
|
if (!deviceHost)
|
|
189
|
-
throw new HttpError(409, '
|
|
189
|
+
throw new HttpError(409, '当前消息未提供客户端设备身份,无法识别「本机」「这台设备」「这台电脑」。请在当前电脑的桌面客户端检查服务器连接、重新登录或更新后重试,无需另选设备。', 'desktop_source_required');
|
|
190
190
|
const s = this.sessions.get(deviceHost);
|
|
191
191
|
if (!s)
|
|
192
192
|
throw new HttpError(409, '消息来源电脑未连接;不会改用其他电脑。', 'desktop_offline');
|
|
@@ -262,6 +262,7 @@ export class DesktopEnvironments {
|
|
|
262
262
|
return states.filter(s => !records.some(record => record.id === s.key && !record.enabled)).map(s => {
|
|
263
263
|
const online = this.hostOnline(s), info = online ? s.info : undefined;
|
|
264
264
|
return { id: s.key, name: this.label(s) || records.find(record => record.id === s.key)?.name || s.key, platform: info?.platform ?? '', online,
|
|
265
|
+
...(info?.environment && (info.full ?? []).includes(this.ownerKey(owner)) ? { ...(info.environment.fileScope === 'all' ? { fileScope: 'all' } : {}), fileRoots: [...info.environment.fileRoots] } : {}),
|
|
265
266
|
...(s === peer ? { sameServer: true } : {}), ...(s.key === 'local' && peer ? { serverPeerId: peer.key } : {}),
|
|
266
267
|
local: { supported: ['darwin', 'win32'].includes(info?.platform ?? ''), authorized: !!info && info.approved.includes(this.ownerKey(owner)), screen: info?.screen === true, accessibility: info?.accessibility === true, ready: !!info && this.ready(s, owner), fullAuthorized: !!info && (info.full ?? []).includes(this.ownerKey(owner)) }, browser: { available: online } };
|
|
267
268
|
}).sort((a, b) => a.id === 'local' ? -1 : b.id === 'local' ? 1 : a.name.localeCompare(b.name));
|
|
@@ -380,7 +381,7 @@ export class DesktopEnvironments {
|
|
|
380
381
|
if (commands.length >= 8)
|
|
381
382
|
break;
|
|
382
383
|
}
|
|
383
|
-
return { commands, capabilities: { environmentMetadata: 1, desktopPlatforms: ['darwin', 'win32'] } };
|
|
384
|
+
return { commands, capabilities: { environmentMetadata: 1, fileAccessScope: 1, desktopPlatforms: ['darwin', 'win32'] } };
|
|
384
385
|
}
|
|
385
386
|
command(s, command, check, timeout = 30000) {
|
|
386
387
|
check();
|
|
@@ -3,7 +3,7 @@ const text = z.string().min(1).max(4096).regex(/^[^\u0000-\u001f\u007f]*$/);
|
|
|
3
3
|
export const desktopHostEnvironmentSchema = z.object({
|
|
4
4
|
version: z.literal(1), osRelease: text, arch: text, shell: text,
|
|
5
5
|
homeDirectory: text, defaultCwd: text, fileRoots: z.array(text).min(1).max(8),
|
|
6
|
-
shellScope: z.literal('user'), timezone: text,
|
|
6
|
+
shellScope: z.literal('user'), timezone: text, fileScope: z.literal('all').optional(),
|
|
7
7
|
}).strict();
|
|
8
8
|
export const desktopHostInfoSchema = z.object({
|
|
9
9
|
id: z.string().uuid(), name: z.string().max(128), platform: z.string().max(16),
|
|
@@ -25,7 +25,7 @@ export function workspaceEnvironmentPrompt(env) {
|
|
|
25
25
|
'客户端电脑清单只描述 desktop_* 控制通道。服务器桌面代理离线不影响已连接的 Yaovi 已授权能力;desktop_* 的 host="server" 专指服务器桌面代理,不是 Yaovi 已授权工具的运行环境。清单明确标记“与当前 Yaovi 服务器同机”时,该客户端是运行服务器的同一台电脑,可按其实际授权显式指定该客户端 host;不要断言它是另一台机器;先检查同机客户端的对应能力,不能仅因专用桌面通道离线就断言无法操作服务器。两种连接仍分别报告,不自动切换或转移权限。关系未确认时不因名称或 IP 相同而合并设备、路径或权限。Yaovi 在容器中时,其路径不能当作宿主机路径;Yaovi 环境也不自动代表用户所说的「本机」。',
|
|
26
26
|
env.cwd ? `Yaovi 本轮工作目录(已确认):${JSON.stringify(env.cwd)}。该路径仅属于 Yaovi 运行环境。` : undefined,
|
|
27
27
|
`全局开放的额外电脑环境:${open.join('、') || '无'}。开放不等于在线或已授权;具体操作仍需目标设备就绪。已开放且获授权的环境可按任务同时使用,工具以本轮实际目录为准。`,
|
|
28
|
-
'
|
|
28
|
+
'先确定操作目标,再选择工具。用户明确点名的设备优先;「本机」只指本轮消息来源电脑,「这台设备」「这台电脑」与其同义。来源已明确时直接使用,不再要求选择设备;来源身份缺失时说明客户端识别失败并提示恢复连接,不反复列出设备让用户猜选。历史消息中的来源不适用于本轮;名称重名或其他目标不明确时先澄清;目标离线、未开放或未授权时说明原因,不得切换其他机器代做。',
|
|
29
29
|
'变量和登录授权跟随实际执行目标:Yaovi 已授权工具使用其节点的配置和登录,虚拟机脚本使用本轮获准的系统变量和临时授权文件,已授权的系统服务和已连接应用独立使用各自授权。默认环境不改变这些授权,不在不同环境之间自动复制登录资料。',
|
|
30
30
|
deviceSourceText(env.desktop),
|
|
31
31
|
deviceInventoryText(env.desktop),
|
|
@@ -154,7 +154,7 @@ export function buildWorkspacePromptParts(input, provenance) {
|
|
|
154
154
|
agent.temporaryGoalId ? '临时助手只处理分派工作,结束后退役,不创建团队或改变自身权限。' : undefined,
|
|
155
155
|
input.mainForeground ? '简短 Bot 资料维护直接用 workspace_get_agent_rules/update_agent_rules 读取和修改,保留主 Bot 自身身份,不交给临时执行器。多步长任务仍按后台规则处理。' : undefined,
|
|
156
156
|
input.teamRules, input.routineRules, input.knowledgeRules,
|
|
157
|
-
'模型运行于服务端 Yaovi
|
|
157
|
+
'模型运行于服务端 Yaovi。可用目录、服务连接、设备在线和操作授权是不同事实;不能因客户端或桌面代理离线就判定服务器原生能力离线。先确定目标再选工具;默认环境只决定未指定时的位置,不限制其他已开放且获授权目标。用户指定目标优先;「本机」「这台设备」「这台电脑」均指本轮消息来源电脑,已确定时不再要求选择设备;来源缺失时说明客户端未提供身份,提示恢复连接,不让用户反复猜选。其他名称重名或目标不明先澄清;离线、未开放或未授权直接说明原因,不擅自换机器。',
|
|
158
158
|
'desktop_* 的 host="server" 指服务器桌面代理,不代表所有原生工具后端。不同设备同名路径不是同一文件,容器路径不等于宿主路径。凭据跟随实际目标,不跨环境自动复制。',
|
|
159
159
|
env.tools.desktopFile ? '电脑文件/命令用 desktop_file_* 和 desktop_shell,路径相对目标电脑用户主目录;不要求先截图。' : undefined,
|
|
160
160
|
env.tools.desktopView ? '看窗口或点击先 desktop_environment_view,变化后重新截图,不猜旧坐标;人工接管时等交还,截图不代替文件统计。' : undefined,
|
|
@@ -2,14 +2,19 @@ export const deviceCapabilityLabels = {
|
|
|
2
2
|
ready: '就绪', offline: '离线', disabled: '未开放', unsupported: '不支持', not_authorized: '未授权',
|
|
3
3
|
system_permission_required: '系统权限未就绪', human_control: '等待人工交还', paused: '接管中断,需重新接管并交还', agent_unavailable: '当前 Bot 不可用',
|
|
4
4
|
};
|
|
5
|
+
export const CURRENT_DEVICE_ALIASES = ['本机', '这台设备', '这台电脑'];
|
|
6
|
+
export function isCurrentDeviceAlias(value) {
|
|
7
|
+
return CURRENT_DEVICE_ALIASES.some(alias => alias === value.trim());
|
|
8
|
+
}
|
|
5
9
|
export function deviceSourceText(snapshot) {
|
|
6
10
|
if (!snapshot.sourceHost)
|
|
7
|
-
return '
|
|
11
|
+
return '本轮未收到客户端设备身份,「本机」无法确定;「这台设备」「这台电脑」同样无法识别。请明确说明来源身份缺失,提示在当前电脑的桌面客户端检查服务器连接、重新登录或更新客户端后重试;不要列出其他电脑让用户反复选择。历史消息的设备来源不适用于本轮。';
|
|
8
12
|
const host = snapshot.hosts.find(host => host.id === snapshot.sourceHost);
|
|
9
13
|
const name = host?.name || snapshot.sourceHost;
|
|
14
|
+
const aliases = '「这台设备」「这台电脑」与「本机」同义,均指本轮消息来源电脑;身份已确定时不要再次要求用户选择设备。';
|
|
10
15
|
if (!host?.online || !host.open)
|
|
11
|
-
return `本轮用户消息来源设备:${JSON.stringify(name)}(${!host?.online ? '当前离线或未连接' : '全局未开放'}
|
|
12
|
-
return `本轮用户消息来自${host.kind === 'server' ? '服务器' : '电脑'}「${name}」。对本轮而言,「本机」指这台电脑(host=${JSON.stringify(host.target)}
|
|
16
|
+
return `本轮用户消息来源设备:${JSON.stringify(name)}(${!host?.online ? '当前离线或未连接' : '全局未开放'})。「本机」仍指这台电脑,不能回退到服务器或其他电脑。${aliases}直接说明当前不可用的原因。`;
|
|
17
|
+
return `本轮用户消息来自${host.kind === 'server' ? '服务器' : '电脑'}「${name}」。对本轮而言,「本机」指这台电脑(host=${JSON.stringify(host.target)})。${aliases}`;
|
|
13
18
|
}
|
|
14
19
|
export function deviceInventoryText(snapshot) {
|
|
15
20
|
if (!snapshot.hosts.length)
|
|
@@ -19,7 +24,8 @@ export function deviceInventoryText(snapshot) {
|
|
|
19
24
|
const details = m ? [
|
|
20
25
|
m.osRelease && `系统版本=${JSON.stringify(m.osRelease)}`, m.arch && `架构=${JSON.stringify(m.arch)}`,
|
|
21
26
|
m.shell && `Shell=${JSON.stringify(m.shell)}`, m.homeDirectory && `主目录=${JSON.stringify(m.homeDirectory)}`,
|
|
22
|
-
m.defaultCwd ? `默认工作目录=${JSON.stringify(m.defaultCwd)}` : '运行路径未上报或无文件与命令授权',
|
|
27
|
+
m.defaultCwd ? `默认工作目录=${JSON.stringify(m.defaultCwd)}` : '运行路径未上报或无文件与命令授权',
|
|
28
|
+
m.fileScope === 'all' ? '文件工具范围=当前系统用户可访问的所有目录(包括其他磁盘和已连接的共享目录,受系统权限限制)' : m.fileRoots && `文件工具允许根目录=${JSON.stringify(m.fileRoots)}`,
|
|
23
29
|
m.shellScope && 'Shell范围=桌面用户的系统权限(不受文件工具根目录约束)', m.timezone && `时区=${JSON.stringify(m.timezone)}`,
|
|
24
30
|
].filter(Boolean).join(';') : '运行路径未上报或无文件与命令授权';
|
|
25
31
|
const state = !host.online ? '离线' : !host.open ? '未开放' : '在线';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { open, realpath, mkdir, mkdtemp, unlink, rename, link, rm } from 'node:fs/promises'
|
|
2
2
|
import { constants } from 'node:fs'
|
|
3
3
|
import { resolve, dirname, join } from 'node:path'
|
|
4
|
-
import { hostPathInput, pathInside } from './hostPaths.mjs'
|
|
4
|
+
import { hostPathInput, pathInside, hostFileError } from './hostPaths.mjs'
|
|
5
5
|
import { tmpdir } from 'node:os'
|
|
6
6
|
import { createHash } from 'node:crypto'
|
|
7
7
|
|
|
@@ -11,13 +11,13 @@ const require = (value, message) => { if (!value) throw new Error(message) }
|
|
|
11
11
|
|
|
12
12
|
/** Bounded, replay-safe file operations shared by native desktops and runner hosts. */
|
|
13
13
|
export class FileTransferFiles {
|
|
14
|
-
constructor(root, { confined = true } = {}) {
|
|
15
|
-
this.root = root; this.confined = confined; this.transfers = new Map(); this.lanes = new Map(); this.closed = false
|
|
14
|
+
constructor(root, { confined = true, allowUNC = false } = {}) {
|
|
15
|
+
this.root = root; this.confined = confined; this.allowUNC = allowUNC; this.transfers = new Map(); this.lanes = new Map(); this.closed = false
|
|
16
16
|
this.timer = setInterval(() => { void this.expire() }, 60000); this.timer.unref()
|
|
17
17
|
}
|
|
18
18
|
async path(input, writing = false) {
|
|
19
19
|
require(typeof input === 'string' && input.length > 0 && input.length <= 4096 && !input.includes('\0'), '文件路径无效')
|
|
20
|
-
const root = await realpath(this.root), raw = resolve(root, hostPathInput(input))
|
|
20
|
+
const root = await realpath(this.root), raw = resolve(root, hostPathInput(input, process.platform, { allowUNC: this.allowUNC }))
|
|
21
21
|
const inside = value => !this.confined || pathInside(root, value)
|
|
22
22
|
require(inside(raw), '路径超出允许范围')
|
|
23
23
|
if (!writing) { const file = await realpath(raw); require(inside(file), '路径超出允许范围'); return file }
|
|
@@ -36,7 +36,7 @@ export class FileTransferFiles {
|
|
|
36
36
|
const id = action.transferId, previous = this.lanes.get(id) ?? Promise.resolve()
|
|
37
37
|
const next = previous.catch(() => {}).then(() => this.perform(action, authorize))
|
|
38
38
|
this.lanes.set(id, next)
|
|
39
|
-
try { return await next } finally { if (this.lanes.get(id) === next) this.lanes.delete(id) }
|
|
39
|
+
try { return await next } catch (error) { throw hostFileError(error) } finally { if (this.lanes.get(id) === next) this.lanes.delete(id) }
|
|
40
40
|
}
|
|
41
41
|
async perform(action, authorize) {
|
|
42
42
|
authorize(); require(!this.closed, '传输通道已关闭')
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
|
|
3
|
-
export function hostPathInput(input, platform = process.platform) {
|
|
3
|
+
export function hostPathInput(input, platform = process.platform, { allowUNC = false } = {}) {
|
|
4
4
|
const value = String(input ?? '')
|
|
5
5
|
if (value.includes('\0')) throw new Error('路径无效')
|
|
6
6
|
if (platform === 'win32') {
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
if (/^[\\/]{2}
|
|
7
|
+
// Only explicitly authorized desktop calls accept ordinary UNC shares.
|
|
8
|
+
// Device namespaces, drive-relative paths and alternate streams stay invalid.
|
|
9
|
+
if (/^[\\/]{2}/.test(value)) {
|
|
10
|
+
const [server, share] = value.slice(2).split(/[\\/]/)
|
|
11
|
+
if (!allowUNC || !server || !share || [server, share].some(part => ['.', '..', '?'].includes(part))) throw new Error('路径无效')
|
|
12
|
+
}
|
|
13
|
+
if (/^[A-Za-z]:(?![\\/])/.test(value) || /[<>"|?*]/.test(value)) throw new Error('路径无效')
|
|
10
14
|
const rest = value.replace(/^[A-Za-z]:/, '')
|
|
11
15
|
if (rest.includes(':') || rest.split(/[\\/]/).some(part =>
|
|
12
16
|
(part !== '.' && part !== '..' && /[ .]$/.test(part)) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))) throw new Error('路径无效')
|
|
@@ -15,6 +19,12 @@ export function hostPathInput(input, platform = process.platform) {
|
|
|
15
19
|
return value.replace(/^~\//, '').replace(/^~$/, '')
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
/** Keep OS failures distinct from application path confinement. */
|
|
23
|
+
export function hostFileError(error) {
|
|
24
|
+
const label = { EACCES: '系统权限拒绝访问', EPERM: '系统权限拒绝访问', EROFS: '文件系统只读', ENOENT: '文件或目录不存在', ENOTDIR: '路径不是目录' }[error?.code]
|
|
25
|
+
return label ? Object.assign(new Error(`${label}(${error.code})`, { cause: error }), { code: error.code }) : error
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
export function pathInside(root, target, platform = process.platform) {
|
|
19
29
|
const api = platform === 'win32' ? path.win32 : path.posix
|
|
20
30
|
const relative = api.relative(root, target)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lsamien/yaovi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Yaovi:单用户、多 Bot、多设备服务器",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"agent:setup": "node scripts/agent-setup.mjs",
|
|
46
46
|
"agent:test": ".venv/bin/python tests/runtime/test_kernel.py && .venv/bin/python tests/runtime/test_compression.py && (cd tests/runtime && ../../.venv/bin/python test_turn_context.py) && .venv/bin/python tests/runtime/test_backup.py && .venv/bin/python tests/runtime/test_hermes_web.py && .venv/bin/python tests/runtime/test_native_openviking.py && .venv/bin/python tests/runtime/test_native_openviking_config.py && .venv/bin/python tests/runtime/test_vault_env.py && vitest run tests/server/embeddedAgent.test.ts tests/server/singleUserDevices.test.ts tests/server/harnessWeb.test.ts",
|
|
47
47
|
"agent:pack": "npm run build && node scripts/build-agent-host.mjs",
|
|
48
|
-
"postinstall": "node scripts/agent-setup.mjs --postinstall"
|
|
48
|
+
"postinstall": "node scripts/agent-setup.mjs --postinstall",
|
|
49
|
+
"desktop:release:client": "npm run desktop:build:client && node scripts/client-release.mjs"
|
|
49
50
|
},
|
|
50
51
|
"dependencies": {
|
|
51
52
|
"@koa/bodyparser": "^5.0.0",
|
package/release.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AboutDialog-CBKv505Y.js","names":[],"sources":["../../package.json","../../src/client/components/app/AboutDialog.vue","../../src/client/components/app/AboutDialog.vue"],"sourcesContent":["{\n \"name\": \"@lsamien/yaovi\",\n \"version\": \"0.1.1\",\n \"license\": \"Apache-2.0\",\n \"description\": \"Yaovi:单用户、多 Bot、多设备服务器\",\n \"type\": \"module\",\n \"main\": \"desktop/main.mjs\",\n \"engines\": {\n \"node\": \">=24.0.0\"\n },\n \"bin\": {\n \"yaovi\": \"bin/yaovi-agent.mjs\",\n \"yaovi-agent\": \"bin/yaovi-agent.mjs\"\n },\n \"scripts\": {\n \"dev\": \"cross-env NODE_ENV=development tsx watch src/server/index.ts\",\n \"build\": \"npm run brand:verify && npm run release:verify && vue-tsc -b tsconfig.client.build.json && vite build && node scripts/clean-server.mjs && tsc -p tsconfig.server.json && node scripts/copy-worker.mjs && node scripts/write-build-info.mjs\",\n \"start\": \"node bin/yaovi-agent.mjs start\",\n \"typecheck\": \"vue-tsc -b && tsc --noEmit -p tsconfig.server.json\",\n \"test\": \"vitest run\",\n \"prepack\": \"npm run build\",\n \"test:watch\": \"vitest\",\n \"test:e2e\": \"npm run build && playwright test\",\n \"test:live\": \"cross-env YAOVI_LIVE_TEST=1 vitest run tests/live\",\n \"brand:verify\": \"node scripts/verify-brand.mjs\",\n \"release:verify\": \"node scripts/verify-release.mjs\",\n \"verify:lifecycle\": \"node scripts/verify-lifecycle.mjs\",\n \"test:e2e:workspace\": \"npm run build && playwright test -c playwright.workspace.config.ts\",\n \"test:e2e:desktop-hosts\": \"npm run build && playwright test -c playwright.desktop-hosts.config.ts\",\n \"test:e2e:team-tools\": \"npm run build && playwright test -c playwright.team-tools.config.ts\",\n \"desktop:build\": \"npm run build && node scripts/build-desktop.mjs\",\n \"desktop:build:client\": \"node scripts/write-build-info.mjs && node scripts/build-desktop-client.mjs\",\n \"desktop:dev:client\": \"npm run desktop:build:client && cross-env YAOVI_DESKTOP_CLIENT_ONLY=1 electron .\",\n \"desktop:pack:client\": \"npm run desktop:build:client && electron-builder --config electron-builder.client.yml --mac --arm64 --publish never\",\n \"desktop:dev\": \"npm run desktop:build && cross-env YAOVI_DESKTOP_CLIENT_ONLY=0 electron .\",\n \"desktop:pack\": \"npm run desktop:build && electron-builder --mac --arm64 --publish never\",\n \"desktop:release\": \"npm run desktop:build && node scripts/desktop-release.mjs\",\n \"desktop:test\": \"cross-env YAOVI_DESKTOP_CLIENT_ONLY=0 node --test desktop/*.test.mjs\",\n \"runner\": \"node dist-server/runner/index.js\",\n \"runner:dev\": \"tsx src/runner/index.ts\",\n \"runner:build\": \"node scripts/build-runner.mjs\",\n \"desktop:build:win\": \"npm run build && node scripts/build-windows-desktop.mjs\",\n \"desktop:pack:win\": \"npm run desktop:build:win && node scripts/windows-desktop-release.mjs\",\n \"desktop:test:win\": \"node scripts/test-windows-desktop.mjs\",\n \"agent:setup\": \"node scripts/agent-setup.mjs\",\n \"agent:test\": \".venv/bin/python tests/runtime/test_kernel.py && .venv/bin/python tests/runtime/test_compression.py && (cd tests/runtime && ../../.venv/bin/python test_turn_context.py) && .venv/bin/python tests/runtime/test_backup.py && .venv/bin/python tests/runtime/test_hermes_web.py && .venv/bin/python tests/runtime/test_native_openviking.py && .venv/bin/python tests/runtime/test_native_openviking_config.py && .venv/bin/python tests/runtime/test_vault_env.py && vitest run tests/server/embeddedAgent.test.ts tests/server/singleUserDevices.test.ts tests/server/harnessWeb.test.ts\",\n \"agent:pack\": \"npm run build && node scripts/build-agent-host.mjs\",\n \"postinstall\": \"node scripts/agent-setup.mjs --postinstall\"\n },\n \"dependencies\": {\n \"@koa/bodyparser\": \"^5.0.0\",\n \"@koa/router\": \"^15.4.0\",\n \"@openviking/sdk\": \"0.1.0\",\n \"@trycua/cua-driver\": \"0.28.2\",\n \"@types/busboy\": \"^1.5.4\",\n \"@types/koa\": \"^2.15.0\",\n \"@types/koa-send\": \"^4.1.6\",\n \"@types/koa-static\": \"^4.0.4\",\n \"@types/markdown-it\": \"^14.1.2\",\n \"@types/mime-types\": \"^2.1.4\",\n \"@types/node\": \"^24.10.0\",\n \"@types/qrcode\": \"^1.5.6\",\n \"@types/ws\": \"^8.18.1\",\n \"@vicons/ionicons5\": \"^0.13.0\",\n \"@vicons/tabler\": \"^0.13.0\",\n \"@vitejs/plugin-vue\": \"^6.0.5\",\n \"@vue/tsconfig\": \"^0.9.1\",\n \"busboy\": \"^1.6.0\",\n \"cookie\": \"^1.0.2\",\n \"docx-preview\": \"^0.3.7\",\n \"dompurify\": \"^3.2.6\",\n \"highlight.js\": \"^11.11.1\",\n \"koa\": \"^2.15.3\",\n \"koa-send\": \"^5.0.1\",\n \"koa-static\": \"^5.0.0\",\n \"libsodium-wrappers-sumo\": \"0.8.4\",\n \"markdown-it\": \"^14.1.0\",\n \"mime-types\": \"^3.0.1\",\n \"naive-ui\": \"^2.44.1\",\n \"pdfjs-dist\": \"6.2.108\",\n \"pinia\": \"^3.0.4\",\n \"pinyin-pro\": \"3.29.5\",\n \"playwright\": \"1.62.1\",\n \"qrcode\": \"^1.5.4\",\n \"read-excel-file\": \"^7.0.3\",\n \"sass\": \"^1.99.0\",\n \"ssh2\": \"1.17.0\",\n \"typescript\": \"~6.0.2\",\n \"undici\": \"7.29.1\",\n \"vite\": \"^8.0.4\",\n \"vue\": \"^3.5.32\",\n \"vue-router\": \"^4.6.4\",\n \"vue-tsc\": \"^3.2.8\",\n \"ws\": \"^8.20.0\",\n \"zod\": \"^4.1.12\"\n },\n \"devDependencies\": {\n \"@playwright/test\": \"^1.60.0\",\n \"@types/jsdom\": \"^21.1.7\",\n \"@types/ssh2\": \"1.15.5\",\n \"@types/supertest\": \"^6.0.3\",\n \"@vitest/coverage-v8\": \"^3.2.4\",\n \"@vue/test-utils\": \"^2.4.6\",\n \"cross-env\": \"^10.1.0\",\n \"electron\": \"43.4.0\",\n \"electron-builder\": \"26.15.3\",\n \"electron-updater\": \"6.8.9\",\n \"esbuild\": \"0.28.2\",\n \"js-yaml\": \"4.3.2\",\n \"jsdom\": \"^27.0.1\",\n \"supertest\": \"^7.1.4\",\n \"tsx\": \"^4.21.0\",\n \"vitest\": \"^3.2.4\"\n },\n \"files\": [\n \"bin\",\n \"dist\",\n \"dist-server\",\n \"licenses\",\n \"LICENSE\",\n \"NOTICE\",\n \"THIRD_PARTY_NOTICES.md\",\n \"release.json\",\n \"build-info.json\",\n \"runtime\",\n \"scripts/agent-backup.py\",\n \"scripts/agent-setup.mjs\",\n \"!runtime/**/__pycache__/**\",\n \"!runtime/**/*.pyc\",\n \"!runtime/**/*.pyo\",\n \"!runtime/**/.env\",\n \"!runtime/**/auth.json\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n }\n}\n","<script setup lang=\"ts\">\nimport { version } from '../../../../package.json'\nimport BrandMark from '@/components/common/BrandMark.vue'\nimport StandaloneDialog from '@/components/common/StandaloneDialog.vue'\nconst emit = defineEmits<{ close: [] }>()\n</script>\n<template>\n <StandaloneDialog title=\"关于Yaovi\" compact @close=\"emit('close')\"><section class=\"about-content\">\n <BrandMark :size=\"56\" :label=\"false\" compact bare /><h3>Yaovi</h3><p class=\"about-version\">版本 {{ version }}</p>\n <p>通过 OpenAI 模型与 Bot 对话、协作完成任务。<br />基于 Apache-2.0 许可证开放源代码。</p>\n\n </section></StandaloneDialog>\n</template>\n<style scoped>\n.about-content{display:grid;justify-items:center;gap:12px;text-align:center}.about-content h3{font-size:21px;margin:0}.about-content p{margin:0;color:var(--text-secondary);font-size:13px;line-height:1.8}.about-content .about-version{font-size:12px}.about-content nav{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:8px}.about-content a{padding:10px 8px;color:var(--accent);font-size:13px;text-decoration:none}.about-content a:hover{text-decoration:underline}.about-content a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\n</style>\n","<script setup lang=\"ts\">\nimport { version } from '../../../../package.json'\nimport BrandMark from '@/components/common/BrandMark.vue'\nimport StandaloneDialog from '@/components/common/StandaloneDialog.vue'\nconst emit = defineEmits<{ close: [] }>()\n</script>\n<template>\n <StandaloneDialog title=\"关于Yaovi\" compact @close=\"emit('close')\"><section class=\"about-content\">\n <BrandMark :size=\"56\" :label=\"false\" compact bare /><h3>Yaovi</h3><p class=\"about-version\">版本 {{ version }}</p>\n <p>通过 OpenAI 模型与 Bot 对话、协作完成任务。<br />基于 Apache-2.0 许可证开放源代码。</p>\n\n </section></StandaloneDialog>\n</template>\n<style scoped>\n.about-content{display:grid;justify-items:center;gap:12px;text-align:center}.about-content h3{font-size:21px;margin:0}.about-content p{margin:0;color:var(--text-secondary);font-size:13px;line-height:1.8}.about-content .about-version{font-size:12px}.about-content nav{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:8px}.about-content a{padding:10px 8px;color:var(--accent);font-size:13px;text-decoration:none}.about-content a:hover{text-decoration:underline}.about-content a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}\n</style>\n"],"mappings":"wVCIA,IAAM,EAAO,gBAGX,EAAA,EAAA,EAI6B,EAAA,CAJX,MAAM,UAAU,QAAA,GAAS,QAAK,AAAA,EAAA,KAAA,GAAE,EAAI,OAAA,IAAW,QAAA,MAIvD,CAJuD,EAIvD,UAJuD,EAIvD,CAHR,EAAoD,EAAA,CAAxC,KAAM,GAAK,MAAO,GAAO,QAAA,GAAQ,KAAA,KAAO,AAAA,EAAA,KAAA,EAAc,KAAA,KAAV,QAAK,EAAA,EAAK,EAA6C,IAA7C,EAAyB,MAAG,EAAG,EAAA,CAAA,CAAO,EAAA,CAAA,EACxG,AAAA,EAAA,KAAA,EAAgE,IAAA,KAAA,CAA7D,EAAA,8BAA4B,EAAA,EAAM,IAAA,EAAA,EAAA,yBAAuB"}
|