feat(page-ai): add skill context and agent profile policy
This commit is contained in:
@@ -320,9 +320,11 @@ export function createSidebarPageAiRuntime(context) {
|
||||
var record = pageAiAgentRecord(next);
|
||||
pageUiState.pageAiAgentId = next;
|
||||
pageUiState.pageAiAcpRuntime = record.acpRuntime || 'reasonix';
|
||||
pageUiState.pageAiAgentPopoverOpen = next === 'hermes';
|
||||
if (next === 'hermes') pageAiSetActiveProfile(pageAiCurrentProfile());
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-agent-id', next);
|
||||
pageAiPersistAiPreference('default_agent_id', next);
|
||||
void pageAiLoadSkills();
|
||||
renderPageAiProviderButtons();
|
||||
renderPageAiControls();
|
||||
}
|
||||
@@ -337,7 +339,62 @@ export function createSidebarPageAiRuntime(context) {
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSetContextPopoverOpen(open) {
|
||||
pageUiState.pageAiContextPopoverOpen = Boolean(open);
|
||||
if (open) pageUiState.pageAiAgentPopoverOpen = false;
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSetAgentPopoverOpen(open) {
|
||||
pageUiState.pageAiAgentPopoverOpen = Boolean(open);
|
||||
if (open) pageUiState.pageAiContextPopoverOpen = false;
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiContextRefLabel(kind) {
|
||||
var record = PAGE_AI_CONTEXT_REF_REGISTRY.find(function(ref) { return ref.id === kind; });
|
||||
return record ? record.label : kind;
|
||||
}
|
||||
|
||||
function pageAiContextButtonSummary() {
|
||||
var selected = pageAiEnsureContextRefState();
|
||||
var labels = PAGE_AI_CONTEXT_REF_REGISTRY
|
||||
.filter(function(ref) { return selected[ref.id] === true; })
|
||||
.map(function(ref) { return ref.label; });
|
||||
if (!labels.length) return '选择上下文';
|
||||
var head = labels.slice(0, 2).join(' + ');
|
||||
return labels.length > 2 ? head + ' +' + String(labels.length - 2) : head;
|
||||
}
|
||||
|
||||
function pageAiContextRefDetail(kind) {
|
||||
var documentId = currentDocumentId();
|
||||
var relativePath = localMarkdownRelativePathFromPageAiDocumentId(documentId);
|
||||
if (kind === 'current_page') return relativePath || documentId || '当前页面';
|
||||
if (kind === 'selection') return currentPageAiSelectedText() ? '当前选区可用' : '当前没有选区';
|
||||
if (kind === 'active_editor') {
|
||||
var target = currentPageAiEditorTarget();
|
||||
return String(target && (target.title || target.documentId) || relativePath || '当前打开资源');
|
||||
}
|
||||
if (kind === 'file') return relativePath || '当前文件';
|
||||
if (kind === 'folder') {
|
||||
var roots = pageAiBuildAllowedRoots();
|
||||
if (!roots.length) return '需要授权后可用';
|
||||
return roots.map(function(root) { return root.rootUri.replace(/^file:\/\//, ''); }).filter(Boolean)[0] || '已授权文件夹';
|
||||
}
|
||||
if (kind === 'changed_files') return '本次 run 后可用于追问';
|
||||
return '';
|
||||
}
|
||||
|
||||
function pageAiContextRefDisabled(kind) {
|
||||
if (kind === 'selection') return !currentPageAiSelectedText();
|
||||
return false;
|
||||
}
|
||||
|
||||
function pageAiPersistAiPreference(key, value) {
|
||||
pageAiPersistRawAiPreference('ai.common.' + key, value);
|
||||
}
|
||||
|
||||
function pageAiPersistRawAiPreference(key, value) {
|
||||
try {
|
||||
var workspaceId = resolveWorkspaceId(document.body);
|
||||
var body = {
|
||||
@@ -347,7 +404,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
documentId: currentDocumentId(),
|
||||
updates: {}
|
||||
};
|
||||
body.updates['ai.common.' + key] = value;
|
||||
body.updates[key] = value;
|
||||
void fetch('/api/ui/preferences', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
@@ -369,16 +426,23 @@ export function createSidebarPageAiRuntime(context) {
|
||||
var preferences = payload.result.aiPreferences && typeof payload.result.aiPreferences === 'object'
|
||||
? payload.result.aiPreferences
|
||||
: {};
|
||||
pageUiState.pageAiSkillPreferences = Object.assign({}, preferences);
|
||||
var defaultAgent = String(preferences['ai.common.default_agent_id'] || '').trim();
|
||||
if (defaultAgent) {
|
||||
var agent = pageAiAgentRecord(defaultAgent);
|
||||
pageUiState.pageAiAgentId = agent.id;
|
||||
pageUiState.pageAiAcpRuntime = agent.acpRuntime || pageUiState.pageAiAcpRuntime || 'reasonix';
|
||||
}
|
||||
var hermesProfile = String(preferences['ai.agent.hermes.profile_id'] || '').trim();
|
||||
if (hermesProfile) pageAiSetActiveProfile(hermesProfile);
|
||||
var selected = preferences['ai.common.context_refs.default_selected'];
|
||||
if (selected && typeof selected === 'object' && !Array.isArray(selected)) {
|
||||
pageUiState.pageAiSelectedContextRefs = Object.assign({}, pageAiEnsureContextRefState(), selected);
|
||||
}
|
||||
var collapsedSkillGroups = preferences['ai.common.skills.groups.collapsed'];
|
||||
if (collapsedSkillGroups && typeof collapsedSkillGroups === 'object' && !Array.isArray(collapsedSkillGroups)) {
|
||||
pageUiState.pageAiCollapsedSkillGroups = Object.assign({}, collapsedSkillGroups);
|
||||
}
|
||||
} catch (_) {}
|
||||
renderPageAiControls();
|
||||
}
|
||||
@@ -531,22 +595,18 @@ export function createSidebarPageAiRuntime(context) {
|
||||
|
||||
function pageAiPageContextForRefs(pageContext, contextRefs) {
|
||||
var cloned = pageAiCloneJson(pageContext) || {};
|
||||
var kinds = pageAiContextKindsFromRefs(contextRefs);
|
||||
var aiContext = cloned.aiContext && typeof cloned.aiContext === 'object' ? cloned.aiContext : {};
|
||||
if (!kinds.current_page) {
|
||||
delete cloned.documentBlocks;
|
||||
delete aiContext.contextBlocks;
|
||||
delete aiContext.pageText;
|
||||
delete aiContext.pageXml;
|
||||
delete aiContext.truncated;
|
||||
delete aiContext.warnings;
|
||||
}
|
||||
if (!kinds.selection) {
|
||||
delete aiContext.selectedText;
|
||||
delete aiContext.selectedBlockIds;
|
||||
delete aiContext.selectedBlocks;
|
||||
delete aiContext.allowedTargetBlockIds;
|
||||
}
|
||||
delete cloned.documentBlocks;
|
||||
delete cloned.evidence;
|
||||
delete aiContext.contextBlocks;
|
||||
delete aiContext.pageText;
|
||||
delete aiContext.pageXml;
|
||||
delete aiContext.truncated;
|
||||
delete aiContext.warnings;
|
||||
delete aiContext.selectedText;
|
||||
delete aiContext.selectedBlockIds;
|
||||
delete aiContext.selectedBlocks;
|
||||
delete aiContext.allowedTargetBlockIds;
|
||||
cloned.aiContext = aiContext;
|
||||
return cloned;
|
||||
}
|
||||
@@ -754,7 +814,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
pageSubtreeSource: scope === 'page' ? (contextSnapshot.pageSubtreeSource || 'none') : 'scope:' + scope,
|
||||
evidence: selectedText ? [{ kind: 'selection', text: selectedText }] : null,
|
||||
pageOptions: scope === 'options' || scope === 'page' ? currentPageOptions() : null,
|
||||
contentAccess: 'mnote.doc.fetch',
|
||||
contentAccess: 'mnote.context.read_current_page',
|
||||
aiContext: aiContext
|
||||
},
|
||||
editorTarget: editorTarget,
|
||||
@@ -1532,13 +1592,17 @@ export function createSidebarPageAiRuntime(context) {
|
||||
skills: pageAiNormalizeArray(category && category.skills).map(function(skill) {
|
||||
return {
|
||||
name: String(skill && skill.name || '').trim(),
|
||||
id: String(skill && skill.id || skill && skill.name || '').trim(),
|
||||
title: String(skill && skill.title || skill && skill.name || skill && skill.id || '').trim(),
|
||||
description: String(skill && skill.description || '').trim(),
|
||||
enabled: skill && skill.enabled !== false,
|
||||
toggleable: skill && skill.toggleable !== false,
|
||||
source: String(skill && skill.source || 'local').trim() || 'local',
|
||||
origin: String(skill && skill.origin || '').trim(),
|
||||
createdBy: String(skill && skill.createdBy || '').trim(),
|
||||
patchCount: Number(skill && skill.patchCount || 0),
|
||||
modified: Boolean(skill && skill.modified)
|
||||
modified: Boolean(skill && skill.modified),
|
||||
category: String(category && category.name || '').trim()
|
||||
};
|
||||
})
|
||||
};
|
||||
@@ -1546,8 +1610,11 @@ export function createSidebarPageAiRuntime(context) {
|
||||
archived: archived.map(function(skill) {
|
||||
return {
|
||||
name: String(skill && skill.name || '').trim(),
|
||||
id: String(skill && skill.id || skill && skill.name || '').trim(),
|
||||
title: String(skill && skill.title || skill && skill.name || skill && skill.id || '').trim(),
|
||||
description: String(skill && skill.description || '').trim(),
|
||||
enabled: skill && skill.enabled !== false,
|
||||
toggleable: skill && skill.toggleable !== false,
|
||||
source: String(skill && skill.source || 'local').trim() || 'local',
|
||||
origin: String(skill && skill.origin || '').trim(),
|
||||
createdBy: String(skill && skill.createdBy || '').trim(),
|
||||
@@ -1565,8 +1632,11 @@ export function createSidebarPageAiRuntime(context) {
|
||||
result.push({
|
||||
category: category.name,
|
||||
name: skill.name,
|
||||
id: skill.id || skill.name,
|
||||
title: skill.title || skill.name,
|
||||
description: skill.description,
|
||||
enabled: skill.enabled !== false,
|
||||
toggleable: skill.toggleable !== false,
|
||||
source: skill.source || 'local',
|
||||
origin: skill.origin || '',
|
||||
createdBy: skill.createdBy || '',
|
||||
@@ -1666,6 +1736,74 @@ export function createSidebarPageAiRuntime(context) {
|
||||
}).filter(Boolean).join('\n');
|
||||
}
|
||||
|
||||
function pageAiParentRelativePath(path) {
|
||||
var normalized = String(path || '').trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
if (!normalized || normalized.indexOf('/') < 0) return '';
|
||||
return normalized.split('/').slice(0, -1).join('/');
|
||||
}
|
||||
|
||||
function pageAiChangedFileRelativePath(file) {
|
||||
return String(file && (file.path || file.relativePath || file.relative_path || file.filePath || file.file_path || '') || '').trim();
|
||||
}
|
||||
|
||||
function pageAiDispatchReceiptRefresh(receipt, changedFiles, fallbackAudit, runId, traceId) {
|
||||
var refresh = receipt && typeof receipt === 'object' && receipt.refresh && typeof receipt.refresh === 'object'
|
||||
? receipt.refresh
|
||||
: {};
|
||||
var files = pageAiNormalizeArray(
|
||||
receipt && typeof receipt === 'object' && receipt.changedFiles !== undefined
|
||||
? receipt.changedFiles
|
||||
: changedFiles
|
||||
);
|
||||
if (files.length) {
|
||||
var changedPaths = files.map(function(file) {
|
||||
var relativePath = pageAiChangedFileRelativePath(file);
|
||||
return {
|
||||
relativePath: relativePath,
|
||||
changeType: String(file && (file.changeType || file.change_type || 'modified') || 'modified')
|
||||
};
|
||||
}).filter(function(item) { return item.relativePath; });
|
||||
var affectedParents = changedPaths.map(function(item) {
|
||||
return { relativePath: pageAiParentRelativePath(item.relativePath), reason: 'agent-run-receipt' };
|
||||
}).filter(function(item, index, list) {
|
||||
return index === list.findIndex(function(candidate) { return candidate.relativePath === item.relativePath; });
|
||||
});
|
||||
if (changedPaths.length) {
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-receipt-filetree-refresh', 'true');
|
||||
window.dispatchEvent(new CustomEvent('tree:local-folder-watch-batch', {
|
||||
detail: {
|
||||
payload: {
|
||||
schema: 'mnote.local_folder.watch_batch.v1',
|
||||
source: 'agent_run_receipt',
|
||||
runId: runId,
|
||||
rootUri: String((receipt && receipt.rootUri) || (fallbackAudit && fallbackAudit.rootUri) || currentRootUri() || ''),
|
||||
changedPaths: changedPaths,
|
||||
affectedParents: affectedParents
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
if (refresh.touchesCurrentFile === true) {
|
||||
var documentId = String(refresh.currentDocumentId || receipt && receipt.documentId || currentDocumentId() || '').trim();
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-receipt-current-refresh', 'true');
|
||||
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
|
||||
detail: {
|
||||
toolName: 'agent.run_receipt',
|
||||
normalizedToolName: 'agent.run_receipt',
|
||||
documentId: documentId,
|
||||
workspaceId: String(receipt && receipt.workspaceId || resolveWorkspaceId(document.body) || ''),
|
||||
rootUri: String(receipt && receipt.rootUri || fallbackAudit && fallbackAudit.rootUri || currentRootUri() || ''),
|
||||
changedFiles: files,
|
||||
receipt: receipt || null,
|
||||
runId: runId,
|
||||
traceId: traceId,
|
||||
toolCallId: runId + ':agent.run_receipt'
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function pageAiToolEventDeepFindString(value, keys, depth) {
|
||||
if (!value || typeof value !== 'object' || depth > 5) return '';
|
||||
for (var index = 0; index < keys.length; index += 1) {
|
||||
@@ -1992,9 +2130,11 @@ export function createSidebarPageAiRuntime(context) {
|
||||
|
||||
function pageAiFilteredSkillEntries() {
|
||||
var query = String(pageUiState.pageAiSkillQuery || '').trim().toLowerCase();
|
||||
return pageAiSkillListEntries().filter(function(skill) {
|
||||
return pageAiAllSkillEntries().filter(function(skill) {
|
||||
if (skill.group === 'hermes' && pageAiHideHermesBuiltinSkills(pageAiCurrentProfile()) && skill.builtin === true) return false;
|
||||
if (!query) return true;
|
||||
return String(skill.name || '').toLowerCase().indexOf(query) >= 0
|
||||
return String(skill.name || skill.id || '').toLowerCase().indexOf(query) >= 0
|
||||
|| String(skill.title || '').toLowerCase().indexOf(query) >= 0
|
||||
|| String(skill.description || '').toLowerCase().indexOf(query) >= 0
|
||||
|| String(skill.category || '').toLowerCase().indexOf(query) >= 0
|
||||
|| pageAiSkillOriginLabel(skill).toLowerCase().indexOf(query) >= 0;
|
||||
@@ -2018,6 +2158,162 @@ export function createSidebarPageAiRuntime(context) {
|
||||
return '本地';
|
||||
}
|
||||
|
||||
function pageAiSkillPreferenceKey(group, profile) {
|
||||
var groupName = String(group || '').trim();
|
||||
if (groupName === 'mnote') return 'ai.common.skills.mnote.enabled';
|
||||
if (groupName === 'reasonix') return 'ai.agent.reasonix.skills.enabled';
|
||||
if (groupName === 'hermes') {
|
||||
var nextProfile = String(profile || pageAiCurrentProfile() || 'default').trim() || 'default';
|
||||
return 'ai.agent.hermes.profile.' + nextProfile + '.skills.enabled';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function pageAiSkillPreferenceTable(group, profile) {
|
||||
var key = pageAiSkillPreferenceKey(group, profile);
|
||||
var preferences = pageUiState.pageAiSkillPreferences || {};
|
||||
var value = preferences[key];
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) return value;
|
||||
return {};
|
||||
}
|
||||
|
||||
function pageAiHermesHideBuiltinPreferenceKey(profile) {
|
||||
var nextProfile = String(profile || pageAiCurrentProfile() || 'default').trim() || 'default';
|
||||
return 'ai.agent.hermes.profile.' + nextProfile + '.skills.hide_builtin';
|
||||
}
|
||||
|
||||
function pageAiHideHermesBuiltinSkills(profile) {
|
||||
var preferences = pageUiState.pageAiSkillPreferences || {};
|
||||
return preferences[pageAiHermesHideBuiltinPreferenceKey(profile)] === true;
|
||||
}
|
||||
|
||||
function pageAiSetHideHermesBuiltinSkills(enabled, profile) {
|
||||
var key = pageAiHermesHideBuiltinPreferenceKey(profile);
|
||||
var preferences = Object.assign({}, pageUiState.pageAiSkillPreferences || {});
|
||||
preferences[key] = Boolean(enabled);
|
||||
pageUiState.pageAiSkillPreferences = preferences;
|
||||
pageAiPersistRawAiPreference(key, Boolean(enabled));
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSkillIsBuiltin(skill) {
|
||||
var origin = String(skill && skill.origin || '').trim();
|
||||
var source = String(skill && skill.source || '').trim();
|
||||
return origin === 'builtin' || source === 'builtin';
|
||||
}
|
||||
|
||||
function pageAiToggleableSkillEntries(group, profile) {
|
||||
var catalog = pageUiState.pageAiSkillCatalogs && pageUiState.pageAiSkillCatalogs[group]
|
||||
? pageUiState.pageAiSkillCatalogs[group]
|
||||
: { categories: [], archived: [] };
|
||||
var overrides = pageAiSkillPreferenceTable(group, profile);
|
||||
var result = [];
|
||||
pageAiNormalizeArray(catalog.categories).forEach(function(category) {
|
||||
pageAiNormalizeArray(category.skills).forEach(function(skill) {
|
||||
var id = String(skill.id || skill.name || '').trim();
|
||||
if (!id) return;
|
||||
result.push({
|
||||
group: group,
|
||||
profile: profile || '',
|
||||
category: category.name,
|
||||
id: id,
|
||||
name: skill.name || id,
|
||||
title: skill.title || skill.name || id,
|
||||
description: skill.description || '',
|
||||
enabled: group === 'hermes' ? skill.enabled !== false : overrides[id] !== false,
|
||||
toggleable: skill.toggleable !== false,
|
||||
source: skill.source || group,
|
||||
origin: skill.origin || '',
|
||||
readOnly: skill.readOnly === true,
|
||||
builtin: pageAiSkillIsBuiltin(skill),
|
||||
toolNames: skill.toolNames || [],
|
||||
requiresContextRefs: skill.requiresContextRefs || []
|
||||
});
|
||||
});
|
||||
});
|
||||
pageAiNormalizeArray(catalog.archived).forEach(function(skill) {
|
||||
var id = String(skill.id || skill.name || '').trim();
|
||||
if (!id) return;
|
||||
result.push({
|
||||
group: group,
|
||||
profile: profile || '',
|
||||
category: 'archived',
|
||||
id: id,
|
||||
name: skill.name || id,
|
||||
title: skill.title || skill.name || id,
|
||||
description: skill.description || '',
|
||||
enabled: group === 'hermes' ? skill.enabled !== false : overrides[id] !== false,
|
||||
toggleable: skill.toggleable !== false,
|
||||
source: skill.source || group,
|
||||
origin: skill.origin || '',
|
||||
readOnly: skill.readOnly === true,
|
||||
builtin: pageAiSkillIsBuiltin(skill),
|
||||
toolNames: skill.toolNames || [],
|
||||
requiresContextRefs: skill.requiresContextRefs || []
|
||||
});
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function pageAiAllSkillEntries() {
|
||||
return []
|
||||
.concat(pageAiToggleableSkillEntries('mnote', ''))
|
||||
.concat(pageAiToggleableSkillEntries('reasonix', ''))
|
||||
.concat(pageAiToggleableSkillEntries('hermes', pageAiCurrentProfile()));
|
||||
}
|
||||
|
||||
function pageAiSkillGroupCollapsed(group) {
|
||||
var table = pageUiState.pageAiCollapsedSkillGroups || {};
|
||||
return table[String(group || '').trim()] === true;
|
||||
}
|
||||
|
||||
function pageAiToggleSkillGroup(group) {
|
||||
var normalized = String(group || '').trim();
|
||||
if (!normalized) return;
|
||||
var table = Object.assign({}, pageUiState.pageAiCollapsedSkillGroups || {});
|
||||
table[normalized] = table[normalized] !== true;
|
||||
pageUiState.pageAiCollapsedSkillGroups = table;
|
||||
pageAiPersistRawAiPreference('ai.common.skills.groups.collapsed', table);
|
||||
renderPageAiControls();
|
||||
}
|
||||
|
||||
function pageAiSetSkillPreference(group, skillId, enabled, profile) {
|
||||
var key = pageAiSkillPreferenceKey(group, profile);
|
||||
if (!key || !skillId) return;
|
||||
var preferences = Object.assign({}, pageUiState.pageAiSkillPreferences || {});
|
||||
var current = preferences[key] && typeof preferences[key] === 'object' && !Array.isArray(preferences[key])
|
||||
? Object.assign({}, preferences[key])
|
||||
: {};
|
||||
current[skillId] = Boolean(enabled);
|
||||
preferences[key] = current;
|
||||
pageUiState.pageAiSkillPreferences = preferences;
|
||||
pageAiPersistRawAiPreference(key, current);
|
||||
}
|
||||
|
||||
function pageAiSkillEnabled(skill) {
|
||||
return skill.enabled !== false;
|
||||
}
|
||||
|
||||
function pageAiLoadSkillCatalog(runtime, profile) {
|
||||
var params = new URLSearchParams();
|
||||
params.set('runtime', runtime);
|
||||
if (runtime === 'mnote') {
|
||||
params.set('agentId', pageUiState.pageAiAgentId || 'reasonix');
|
||||
} else if (runtime === 'reasonix') {
|
||||
params.set('runtime', 'reasonix');
|
||||
} else if (runtime === 'hermes' && profile) {
|
||||
params.set('profile', profile);
|
||||
}
|
||||
return fetch('/api/hermes/client/skills?' + params.toString(), {
|
||||
headers: { 'accept': 'application/json' }
|
||||
}).then(function(response) {
|
||||
return response.json().catch(function(){ return null; }).then(function(payload) {
|
||||
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'skills_failed_' + response.status));
|
||||
return pageAiNormalizeSkills(payload);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function pageAiLoadProfiles() {
|
||||
try {
|
||||
var response = await fetch('/api/hermes/client/profiles', {
|
||||
@@ -2050,6 +2346,12 @@ export function createSidebarPageAiRuntime(context) {
|
||||
var next = String(profileName || '').trim();
|
||||
if (!next) return;
|
||||
pageAiSetActiveProfile(next);
|
||||
pageAiPersistRawAiPreference('ai.agent.hermes.profile_id', next);
|
||||
pageUiState.pageAiSkills = { categories: [], archived: [] };
|
||||
pageUiState.pageAiSkillCatalogs = Object.assign({}, pageUiState.pageAiSkillCatalogs || {}, {
|
||||
hermes: { categories: [], archived: [] }
|
||||
});
|
||||
pageUiState.pageAiSkillError = '';
|
||||
renderPageAiProviderButtons();
|
||||
renderPageAiControls();
|
||||
try {
|
||||
@@ -2121,59 +2423,81 @@ export function createSidebarPageAiRuntime(context) {
|
||||
}
|
||||
|
||||
async function pageAiLoadSkills() {
|
||||
var requestedProfile = pageAiCurrentProfile();
|
||||
var loadSeq = (Number(pageUiState.pageAiSkillLoadSeq || 0) || 0) + 1;
|
||||
pageUiState.pageAiSkillLoadSeq = loadSeq;
|
||||
try {
|
||||
var runtime = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim();
|
||||
var params = runtime === 'reasonix'
|
||||
? 'runtime=reasonix'
|
||||
: 'profile=' + encodeURIComponent(pageAiCurrentProfile());
|
||||
var response = await fetch('/api/hermes/client/skills?' + params, {
|
||||
headers: { 'accept': 'application/json' }
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'skills_failed_' + response.status));
|
||||
pageUiState.pageAiSkills = pageAiNormalizeSkills(payload);
|
||||
var catalogs = await Promise.all([
|
||||
pageAiLoadSkillCatalog('mnote', ''),
|
||||
pageAiLoadSkillCatalog('reasonix', ''),
|
||||
pageAiLoadSkillCatalog('hermes', requestedProfile)
|
||||
]);
|
||||
if (pageUiState.pageAiSkillLoadSeq !== loadSeq || pageAiCurrentProfile() !== requestedProfile) return;
|
||||
pageUiState.pageAiSkillCatalogs = {
|
||||
mnote: catalogs[0],
|
||||
reasonix: catalogs[1],
|
||||
hermes: catalogs[2]
|
||||
};
|
||||
pageUiState.pageAiSkills = catalogs[2];
|
||||
pageUiState.pageAiSkillError = '';
|
||||
} catch (error) {
|
||||
if (pageUiState.pageAiSkillLoadSeq !== loadSeq || pageAiCurrentProfile() !== requestedProfile) return;
|
||||
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
|
||||
pageUiState.pageAiSkills = { categories: [], archived: [] };
|
||||
pageUiState.pageAiSkillCatalogs = pageUiState.pageAiSkillCatalogs || { mnote: { categories: [], archived: [] }, reasonix: { categories: [], archived: [] }, hermes: { categories: [], archived: [] } };
|
||||
}
|
||||
renderPageAiControls();
|
||||
renderPageAiConversation();
|
||||
}
|
||||
|
||||
async function pageAiToggleSkill(skillName, enabled) {
|
||||
async function pageAiToggleSkill(skillName, enabled, group, profile) {
|
||||
var name = String(skillName || '').trim();
|
||||
if (!name) return;
|
||||
if (String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix') return;
|
||||
var skillGroup = String(group || 'hermes').trim() || 'hermes';
|
||||
var skillProfile = String(profile || pageAiCurrentProfile() || '').trim();
|
||||
if (skillGroup === 'mnote' || skillGroup === 'reasonix') {
|
||||
pageAiSetSkillPreference(skillGroup, name, Boolean(enabled), skillProfile);
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-skill-toggled', skillGroup + ':' + name);
|
||||
renderPageAiControls();
|
||||
return;
|
||||
}
|
||||
var previous = null;
|
||||
pageAiSkillListEntries().forEach(function(skill) {
|
||||
if (skill.name === name && previous == null) previous = skill.enabled !== false;
|
||||
pageAiToggleableSkillEntries('hermes', pageAiCurrentProfile()).forEach(function(skill) {
|
||||
if (skill.id === name || skill.name === name) previous = skill.enabled !== false;
|
||||
});
|
||||
try {
|
||||
var response = await fetch('/api/hermes/client/skills/toggle', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
profile: pageAiCurrentProfile(),
|
||||
profile: skillProfile || pageAiCurrentProfile(),
|
||||
name: name,
|
||||
enabled: Boolean(enabled)
|
||||
})
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'skill_toggle_failed_' + response.status));
|
||||
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
|
||||
var hermesCatalog = pageUiState.pageAiSkillCatalogs && pageUiState.pageAiSkillCatalogs.hermes
|
||||
? pageUiState.pageAiSkillCatalogs.hermes
|
||||
: pageUiState.pageAiSkills;
|
||||
pageAiNormalizeArray(hermesCatalog.categories).forEach(function(category) {
|
||||
pageAiNormalizeArray(category.skills).forEach(function(skill) {
|
||||
if (skill.name === name) skill.enabled = Boolean(enabled);
|
||||
if (skill.name === name || skill.id === name) skill.enabled = Boolean(enabled);
|
||||
});
|
||||
});
|
||||
pageUiState.pageAiSkills = hermesCatalog;
|
||||
pageAiSetSkillPreference('hermes', name, Boolean(enabled), skillProfile || pageAiCurrentProfile());
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-skill-toggled', name);
|
||||
pageUiState.pageAiSkillError = '';
|
||||
} catch (error) {
|
||||
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
|
||||
if (previous != null) {
|
||||
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
|
||||
var rollbackCatalog = pageUiState.pageAiSkillCatalogs && pageUiState.pageAiSkillCatalogs.hermes
|
||||
? pageUiState.pageAiSkillCatalogs.hermes
|
||||
: pageUiState.pageAiSkills;
|
||||
pageAiNormalizeArray(rollbackCatalog.categories).forEach(function(category) {
|
||||
pageAiNormalizeArray(category.skills).forEach(function(skill) {
|
||||
if (skill.name === name) skill.enabled = previous;
|
||||
if (skill.name === name || skill.id === name) skill.enabled = previous;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -2240,25 +2564,85 @@ export function createSidebarPageAiRuntime(context) {
|
||||
var drawer = ensurePageAiDrawer();
|
||||
var isAcp = pageUiState.pageAiAcpRuntime !== '';
|
||||
var activeAgentId = pageAiCurrentAgentId();
|
||||
var activeProfile = pageAiRunProfile();
|
||||
var activeProfile = pageAiCurrentProfile();
|
||||
drawer.setAttribute('data-page-ai-page', pageUiState.pageAiPage || 'chat');
|
||||
drawer.setAttribute('data-page-ai-active-agent-id', activeAgentId);
|
||||
drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || 'reasonix');
|
||||
document.documentElement.setAttribute('data-mnote-page-ai-agent-id', activeAgentId);
|
||||
var agentSelector = drawer.querySelector('[data-page-ai-agent-selector]');
|
||||
if (agentSelector instanceof HTMLElement) {
|
||||
agentSelector.innerHTML = PAGE_AI_AGENT_REGISTRY.map(function(agent) {
|
||||
var active = agent.id === activeAgentId;
|
||||
return '<button type="button" class="wolai-page-ai-tab' + (active ? ' is-active' : '') + '" data-page-ai-agent-id="' + escapeHtml(agent.id) + '" aria-pressed="' + (active ? 'true' : 'false') + '">' + escapeHtml(agent.label) + '</button>';
|
||||
}).join('');
|
||||
var agentButton = drawer.querySelector('[data-page-ai-agent-button]');
|
||||
var agentPopoverId = 'mnote-page-ai-agent-popover';
|
||||
if (agentButton instanceof HTMLElement) {
|
||||
var activeAgent = pageAiAgentRecord(activeAgentId);
|
||||
agentButton.textContent = 'AI';
|
||||
agentButton.setAttribute('aria-expanded', pageUiState.pageAiAgentPopoverOpen ? 'true' : 'false');
|
||||
agentButton.setAttribute('aria-controls', agentPopoverId);
|
||||
agentButton.setAttribute('data-page-ai-agent-summary', activeAgent.label);
|
||||
agentButton.setAttribute('aria-label', 'Agent:' + activeAgent.label);
|
||||
agentButton.setAttribute('title', 'Agent:' + activeAgent.label);
|
||||
}
|
||||
var contextRefs = drawer.querySelector('[data-page-ai-context-refs]');
|
||||
if (contextRefs instanceof HTMLElement) {
|
||||
var agentPopover = drawer.querySelector('[data-page-ai-agent-popover]');
|
||||
if (agentPopover instanceof HTMLElement) {
|
||||
agentPopover.id = agentPopoverId;
|
||||
agentPopover.hidden = !pageUiState.pageAiAgentPopoverOpen;
|
||||
agentPopover.innerHTML = '' +
|
||||
'<div class="wolai-page-ai-context-popover-head">' +
|
||||
'<strong>选择 Agent</strong>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="close-agent-popover">完成</button>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-agent-option-list">' +
|
||||
PAGE_AI_AGENT_REGISTRY.map(function(agent) {
|
||||
var active = agent.id === activeAgentId;
|
||||
var detail = agent.canWriteFiles ? '可读取并在授权目录内写文件' : '只聊天,不申请文件写权限';
|
||||
return '' +
|
||||
'<button type="button" class="wolai-page-ai-agent-option' + (active ? ' is-active' : '') + '" data-page-ai-agent-id="' + escapeHtml(agent.id) + '" aria-pressed="' + (active ? 'true' : 'false') + '">' +
|
||||
'<span class="wolai-page-ai-agent-option-label">' + escapeHtml(agent.label) + '</span>' +
|
||||
'<span class="wolai-page-ai-agent-option-detail">' + escapeHtml(detail) + '</span>' +
|
||||
'</button>';
|
||||
}).join('') +
|
||||
'</div>' +
|
||||
(activeAgentId === 'hermes'
|
||||
? '<label class="wolai-page-ai-agent-profile"><span>Hermes profile</span><select data-page-ai-profile-select></select></label>'
|
||||
: '');
|
||||
}
|
||||
var contextButton = drawer.querySelector('[data-page-ai-context-button]');
|
||||
var contextPopoverId = 'mnote-page-ai-context-popover';
|
||||
if (contextButton instanceof HTMLElement) {
|
||||
var summary = pageAiContextButtonSummary();
|
||||
contextButton.textContent = '⇅';
|
||||
contextButton.setAttribute('aria-expanded', pageUiState.pageAiContextPopoverOpen ? 'true' : 'false');
|
||||
contextButton.setAttribute('aria-controls', contextPopoverId);
|
||||
contextButton.setAttribute('data-page-ai-context-summary', summary);
|
||||
contextButton.setAttribute('aria-label', '上下文:' + summary);
|
||||
contextButton.setAttribute('title', '上下文:' + summary);
|
||||
}
|
||||
var contextPopover = drawer.querySelector('[data-page-ai-context-popover]');
|
||||
if (contextPopover instanceof HTMLElement) {
|
||||
var selectedRefs = pageAiEnsureContextRefState();
|
||||
contextRefs.innerHTML = PAGE_AI_CONTEXT_REF_REGISTRY.map(function(ref) {
|
||||
var active = selectedRefs[ref.id] !== false && selectedRefs[ref.id] === true;
|
||||
return '<button type="button" class="wolai-page-ai-ghost' + (active ? ' is-active' : '') + '" data-page-ai-context-ref="' + escapeHtml(ref.id) + '" aria-pressed="' + (active ? 'true' : 'false') + '">' + escapeHtml(ref.label) + '</button>';
|
||||
}).join('');
|
||||
contextPopover.id = contextPopoverId;
|
||||
contextPopover.hidden = !pageUiState.pageAiContextPopoverOpen;
|
||||
contextPopover.innerHTML = '' +
|
||||
'<div class="wolai-page-ai-context-popover-head">' +
|
||||
'<strong>发送给 AI 的上下文</strong>' +
|
||||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="close-context-popover">完成</button>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-context-option-list">' +
|
||||
PAGE_AI_CONTEXT_REF_REGISTRY.map(function(ref) {
|
||||
var checked = selectedRefs[ref.id] === true;
|
||||
var disabled = pageAiContextRefDisabled(ref.id);
|
||||
return '' +
|
||||
'<label class="wolai-page-ai-context-option' + (disabled ? ' is-disabled' : '') + '">' +
|
||||
'<input type="checkbox" data-page-ai-context-ref="' + escapeHtml(ref.id) + '"' + (checked ? ' checked' : '') + (disabled ? ' disabled' : '') + ' />' +
|
||||
'<span class="wolai-page-ai-context-option-main">' +
|
||||
'<span class="wolai-page-ai-context-option-label">' + escapeHtml(ref.label) + '</span>' +
|
||||
'<span class="wolai-page-ai-context-option-detail">' + escapeHtml(pageAiContextRefDetail(ref.id)) + '</span>' +
|
||||
'</span>' +
|
||||
'</label>';
|
||||
}).join('') +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-context-popover-foot">' +
|
||||
'<span>授权区域</span>' +
|
||||
'<span>' + escapeHtml(pageAiBuildAllowedRoots().length ? pageAiBuildAllowedRoots().map(function(root) { return root.permission + ' · ' + root.rootUri.replace(/^file:\/\//, ''); }).join(' / ') : '未选择授权区域') + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
var allowedRootsNode = drawer.querySelector('[data-page-ai-allowed-roots]');
|
||||
if (allowedRootsNode instanceof HTMLElement) {
|
||||
@@ -2424,29 +2808,40 @@ export function createSidebarPageAiRuntime(context) {
|
||||
if (skillList instanceof HTMLElement) {
|
||||
var skills = pageAiFilteredSkillEntries();
|
||||
if (!skills.length) {
|
||||
var emptyText = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix'
|
||||
? '没有匹配的 Reasonix skill。'
|
||||
: '没有匹配的 Hermes skill。';
|
||||
skillList.innerHTML = '<div class="wolai-page-ai-empty">' + escapeHtml(emptyText) + '</div>';
|
||||
skillList.innerHTML = '<div class="wolai-page-ai-empty">没有匹配的技能。</div>';
|
||||
} else {
|
||||
skillList.innerHTML = skills.map(function(skill) {
|
||||
var sourceText = pageAiSkillOriginLabel(skill) + (skill.modified ? ' · modified' : '');
|
||||
var description = String(skill.description || '').trim();
|
||||
var hasDescription = description && description !== '---' && description !== '无描述';
|
||||
var canToggle = skill.toggleable !== false && String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() !== 'reasonix';
|
||||
var groupLabels = { mnote: 'MNote 内置技能', reasonix: 'Reasonix 技能', hermes: 'Hermes 技能' };
|
||||
skillList.innerHTML = ['mnote', 'reasonix', 'hermes'].map(function(group) {
|
||||
var groupSkills = skills.filter(function(skill) { return skill.group === group; });
|
||||
if (!groupSkills.length) return '';
|
||||
var headingExtra = group === 'hermes' ? ' · ' + pageAiCurrentProfile() : '';
|
||||
var collapsed = pageAiSkillGroupCollapsed(group);
|
||||
return '' +
|
||||
'<div class="wolai-page-ai-skill-row">' +
|
||||
'<div class="wolai-page-ai-skill-copy">' +
|
||||
'<div class="wolai-page-ai-skill-main">' +
|
||||
'<div class="wolai-page-ai-skill-name">' + escapeHtml(skill.name) + '</div>' +
|
||||
'<div class="wolai-page-ai-skill-source">' + escapeHtml(sourceText) + '</div>' +
|
||||
'</div>' +
|
||||
(hasDescription ? '<div class="wolai-page-ai-skill-desc">' + escapeHtml(description) + '</div>' : '') +
|
||||
'</div>' +
|
||||
'<button type="button" class="wolai-page-ai-skill-switch' + (skill.enabled !== false ? ' is-on' : '') + '" data-page-ai-skill-toggle="' + escapeHtml(skill.name) + '" aria-pressed="' + (skill.enabled !== false ? 'true' : 'false') + '"' + (canToggle ? '' : ' disabled title="Reasonix skills 当前为只读展示"') + '>' +
|
||||
'<span></span>' +
|
||||
'<section class="wolai-page-ai-skill-group' + (collapsed ? ' is-collapsed' : '') + '" data-page-ai-skill-group="' + escapeHtml(group) + '">' +
|
||||
'<button type="button" class="wolai-page-ai-skill-group-head" data-page-ai-skill-group-toggle="' + escapeHtml(group) + '" aria-expanded="' + (collapsed ? 'false' : 'true') + '">' +
|
||||
'<span class="wolai-page-ai-skill-group-title"><span class="wolai-page-ai-skill-group-chevron">' + (collapsed ? '›' : '⌄') + '</span>' + escapeHtml(groupLabels[group] || group) + '</span>' +
|
||||
'<span>' + escapeHtml(String(groupSkills.length) + headingExtra) + '</span>' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
(collapsed ? '' : groupSkills.map(function(skill) {
|
||||
var sourceText = pageAiSkillOriginLabel(skill) + (skill.modified ? ' · modified' : '');
|
||||
var description = String(skill.description || '').trim();
|
||||
var hasDescription = description && description !== '---' && description !== '无描述';
|
||||
var displayName = String(skill.title || skill.name || skill.id || '').trim();
|
||||
return '' +
|
||||
'<div class="wolai-page-ai-skill-row">' +
|
||||
'<div class="wolai-page-ai-skill-copy">' +
|
||||
'<div class="wolai-page-ai-skill-main">' +
|
||||
'<div class="wolai-page-ai-skill-name">' + escapeHtml(displayName) + '</div>' +
|
||||
'<div class="wolai-page-ai-skill-source">' + escapeHtml(sourceText) + '</div>' +
|
||||
'</div>' +
|
||||
(hasDescription ? '<div class="wolai-page-ai-skill-desc">' + escapeHtml(description) + '</div>' : '') +
|
||||
'</div>' +
|
||||
'<button type="button" class="wolai-page-ai-skill-switch' + (pageAiSkillEnabled(skill) ? ' is-on' : '') + '" data-page-ai-skill-toggle="' + escapeHtml(skill.id || skill.name) + '" data-page-ai-skill-group="' + escapeHtml(group) + '" data-page-ai-skill-profile="' + escapeHtml(skill.profile || '') + '" aria-pressed="' + (pageAiSkillEnabled(skill) ? 'true' : 'false') + '"' + (skill.toggleable === false ? ' disabled' : '') + '>' +
|
||||
'<span></span>' +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
}).join('')) +
|
||||
'</section>';
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
@@ -2574,6 +2969,14 @@ export function createSidebarPageAiRuntime(context) {
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-header-actions">' +
|
||||
'<button type="button" class="wolai-page-ai-icon" data-page-ai-tab="skills" aria-label="技能" title="技能">' +
|
||||
'<svg class="wolai-page-ai-icon-svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">' +
|
||||
'<path d="M12 3l1.35 4.15L17.5 8.5l-4.15 1.35L12 14l-1.35-4.15L6.5 8.5l4.15-1.35L12 3z" />' +
|
||||
'<path d="M18.5 13l.8 2.4 2.2.8-2.2.8-.8 2.4-.8-2.4-2.2-.8 2.2-.8.8-2.4z" />' +
|
||||
'<path d="M5.5 14l.65 1.85L8 16.5l-1.85.65L5.5 19l-.65-1.85L3 16.5l1.85-.65L5.5 14z" />' +
|
||||
'</svg>' +
|
||||
'</button>' +
|
||||
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="history" aria-label="历史会话">⌕</button>' +
|
||||
'<button type="button" class="wolai-page-ai-icon" data-page-ai-tab="agent" aria-label="打开页面 AI 设置">⚙</button>' +
|
||||
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
|
||||
'</div>' +
|
||||
@@ -2581,9 +2984,9 @@ export function createSidebarPageAiRuntime(context) {
|
||||
'<div class="wolai-page-ai-body">' +
|
||||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="chat">' +
|
||||
'<div class="wolai-page-ai-chat-meta">' +
|
||||
'<button type="button" class="wolai-page-ai-session-button" data-page-ai-action="history">' +
|
||||
'<span class="wolai-page-ai-session-summary">' +
|
||||
'<span data-page-ai-session-status>等待 AI session</span>' +
|
||||
'</button>' +
|
||||
'</span>' +
|
||||
'<span data-page-ai-context-scope-label>当前页</span>' +
|
||||
'<span data-page-ai-run-status>空闲</span>' +
|
||||
'</div>' +
|
||||
@@ -2681,6 +3084,14 @@ export function createSidebarPageAiRuntime(context) {
|
||||
'<span>搜索技能</span>' +
|
||||
'<input type="search" data-page-ai-skill-search placeholder="搜索技能…" />' +
|
||||
'</label>' +
|
||||
'<label class="wolai-page-ai-profile-select" data-page-ai-skills-hermes-profile>' +
|
||||
'<span>Hermes profile</span>' +
|
||||
'<select data-page-ai-profile-select></select>' +
|
||||
'</label>' +
|
||||
'<label class="wolai-page-ai-skill-filter-toggle">' +
|
||||
'<input type="checkbox" data-page-ai-hide-hermes-builtin' + (pageAiHideHermesBuiltinSkills(pageAiCurrentProfile()) ? ' checked' : '') + ' />' +
|
||||
'<span>隐藏 Hermes 内置</span>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-inline-error" data-page-ai-skill-error hidden></div>' +
|
||||
'<div class="wolai-page-ai-skill-list" data-page-ai-skill-list></div>' +
|
||||
@@ -2742,13 +3153,17 @@ export function createSidebarPageAiRuntime(context) {
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-footer">' +
|
||||
'<div class="wolai-page-ai-composer">' +
|
||||
'<div class="wolai-page-ai-agent-selector" data-page-ai-agent-selector></div>' +
|
||||
'<div class="wolai-page-ai-context-refs" data-page-ai-context-refs></div>' +
|
||||
'<div class="wolai-page-ai-allowed-roots" data-page-ai-allowed-roots></div>' +
|
||||
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="输入消息…"></textarea>' +
|
||||
'<div class="wolai-page-ai-composer-bar">' +
|
||||
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="new-session" title="新会话">+</button>' +
|
||||
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="history" title="历史会话">⌕</button>' +
|
||||
'<div class="wolai-page-ai-agent-picker">' +
|
||||
'<button type="button" class="wolai-page-ai-tool-button wolai-page-ai-agent-button" data-page-ai-agent-button aria-expanded="false" aria-label="Agent:Reasonix" title="Agent:Reasonix">AI</button>' +
|
||||
'<div class="wolai-page-ai-agent-popover" data-page-ai-agent-popover hidden></div>' +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-ai-context-picker">' +
|
||||
'<button type="button" class="wolai-page-ai-tool-button wolai-page-ai-context-button" data-page-ai-context-button aria-expanded="false" aria-label="上下文:当前页 + 打开资源" title="上下文:当前页 + 打开资源">⇅</button>' +
|
||||
'<div class="wolai-page-ai-context-popover" data-page-ai-context-popover hidden></div>' +
|
||||
'</div>' +
|
||||
'<span class="wolai-page-ai-composer-spacer"></span>' +
|
||||
'<button type="button" class="wolai-page-ai-stop" data-page-ai-action="stop-run" disabled>停止</button>' +
|
||||
'<button type="button" class="wolai-page-ai-send" data-page-ai-action="send" aria-label="发送">发送</button>' +
|
||||
@@ -3177,13 +3592,18 @@ export function createSidebarPageAiRuntime(context) {
|
||||
contextScope: pageUiState.pageAiContextScope,
|
||||
contextRefs: contextRefs,
|
||||
allowedRoots: allowedRoots,
|
||||
skillPreferences: {
|
||||
mnote: pageAiSkillPreferenceTable('mnote', ''),
|
||||
reasonix: pageAiSkillPreferenceTable('reasonix', ''),
|
||||
hermes: pageAiSkillPreferenceTable('hermes', pageAiCurrentProfile())
|
||||
},
|
||||
message: prompt,
|
||||
model: pageAiMnoteToolModel(),
|
||||
pageContext: requestPageContext,
|
||||
editorTarget: scopedContext.editorTarget,
|
||||
runTargetSnapshot: runTargetSnapshot,
|
||||
selectedBlockId: scopedContext.selectedBlockId,
|
||||
selectedText: pageAiContextKindsFromRefs(contextRefs).selection ? scopedContext.selectedText : '',
|
||||
selectedText: '',
|
||||
traceId: 'page-ai-run-' + Date.now().toString(36)
|
||||
})
|
||||
});
|
||||
@@ -3282,6 +3702,9 @@ export function createSidebarPageAiRuntime(context) {
|
||||
if (completedSession) completedSession.usage = completed.usage;
|
||||
}
|
||||
var agentAudit = completed && completed.agentAudit && typeof completed.agentAudit === 'object' ? completed.agentAudit : null;
|
||||
var agentRunReceipt = agentAudit && agentAudit.agentRunReceipt && typeof agentAudit.agentRunReceipt === 'object'
|
||||
? agentAudit.agentRunReceipt
|
||||
: null;
|
||||
var changedFiles = pageAiNormalizeArray(agentAudit && agentAudit.changedFiles).map(function(file) {
|
||||
if (!file || typeof file !== 'object') return file;
|
||||
return Object.assign({
|
||||
@@ -3307,26 +3730,10 @@ export function createSidebarPageAiRuntime(context) {
|
||||
traceId: runTraceId,
|
||||
auditId: String(agentAudit.eventId || '')
|
||||
});
|
||||
}
|
||||
if (agentRunReceipt || changedFiles.length) {
|
||||
try {
|
||||
var currentId = currentDocumentId();
|
||||
var currentPath = String(currentId || '').replace(/^local-md:/, '').replace(/~2F/g, '/');
|
||||
var touchesCurrent = changedFiles.some(function(file) {
|
||||
var path = String(file && (file.documentId || file.path || file.filePath || '') || '');
|
||||
return path === currentId || (currentPath && path.indexOf(currentPath) >= 0);
|
||||
});
|
||||
if (touchesCurrent) {
|
||||
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
|
||||
detail: {
|
||||
toolName: 'agent.changed_files',
|
||||
normalizedToolName: 'agent.changed_files',
|
||||
documentId: currentId,
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
runId: runId,
|
||||
traceId: runTraceId,
|
||||
toolCallId: runId + ':agent.changed_files'
|
||||
}
|
||||
}));
|
||||
}
|
||||
pageAiDispatchReceiptRefresh(agentRunReceipt, changedFiles, agentAudit, runId, runTraceId);
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -3447,9 +3854,13 @@ export function createSidebarPageAiRuntime(context) {
|
||||
pageAiLoadProfileMemory: (...args) => pageAiLoadProfileMemory(...args),
|
||||
pageAiLoadSkills: (...args) => pageAiLoadSkills(...args),
|
||||
pageAiSwitchProfile: (...args) => pageAiSwitchProfile(...args),
|
||||
pageAiToggleSkillGroup: (...args) => pageAiToggleSkillGroup(...args),
|
||||
pageAiSetHideHermesBuiltinSkills: (...args) => pageAiSetHideHermesBuiltinSkills(...args),
|
||||
pageAiSetContextScope: (...args) => pageAiSetContextScope(...args),
|
||||
pageAiSetAgentId: (...args) => pageAiSetAgentId(...args),
|
||||
pageAiToggleContextRef: (...args) => pageAiToggleContextRef(...args),
|
||||
pageAiSetContextPopoverOpen: (...args) => pageAiSetContextPopoverOpen(...args),
|
||||
pageAiSetAgentPopoverOpen: (...args) => pageAiSetAgentPopoverOpen(...args),
|
||||
pageAiLoadAllowedRoots: (...args) => pageAiLoadAllowedRoots(...args),
|
||||
updatePageAiTriggerState: (...args) => updatePageAiTriggerState(...args)
|
||||
};
|
||||
|
||||
@@ -59,8 +59,12 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
pageAiProfileMemoryDrafts: { memory: '', user: '', soul: '' },
|
||||
pageAiProfileMemoryError: '',
|
||||
pageAiSkills: { categories: [], archived: [] },
|
||||
pageAiSkillCatalogs: { mnote: { categories: [], archived: [] }, reasonix: { categories: [], archived: [] }, hermes: { categories: [], archived: [] } },
|
||||
pageAiSkillPreferences: {},
|
||||
pageAiCollapsedSkillGroups: {},
|
||||
pageAiSkillQuery: '',
|
||||
pageAiSkillError: '',
|
||||
pageAiSkillLoadSeq: 0,
|
||||
pageAiSessions: [],
|
||||
pageAiActiveSessionId: '',
|
||||
pageAiSessionSearchQuery: '',
|
||||
@@ -2378,9 +2382,12 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
const pageAiLoadProfileMemory = (...args) => sidebarPageAi.pageAiLoadProfileMemory(...args);
|
||||
const pageAiLoadSkills = (...args) => sidebarPageAi.pageAiLoadSkills(...args);
|
||||
const pageAiSwitchProfile = (...args) => sidebarPageAi.pageAiSwitchProfile(...args);
|
||||
const pageAiToggleSkillGroup = (...args) => sidebarPageAi.pageAiToggleSkillGroup(...args);
|
||||
const pageAiSetHideHermesBuiltinSkills = (...args) => sidebarPageAi.pageAiSetHideHermesBuiltinSkills(...args);
|
||||
const pageAiSetContextScope = (...args) => sidebarPageAi.pageAiSetContextScope(...args);
|
||||
const pageAiSetAgentId = (...args) => sidebarPageAi.pageAiSetAgentId(...args);
|
||||
const pageAiToggleContextRef = (...args) => sidebarPageAi.pageAiToggleContextRef(...args);
|
||||
const pageAiSetAgentPopoverOpen = (...args) => sidebarPageAi.pageAiSetAgentPopoverOpen(...args);
|
||||
const updatePageAiTriggerState = (...args) => sidebarPageAi.updatePageAiTriggerState(...args);
|
||||
|
||||
const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({
|
||||
@@ -2593,6 +2600,36 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiAgentButton = closestAction(e.target, '[data-page-ai-agent-button]');
|
||||
if (pageAiAgentButton) {
|
||||
e.preventDefault();
|
||||
pageAiSetAgentPopoverOpen(pageAiAgentButton.getAttribute('aria-expanded') !== 'true');
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiAgentPopoverClose = closestAction(e.target, '[data-page-ai-action="close-agent-popover"]');
|
||||
if (pageAiAgentPopoverClose) {
|
||||
e.preventDefault();
|
||||
pageAiSetAgentPopoverOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiContextButton = closestAction(e.target, '[data-page-ai-context-button]');
|
||||
if (pageAiContextButton) {
|
||||
e.preventDefault();
|
||||
sidebarPageAi.pageAiSetContextPopoverOpen(
|
||||
pageAiContextButton.getAttribute('aria-expanded') !== 'true'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiContextPopoverClose = closestAction(e.target, '[data-page-ai-action="close-context-popover"]');
|
||||
if (pageAiContextPopoverClose) {
|
||||
e.preventDefault();
|
||||
sidebarPageAi.pageAiSetContextPopoverOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiContextRef = closestAction(e.target, '[data-page-ai-context-ref]');
|
||||
if (pageAiContextRef) {
|
||||
e.preventDefault();
|
||||
@@ -2611,8 +2648,17 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
if (pageAiSkillToggle) {
|
||||
e.preventDefault();
|
||||
var skillName = pageAiSkillToggle.getAttribute('data-page-ai-skill-toggle') || '';
|
||||
var skillGroup = pageAiSkillToggle.getAttribute('data-page-ai-skill-group') || '';
|
||||
var skillProfile = pageAiSkillToggle.getAttribute('data-page-ai-skill-profile') || '';
|
||||
var nextEnabled = pageAiSkillToggle.getAttribute('aria-pressed') !== 'true';
|
||||
void pageAiToggleSkill(skillName, nextEnabled);
|
||||
void pageAiToggleSkill(skillName, nextEnabled, skillGroup, skillProfile);
|
||||
return;
|
||||
}
|
||||
|
||||
var pageAiSkillGroupToggle = closestAction(e.target, '[data-page-ai-skill-group-toggle]');
|
||||
if (pageAiSkillGroupToggle) {
|
||||
e.preventDefault();
|
||||
pageAiToggleSkillGroup(pageAiSkillGroupToggle.getAttribute('data-page-ai-skill-group-toggle') || '');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3128,6 +3174,11 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
void pageAiSwitchProfile(pageAiProfileSelect.value);
|
||||
return;
|
||||
}
|
||||
var pageAiHideHermesBuiltin = closestAction(event.target, '[data-page-ai-hide-hermes-builtin]');
|
||||
if (pageAiHideHermesBuiltin instanceof HTMLInputElement) {
|
||||
pageAiSetHideHermesBuiltinSkills(pageAiHideHermesBuiltin.checked);
|
||||
return;
|
||||
}
|
||||
var pageAiContextSelect = closestAction(event.target, '[data-page-ai-context-scope]');
|
||||
if (pageAiContextSelect instanceof HTMLSelectElement) {
|
||||
pageAiSetContextScope(pageAiContextSelect.value);
|
||||
|
||||
@@ -68,6 +68,7 @@ pub struct AcpMnoteToolContext {
|
||||
pub trace_id: Option<String>,
|
||||
pub workspace_id: Option<String>,
|
||||
pub document_id: Option<String>,
|
||||
pub mnote_capabilities: Option<Value>,
|
||||
}
|
||||
|
||||
/// Handler for session events.
|
||||
@@ -567,6 +568,7 @@ impl AcpSessionManager {
|
||||
trace_id: mnote_context.trace_id,
|
||||
workspace_id: mnote_context.workspace_id,
|
||||
document_id: mnote_context.document_id,
|
||||
mnote_capabilities: mnote_context.mnote_capabilities,
|
||||
};
|
||||
|
||||
debug!("ACP session/prompt (session={})", session_id);
|
||||
|
||||
@@ -193,6 +193,8 @@ pub struct SessionPromptParams {
|
||||
pub workspace_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub document_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mnote_capabilities: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::{doc, ToolCallInput};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub async fn context_snapshot(
|
||||
_state: &AppState,
|
||||
_context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let context_refs = input
|
||||
.arg_value("contextRefs")
|
||||
.or_else(|| input.arg_value("context_refs"))
|
||||
.unwrap_or_else(|| json!([]));
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.context_snapshot.v1",
|
||||
"runId": input.run_id,
|
||||
"sessionId": input.session_id,
|
||||
"workspace": {
|
||||
"workspaceId": input.effective_workspace_id(),
|
||||
"sourceKind": input.effective_source_kind(),
|
||||
"rootUri": input.effective_root_uri()
|
||||
},
|
||||
"contextRefs": context_refs,
|
||||
"primaryTarget": {
|
||||
"documentId": input.effective_document_id(),
|
||||
"fileVersion": input.arg_value("fileVersion").or_else(|| input.arg_value("file_version"))
|
||||
},
|
||||
"availableReads": {
|
||||
"currentPage": context_ref_enabled(input, "current_page"),
|
||||
"selection": context_ref_enabled(input, "selection"),
|
||||
"folder": context_ref_enabled(input, "folder"),
|
||||
"changedFiles": context_ref_enabled(input, "changed_files")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn resolve_target(
|
||||
_state: &AppState,
|
||||
_context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.context_target.v1",
|
||||
"workspaceId": input.effective_workspace_id(),
|
||||
"documentId": input.effective_document_id(),
|
||||
"sourceKind": input.effective_source_kind(),
|
||||
"rootUri": input.effective_root_uri(),
|
||||
"relativePath": input.arg_string("relativePath").or_else(|| input.arg_string("relative_path")),
|
||||
"fileVersion": input.arg_value("fileVersion").or_else(|| input.arg_value("file_version")),
|
||||
"contextRefs": input.arg_value("contextRefs").or_else(|| input.arg_value("context_refs")).unwrap_or_else(|| json!([]))
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn read_current_page(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
if !context_ref_enabled(input, "current_page") {
|
||||
return Err(WebError::new(
|
||||
axum::http::StatusCode::FORBIDDEN,
|
||||
"mnote_context_current_page_not_allowed",
|
||||
"本次 run 未授权 current_page 上下文",
|
||||
)
|
||||
.with_context(context));
|
||||
}
|
||||
doc::doc_fetch(state, context, input).await
|
||||
}
|
||||
|
||||
fn context_ref_enabled(input: &ToolCallInput, expected: &str) -> bool {
|
||||
input
|
||||
.arg_value("contextRefs")
|
||||
.or_else(|| input.arg_value("context_refs"))
|
||||
.and_then(|value| value.as_array().cloned())
|
||||
.map(|items| {
|
||||
items.iter().any(|item| {
|
||||
item.as_str() == Some(expected)
|
||||
|| item.get("kind").and_then(Value::as_str).map(str::trim) == Some(expected)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn input_with_refs(context_refs: Value) -> ToolCallInput {
|
||||
ToolCallInput {
|
||||
tool_name: "mnote.context.read_current_page".into(),
|
||||
workspace_id: Some("ws_1".into()),
|
||||
document_id: Some("doc_1".into()),
|
||||
source_kind: Some("local_folder".into()),
|
||||
root_uri: Some("file:///tmp/mnote".into()),
|
||||
actor_id: Some("user_1".into()),
|
||||
profile: None,
|
||||
session_id: Some("sess_1".into()),
|
||||
run_id: Some("run_1".into()),
|
||||
tool_call_id: Some("tool_1".into()),
|
||||
trace_id: Some("trace_1".into()),
|
||||
idempotency_key: None,
|
||||
dry_run: None,
|
||||
capability_scope: Some(vec!["context.read".into()]),
|
||||
args: Some(json!({ "contextRefs": context_refs })),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_ref_enabled_accepts_string_and_object_refs() {
|
||||
assert!(context_ref_enabled(
|
||||
&input_with_refs(json!(["current_page"])),
|
||||
"current_page"
|
||||
));
|
||||
assert!(context_ref_enabled(
|
||||
&input_with_refs(json!([{ "kind": "current_page" }])),
|
||||
"current_page"
|
||||
));
|
||||
assert!(!context_ref_enabled(
|
||||
&input_with_refs(json!(["selection"])),
|
||||
"current_page"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@ pub fn manifest() -> Value {
|
||||
"writeOwner": "rust-runtime-kernel"
|
||||
},
|
||||
"tools": [
|
||||
skill_read_tool(),
|
||||
context_snapshot_tool(),
|
||||
context_read_current_page_tool(),
|
||||
context_resolve_target_tool(),
|
||||
doc_fetch_tool(),
|
||||
doc_find_tool(),
|
||||
block_fetch_tool(),
|
||||
@@ -37,6 +41,95 @@ pub fn manifest() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn skill_read_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("skillId".into(), json!({ "type": "string" }));
|
||||
map.insert("agentId".into(), json!({ "type": "string" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.skill.read",
|
||||
"description": "按需读取 MNote skill 正文。默认 prompt 只列 skill 摘要,正文必须通过本工具懒加载。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["skill.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["skillId"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn context_snapshot_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert(
|
||||
"contextRefs".into(),
|
||||
json!({ "type": "array", "items": { "type": ["string", "object"] } }),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.context.snapshot",
|
||||
"description": "返回本次 run 可用的 MNote 上下文摘要,不返回页面正文全文。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["context.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn context_read_current_page_tool() -> Value {
|
||||
let mut tool = doc_fetch_tool();
|
||||
if let Value::Object(map) = &mut tool {
|
||||
map.insert(
|
||||
"name".into(),
|
||||
Value::String("mnote.context.read_current_page".into()),
|
||||
);
|
||||
map.insert(
|
||||
"description".into(),
|
||||
Value::String("在 current_page contextRef 被授权时读取当前 Markdown 页面。".into()),
|
||||
);
|
||||
map.insert(
|
||||
"capabilityScope".into(),
|
||||
json!(["context.read", "page.read"]),
|
||||
);
|
||||
}
|
||||
tool
|
||||
}
|
||||
|
||||
fn context_resolve_target_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert(
|
||||
"contextRefs".into(),
|
||||
json!({ "type": "array", "items": { "type": ["string", "object"] } }),
|
||||
);
|
||||
map.insert("relativePath".into(), json!({ "type": "string" }));
|
||||
map.insert(
|
||||
"fileVersion".into(),
|
||||
json!({ "type": ["string", "object"] }),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.context.resolve_target",
|
||||
"description": "解析当前 Page AI run 的工作区、文档、rootUri、relativePath 与 file version。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["context.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn base_identity_properties() -> Value {
|
||||
json!({
|
||||
"workspaceId": { "type": "string" },
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
pub mod artifact;
|
||||
pub mod block;
|
||||
pub mod context_tools;
|
||||
pub mod doc;
|
||||
pub mod manifest;
|
||||
pub mod page;
|
||||
pub mod resource;
|
||||
pub mod skill;
|
||||
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::ToolCallInput;
|
||||
use axum::http::StatusCode;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct MnoteSkill {
|
||||
pub id: &'static str,
|
||||
pub title: &'static str,
|
||||
pub description: &'static str,
|
||||
pub agent_ids: &'static [&'static str],
|
||||
pub read_only: bool,
|
||||
pub requires_context_refs: &'static [&'static str],
|
||||
pub tool_names: &'static [&'static str],
|
||||
pub content: &'static str,
|
||||
}
|
||||
|
||||
const SKILLS: &[MnoteSkill] = &[
|
||||
MnoteSkill {
|
||||
id: "mnote-current-page",
|
||||
title: "MNote current page",
|
||||
description: "Read the current MNote Markdown page only when the task needs page content.",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: true,
|
||||
requires_context_refs: &["current_page"],
|
||||
tool_names: &[
|
||||
"mnote.context.snapshot",
|
||||
"mnote.context.resolve_target",
|
||||
"mnote.context.read_current_page",
|
||||
],
|
||||
content: include_str!("../../../../../skills/mnote-current-page/SKILL.md"),
|
||||
},
|
||||
MnoteSkill {
|
||||
id: "mnote-local-file",
|
||||
title: "MNote local file editing",
|
||||
description: "Read and patch local Markdown files inside MNote allowed roots.",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: false,
|
||||
requires_context_refs: &["current_page", "file", "folder"],
|
||||
tool_names: &["mnote.context.snapshot", "mnote.context.resolve_target"],
|
||||
content: include_str!("../../../../../skills/mnote-local-file/SKILL.md"),
|
||||
},
|
||||
MnoteSkill {
|
||||
id: "mnote-chat-only",
|
||||
title: "MNote chat only",
|
||||
description: "Reply conversationally without reading or writing MNote page/file context.",
|
||||
agent_ids: &["chat_only", "hermes", "reasonix"],
|
||||
read_only: true,
|
||||
requires_context_refs: &[],
|
||||
tool_names: &[],
|
||||
content: include_str!("../../../../../skills/mnote-chat-only/SKILL.md"),
|
||||
},
|
||||
];
|
||||
|
||||
pub fn skill_summaries_for_agent(agent_id: Option<&str>) -> Vec<Value> {
|
||||
SKILLS
|
||||
.iter()
|
||||
.filter(|skill| skill_matches_agent(skill, agent_id))
|
||||
.map(skill_summary)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn find_skill(skill_id: &str, agent_id: Option<&str>) -> Option<&'static MnoteSkill> {
|
||||
let requested = skill_id.trim();
|
||||
SKILLS
|
||||
.iter()
|
||||
.find(|skill| skill.id == requested && skill_matches_agent(skill, agent_id))
|
||||
}
|
||||
|
||||
pub async fn skill_read(
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let skill_id = input
|
||||
.arg_string("skillId")
|
||||
.or_else(|| input.arg_string("skill_id"))
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("mnote_skill_id_required", "缺少 skillId")
|
||||
.with_context(context)
|
||||
})?;
|
||||
let agent_id = input
|
||||
.arg_string("agentId")
|
||||
.or_else(|| input.arg_string("agent_id"));
|
||||
let skill = find_skill(&skill_id, agent_id.as_deref()).ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
"mnote_skill_not_found",
|
||||
"未知或当前 agent 不可用的 MNote skill",
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.skill.v1",
|
||||
"skill": skill_summary(skill),
|
||||
"content": skill.content,
|
||||
"tools": skill.tool_names,
|
||||
"constraints": {
|
||||
"allowedRootsRequired": skill.requires_context_refs.contains(&"folder"),
|
||||
"mustReadBackAfterWrite": !skill.read_only
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn skill_matches_agent(skill: &MnoteSkill, agent_id: Option<&str>) -> bool {
|
||||
let Some(agent_id) = agent_id.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return true;
|
||||
};
|
||||
skill
|
||||
.agent_ids
|
||||
.iter()
|
||||
.any(|candidate| *candidate == agent_id)
|
||||
}
|
||||
|
||||
fn skill_summary(skill: &MnoteSkill) -> Value {
|
||||
json!({
|
||||
"id": skill.id,
|
||||
"title": skill.title,
|
||||
"description": skill.description,
|
||||
"agentIds": skill.agent_ids,
|
||||
"readOnly": skill.read_only,
|
||||
"requiresContextRefs": skill.requires_context_refs,
|
||||
"toolNames": skill.tool_names
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn skill_registry_filters_by_agent() {
|
||||
let chat_skills = skill_summaries_for_agent(Some("chat_only"));
|
||||
assert!(chat_skills
|
||||
.iter()
|
||||
.any(|skill| skill["id"] == "mnote-chat-only"));
|
||||
assert!(!chat_skills
|
||||
.iter()
|
||||
.any(|skill| skill["id"] == "mnote-local-file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_lookup_rejects_unknown_or_agent_mismatch() {
|
||||
assert!(find_skill("mnote-current-page", Some("reasonix")).is_some());
|
||||
assert!(find_skill("mnote-local-file", Some("chat_only")).is_none());
|
||||
assert!(find_skill("missing", Some("reasonix")).is_none());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,9 @@ use super::hermes_client;
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::{artifact, block, doc, manifest, page, resource, ToolCallInput};
|
||||
use crate::hermes_tools::{
|
||||
artifact, block, context_tools, doc, manifest, page, resource, skill, ToolCallInput,
|
||||
};
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::Json;
|
||||
@@ -347,6 +349,14 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
return Ok(cached);
|
||||
}
|
||||
let result = match input.tool_name.as_str() {
|
||||
"mnote.skill.read" => skill::skill_read(&context, &input).await,
|
||||
"mnote.context.snapshot" => context_tools::context_snapshot(&state, &context, &input).await,
|
||||
"mnote.context.read_current_page" => {
|
||||
context_tools::read_current_page(&state, &context, &input).await
|
||||
}
|
||||
"mnote.context.resolve_target" => {
|
||||
context_tools::resolve_target(&state, &context, &input).await
|
||||
}
|
||||
"mnote.doc.fetch" => doc::doc_fetch(&state, &context, &input).await,
|
||||
"mnote.doc.find" => doc::doc_find(&state, &context, &input).await,
|
||||
"mnote.doc.plan_update" => doc::plan_update(&state, &context, &input).await,
|
||||
@@ -538,6 +548,10 @@ fn is_read_tool(tool_name: &str) -> bool {
|
||||
matches!(
|
||||
tool_name,
|
||||
"mnote.page.get"
|
||||
| "mnote.skill.read"
|
||||
| "mnote.context.snapshot"
|
||||
| "mnote.context.read_current_page"
|
||||
| "mnote.context.resolve_target"
|
||||
| "mnote.doc.fetch"
|
||||
| "mnote.doc.find"
|
||||
| "mnote.block.fetch"
|
||||
@@ -1359,6 +1373,80 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_context_read_current_page_reads_local_markdown() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-context-read-current-page-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(&root).expect("root");
|
||||
fs::write(
|
||||
root.join("README.md"),
|
||||
"# 当前页\n\n来自 mnote.context.read_current_page 的正文。\n",
|
||||
)
|
||||
.expect("markdown");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
crate::routes::local_folder_source::initialize_local_workspace_for_actor(
|
||||
"user_1", &root_uri,
|
||||
)
|
||||
.expect("workspace");
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.context.read_current_page",
|
||||
"workspaceId": "local-ws-context",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"profile": "reasonix",
|
||||
"actorId": "user_1",
|
||||
"sessionId": "sess_context_read",
|
||||
"runId": "run_context_read",
|
||||
"toolCallId": "call_context_read",
|
||||
"traceId": "trace_context_read",
|
||||
"args": {
|
||||
"format": "markdown",
|
||||
"contextRefs": [{ "kind": "current_page" }],
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "read_write",
|
||||
"allowedRoots": [{ "rootUri": root_uri, "permission": "write" }],
|
||||
"allowedResourceIds": ["local-md:README.md"]
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
let status = response.status();
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(status, StatusCode::OK, "{payload}");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["result"]["ok"], true);
|
||||
assert!(
|
||||
payload["result"]["content"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("mnote.context.read_current_page"),
|
||||
"{payload}"
|
||||
);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_mindmap_fetch_reads_authorized_local_resource() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -3791,6 +3791,9 @@ body {
|
||||
.wolai-page-ai-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
@@ -3800,6 +3803,13 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-icon-svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.wolai-page-ai-icon:hover,
|
||||
.wolai-page-ai-icon.is-active {
|
||||
background: #F3F3F2;
|
||||
@@ -3878,7 +3888,8 @@ body {
|
||||
|
||||
.wolai-page-ai-profile-select,
|
||||
.wolai-page-ai-context-select,
|
||||
.wolai-page-ai-skill-search {
|
||||
.wolai-page-ai-skill-search,
|
||||
.wolai-page-ai-agent-profile {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
@@ -3886,7 +3897,8 @@ body {
|
||||
|
||||
.wolai-page-ai-profile-select select,
|
||||
.wolai-page-ai-context-select select,
|
||||
.wolai-page-ai-skill-search input {
|
||||
.wolai-page-ai-skill-search input,
|
||||
.wolai-page-ai-agent-profile select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 34px;
|
||||
@@ -3898,6 +3910,10 @@ body {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-profile {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-inline-error {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(198, 80, 80, 0.18);
|
||||
@@ -3943,23 +3959,15 @@ body {
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-session-button {
|
||||
.wolai-page-ai-session-summary {
|
||||
min-width: 0;
|
||||
max-width: 190px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #5A5A5A;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-session-button:hover {
|
||||
color: #1B1C1C;
|
||||
}
|
||||
|
||||
.wolai-page-ai-settings-head {
|
||||
@@ -4171,10 +4179,29 @@ body {
|
||||
}
|
||||
|
||||
.wolai-page-ai-skills-toolbar {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(124px, 160px) max-content;
|
||||
align-items: end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-filter-toggle {
|
||||
display: inline-flex;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #5A5A5A;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-filter-toggle input {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-list {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
@@ -4184,6 +4211,45 @@ body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group-head {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 8px 2px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #8B8782;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group-head:hover {
|
||||
background: #F7F7F6;
|
||||
color: #5A5A5A;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group-title {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group-chevron {
|
||||
width: 12px;
|
||||
color: #AAA6A0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-row {
|
||||
display: flex;
|
||||
min-height: 38px;
|
||||
@@ -4385,6 +4451,8 @@ button.wolai-page-ai-message-text {
|
||||
}
|
||||
|
||||
.wolai-page-ai-footer {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@@ -4412,16 +4480,16 @@ button.wolai-page-ai-message-text {
|
||||
}
|
||||
|
||||
.wolai-page-ai-composer {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 10px;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-selector,
|
||||
.wolai-page-ai-context-refs,
|
||||
.wolai-page-ai-allowed-roots {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -4430,6 +4498,123 @@ button.wolai-page-ai-message-text {
|
||||
padding: 6px 8px 0;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-picker,
|
||||
.wolai-page-ai-context-picker {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-button,
|
||||
.wolai-page-ai-context-button {
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-button[aria-expanded="true"],
|
||||
.wolai-page-ai-context-button[aria-expanded="true"] {
|
||||
border-color: rgba(27, 28, 28, 0.32);
|
||||
background: #F7F6F4;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-popover,
|
||||
.wolai-page-ai-context-popover {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: min(320px, calc(100vw - 44px));
|
||||
bottom: calc(100% + 8px);
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 10px;
|
||||
background: #FFF;
|
||||
box-shadow: 0 16px 36px rgba(27, 28, 28, 0.16);
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-popover[hidden],
|
||||
.wolai-page-ai-context-popover[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-popover-head,
|
||||
.wolai-page-ai-context-popover-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
color: #5A5A5A;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.08);
|
||||
border-radius: 8px;
|
||||
background: #FFF;
|
||||
color: #1B1C1C;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option.is-active {
|
||||
border-color: rgba(27, 28, 28, 0.2);
|
||||
background: #F7F6F4;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option-detail {
|
||||
color: #8B8782;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option {
|
||||
display: grid;
|
||||
grid-template-columns: 16px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
color: #1B1C1C;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option.is-disabled {
|
||||
color: #AAA6A0;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option-main {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option-detail {
|
||||
color: #8B8782;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wolai-page-ai-allowed-roots {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user