945b0a609b
Diagnosed via production session logs (RDS + shared goose PG) that some
users' page-generation attempts drifted: the agent used apps__create_app
(no public URL) then fabricated a placeholder domain when asked for a
link, and separately burned many turns scraping unreachable/anti-bot
search engines. Steer both flows toward the already-working path instead
of banning tools outright:
- apps__create_app is fine for designing/previewing a page, but the
agent must still write_file the result into public/*.html per the
static-page-publish skill and return a link built from the real
public URL template (no invented domains)
- for real-world lookups, load_skill('web') first and prefer
Bing/360 over retrying google.com or hammering anti-bot sites
Also folds in generate_docx guidance for Word-download attachments
(sandbox-fs tool) that landed in the same files during this pass.
Co-Authored-By: Claude <noreply@anthropic.com>
380 lines
19 KiB
JavaScript
380 lines
19 KiB
JavaScript
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
||
export const PUBLISH_SKILL_NAME = 'static-page-publish';
|
||
export const PUBLISH_ROOT_DIR = 'MindSpace';
|
||
export const PUBLIC_ZONE_DIR = 'public';
|
||
export const PUBLISH_SKILL_DIR = path.join(__dirname, 'skills', PUBLISH_SKILL_NAME);
|
||
export const WORKSPACE_HINTS_FILENAME = '.tkmindhints';
|
||
export const LEGACY_WORKSPACE_HINTS_FILENAME = '.goosehints';
|
||
|
||
function renderBrandingBlock(userAddressName) {
|
||
const name = userAddressName || '用户';
|
||
return `## 品牌与称呼(硬性)
|
||
|
||
- 你是 **TKMind** 助手;介绍产品时用 TKMind,不要称 goose、Goose、goosed
|
||
- 与用户对话时,用 **${name}** 称呼用户(可辅以「你/您」),**禁止**把用户叫作 TKMind
|
||
- 问候示例:「${name},下午好」——不要用「TKMind,下午好」
|
||
- 不要描述本工作区为「Rust goose 项目」或「goose AI 框架」
|
||
- 本工作区是 TKMind **MindSpace 用户空间**,用于文件管理与静态页面生成
|
||
`;
|
||
}
|
||
|
||
/** Preferred name when addressing the user in chat (display name > username > slug). */
|
||
const INTERNAL_ID_PATTERN =
|
||
/^wx_[a-z0-9_]{4,64}$|^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||
|
||
export function resolveUserAddressName({ displayName, username, slug } = {}) {
|
||
const preferred = String(displayName ?? '').trim();
|
||
if (preferred && !INTERNAL_ID_PATTERN.test(preferred)) return preferred;
|
||
const uname = String(username ?? '').trim();
|
||
if (uname && !INTERNAL_ID_PATTERN.test(uname)) return uname;
|
||
const fallback = String(slug ?? '').trim();
|
||
if (fallback && !INTERNAL_ID_PATTERN.test(fallback)) return fallback;
|
||
return '用户';
|
||
}
|
||
|
||
export function resolvePublicBaseUrl(env = process.env) {
|
||
return (env.H5_PUBLIC_BASE_URL ?? 'https://m.tkmind.cn').replace(/\/$/, '');
|
||
}
|
||
|
||
export const PUBLISH_KEY_UUID =
|
||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||
|
||
/** Stable per-user directory key (UUID). */
|
||
export function resolvePublishKey(user) {
|
||
if (!user?.id) throw new Error('缺少用户 ID');
|
||
return String(user.id).trim().toLowerCase();
|
||
}
|
||
|
||
/** @deprecated Use resolvePublishKey — kept as alias for URL/path segments. */
|
||
export function resolvePublishSlug(user) {
|
||
return resolvePublishKey(user);
|
||
}
|
||
|
||
export function resolveUsernameSlug(user) {
|
||
if (!user?.username) throw new Error('缺少用户名');
|
||
return String(user.username).trim().toLowerCase();
|
||
}
|
||
|
||
export function resolvePublishDir(h5Root, user) {
|
||
return path.join(h5Root, PUBLISH_ROOT_DIR, resolvePublishKey(user));
|
||
}
|
||
|
||
export function resolveLegacyPublishDir(h5Root, user) {
|
||
if (!user?.username) return null;
|
||
return path.join(h5Root, PUBLISH_ROOT_DIR, resolveUsernameSlug(user));
|
||
}
|
||
|
||
function mergePublishTrees(sourceDir, targetDir) {
|
||
if (!fs.existsSync(sourceDir)) return;
|
||
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
||
const from = path.join(sourceDir, entry.name);
|
||
const to = path.join(targetDir, entry.name);
|
||
if (entry.isDirectory()) {
|
||
fs.mkdirSync(to, { recursive: true });
|
||
mergePublishTrees(from, to);
|
||
} else if (!fs.existsSync(to)) {
|
||
fs.copyFileSync(from, to);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Move username-based MindSpace/<username>/ into MindSpace/<userId>/ once. */
|
||
export function migrateUserPublishDir(h5Root, user, legacyUsersRoot = null) {
|
||
const target = resolvePublishDir(h5Root, user);
|
||
fs.mkdirSync(target, { recursive: true });
|
||
const legacyDirs = [
|
||
resolveLegacyPublishDir(h5Root, user),
|
||
legacyUsersRoot ? path.join(legacyUsersRoot, resolveUsernameSlug(user)) : null,
|
||
].filter(Boolean);
|
||
for (const legacyDir of legacyDirs) {
|
||
if (path.resolve(legacyDir) === path.resolve(target)) continue;
|
||
mergePublishTrees(legacyDir, target);
|
||
}
|
||
return target;
|
||
}
|
||
|
||
export function buildPublicUrl(publicBaseUrl, publishKey, filename = '') {
|
||
const base = `${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${encodeURIComponent(publishKey)}`;
|
||
if (!filename) return `${base}/`;
|
||
const clean = filename.replace(/^\/+/, '');
|
||
return `${base}/${clean.split('/').map(encodeURIComponent).join('/')}`;
|
||
}
|
||
|
||
/** Public-zone HTML pages live under \`public/\`; share URLs must include that segment. */
|
||
export function buildPublicZonePageUrl(publicBaseUrl, publishKey, filename) {
|
||
const clean = String(filename ?? '').replace(/^\/+/, '');
|
||
const relative = clean.startsWith(`${PUBLIC_ZONE_DIR}/`) ? clean : `${PUBLIC_ZONE_DIR}/${clean}`;
|
||
return buildPublicUrl(publicBaseUrl, publishKey, relative);
|
||
}
|
||
|
||
export function renderPublishSkill({ slug, username, publicBaseUrl, publishDir, displayName }) {
|
||
const addressName = resolveUserAddressName({ displayName, username, slug });
|
||
const exampleUrl = buildPublicZonePageUrl(publicBaseUrl, slug, 'report.html');
|
||
return `---
|
||
name: ${PUBLISH_SKILL_NAME}
|
||
description: 在专属 MindSpace 目录生成可公开访问的静态 HTML 报告与页面(TKMind H5)
|
||
---
|
||
|
||
# 静态页面 / 报告发布(TKMind)
|
||
|
||
当用户需要**网页、HTML 报告、可视化页面、可分享链接**时,**必须先加载本技能**并按以下规则执行。
|
||
|
||
## 硬性约束(不可违反)
|
||
|
||
1. **唯一可写目录**:\`${publishDir}\`
|
||
2. **禁止**使用绝对路径(如 \`/Users/...\`、\`../\` 跳出目录)
|
||
3. **允许**在本目录内使用 \`write_file\`、\`edit_file\`、\`read_file\`、\`list_dir\`、\`generate_docx\`;**禁止**访问此目录外的路径(含 \`../\`、其它用户目录、项目根目录;系统会在 OS 层拦截越界访问)
|
||
4. **禁止**子 Agent、扩展管理、修改工作区外文件
|
||
5. 页面默认写入 \`public/\` 分区,使用相对路径如 \`public/report.html\`、\`public/assets/chart.png\`
|
||
6. 生成 HTML 后,向用户提供可访问链接,格式:
|
||
\`${publicBaseUrl}/${PUBLISH_ROOT_DIR}/${slug}/public/<文件名>\`
|
||
(写入 \`public/页面.html\` 时 URL **必须**含 \`public/\`;仅当 HTML 直接写在 workspace 根目录时才省略)
|
||
|
||
## 推荐工作流
|
||
|
||
1. 确认需求(标题、章节、是否要图表/样式)
|
||
2. 使用 \`write_file\` 创建 \`public/页面.html\`(可含内联 CSS;需要时在 \`public/assets/\` 或工作区 \`assets/\` 放资源)
|
||
3. 在 \`<head>\` 写入 **mindspace-cover** 元数据(见下文,必须与页面主题一致)
|
||
4. 页面内资源使用**相对路径**(\`assets/foo.png\`),不要用磁盘绝对路径
|
||
5. 保存 HTML 后,服务端会**立即**生成同名预览图 \`<文件名>.thumbnail.svg\`(Agent 交互阶段即生效,无需等用户保存到「我的空间」)
|
||
6. 下载按钮的相对链接目标(如 \`public/report.docx\`)必须通过 \`generate_docx\` 落盘,且 basename 与 HTML 一致
|
||
7. 完成后按「回复格式」返回可点击链接
|
||
|
||
## 伴生下载文件(必须)
|
||
|
||
- 相对路径下载链接(\`.docx\` / \`.pdf\` 等)必须在 HTML 同目录或子目录真实存在
|
||
- Word 文档必须用 sandbox-fs 的 \`generate_docx\` 生成;禁止用 \`computercontroller\` / shell 生成生产下载文件作为交付依据
|
||
- 改 HTML 文件名时同步 rename/copy 伴生文件;从 \`oa/\` 复制到 \`public/\` 再链接
|
||
- 可跑 \`node scripts/check-mindspace-public-links.mjs\` 自检
|
||
|
||
## 回复格式(必须)
|
||
|
||
向用户交付页面时,**必须使用 Markdown 可点击链接**,让用户在聊天里直接点开预览:
|
||
|
||
\`\`\`markdown
|
||
[普拉提 618 活动页](${exampleUrl})
|
||
\`\`\`
|
||
|
||
要求:
|
||
- **必须**使用 \`[标题](URL)\` 格式,不要只给裸 URL
|
||
- 标题用页面真实主题(不要用「点击这里」「链接」)
|
||
- 可同时给出本地相对路径(如 \`public/pilates-618.html\`)与公网链接
|
||
- 说明:静态文件保存即生效,**无需重启**
|
||
|
||
## 信息流预览图(必须)
|
||
|
||
每个 HTML 页面都必须在 \`<head>\` 包含与**页面主题一致**的预览元数据。系统据此生成 **精美的 3:4 信息流封面**(「我的空间」卡片 + 工作区 \`.thumbnail.svg\` + 保存弹窗预览):
|
||
|
||
\`\`\`html
|
||
<meta name="description" content="一句话摘要,显示在预览图副标题">
|
||
<meta name="mindspace-cover" content='{"tag":"运动","emoji":"🧘","accent":"#eeb04e","accent2":"#1a0a2e","subtitle":"618 限时 5 节课","cover":"assets/hero.jpg"}'>
|
||
\`\`\`
|
||
|
||
| 字段 | 要求 |
|
||
|------|------|
|
||
| \`tag\` | 与页面主题一致:旅行 / 美食 / 报告 / **运动** / **活动** 等;决定默认配色 |
|
||
| \`accent\` / \`accent2\` | 从页面主色提取,与 hero/背景一致 |
|
||
| \`subtitle\` | 一句话卖点;未写时用 description |
|
||
| \`cover\` / \`image\` | **必须**指向高质量主图(相对 HTML 或 https);见下文 |
|
||
| \`emoji\` | 可选;也可写在 title 中 |
|
||
|
||
### 精美预览图(必须达标)
|
||
|
||
保存 HTML 后,服务端会**立即**生成同名预览图 \`<文件名>.thumbnail.svg\`(Agent 交互阶段即生效,无需等用户保存到「我的空间」)。要产出**可在信息流中直接展示的精美封面**,必须:
|
||
|
||
1. **视觉类页面**(旅行、美食、活动、运动、品牌、产品、促销等)**必须**在 \`assets/\` 放置高质量 hero 主图(建议宽度 ≥1200px),并在 \`cover\` 字段引用
|
||
2. **纯文字报告**可仅用配色 + tag,但仍须保证 \`accent\` / \`subtitle\` 与页面风格一致
|
||
3. \`tag\`、\`accent\`、\`accent2\`、\`subtitle\` 必须与页面实际视觉一致;**禁止**省略 mindspace-cover 或填无关默认值
|
||
4. 若缺少 hero 主图,封面会退化为简陋默认图,**视为未达标**
|
||
|
||
**禁止**使用与页面无关的通用风景图逻辑;促销/运动/品牌页必须写明 \`tag\`、\`accent\` 和 \`cover\`。
|
||
|
||
## HTML 模板建议
|
||
|
||
- 完整 \`<!DOCTYPE html>\`,\`lang="zh-CN"\`
|
||
- 移动端友好:\`<meta name="viewport" content="width=device-width, initial-scale=1">\`
|
||
- 深色/浅色与内容一致;主色在 CSS 与 mindspace-cover 中保持一致
|
||
|
||
## 查找文件(CSV、文档等)
|
||
|
||
- 只在**当前工作区**内从 \`.\` 搜索(如 \`list_dir\`、\`list_dir oa\`,或 shell 可用时 \`find . -name '*.csv'\`)
|
||
- 用户说的「OA 区」等视为工作区**子目录**(如 \`oa/\`),用相对路径查找
|
||
- **禁止**搜索上级目录、其它用户目录、\`${PUBLISH_ROOT_DIR}\` 根目录、\`/Users\`、项目根目录
|
||
- 找不到时告知用户上传或提供相对路径,**不要**扩大到工作区外
|
||
|
||
## 禁止事项
|
||
|
||
- 不要写入当前工作区以外的任何目录
|
||
- shell 仅用于本目录内整理文件/简单脚本;不要 \`rm -rf\` 越界路径、不要安装系统级依赖
|
||
|
||
## 示例
|
||
|
||
用户:「写一份越南旅游指南网页」
|
||
|
||
正确做法:
|
||
- \`write_file\` → \`public/vietnam-guide.html\`(内容完整)
|
||
- 回复链接:\`${buildPublicZonePageUrl(publicBaseUrl, slug, 'vietnam-guide.html')}\`
|
||
`;
|
||
}
|
||
|
||
export function renderWorkspaceHints({ slug, username, publishDir, displayName }) {
|
||
const addressName = resolveUserAddressName({ displayName, username, slug });
|
||
return `# TKMind 用户工作区边界
|
||
|
||
你是用户 **${addressName}** 的专属 TKMind 助手。当前会话**唯一**文件根目录:
|
||
\`${publishDir}\`
|
||
|
||
${renderBrandingBlock(addressName)}
|
||
## 查找 / 读取文件(CSV、文档、OA 区等)
|
||
|
||
1. **只能**在本工作区内搜索,例如 \`list_dir\`、\`list_dir oa\`,或 shell 可用时 \`find . -name '*.csv'\`、\`rg keyword .\`
|
||
2. 用户说的「OA 区」「文档区」等,**先当作工作区内的子目录**(如 \`oa/\`、\`docs/\`),只用相对路径查找
|
||
3. **绝对禁止**:
|
||
- 去上级目录、\`${PUBLISH_ROOT_DIR}\` 根目录、其它用户名目录、项目根目录搜索
|
||
- 使用 \`/Users\`、\`/home\`、Desktop、Documents、Downloads 等主机路径
|
||
- 在回复中建议「在 MindSpace 目录或附近目录搜索」——你没有这个权限
|
||
4. 找不到文件时:说明在其工作区内未找到,请用户上传或提供相对路径;**不得**扩大到工作区外
|
||
|
||
## 公网链接 vs 本地文件
|
||
|
||
- 公网 URL(如 \`https://…/MindSpace/<用户>/\`)**仅供用户浏览器打开已发布的 HTML 页面**
|
||
- **禁止**用公网 URL、curl、wget 或 read_image 去「列目录」「读 CSV」——静态站点不提供目录索引
|
||
- 有 \`shell\` 时用 \`ls oa/\`、\`find .\`、\`cat oa/file.csv\`;有 \`tree\` 时用 \`tree oa/\`
|
||
- 不要说「我没有 shell」——若系统提示已列出 shell,就必须用 shell 在工作区内操作
|
||
`;
|
||
}
|
||
|
||
export function buildSandboxSessionConstraints({ baseConstraints, developerTools = [] }) {
|
||
const tools = developerTools.length > 0 ? developerTools : ['write_file', 'edit_file'];
|
||
const hasSandboxMcp = tools.includes('write_file') || tools.includes('read_file');
|
||
const hasShell = tools.includes('shell');
|
||
const hasListDir = tools.includes('list_dir') || tools.includes('tree');
|
||
|
||
const lines = [
|
||
baseConstraints,
|
||
'',
|
||
'## 当前会话可用文件工具',
|
||
`- ${tools.join(', ')}`,
|
||
hasSandboxMcp
|
||
? '- 文件工具由**沙箱 MCP** 提供:所有路径在 OS 层强制限制在工作区内,越界访问会立即报错'
|
||
: '',
|
||
].filter((l) => l !== '');
|
||
|
||
if (hasShell) {
|
||
lines.push(
|
||
'- **shell 已开启**:只能在本工作区内执行(如 `ls oa/`、`find . -name "*.csv"`、`cat oa/文件.csv`)',
|
||
'- **禁止** curl/wget 或访问公网 URL 来读取工作区文件',
|
||
);
|
||
} else {
|
||
lines.push(
|
||
'- **shell 未开启**:不能执行 shell;也不要用公网 URL 代替本地读文件',
|
||
);
|
||
}
|
||
if (hasListDir) {
|
||
lines.push('- **list_dir 可用**:`list_dir` 或 `list_dir oa` 查看目录结构');
|
||
} else {
|
||
lines.push('- 需要列目录或读 CSV 时,请用户上传文件内容或开启 list_dir 能力');
|
||
}
|
||
lines.push(
|
||
'',
|
||
'## 生成 / 发布 HTML 页面',
|
||
'- 你有 write_file/edit_file 工具:**必须由你**写入 `public/xxx.html`(或工作区根目录 `.html`)',
|
||
'- 开始前执行 load_skill → `static-page-publish`,按技能说明写入 mindspace-cover 元数据',
|
||
'- 用 `apps__create_app` 设计页面时,最后仍要按 `static-page-publish` skill 把内容 write_file 落到 `public/`,才有公网链接',
|
||
'- **禁止**让用户手动保存到 public 或说无法生成页面(除非 write_file 调用失败)',
|
||
'- 完成后回复 `[页面标题](公网URL)` 可点击链接;写入 `public/` 时 URL 必须含 `/public/` 路径段;按本会话给出的公网前缀模板拼接真实地址',
|
||
'- 下载附件:Word 用 `generate_docx` 写入 `public/*.docx`,相对链接文件必须已在 `public/` 落盘,basename 与 HTML 一致',
|
||
);
|
||
return lines.join('\n');
|
||
}
|
||
|
||
export function buildPublishConstraints({ slug, username, publicBaseUrl, publishDir, displayName }) {
|
||
const addressName = resolveUserAddressName({ displayName, username, slug });
|
||
return [
|
||
'## TKMind 用户空间沙箱(硬性约束)',
|
||
'',
|
||
'- 你是 **TKMind** 助手;与用户对话时用 **' + addressName + '** 称呼用户,禁止把用户叫作 TKMind',
|
||
'- 禁止称 goose / Goose / goosed 或「Rust goose 项目」',
|
||
`- 当前用户 **${addressName}** 的 Agent 工作区(唯一文件根目录):\`${publishDir}\``,
|
||
'- 用户上传落在分区子目录:`oa/`、`private/`、`public/`(均在上述工作区内)',
|
||
`- 公网 HTML 前缀(公开区页面):\`${buildPublicUrl(publicBaseUrl, slug, `${PUBLIC_ZONE_DIR}/`)}\``,
|
||
'- **查找文件**:在工作区内对应分区搜索(如 `oa/2025-12-06T13-34_export.csv`),不要搜其它用户目录或公网 URL',
|
||
'- **禁止**:访问 `assets/` 内部路径、其它用户目录、主机绝对路径;禁止用公网 URL 列目录或读 CSV',
|
||
'- **路径规则**:只用相对路径;禁止 `../`;工作区外的路径会被系统拒绝(OS 层强制,非软约束)',
|
||
'- **生成页面(必须亲自完成)**:先 `load_skill` → `static-page-publish`,再用 `write_file`/`edit_file` 写入 `public/页面.html`',
|
||
'- 若先用 `apps__create_app` 设计/预览,最后仍要把内容 write_file 落到 `public/页面.html` 才有公网链接',
|
||
'- **禁止**让用户「手动保存到 public 目录」或说「我无法生成页面」——除非 write_file 已失败并报告错误',
|
||
'- 完成后给出 Markdown 可点击公网链接 `[标题](URL)`;写入 `public/页面.html` 时 URL 为 `.../MindSpace/<用户ID>/public/页面.html`,按模板拼真实地址',
|
||
'- **需要查实时/真实世界信息时**:先 `load_skill` → `web`,优先 `web_search`/`fetch_url`;国内 google.com 不可达,多次无果时改走 Bing/360 等可达搜索源',
|
||
'- 下载链接(如 `report.docx`)必须与 HTML 同目录且文件名一致;Word 必须用 sandbox-fs `generate_docx` 生成,不要用 `computercontroller` / shell 作为交付依据',
|
||
`- 发布技能:\`${PUBLISH_SKILL_NAME}\`(生成页面前应 load_skill)`,
|
||
].join('\n');
|
||
}
|
||
|
||
export function ensureWorkspaceHintsInstalled(publishDir, context) {
|
||
const hintsPath = path.join(publishDir, WORKSPACE_HINTS_FILENAME);
|
||
const legacyPath = path.join(publishDir, LEGACY_WORKSPACE_HINTS_FILENAME);
|
||
const content = renderWorkspaceHints(context);
|
||
fs.writeFileSync(hintsPath, content, 'utf8');
|
||
if (fs.existsSync(legacyPath)) {
|
||
fs.unlinkSync(legacyPath);
|
||
}
|
||
return hintsPath;
|
||
}
|
||
|
||
export function ensurePublishSkillInstalled(publishDir, context) {
|
||
const skillRoot = path.join(publishDir, '.agents', 'skills', PUBLISH_SKILL_NAME);
|
||
fs.mkdirSync(skillRoot, { recursive: true });
|
||
const skillPath = path.join(skillRoot, 'SKILL.md');
|
||
const content = renderPublishSkill(context);
|
||
const existing = fs.existsSync(skillPath) ? fs.readFileSync(skillPath, 'utf8') : '';
|
||
if (existing !== content) {
|
||
fs.writeFileSync(skillPath, content, 'utf8');
|
||
}
|
||
return skillPath;
|
||
}
|
||
|
||
export function ensureUserPublishLayout({
|
||
h5Root,
|
||
publicBaseUrl,
|
||
user,
|
||
legacyUsersRoot = null,
|
||
installWorkspaceHints = false,
|
||
}) {
|
||
migrateUserPublishDir(h5Root, user, legacyUsersRoot);
|
||
const slug = resolvePublishKey(user);
|
||
const username = user.username ? resolveUsernameSlug(user) : slug;
|
||
const displayName = user.displayName ?? user.display_name ?? null;
|
||
const publishDir = resolvePublishDir(h5Root, user);
|
||
fs.mkdirSync(path.join(publishDir, 'assets'), { recursive: true });
|
||
|
||
const context = { slug, username, displayName, publicBaseUrl, publishDir };
|
||
|
||
if (installWorkspaceHints) {
|
||
ensureWorkspaceHintsInstalled(publishDir, context);
|
||
ensurePublishSkillInstalled(publishDir, context);
|
||
}
|
||
|
||
return {
|
||
slug,
|
||
username,
|
||
displayName,
|
||
publishDir,
|
||
publicBaseUrl,
|
||
publicUrl: buildPublicUrl(publicBaseUrl, slug),
|
||
skillName: PUBLISH_SKILL_NAME,
|
||
constraints: buildPublishConstraints(context),
|
||
};
|
||
}
|
||
|
||
export function isPathInsidePublishDir(publishDir, requestedPath) {
|
||
const resolvedPublish = path.resolve(publishDir);
|
||
const resolved = path.resolve(requestedPath);
|
||
return resolved === resolvedPublish || resolved.startsWith(`${resolvedPublish}${path.sep}`);
|
||
}
|