fix: mask stored credentials and validate agent saves

This commit is contained in:
Ed1s0nZ committed 2026-10-06 02:04:14 +08:00
1 parent 4a5b177947
commit 76da98944c
18 files changed
+780 -22

No files matched your search

+1
View File
@@ -3495,6 +3495,7 @@
"newFilePathInvalid": "Invalid path (no .. or absolute paths)",
"noPackageFiles": "No files listed",
"unsavedSwitch": "You have unsaved changes. Switch file anyway?",
"saveDescriptionFirst": "Save the description in SKILL.md before switching to another file",
"contentLabel": "Content",
"contentPlaceholder": "Edit the selected file…",
"contentPlaceholderAdd": "SKILL.md body only (front matter is generated)…",
+1
View File
@@ -3508,6 +3508,7 @@
"newFilePathInvalid": "Недопустимый путь (без .. и абсолютных путей)",
"noPackageFiles": "Файлы не указаны",
"unsavedSwitch": "Есть несохранённые изменения. Всё равно переключить файл?",
"saveDescriptionFirst": "Сохраните описание в SKILL.md перед переходом к другому файлу",
"contentLabel": "Содержимое",
"contentPlaceholder": "Измените выбранный файл…",
"contentPlaceholderAdd": "Только тело SKILL.md (front matter создаётся автоматически)…",
+1
View File
@@ -3483,6 +3483,7 @@
"newFilePathInvalid": "路径无效(禁止 .. 与绝对路径)",
"noPackageFiles": "未列出文件",
"unsavedSwitch": "当前文件有未保存修改,确定切换?",
"saveDescriptionFirst": "请先在 SKILL.md 中保存描述,再切换到辅助文件",
"contentLabel": "内容",
"contentPlaceholder": "编辑当前选中的文件…",
"contentPlaceholderAdd": "SKILL.md 正文(无需手写 YAML 头)…",
+11 -1
View File
@@ -3199,6 +3199,7 @@ async function probeSelectedAIChannels() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: ch.provider || 'openai_compatible',
channel_id: id,
base_url: ch.base_url || '',
api_key: ch.api_key || '',
model: ch.model || ''
@@ -3527,6 +3528,12 @@ function populateModelSelect(scope, models, currentValue) {
async function fetchModelList(scope) {
const tFn = typeof window.t === 'function' ? window.t : (k) => k;
const creds = resolveModelListCredentials(scope);
creds.credential_scope = scope;
if (scope === 'openai') creds.channel_id = selectedAIChannelId;
const keyInputByScope = {vision: 'vision-api-key', hitlAudit: 'hitl-audit-model-api-key', knowledgeEmbedding: 'knowledge-embedding-api-key'};
if (keyInputByScope[scope] && !document.getElementById(keyInputByScope[scope])?.value.trim()) {
creds.channel_id = selectedAIChannelId;
}
const modelListUiIds = {
openai: {
btnId: 'fetch-openai-models-btn',
@@ -3641,7 +3648,7 @@ async function testVisionConnection() {
const response = await apiFetch('/api/config/test-vision', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vision: vision, openai: openai })
body: JSON.stringify({ vision: vision, openai: openai, channel_id: selectedAIChannelId })
});
const result = await response.json();
if (result.success) {
@@ -3763,6 +3770,8 @@ async function testHitlAuditModelConnection() {
const payload = typeSafe
? { base_url: baseUrl, api_key: apiKey, model: model }
: cfg;
payload.credential_scope = 'hitlAudit';
if (!document.getElementById('hitl-audit-model-api-key')?.value.trim()) payload.channel_id = selectedAIChannelId;
const response = await apiFetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -3886,6 +3895,7 @@ async function testOpenAIConnection() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: provider,
channel_id: channelId,
base_url: baseUrl,
api_key: apiKey,
model: model
@@ -0,0 +1,33 @@
const fs = require('node:fs');
const vm = require('node:vm');
const test = require('node:test');
const assert = require('node:assert/strict');
const source = fs.readFileSync('web/static/js/skills.js', 'utf8');
const start = source.indexOf('async function selectSkillPackageFile(');
const end = source.indexOf('// 编辑skill', start);
function harness(dirty) {
const inputs = {'skill-description': {disabled:false}, 'skill-content': {value:''}};
let calls = 0;
const ctx = {
skillDescriptionDirty:dirty, skillFileDirty:false, skillActivePath:'SKILL.md',
document:{getElementById:id => inputs[id] || null},
_t:x => x, showNotification() {}, renderSkillPackageTree() {}, confirm:() => true,
apiFetch:async () => {calls++; return {ok:true,json:async () => ({content:'text',skill:{content:'body'}})};},
};
vm.createContext(ctx);
vm.runInContext(source.slice(start,end),ctx);
return {ctx,inputs,calls:() => calls};
}
test('auxiliary file editing disables description and returning to SKILL.md enables it', async () => {
const {ctx,inputs} = harness(false);
await ctx.selectSkillPackageFile('test','references/example.md',{});
assert.equal(inputs['skill-description'].disabled,true);
await ctx.selectSkillPackageFile('test','SKILL.md',{});
assert.equal(inputs['skill-description'].disabled,false);
});
test('unsaved description cannot disappear when switching files', async () => {
const h = harness(true);
await h.ctx.selectSkillPackageFile('test','references/example.md',{});
assert.equal(h.ctx.skillActivePath,'SKILL.md');
assert.equal(h.calls(),0);
});
+16
View File
@@ -7,6 +7,7 @@ let currentEditingSkillName = null;
let skillModalAddMode = true;
let skillActivePath = 'SKILL.md';
let skillFileDirty = false;
let skillDescriptionDirty = false;
let skillPackageFiles = [];
let skillModalControlsWired = false;
let isSavingSkill = false; // 防止重复提交
@@ -454,6 +455,8 @@ function wireSkillModalOnce() {
skillModalControlsWired = true;
const addTa = document.getElementById('skill-content-add');
const edTa = document.getElementById('skill-content');
const descriptionInput = document.getElementById('skill-description');
if (descriptionInput) descriptionInput.addEventListener('input', () => { skillDescriptionDirty = true; });
if (addTa) addTa.addEventListener('input', () => { if (skillModalAddMode) skillFileDirty = true; });
if (edTa) edTa.addEventListener('input', () => { if (!skillModalAddMode) skillFileDirty = true; });
const nb = document.getElementById('skill-new-file-btn');
@@ -483,6 +486,8 @@ function showAddSkillModal() {
if (!modal) return;
skillModalAddMode = true;
skillDescriptionDirty = false;
document.getElementById('skill-description').disabled = false;
skillFileDirty = false;
skillActivePath = 'SKILL.md';
skillPackageFiles = [];
@@ -544,12 +549,18 @@ async function selectSkillPackageFile(skillId, path, opts) {
const freshContent = opts && Object.prototype.hasOwnProperty.call(opts, 'freshContent')
? opts.freshContent
: null;
if (path !== 'SKILL.md' && skillDescriptionDirty) {
showNotification(_t('skillModal.saveDescriptionFirst'), 'error');
return;
}
if (!force && skillFileDirty) {
if (!confirm(_t('skillModal.unsavedSwitch'))) {
return;
}
}
skillActivePath = path;
const descriptionInput = document.getElementById('skill-description');
if (descriptionInput) descriptionInput.disabled = path !== 'SKILL.md';
const label = document.getElementById('skill-active-path');
if (label) label.textContent = path;
const hint = document.getElementById('skill-body-hint-edit');
@@ -591,6 +602,8 @@ async function editSkill(skillId) {
const modal = document.getElementById('skill-modal');
if (!modal) return;
skillModalAddMode = false;
skillDescriptionDirty = false;
document.getElementById('skill-description').disabled = false;
skillFileDirty = false;
skillActivePath = 'SKILL.md';
const pkg = document.getElementById('skill-package-editor');
@@ -739,6 +752,8 @@ function closeSkillModal() {
currentEditingSkillName = null;
skillModalAddMode = true;
skillFileDirty = false;
skillDescriptionDirty = false;
document.getElementById('skill-description').disabled = false;
skillPackageFiles = [];
skillActivePath = 'SKILL.md';
}
@@ -833,6 +848,7 @@ async function saveSkill() {
}
skillFileDirty = false;
if (path === 'SKILL.md') skillDescriptionDirty = false;
showNotification(_t('skills.saveSuccess'), 'success');
const filesRes = await apiFetch(`/api/skills/${encodeURIComponent(currentEditingSkillName)}/files`);
if (filesRes.ok) {
+2 -1
View File
@@ -1244,7 +1244,8 @@ function probeWebshellConnection(conn) {
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
connection_id: conn.id || '',
command: buildWebshellProbeCommand(probeToken)
command: buildWebshellProbeCommand(probeToken),
connection_id: password === '********' ? (document.getElementById('webshell-edit-id')?.value || '') : ''
})
})
.then(function (r) { return r.json(); })