ui(sandbox): redesign personal sandbox secrets settings

Lead with skill credentials, hide empty inputs until edit, and rename the page to 沙箱密钥 so it is not mistaken for system env vars.
This commit is contained in:
wizardchen
2026-08-27 16:33:49 +08:00
committed by lyingbug
parent 4ce08b16ee
commit 83d661af55
26 changed files with 876 additions and 389 deletions
+5 -3
View File
@@ -32,9 +32,10 @@ type EnvVarView struct {
// SkillEnvGroup is one skill's declared credentials.
type SkillEnvGroup struct {
SkillID string `json:"skill_id"`
SkillName string `json:"skill_name"`
Vars []EnvVarView `json:"vars"`
SkillID string `json:"skill_id"`
SkillName string `json:"skill_name"`
Description string `json:"description,omitempty"`
Vars []EnvVarView `json:"vars"`
}
// ConfigEnvGroup is one sandbox config: the caller's own config-wide variables
@@ -42,6 +43,7 @@ type SkillEnvGroup struct {
type ConfigEnvGroup struct {
SandboxConfigID string `json:"sandbox_config_id"`
SandboxConfigName string `json:"sandbox_config_name"`
Description string `json:"description,omitempty"`
Vars []EnvVarView `json:"vars"`
Skills []SkillEnvGroup `json:"skills"`
}
+2 -2
View File
@@ -242,10 +242,10 @@ builtin_agents:
description: "Internal agent that installs an uploaded skill and its dependencies into the sandbox image"
zh-CN:
name: "技能安装器"
description: "把上传的技能及其依赖装进沙镜像的内部智能体"
description: "把上传的技能及其依赖装进沙镜像的内部智能体"
zh-TW:
name: "技能安裝器"
description: "把上傳的技能及其相依套件裝進沙鏡像的內部智能體"
description: "把上傳的技能及其相依套件裝進沙鏡像的內部智能體"
ja-JP:
name: "スキルインストーラー"
description: "アップロードされたスキルとその依存関係をサンドボックスイメージにインストールする内部エージェント"
@@ -494,7 +494,7 @@ templates:
i18n:
zh-CN:
name: "技能安装系统提示词"
description: "用于把上传的技能及其依赖装进沙镜像的系统提示词"
description: "用于把上传的技能及其依赖装进沙镜像的系统提示词"
en-US:
name: "Skill Installer System Prompt"
description: "System prompt for installing an uploaded skill and its dependencies into the sandbox image"
+2
View File
@@ -224,6 +224,7 @@ curl --location 'http://localhost:8080/api/v1/me/env-vars' \
{
"sandbox_config_id": "cfg-1",
"sandbox_config_name": "默认沙箱",
"description": "日常对话用的沙箱",
"vars": [
{ "name": "HTTP_PROXY", "source": "user", "updated_at": "2026-08-27T10:00:00Z" }
],
@@ -231,6 +232,7 @@ curl --location 'http://localhost:8080/api/v1/me/env-vars' \
{
"skill_id": "sk-1",
"skill_name": "web-search",
"description": "通过 Tavily 检索网页",
"vars": [
{ "name": "TAVILY_API_KEY", "description": "Tavily 搜索密钥", "required": true, "source": "workspace" },
{ "name": "REGION", "source": "unset" }
+2 -2
View File
@@ -13333,7 +13333,7 @@ const docTemplate = `{
"ApiKeyAuth": []
}
],
"description": "返回指定沙配置镜像内、智能体实际能调用的已安装技能(ready 且启用)。不传 sandbox_config_id 时列表为空。",
"description": "返回指定沙配置镜像内、智能体实际能调用的已安装技能(ready 且启用)。不传 sandbox_config_id 时列表为空。",
"consumes": [
"application/json"
],
@@ -13343,7 +13343,7 @@ const docTemplate = `{
"tags": [
"Skills"
],
"summary": "获取当前沙配置上可执行的 Skills",
"summary": "获取当前沙配置上可执行的 Skills",
"parameters": [
{
"type": "string",
+2 -2
View File
@@ -13326,7 +13326,7 @@
"ApiKeyAuth": []
}
],
"description": "返回指定沙配置镜像内、智能体实际能调用的已安装技能(ready 且启用)。不传 sandbox_config_id 时列表为空。",
"description": "返回指定沙配置镜像内、智能体实际能调用的已安装技能(ready 且启用)。不传 sandbox_config_id 时列表为空。",
"consumes": [
"application/json"
],
@@ -13336,7 +13336,7 @@
"tags": [
"Skills"
],
"summary": "获取当前沙配置上可执行的 Skills",
"summary": "获取当前沙配置上可执行的 Skills",
"parameters": [
{
"type": "string",
+2 -2
View File
@@ -14813,7 +14813,7 @@ paths:
get:
consumes:
- application/json
description: 返回指定沙配置镜像内、智能体实际能调用的已安装技能(ready 且启用)。不传 sandbox_config_id 时列表为空。
description: 返回指定沙配置镜像内、智能体实际能调用的已安装技能(ready 且启用)。不传 sandbox_config_id 时列表为空。
parameters:
- description: Sandbox config ID
in: query
@@ -14830,7 +14830,7 @@ paths:
security:
- Bearer: []
- ApiKeyAuth: []
summary: 获取当前沙配置上可执行的 Skills
summary: 获取当前沙配置上可执行的 Skills
tags:
- Skills
/storage-backends:
+2 -2
View File
@@ -65,8 +65,8 @@ export interface CustomAgentConfig {
skills_selection_mode?: 'all' | 'selected' | 'none';
selected_skills?: string[]; // 选择的Skill名称列表
// ===== 沙设置 =====
// 该智能体的技能脚本运行在哪个沙配置上;为空表示不启用沙执行。
// ===== 沙设置 =====
// 该智能体的技能脚本运行在哪个沙配置上;为空表示不启用沙执行。
// 指向逻辑配置而非某个具体版本,凭据轮换时无需重新指派每个智能体。
sandbox_config_id?: string;
+3
View File
@@ -26,6 +26,8 @@ export interface EnvVarView {
export interface SkillEnvGroup {
skill_id: string
skill_name: string
/** SKILL.md one-liner; not the instruction body. */
description?: string
vars: EnvVarView[]
}
@@ -36,6 +38,7 @@ export interface SkillEnvGroup {
export interface ConfigEnvGroup {
sandbox_config_id: string
sandbox_config_name: string
description?: string
vars: EnvVarView[]
skills: SkillEnvGroup[]
}
+1 -1
View File
@@ -6,7 +6,7 @@ export interface SkillInfo {
description: string;
}
// 获取当前沙配置上可执行的 Skills;未传 sandboxConfigId 或
// 获取当前沙配置上可执行的 Skills;未传 sandboxConfigId 或
// skills_available 为 false 时,前端应隐藏/禁用 Skills 配置
export function listSkills(sandboxConfigId?: string) {
return get<{ data: SkillInfo[]; skills_available?: boolean }>('/api/v1/skills', {
+24 -12
View File
@@ -1366,7 +1366,7 @@ export default {
toggleHide: 'Hide environment variables',
none: 'This skill declared no environment variables.',
workspaceTitle: 'Workspace-wide values',
workspaceHint: 'Every member who has not set their own value uses the value here. Members set their own under Settings, Environment variables.',
workspaceHint: 'Every member who has not set their own value uses the value here. Members set their own under Settings → Sandbox secrets.',
required: 'Required',
isSet: 'Set',
notSet: 'Not set',
@@ -1834,24 +1834,36 @@ export default {
}
},
envVarSettings: {
title: 'Environment variables',
description: 'Values you enter here belong to you alone. A sandbox variable is passed to every skill script and shell command you run on that sandbox; a skill credential is passed only when that skill runs. Nothing is displayed again once saved.',
title: 'Sandbox secrets',
description: 'Personal keys for skills and sandboxes, not WeKnora system or deployment settings.',
helpAria: 'About sandbox secrets',
introPersonalTitle: 'Yours only',
introPersonalBody: 'Injected into your own chats and runs. Other members cannot see them, and theirs will not replace yours.',
introRuntimeTitle: 'Passed in when something runs',
introRuntimeBody: 'Injected when a skill runs, or when a command runs in that sandbox. You can also supply them in the conversation. Saved values are never shown again.',
loading: 'Loading…',
loadFailed: 'Could not load the environment variables.',
loadFailed: 'Could not load sandbox secrets.',
retry: 'Try again',
noConfigTitle: 'No sandbox configured',
noConfigDescription: 'This workspace has no sandbox backend yet, so there is nothing to set variables for.',
sandboxTitle: 'Sandbox variables',
sandboxHint: 'Passed to everything you run on this sandbox, whether or not a skill is involved.',
sandboxEmpty: 'You have no variables on this sandbox.',
skillTitle: 'Skill credentials',
skillHint: 'What each skill declared it needs. Passed only when that skill runs.',
noConfigDescription: 'This workspace has no sandbox backend yet, so there is nothing to set keys for.',
sandboxTitle: 'Always passed in a chosen sandbox',
sandboxHint: 'Passed only to commands you run in that sandbox. Most people never need this; you can also supply values in the conversation.',
sandboxEmpty: 'Nothing added yet.',
sandboxPick: 'Which sandbox',
skillTitle: 'Keys a skill needs',
skillHint: 'Each skill declares what it needs. Save them here ahead of time, or supply them in the conversation. Workspace admins can also fill in shared values on the skill card.',
skillEmptyTitle: 'No skill has asked for keys yet',
skillEmptyDesc: 'After you install a skill that declares credentials, the fields to fill in will show up here.',
skillOnSandbox: 'Sandbox: {name}',
skillNeedsCount: '{count} not saved here',
skillReady: 'Saved on this page',
requiredTag: 'Required',
statusUnset: 'Not set',
statusWorkspace: 'Using the workspace value',
statusUser: 'Set by you',
blocking: 'Required but not set, so this skill cannot run.',
addRow: 'Add a variable',
setValue: 'Set',
replaceValue: 'Replace',
addRow: 'Add',
namePlaceholder: 'VARIABLE_NAME',
nameRule: 'Upper-case letters, digits and underscores, starting with a letter or an underscore.',
nameInvalid: 'That name cannot be used. Names the sandbox reserves, such as PATH or anything starting with WEKNORA_, are not accepted.',
+24 -12
View File
@@ -4782,24 +4782,36 @@ export default {
}
},
envVarSettings: {
title: '환경 변수',
description: '여기에 입력한 값은 나에게만 속합니다. 샌드박스 변수는 해당 샌드박스에서 실행하는 모든 스킬 스크립트와 명령에 전달되고, 스킬 자격 증명은 그 스킬이 실행될 때만 전달됩니다. 저장한 뒤에는 다시 표시되지 않습니다.',
title: '샌드박스 키',
description: '스킬과 샌드박스에 쓰는 개인 키이며, WeKnora 시스템이나 배포 설정이 아닙니다.',
helpAria: '샌드박스 키 설명',
introPersonalTitle: '나만의 값',
introPersonalBody: '내 대화와 실행에만 주입됩니다. 다른 구성원은 볼 수 없고, 그들의 값이 내 값을 대체하지도 않습니다.',
introRuntimeTitle: '실행할 때만 전달',
introRuntimeBody: '스킬이 실행되거나 해당 샌드박스에서 명령을 실행할 때 주입됩니다. 대화에서 바로 제공할 수도 있습니다. 저장한 뒤에는 다시 표시되지 않습니다.',
loading: '불러오는 중…',
loadFailed: '환경 변수를 불러오지 못했습니다.',
loadFailed: '샌드박스 키를 불러오지 못했습니다.',
retry: '다시 시도',
noConfigTitle: '샌드박스가 없습니다',
noConfigDescription: '이 워크스페이스에는 아직 샌드박스 백엔드가 없어 설정할 변수가 없습니다.',
sandboxTitle: '샌드박스 변수',
sandboxHint: '스킬과 무관하게 이 샌드박스에서 실행하는 모든 것에 전달됩니다.',
sandboxEmpty: '이 샌드박스에 등록한 변수가 없습니다.',
skillTitle: '스킬 자격 증명',
skillHint: '스킬이 필요하다고 선언한 변수입니다. 그 스킬이 실행될 때만 전달됩니다.',
noConfigDescription: '이 워크스페이스에는 아직 샌드박스 백엔드가 없어 설정할 가 없습니다.',
sandboxTitle: '선택한 샌드박스에서 항상 전달되는 값',
sandboxHint: ' 샌드박스에서 실행하는 명령에만 전달됩니다. 대부분은 필요 없고, 대화에서 바로 제공할 수도 있습니다.',
sandboxEmpty: '아직 추가한 값이 없습니다.',
sandboxPick: '어느 샌드박스에 쓸지',
skillTitle: '스킬이 필요한 키',
skillHint: '각 스킬이 필요하다고 선언한 값입니다. 여기에 미리 저장하거나 대화에서 바로 제공할 수 있습니다. 워크스페이스 관리자는 스킬 카드에서 공용 값을 넣을 수도 있습니다.',
skillEmptyTitle: '아직 키가 필요한 스킬이 없습니다',
skillEmptyDesc: '자격 증명을 선언하는 스킬을 설치하면 여기에 입력란이 나타납니다.',
skillOnSandbox: '샌드박스: {name}',
skillNeedsCount: '여기에 저장되지 않음 {count}개',
skillReady: '이 페이지에 저장됨',
requiredTag: '필수',
statusUnset: '설정되지 않음',
statusWorkspace: '워크스페이스 공용 값 사용 중',
statusUser: '내가 설정함',
blocking: '필수인데 설정되지 않아 이 스킬을 실행할 수 없습니다.',
addRow: '변수 추가',
setValue: '설정',
replaceValue: '변',
addRow: '추가',
namePlaceholder: 'VARIABLE_NAME',
nameRule: '대문자, 숫자, 밑줄만 사용하며 문자 또는 밑줄로 시작해야 합니다.',
nameInvalid: '이 이름은 사용할 수 없습니다. PATH처럼 샌드박스가 예약한 이름이나 WEKNORA_로 시작하는 이름은 허용되지 않습니다.',
@@ -5261,7 +5273,7 @@ export default {
toggleHide: '환경 변수 접기',
none: '이 스킬은 환경 변수를 선언하지 않았습니다.',
workspaceTitle: '워크스페이스 공용 값',
workspaceHint: '자기 값을 설정하지 않은 모든 구성원이 여기의 값을 사용합니다. 구성원은 설정의 환경 변수에서 자기 값을 설정할 수 있습니다.',
workspaceHint: '자기 값을 설정하지 않은 모든 구성원이 여기의 값을 사용합니다. 구성원은 설정의 「샌드박스 키」에서 자기 값을 설정할 수 있습니다.',
required: '필수',
isSet: '설정됨',
notSet: '설정되지 않음',
+24 -12
View File
@@ -4782,24 +4782,36 @@ export default {
}
},
envVarSettings: {
title: 'Переменные окружения',
description: 'Введённые здесь значения принадлежат только вам. Переменная песочницы передаётся во все скрипты навыков и команды, которые вы запускаете в этой песочнице; учётные данные навыка — только при запуске этого навыка. После сохранения значение больше не отображается.',
title: 'Ключи песочницы',
description: 'Личные ключи для навыков и песочниц, а не системные или деплой-настройки WeKnora.',
helpAria: 'Справка по ключам песочницы',
introPersonalTitle: 'Только ваши',
introPersonalBody: 'Подставляются только в ваши диалоги и запуски. Другие участники их не видят, и их значения не заменяют ваши.',
introRuntimeTitle: 'Передаются в момент запуска',
introRuntimeBody: 'Подставляются, когда навык выполняется или когда команда идёт в этой песочнице. Их также можно передать прямо в разговоре. Сохранённые значения больше не показываются.',
loading: 'Загрузка…',
loadFailed: 'Не удалось загрузить переменные окружения.',
loadFailed: 'Не удалось загрузить ключи песочницы.',
retry: 'Повторить',
noConfigTitle: 'Песочница не настроена',
noConfigDescription: 'В этом рабочем пространстве пока нет песочницы, поэтому задавать переменные не для чего.',
sandboxTitle: 'Переменные песочницы',
sandboxHint: 'Передаются во всё, что вы запускаете в этой песочнице, независимо от навыков.',
sandboxEmpty: 'У вас нет переменных в этой песочнице.',
skillTitle: 'Учётные данные навыков',
skillHint: 'То, что каждый навык объявил необходимым. Передаётся только при запуске этого навыка.',
noConfigDescription: 'В этом рабочем пространстве пока нет песочницы, поэтому задавать ключи не для чего.',
sandboxTitle: 'Всегда передаются в выбранной песочнице',
sandboxHint: 'Передаются только командам в этой песочнице. Обычно не нужны; значения можно также дать в разговоре.',
sandboxEmpty: 'Пока ничего не добавлено.',
sandboxPick: 'В какой песочнице',
skillTitle: 'Ключи, которые нужны навыку',
skillHint: 'Каждый навык объявляет, что ему нужно. Можно сохранить заранее здесь или передать в разговоре. Администратор рабочего пространства может также задать общие значения на карточке навыка.',
skillEmptyTitle: 'Пока ни один навык не запросил ключи',
skillEmptyDesc: 'После установки навыка, который объявляет учётные данные, поля для заполнения появятся здесь.',
skillOnSandbox: 'Песочница: {name}',
skillNeedsCount: 'Не сохранено здесь: {count}',
skillReady: 'Сохранено на этой странице',
requiredTag: 'Обязательно',
statusUnset: 'Не задано',
statusWorkspace: 'Используется значение рабочего пространства',
statusUser: 'Задано вами',
blocking: 'Обязательная переменная не задана, поэтому навык не может работать.',
addRow: 'Добавить переменную',
setValue: 'Задать',
replaceValue: 'Заменить',
addRow: 'Добавить',
namePlaceholder: 'VARIABLE_NAME',
nameRule: 'Только заглавные латинские буквы, цифры и подчёркивания; первым символом должна быть буква или подчёркивание.',
nameInvalid: 'Это имя использовать нельзя. Имена, зарезервированные песочницей, например PATH или начинающиеся с WEKNORA_, не принимаются.',
@@ -5261,7 +5273,7 @@ export default {
toggleHide: 'Скрыть переменные окружения',
none: 'Этот навык не объявил переменных окружения.',
workspaceTitle: 'Значения для всего рабочего пространства',
workspaceHint: 'Каждый участник, не задавший своё значение, использует значение отсюда. Своё значение участник задаёт в настройках, раздел «Переменные окружения».',
workspaceHint: 'Каждый участник, не задавший своё значение, использует значение отсюда. Своё значение участник задаёт в настройках, раздел «Ключи песочницы».',
required: 'Обязательно',
isSet: 'Задано',
notSet: 'Не задано',
+26 -14
View File
@@ -4784,24 +4784,36 @@ export default {
}
},
envVarSettings: {
title: '环境变量',
description: '这里填的值只属于你自己。沙盒变量会带给你在该沙盒上运行的所有技能脚本和命令;技能凭据只在该技能运行时带上。保存之后都不会再显示出来。',
title: '沙箱密钥',
description: '给技能和沙箱用的个人密钥,不是 WeKnora 的系统或部署配置。',
helpAria: '沙箱密钥说明',
introPersonalTitle: '只属于你',
introPersonalBody: '只注入到你自己的对话和执行里,空间里的其他人看不到,也不会改成他们的值。',
introRuntimeTitle: '用的时候才带上',
introRuntimeBody: '技能运行或在沙箱里执行命令时才会注入;对话里也可以当场提供。保存后不再显示明文。',
loading: '加载中…',
loadFailed: '环境变量加载失败。',
loadFailed: '沙箱密钥加载失败。',
retry: '重试',
noConfigTitle: '还没有沙',
noConfigDescription: '这个空间还没有配置沙后端,暂时没有可设置的变量。',
sandboxTitle: '沙盒变量',
sandboxHint: '你在个沙盒上运行的任何东西都会带上,不限于技能。',
sandboxEmpty: '你在这个沙盒上还没有变量。',
skillTitle: '技能凭据',
skillHint: '技能声明自己需要的变量,只在该技能运行时带上。',
noConfigTitle: '还没有沙',
noConfigDescription: '这个空间还没有配置沙后端,暂时没有可设置的密钥。',
sandboxTitle: '某个沙箱里始终带上的值',
sandboxHint: '只带给你在个沙箱里跑的命令。多数情况用不到;需要时再加,对话里也可以当场提供。',
sandboxEmpty: '还没有添加过。',
sandboxPick: '用在哪个沙箱',
skillTitle: '技能要用的密钥',
skillHint: '技能声明需要的值。可以预先保存在这里,也可以在对话里当场提供。空间管理员还可以在技能卡上填空间共用值。',
skillEmptyTitle: '还没有技能需要密钥',
skillEmptyDesc: '安装会声明凭据的技能后,要填的项会出现在这里。',
skillOnSandbox: '用于沙箱「{name}」',
skillNeedsCount: '{count} 项未在此保存',
skillReady: '此页已保存',
requiredTag: '必填',
statusUnset: '未设置',
statusWorkspace: '正在使用空间共用值',
statusUser: '你已设置',
blocking: '必填但未设置,该技能无法运行。',
addRow: '添加变量',
setValue: '设置',
replaceValue: '更换',
addRow: '添加',
namePlaceholder: 'VARIABLE_NAME',
nameRule: '只能用大写字母、数字和下划线,且以字母或下划线开头。',
nameInvalid: '这个名字不能用。沙箱保留的名字(例如 PATH,或以 WEKNORA_ 开头的名字)不接受。',
@@ -4815,7 +4827,7 @@ export default {
saveSuccess: '已保存。值已存好,之后不会再显示。',
saveFailed: '保存失败。',
delete: '删除',
deleteConfirm: '删除 {name}?之后在这个沙上运行的东西都不会再带上它。',
deleteConfirm: '删除 {name}?之后在这个沙上运行的东西都不会再带上它。',
deleteSuccess: '变量已删除。',
clear: '清除我的值',
clearConfirm: '清除你为 {name} 设置的值?之后会重新使用空间共用值(如果有)。',
@@ -5263,7 +5275,7 @@ export default {
toggleHide: '收起环境变量',
none: '这个技能没有声明环境变量。',
workspaceTitle: '空间共用值',
workspaceHint: '所有没有填写自己值的成员都会用这里的值。成员可以在「设置 - 环境变量」里填自己的值。',
workspaceHint: '所有没有填写自己值的成员都会用这里的值。成员可以在「设置 → 沙箱密钥」里填自己的值。',
required: '必填',
isSet: '已设置',
notSet: '未设置',
+4 -4
View File
@@ -1,7 +1,7 @@
/**
* 把回答正文里对「沙生成文件」的引用,接到 artifact 下载链路上。
* 把回答正文里对「沙生成文件」的引用,接到 artifact 下载链路上。
*
* 模型会用 Markdown 图片语法引用它在沙里生成的文件(提示词规定写成
* 模型会用 Markdown 图片语法引用它在沙里生成的文件(提示词规定写成
* `![说明](sandbox:文件名)`),服务端在落库前把它改写成该文件的稳定句柄
* `resource://<handle>` —— 与知识库图片、聊天附件同一种引用形式。两种写法
* 都指向同一份 `Message.Artifacts`
@@ -77,7 +77,7 @@ function parseArtifactRef(href: string): ArtifactRef | null {
}
/**
* 该链接目标是否可能是沙产物引用。
* 该链接目标是否可能是沙产物引用。
*
* 句柄形式与知识库图片同形,因此这里为真只说明「值得交给产物解析试一次」,
* 不代表本消息真有这个文件。
@@ -286,7 +286,7 @@ function renderImage(
/**
* 渲染一个 Markdown 图片/链接目标。
*
* 返回 null 表示这不是沙产物引用,调用方应回落到默认渲染(普通图片、
* 返回 null 表示这不是沙产物引用,调用方应回落到默认渲染(普通图片、
* `resource://` 受保护图片、外链等一律不受影响)。
*/
export function renderArtifactReference(args: {
@@ -1940,7 +1940,7 @@ const showMcpServiceSelect = computed(() =>
);
const webSearchProviderList = ref<WebSearchProviderEntity[]>([]);
const skillOptions = ref<{ name: string; description: string }[]>([]);
// 是否允许启用 Skills(当前沙配置上有可执行技能时为 true;未选配置前为 false)
// 是否允许启用 Skills(当前沙配置上有可执行技能时为 true;未选配置前为 false)
const skillsAvailable = ref(false);
const hasSandboxSelected = computed(() => !!formData.value.config.sandbox_config_id);
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -133,7 +133,7 @@
<MemorySettings />
</div>
<!-- 技能凭据成员自己的技能环境变量 -->
<!-- 沙箱密钥成员自己的技能 / 沙箱密钥 -->
<div v-if="currentSection === 'envvars'" class="section">
<EnvVarSettings />
</div>
@@ -379,7 +379,7 @@ const navItems = computed(() => {
{ key: 'system-audit-log', icon: 'history', label: t('system.globalSettings.audit.tabLabel') },
{ key: 'userprofile', icon: 'user', label: t('userProfile.title') },
{ key: 'mymemory', icon: 'bookmark', label: t('memorySettings.title') },
{ key: 'envvars', icon: 'lock-on', label: t('envVarSettings.title') },
{ key: 'envvars', icon: 'key', label: t('envVarSettings.title') },
{ key: 'tenant', icon: 'user-circle', label: t('settings.tenantInfo') },
{ key: 'members', icon: 'usergroup', label: t('tenantMember.title') },
...integrationItems,
@@ -8,6 +8,7 @@ import {
RESERVED_ENV_NAMES,
addSkillEnvSaveInFlight,
adminSkillEnvClearPayload,
blockingVarCount,
canAddEnvVar,
canClearAdminSkillEnv,
clearSkillEnvSaveInFlight,
@@ -18,6 +19,8 @@ import {
isValidEnvName,
isValidEnvValueLength,
skillHasDeclaredEnvs,
skillSecretCards,
sandboxGroupsWithVars,
sortedConfigGroups,
statusOf,
} from './envVarState'
@@ -239,3 +242,40 @@ test('skill env save completion removes exactly its own in-flight entry', () =>
assert.equal(isSkillEnvSaveInFlight(remaining, 'cfg-a', 'skill-s'), false)
assert.equal(isSkillEnvSaveInFlight(remaining, 'cfg-b', 'skill-t'), true)
})
test('sandboxGroupsWithVars keeps only configs that already have stored values', () => {
const populated = sandboxGroupsWithVars([
config('c1', 'cube-TEST'),
config('c2', 'Docker-Test', [envVar({ name: 'PROXY', source: 'user' })]),
config('c3', 'Empty', []),
])
assert.equal(populated.length, 1)
assert.equal(populated[0].sandbox_config_id, 'c2')
})
test('blockingVarCount counts only required names with no value at all', () => {
assert.equal(blockingVarCount(undefined), 0)
assert.equal(
blockingVarCount([
envVar({ name: 'A', required: true, source: 'unset' }),
envVar({ name: 'B', required: true, source: 'user' }),
envVar({ name: 'C', required: false, source: 'unset' }),
]),
1,
)
})
test('skillSecretCards lifts declared skills out of config groups and skips empty ones', () => {
const cards = skillSecretCards([
config('c2', 'Docker-Test', [], [
skill('weknora', 'weknora', [envVar({ name: 'WEKNORA_API_KEY', required: true })]),
]),
config('c1', 'cube-TEST'),
config('c3', 'Other', [], [skill('empty', 'empty', [])]),
])
assert.equal(cards.length, 1)
assert.equal(cards[0].skill.skill_id, 'weknora')
assert.equal(cards[0].sandbox_config_name, 'Docker-Test')
})
+45 -1
View File
@@ -1,4 +1,4 @@
import type { ConfigEnvGroup, EnvVarSource, EnvVarView } from '@/api/env-vars'
import type { ConfigEnvGroup, EnvVarSource, EnvVarView, SkillEnvGroup } from '@/api/env-vars'
/**
* Pure state helpers for the environment variable UIs.
@@ -165,3 +165,47 @@ export function sortedConfigGroups(groups: ConfigEnvGroup[]): ConfigEnvGroup[] {
return a.sandbox_config_id.localeCompare(b.sandbox_config_id)
})
}
/**
* Sandbox-wide extras the member already stored. Empty configs stay off the
* settings page — listing every backend as a blank card is noise.
*/
export function sandboxGroupsWithVars(groups: ConfigEnvGroup[]): ConfigEnvGroup[] {
return groups.filter((group) => (group.vars?.length ?? 0) > 0)
}
/** How many required declarations still have no user or workspace value. */
export function blockingVarCount(vars: EnvVarView[] | undefined): number {
return (vars || []).filter((entry) => statusOf(entry).blocking).length
}
/**
* One skill's credentials, lifted out of the config group it was installed on.
* The settings page leads with these; sandbox-wide extras stay secondary.
*/
export interface SkillSecretCard {
sandbox_config_id: string
sandbox_config_name: string
skill: SkillEnvGroup
}
export function skillSecretCards(groups: ConfigEnvGroup[]): SkillSecretCard[] {
const cards: SkillSecretCard[] = []
for (const group of groups) {
for (const skill of group.skills || []) {
if (!skill.vars?.length) continue
cards.push({
sandbox_config_id: group.sandbox_config_id,
sandbox_config_name: configLabel(group),
skill,
})
}
}
return cards.sort((a, b) => {
const bySkill = (a.skill.skill_name || a.skill.skill_id).localeCompare(
b.skill.skill_name || b.skill.skill_id,
)
if (bySkill !== 0) return bySkill
return a.sandbox_config_name.localeCompare(b.sandbox_config_name)
})
}
+1 -1
View File
@@ -42,7 +42,7 @@ func (e *MissingSkillEnvError) Error() string {
"skill %q needs the environment variable(s) %s, which nobody has set yet. "+
"Ask the user for them, then run the skill through shell_exec with "+
"skill_name=%q and the values in env — they are stored for that user "+
"afterwards. They can also be set under Settings → Environment variables.",
"afterwards. They can also be set under Settings → Sandbox secrets.",
e.SkillName, strings.Join(e.Names, ", "), e.SkillName,
)
}
+1 -1
View File
@@ -48,7 +48,7 @@ func TestMissingSkillEnvErrorMessageNamesSkillVarsAndWhereToSetThem(t *testing.T
require.Contains(t, msg, "web-search")
require.Contains(t, msg, "TAVILY_API_KEY")
require.Contains(t, msg, "SERP_TOKEN")
require.Contains(t, msg, "Environment variables")
require.Contains(t, msg, "Sandbox secrets")
var typed *MissingSkillEnvError
require.True(t, errors.As(err, &typed))
+1 -1
View File
@@ -432,7 +432,7 @@ func (t *ShellExecTool) Execute(ctx context.Context, args json.RawMessage) (*typ
Error: fmt.Sprintf(
"skill %q needs the environment variable(s) %s, which nobody has set yet. "+
"Ask the user for them and pass them in this call's env, "+
"or have them set the values under Settings → Environment variables.",
"or have them set the values under Settings → Sandbox secrets.",
input.SkillName, strings.Join(missing, ", ")),
}, nil
}
+13 -8
View File
@@ -36,12 +36,14 @@ type EnvVarView struct {
}
// SkillEnvGroup is one skill's declared credentials. It carries the skill's
// identity and nothing else about it — not the instructions, not the bundle,
// not the install state, all of which are Admin+ disclosure.
// identity (name and the SKILL.md one-liner) and nothing else about it — not
// the instructions, not the bundle, not the install state, all of which are
// Admin+ disclosure.
type SkillEnvGroup struct {
SkillID string `json:"skill_id"`
SkillName string `json:"skill_name"`
Vars []EnvVarView `json:"vars"`
SkillID string `json:"skill_id"`
SkillName string `json:"skill_name"`
Description string `json:"description,omitempty"`
Vars []EnvVarView `json:"vars"`
}
// ConfigEnvGroup is one sandbox config: the caller's own config-wide variables
@@ -49,6 +51,7 @@ type SkillEnvGroup struct {
type ConfigEnvGroup struct {
SandboxConfigID string `json:"sandbox_config_id"`
SandboxConfigName string `json:"sandbox_config_name"`
Description string `json:"description,omitempty"`
Vars []EnvVarView `json:"vars"`
Skills []SkillEnvGroup `json:"skills"`
}
@@ -145,14 +148,16 @@ func (s *UserEnvService) ListMine(ctx context.Context) ([]ConfigEnvGroup, error)
group := ConfigEnvGroup{
SandboxConfigID: cfg.ID,
SandboxConfigName: cfg.Name,
Description: cfg.Description,
Vars: configWideViews(mineBySkill[""]),
Skills: make([]SkillEnvGroup, 0, len(skillsByConfig[cfg.ID])),
}
for _, row := range skillsByConfig[cfg.ID] {
group.Skills = append(group.Skills, SkillEnvGroup{
SkillID: row.ID,
SkillName: row.Name,
Vars: declaredViews(row.Envs, mineBySkill[row.ID]),
SkillID: row.ID,
SkillName: row.Name,
Description: row.Description,
Vars: declaredViews(row.Envs, mineBySkill[row.ID]),
})
}
// The repository orders skills by creation; sorting by name is what
@@ -86,7 +86,8 @@ func newUserEnvFixture(t *testing.T) (*UserEnvService, *installSkillRepo) {
for _, skill := range []*types.TenantSkillEntity{
{
ID: "sk-ready", TenantID: userEnvTenantID, SandboxConfigID: "cfg-1",
Name: "pdf-tools", Enabled: true, Status: types.SkillStatusReady,
Name: "pdf-tools", Description: "Extracts text from PDFs",
Enabled: true, Status: types.SkillStatusReady,
Envs: types.SkillEnvVars{
{Name: "API_TOKEN", Description: "workspace token", Required: true, Value: "admin-secret"},
{Name: "USER_TOKEN", Description: "your own token", Required: true},
@@ -116,7 +117,7 @@ func newUserEnvFixture(t *testing.T) (*UserEnvService, *installSkillRepo) {
require.NoError(t, repo.CreateSkill(ctx, skill))
}
configs := &userEnvConfigRepo{rows: []*types.TenantSandboxConfigEntity{
{ID: "cfg-1", TenantID: userEnvTenantID, Name: "Production"},
{ID: "cfg-1", TenantID: userEnvTenantID, Name: "Production", Description: "Prod cluster"},
{ID: "cfg-2", TenantID: userEnvTenantID, Name: "Staging"},
{ID: "cfg-9", TenantID: 8, Name: "Theirs"},
}}
@@ -165,6 +166,7 @@ func TestListMineReportsTheThreeSourceStates(t *testing.T) {
require.NoError(t, err)
skill := skillByID(t, configByID(t, groups, "cfg-1"), "sk-ready")
require.Equal(t, "pdf-tools", skill.SkillName)
require.Equal(t, "Extracts text from PDFs", skill.Description)
workspace := viewByName(t, skill.Vars, "API_TOKEN")
require.Equal(t, EnvSourceWorkspace, workspace.Source)
@@ -188,7 +190,9 @@ func TestListMineListsEveryConfigWithItsName(t *testing.T) {
require.NoError(t, err)
require.Len(t, groups, 2)
require.Equal(t, "Production", groups[0].SandboxConfigName)
require.Equal(t, "Prod cluster", groups[0].Description)
require.Equal(t, "Staging", groups[1].SandboxConfigName)
require.Empty(t, groups[1].Description)
require.Empty(t, configByID(t, groups, "cfg-2").Skills)
}
+2 -2
View File
@@ -34,8 +34,8 @@ type SkillInfoResponse struct {
}
// ListSkills godoc
// @Summary 获取当前沙配置上可执行的 Skills
// @Description 返回指定沙配置镜像内、智能体实际能调用的已安装技能(ready 且启用)。不传 sandbox_config_id 时列表为空。
// @Summary 获取当前沙配置上可执行的 Skills
// @Description 返回指定沙配置镜像内、智能体实际能调用的已安装技能(ready 且启用)。不传 sandbox_config_id 时列表为空。
// @Tags Skills
// @Accept json
// @Produce json