feat: stabilize page AI ACP runtimes

实现并稳定页面 AI 的 ACP Hermes / ACP Reasonix 运行路径。

主要内容:

- 分离 profile 与 acpRuntime,ACP Hermes 按所选 Hermes profile 启动并注入 provider key。

- 修复 Reasonix ACP wrapper 的 API key 读取、ToolRegistry 注册、LoopEvent role 映射和 reasoning/final 分流。

- 修复 ACP agent_thought_chunk 被 untagged enum 误解析为 message.delta 的问题,补充 thought 相关单测。

- 补充页面 AI 浏览器验证 skill 证据到 7-15 设计稿,并记录严格验收标准。

- 同步提交当前仓库中已存在的 rust-web / Hermes tools / SSE / bug 文档相关改动。

验证:

- node --check scripts/reasonix-acp-wrapper.mjs

- cargo test -p mnote-web acp -- --nocapture

- 页面 AI ACP 浏览器验证:tmp/page-ai-acp-browser-UAYwyM/
This commit is contained in:
lix-2026
2026-05-17 20:11:39 +08:00
parent 2ea559beaa
commit bb2f190f50
19 changed files with 1307 additions and 451 deletions
+33 -13
View File
@@ -4117,6 +4117,10 @@ const SIDEBAR_TREE_JS: &str = r##"
return pageAiProfileValue(selected) || 'mnoteai';
}
function pageAiRunProfile() {
return String(pageUiState.pageAiAcpRuntime || '').trim() === 'reasonix' ? 'reasonix' : pageAiCurrentProfile();
}
function pageAiMnoteToolModel() {
return String(document.documentElement.getAttribute('data-mnote-page-ai-tool-model') || 'deepseek-v4-flash').trim() || 'deepseek-v4-flash';
}
@@ -4570,7 +4574,8 @@ const SIDEBAR_TREE_JS: &str = r##"
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiCurrentProfile(),
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || '',
reason: 'page_ai_user_stop'
})
});
@@ -4610,6 +4615,11 @@ const SIDEBAR_TREE_JS: &str = r##"
var source = String(skill && skill.source || '').trim();
if (source === 'hub') return '';
if (source === 'builtin') return '';
if (source === 'reasonix') {
if (origin === 'project') return 'Reasonix ';
if (origin === 'global') return 'Reasonix ';
return 'Reasonix';
}
return '';
}
@@ -4716,7 +4726,11 @@ const SIDEBAR_TREE_JS: &str = r##"
async function pageAiLoadSkills() {
try {
var response = await fetch('/api/hermes/client/skills?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
var runtime = String(pageUiState.pageAiAcpRuntime || '').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; });
@@ -4734,6 +4748,7 @@ const SIDEBAR_TREE_JS: &str = r##"
async function pageAiToggleSkill(skillName, enabled) {
var name = String(skillName || '').trim();
if (!name) return;
if (String(pageUiState.pageAiAcpRuntime || '').trim() === 'reasonix') return;
var previous = null;
pageAiSkillListEntries().forEach(function(skill) {
if (skill.name === name && previous == null) previous = skill.enabled !== false;
@@ -4828,7 +4843,7 @@ const SIDEBAR_TREE_JS: &str = r##"
function renderPageAiControls() {
var drawer = ensurePageAiDrawer();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
var activeProfile = isAcp ? pageUiState.pageAiAcpRuntime : pageAiCurrentProfile();
var activeProfile = pageAiRunProfile();
drawer.setAttribute('data-page-ai-page', pageUiState.pageAiPage || 'chat');
drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || '');
// Populate ACP runtime dropdown
@@ -4844,7 +4859,7 @@ const SIDEBAR_TREE_JS: &str = r##"
// Show/hide Hermes-specific profile select
var profileLabel = drawer.querySelector('[data-page-ai-hermes-profile]');
if (profileLabel instanceof HTMLElement) {
profileLabel.style.display = isAcp ? 'none' : '';
profileLabel.style.display = pageUiState.pageAiAcpRuntime === 'reasonix' ? 'none' : '';
}
// When ACP is selected, populate agent panel with ACP runtime info
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
@@ -4982,12 +4997,16 @@ const SIDEBAR_TREE_JS: &str = r##"
if (skillList instanceof HTMLElement) {
var skills = pageAiFilteredSkillEntries();
if (!skills.length) {
skillList.innerHTML = '<div class="wolai-page-ai-empty"> Hermes skill</div>';
var emptyText = String(pageUiState.pageAiAcpRuntime || '').trim() === 'reasonix'
? ' Reasonix skill'
: ' Hermes skill';
skillList.innerHTML = '<div class="wolai-page-ai-empty">' + escapeHtml(emptyText) + '</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 || '').trim() !== 'reasonix';
return '' +
'<div class="wolai-page-ai-skill-row">' +
'<div class="wolai-page-ai-skill-copy">' +
@@ -4997,7 +5016,7 @@ const SIDEBAR_TREE_JS: &str = r##"
'</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') + '">' +
'<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>' +
'</button>' +
'</div>';
@@ -5515,7 +5534,8 @@ const SIDEBAR_TREE_JS: &str = r##"
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageUiState.pageAiAcpRuntime || pageAiCurrentProfile(),
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || '',
contextScope: pageUiState.pageAiContextScope,
message: prompt,
model: pageAiMnoteToolModel(),
@@ -6557,7 +6577,10 @@ const SIDEBAR_TREE_JS: &str = r##"
if (pageAiAcpRuntimeSelect instanceof HTMLSelectElement) {
var next = String(pageAiAcpRuntimeSelect.value || '').trim();
pageUiState.pageAiAcpRuntime = next;
pageUiState.pageAiSkills = { categories: [], archived: [] };
pageUiState.pageAiSkillError = '';
void pageAiLoadProfiles();
void pageAiLoadSkills();
renderPageAiControls();
renderPageAiProviderButtons();
return;
@@ -7326,12 +7349,9 @@ mod tests {
assert!(SIDEBAR_TREE_JS.contains(
r#".tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title"#
));
assert!(SIDEBAR_TREE_JS.contains(
r#"[data-page-title-input="true"][data-document-id="' + escaped + '"]"#
));
assert!(SIDEBAR_TREE_JS.contains(
r#".wolai-breadcrumb-current [data-page-title-current]"#
));
assert!(SIDEBAR_TREE_JS
.contains(r#"[data-page-title-input="true"][data-document-id="' + escaped + '"]"#));
assert!(SIDEBAR_TREE_JS.contains(r#".wolai-breadcrumb-current [data-page-title-current]"#));
assert!(!SIDEBAR_TREE_JS.contains(
r#".tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));