Files
tutti/packages/agent/gui/actions/workspaceFilePathCandidate.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

315 lines
8.4 KiB
TypeScript

import { normalizeWorkspaceFilePath as normalizeLogicalWorkspaceFilePath } from "@tutti-os/workspace-file-manager/services";
const URL_LIKE_LINK_PATTERN = /^[a-zA-Z][a-zA-Z\d+.-]*:|^#/;
const LOCAL_ASSET_ROOT = "/var/cache/tsh/local-assets";
export interface ResolveWorkspaceFilePathCandidateInput {
path: string;
workspaceRoot?: string | null;
basePath?: string | null;
}
export interface ResolvedWorkspaceFilePathCandidate {
path: string;
directoryPath: string;
workspaceRoot: string;
}
/**
* Pure file-path policy shared by transcript projection and host link actions.
* It intentionally has no Workspace mention, app, issue, registry, or renderer
* dependencies.
*/
export function resolveWorkspaceFilePathCandidate({
path,
workspaceRoot,
basePath
}: ResolveWorkspaceFilePathCandidateInput): ResolvedWorkspaceFilePathCandidate | null {
const rawPath = decodeWorkspaceLinkPath(path.trim());
if (
!rawPath ||
isUrlLikeWorkspaceFilePath(rawPath) ||
isUncWorkspaceFilePath(rawPath)
) {
return null;
}
const selectedRoot = normalizeWorkspaceFilePath(workspaceRoot?.trim() ?? "");
const sessionRoot = normalizeWorkspaceFilePath(basePath?.trim() ?? "");
const root = selectedRoot || sessionRoot;
const normalizedPath = normalizeWorkspaceFilePath(
stripWorkspaceFileLineAnchor(rawPath),
sessionRoot || root
);
if (isUnsupportedSpecialWorkspaceFilePath(normalizedPath)) {
return null;
}
if (isStagedLocalAssetPath(normalizedPath)) {
return null;
}
if (isHomeRelativeWorkspaceFilePath(normalizedPath)) {
const directoryPath = dirnameForHomeRelativePath(normalizedPath);
return {
path: normalizedPath,
directoryPath,
workspaceRoot: selectedRoot || directoryPath
};
}
if (
isAbsoluteLocalPath(normalizedPath) &&
(isDirectAgentGeneratedMediaPath(normalizedPath) ||
isDirectWorkspaceAppDataPath(normalizedPath))
) {
const directoryPath = dirname(normalizedPath);
return {
path: normalizedPath,
directoryPath,
workspaceRoot: selectedRoot || directoryPath
};
}
if (!root) {
return null;
}
if (isAbsoluteLocalPath(normalizedPath)) {
const directoryPath = dirname(normalizedPath);
return {
path: normalizedPath,
directoryPath,
workspaceRoot: root
};
}
const base = normalizeWorkspaceFilePath(basePath?.trim() || root);
const resolvedPath = normalizeWorkspaceFilePath(`${base}/${normalizedPath}`);
if (
!isInsideOrEqualWorkspaceFilePath(resolvedPath, root) &&
!isDirectAgentGeneratedMediaPath(resolvedPath)
) {
return null;
}
return {
path: resolvedPath,
directoryPath: resolvedPath === root ? root : dirname(resolvedPath),
workspaceRoot: root
};
}
export function normalizeWorkspaceFilePath(
path: string,
rootPath?: string | null
): string {
const normalizedPath = path.trim().replaceAll("\\", "/");
const normalizedRootPath = rootPath?.trim().replaceAll("\\", "/");
if (
isWindowsAbsolutePath(normalizedPath) ||
isGitBashWindowsAbsolutePath(normalizedPath, normalizedRootPath)
) {
return normalizeLogicalWorkspaceFilePath(
normalizedPath,
normalizedRootPath
);
}
const drive = /^[A-Za-z]:/.exec(normalizedPath)?.[0] ?? "";
const startsWithSlash = normalizedPath.startsWith("/");
const pathBody = drive
? normalizedPath.slice(drive.length)
: startsWithSlash
? normalizedPath.slice(1)
: normalizedPath;
const parts: string[] = [];
for (const part of pathBody.split("/")) {
if (!part || part === ".") {
continue;
}
if (part === "..") {
parts.pop();
continue;
}
parts.push(part);
}
if (drive) {
return parts.length > 0 ? `${drive}/${parts.join("/")}` : `${drive}/`;
}
if (startsWithSlash) {
return parts.length > 0 ? `/${parts.join("/")}` : "/";
}
return parts.join("/");
}
export function isUrlLikeWorkspaceFilePath(path: string): boolean {
if (path.startsWith("#")) {
return true;
}
if (isWindowsAbsolutePath(path.trim().replaceAll("\\", "/"))) {
return false;
}
return URL_LIKE_LINK_PATTERN.test(path);
}
export function decodeWorkspaceLinkPath(path: string): string {
if (!path.includes("%")) {
return path;
}
try {
return decodeURI(path);
} catch {
return path;
}
}
/**
* Agent Markdown source links use a terminal `:line` suffix. File launchers
* operate on filesystem paths, so remove that presentation-only location
* anchor before resolving the path.
*/
function stripWorkspaceFileLineAnchor(path: string): string {
return path.replace(/:[1-9]\d*$/, "");
}
export function workspaceFilePathBasename(path: string): string {
const normalized = path.trim().replaceAll("\\", "/");
return normalized.split("/").filter(Boolean).at(-1) ?? path;
}
export function isInsideOrEqualWorkspaceFilePath(
path: string,
root: string
): boolean {
if (root === "/") {
return path.startsWith("/");
}
const comparison =
isWindowsAbsolutePath(root) || isWindowsAbsolutePath(path)
? { path: path.toLowerCase(), root: root.toLowerCase() }
: { path, root };
return (
comparison.path === comparison.root ||
comparison.path.startsWith(`${comparison.root}/`)
);
}
export function isDirectAgentGeneratedMediaPath(path: string): boolean {
if (!isAbsoluteLocalPath(path)) {
return false;
}
const statePath = getTuttiStatePathSegments(path);
if (!statePath) {
return false;
}
if (
statePath[1] !== "agent" ||
statePath[2] !== "runs" ||
(!statePath.includes("generated_images") &&
!statePath.includes("generated_videos"))
) {
return false;
}
return /\.(?:png|jpe?g|gif|webp|bmp|mp4|webm)$/i.test(path);
}
function isAbsoluteLocalPath(path: string): boolean {
return path.startsWith("/") || isWindowsAbsolutePath(path);
}
function isHomeRelativeWorkspaceFilePath(path: string): boolean {
return path === "~" || path.startsWith("~/");
}
function dirnameForHomeRelativePath(path: string): string {
return path === "~" ? "~" : dirname(path);
}
function isWindowsAbsolutePath(path: string): boolean {
return /^\/?[A-Za-z]:\//.test(path);
}
function isGitBashWindowsAbsolutePath(
path: string,
rootPath?: string | null
): boolean {
const rootDrive = readWindowsDrive(rootPath ?? "");
const pathDrive = /^\/([A-Za-z])(?=\/|$)/.exec(path)?.[1];
return Boolean(
rootDrive && pathDrive && `${pathDrive}:`.toUpperCase() === rootDrive
);
}
function readWindowsDrive(path: string): string {
return /^\/?([A-Za-z]:)(?=\/|$)/.exec(path)?.[1]?.toUpperCase() ?? "";
}
function isUncWorkspaceFilePath(path: string): boolean {
return /^(?:\\\\|\/\/)[^/\\]+[/\\][^/\\]+/.test(path);
}
function isUnsupportedSpecialWorkspaceFilePath(path: string): boolean {
const comparisonPath = cleanWorkspaceFilePathForComparison(path);
return (
comparisonPath === "/dev/null" ||
comparisonPath.split("/").some((segment) => {
const normalized = segment.trim().replace(/[. ]+$/g, "");
const deviceName = normalized.split(".", 1)[0]?.toUpperCase();
return deviceName === "NUL";
})
);
}
function cleanWorkspaceFilePathForComparison(path: string): string {
const normalized = path.replace(/\/+/g, "/");
const parts: string[] = [];
for (const part of normalized.split("/")) {
if (!part || part === ".") {
continue;
}
if (part === "..") {
parts.pop();
continue;
}
parts.push(part);
}
return normalized.startsWith("/") ? `/${parts.join("/")}` : parts.join("/");
}
function dirname(path: string): string {
const index = path.lastIndexOf("/");
if (index <= 0) {
return "/";
}
return path.slice(0, index);
}
function isStagedLocalAssetPath(path: string): boolean {
return (
path !== LOCAL_ASSET_ROOT &&
isInsideOrEqualWorkspaceFilePath(path, LOCAL_ASSET_ROOT)
);
}
function isDirectWorkspaceAppDataPath(path: string): boolean {
if (!isAbsoluteLocalPath(path)) {
return false;
}
const statePath = getTuttiStatePathSegments(path);
if (!statePath) {
return false;
}
return (
statePath[1] === "apps" &&
statePath[2] === "workspaces" &&
statePath.length > 5
);
}
function getTuttiStatePathSegments(path: string): string[] | null {
const segments = path.split("/").filter(Boolean);
const stateRootIndex = segments.findIndex(
(segment) => segment === ".tutti" || segment === ".tutti-dev"
);
if (stateRootIndex < 0) {
return null;
}
return segments.slice(stateRootIndex);
}