feat(page-ai): add resumable run journal descriptors

This commit is contained in:
lix-2026
2026-06-09 18:48:46 +08:00
parent 6ef233772e
commit 922965d30f
17 changed files with 3254 additions and 58 deletions
+6
View File
@@ -414,6 +414,12 @@ pub struct AiRuntimeEventRecord {
pub created_at: Timestamp,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AiRuntimeJournalEventRecord {
pub seq: i64,
pub event: AiRuntimeEventRecord,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AppendAiRuntimeEventInput {
pub id: Option<EntityId>,
+110 -10
View File
@@ -10,15 +10,15 @@ use crate::migrations;
use crate::model::{
password_hash_v1, share_token_hash_v1, AiAgentProfileAccessRecord, AiAgentProfileGrantRecord,
AiAgentProfileRecord, AiExternalConversationBindingRecord, AiPolicyRecord,
AiRuntimeEventRecord, AiRuntimeRunRecord, AppendAiRuntimeEventInput, AppendAuditInput,
AuditLogRecord, AuthSessionRecord, AuthenticatePasswordInput, CreatePasswordIdentityInput,
CreateSessionInput, CreateShareLinkInput, CreatedShareLink, DirectoryGrantInput,
DirectoryGrantLookup, DirectoryGrantRecord, NavigationRecentRecord, OutboxEventInput,
OutboxEventRecord, ResolvedAccess, ResolvedAuthSession, ShareLinkRecord, SidebarShortcutRecord,
SyncStateRecord, UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput,
UpsertAiRuntimeRunInput, UpsertNavigationRecentInput, UpsertSidebarShortcutInput,
UpsertSyncStateInput, UpsertUserInput, UpsertUserUiPreferenceInput, UserRecord,
UserUiPreferenceRecord, WorkspaceRecord,
AiRuntimeEventRecord, AiRuntimeJournalEventRecord, AiRuntimeRunRecord,
AppendAiRuntimeEventInput, AppendAuditInput, AuditLogRecord, AuthSessionRecord,
AuthenticatePasswordInput, CreatePasswordIdentityInput, CreateSessionInput,
CreateShareLinkInput, CreatedShareLink, DirectoryGrantInput, DirectoryGrantLookup,
DirectoryGrantRecord, NavigationRecentRecord, OutboxEventInput, OutboxEventRecord,
ResolvedAccess, ResolvedAuthSession, ShareLinkRecord, SidebarShortcutRecord, SyncStateRecord,
UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput, UpsertAiRuntimeRunInput,
UpsertNavigationRecentInput, UpsertSidebarShortcutInput, UpsertSyncStateInput, UpsertUserInput,
UpsertUserUiPreferenceInput, UserRecord, UserUiPreferenceRecord, WorkspaceRecord,
};
use crate::store::ControlPlaneStore;
@@ -424,6 +424,27 @@ fn row_to_ai_runtime_event(row: &rusqlite::Row<'_>) -> rusqlite::Result<AiRuntim
})
}
fn row_to_ai_runtime_journal_event(
row: &rusqlite::Row<'_>,
) -> rusqlite::Result<AiRuntimeJournalEventRecord> {
Ok(AiRuntimeJournalEventRecord {
seq: row.get(0)?,
event: AiRuntimeEventRecord {
id: row.get(1)?,
user_id: row.get(2)?,
workspace_id: row.get(3)?,
document_id: row.get(4)?,
session_id: row.get(5)?,
run_id: row.get(6)?,
profile: row.get(7)?,
acp_runtime: row.get(8)?,
event_type: row.get(9)?,
payload_json: row.get(10)?,
created_at: row.get(11)?,
},
})
}
fn row_to_ai_external_conversation_binding(
row: &rusqlite::Row<'_>,
) -> rusqlite::Result<AiExternalConversationBindingRecord> {
@@ -2355,6 +2376,24 @@ impl ControlPlaneStore for SqliteControlPlaneStore {
Ok(rows)
}
fn find_ai_runtime_run(
&self,
user_id: &str,
run_id: &str,
) -> Result<Option<AiRuntimeRunRecord>, ControlPlaneError> {
let conn = self.conn.lock().unwrap();
conn.query_row(
"SELECT id, user_id, workspace_id, document_id, session_id, run_id, title, profile, acp_runtime, trace_id, status, runtime_json, payload_json, deleted_at, created_at, updated_at, revision
FROM ai_runtime_runs
WHERE user_id = ?1 AND run_id = ?2 AND deleted_at IS NULL
LIMIT 1",
params![user_id, run_id],
row_to_ai_runtime_run,
)
.optional()
.map_err(ControlPlaneError::from)
}
fn append_ai_runtime_event(
&self,
input: AppendAiRuntimeEventInput,
@@ -2427,6 +2466,40 @@ impl ControlPlaneStore for SqliteControlPlaneStore {
Ok(rows)
}
fn list_ai_runtime_journal_events(
&self,
user_id: &str,
run_id: &str,
after_seq: i64,
limit: usize,
) -> Result<Vec<AiRuntimeJournalEventRecord>, ControlPlaneError> {
let conn = self.conn.lock().unwrap();
let mut stmt = conn.prepare(
"WITH ordered AS (
SELECT
ROW_NUMBER() OVER (ORDER BY created_at ASC, id ASC) AS seq,
id, user_id, workspace_id, document_id, session_id, run_id, profile,
acp_runtime, event_type, payload_json, created_at
FROM ai_runtime_events
WHERE user_id = ?1 AND run_id = ?2
)
SELECT seq, id, user_id, workspace_id, document_id, session_id, run_id, profile,
acp_runtime, event_type, payload_json, created_at
FROM ordered
WHERE seq > ?3
ORDER BY seq ASC
LIMIT ?4",
)?;
let rows = stmt
.query_map(
params![user_id, run_id, after_seq.max(0), limit.max(1) as i64],
row_to_ai_runtime_journal_event,
)?
.collect::<Result<Vec<_>, _>>()
.map_err(ControlPlaneError::from)?;
Ok(rows)
}
fn upsert_ai_external_conversation_binding(
&self,
input: UpsertAiExternalConversationBindingInput,
@@ -3666,12 +3739,39 @@ mod tests {
})
.expect("append runtime event");
assert_eq!(event.event_type, "message.delta");
store
.append_ai_runtime_event(AppendAiRuntimeEventInput {
id: None,
user_id: "ai_runtime_user".to_string(),
workspace_id: Some("ws_1".to_string()),
document_id: Some("doc_1".to_string()),
session_id: "sess_1".to_string(),
run_id: "run_1".to_string(),
profile: "reasonix".to_string(),
acp_runtime: "reasonix".to_string(),
event_type: "run.completed".to_string(),
payload_json: "{\"status\":\"completed\"}".to_string(),
})
.expect("append second runtime event");
let events = store
.list_ai_runtime_events("ai_runtime_user", "run_1", 10)
.expect("list runtime events");
assert_eq!(events.len(), 1);
assert_eq!(events.len(), 2);
assert_eq!(events[0].payload_json, "{\"text\":\"hello\"}");
let found = store
.find_ai_runtime_run("ai_runtime_user", "run_1")
.expect("find runtime run")
.expect("runtime run exists");
assert_eq!(found.status, "completed");
let journal_events = store
.list_ai_runtime_journal_events("ai_runtime_user", "run_1", 1, 10)
.expect("list runtime journal events");
assert_eq!(journal_events.len(), 1);
assert_eq!(journal_events[0].seq, 2);
assert_eq!(journal_events[0].event.event_type, "run.completed");
}
#[test]
+23 -9
View File
@@ -3,15 +3,15 @@
use crate::error::ControlPlaneError;
use crate::model::{
AiAgentProfileAccessRecord, AiExternalConversationBindingRecord, AiPolicyRecord,
AiRuntimeEventRecord, AiRuntimeRunRecord, AppendAiRuntimeEventInput, AppendAuditInput,
AuditLogRecord, AuthSessionRecord, AuthenticatePasswordInput, CreatePasswordIdentityInput,
CreateSessionInput, CreateShareLinkInput, CreatedShareLink, DirectoryGrantInput,
DirectoryGrantLookup, DirectoryGrantRecord, NavigationRecentRecord, OutboxEventInput,
OutboxEventRecord, ResolvedAccess, ResolvedAuthSession, ShareLinkRecord, SidebarShortcutRecord,
SyncStateRecord, UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput,
UpsertAiRuntimeRunInput, UpsertNavigationRecentInput, UpsertSidebarShortcutInput,
UpsertSyncStateInput, UpsertUserInput, UpsertUserUiPreferenceInput, UserRecord,
UserUiPreferenceRecord, WorkspaceRecord,
AiRuntimeEventRecord, AiRuntimeJournalEventRecord, AiRuntimeRunRecord,
AppendAiRuntimeEventInput, AppendAuditInput, AuditLogRecord, AuthSessionRecord,
AuthenticatePasswordInput, CreatePasswordIdentityInput, CreateSessionInput,
CreateShareLinkInput, CreatedShareLink, DirectoryGrantInput, DirectoryGrantLookup,
DirectoryGrantRecord, NavigationRecentRecord, OutboxEventInput, OutboxEventRecord,
ResolvedAccess, ResolvedAuthSession, ShareLinkRecord, SidebarShortcutRecord, SyncStateRecord,
UpsertAiExternalConversationBindingInput, UpsertAiPolicyInput, UpsertAiRuntimeRunInput,
UpsertNavigationRecentInput, UpsertSidebarShortcutInput, UpsertSyncStateInput, UpsertUserInput,
UpsertUserUiPreferenceInput, UserRecord, UserUiPreferenceRecord, WorkspaceRecord,
};
pub trait ControlPlaneStore: Send + Sync {
@@ -208,6 +208,12 @@ pub trait ControlPlaneStore: Send + Sync {
limit: usize,
) -> Result<Vec<AiRuntimeRunRecord>, ControlPlaneError>;
fn find_ai_runtime_run(
&self,
user_id: &str,
run_id: &str,
) -> Result<Option<AiRuntimeRunRecord>, ControlPlaneError>;
fn append_ai_runtime_event(
&self,
input: AppendAiRuntimeEventInput,
@@ -220,6 +226,14 @@ pub trait ControlPlaneStore: Send + Sync {
limit: usize,
) -> Result<Vec<AiRuntimeEventRecord>, ControlPlaneError>;
fn list_ai_runtime_journal_events(
&self,
user_id: &str,
run_id: &str,
after_seq: i64,
limit: usize,
) -> Result<Vec<AiRuntimeJournalEventRecord>, ControlPlaneError>;
fn upsert_ai_external_conversation_binding(
&self,
input: UpsertAiExternalConversationBindingInput,
@@ -132,7 +132,7 @@ export function createSidebarPageAiProfileRuntime(context) {
function pageAiSessionAgentLabel(session) {
var agentId = pageAiNormalizeAgentId(session && session.agentId);
if (agentId === 'reasonix') return 'Reasonix';
if (agentId === 'reasonix') return pageAiAgentRecord(agentId).label || 'Reasonix';
var profileId = String(session && (session.profileId || session.profile) || '').trim();
var profile = pageAiProfileRecordById(profileId) || { profileId: profileId, name: profileId };
var chatOnlySpec = pageAiChatOnlyProfileSpec(profile);
@@ -6,6 +6,7 @@ export function createSidebarPageAiRenderRuntime(context) {
documentRef,
escapeHtml,
localMarkdownRelativePathFromPageAiDocumentId,
pageAiAgentDescriptor,
pageAiAgentRecord,
pageAiAllSkillEntries,
pageAiBuildAllowedRoots,
@@ -18,6 +19,8 @@ export function createSidebarPageAiRenderRuntime(context) {
pageAiCurrentSkillSource,
pageAiCurrentSkillSourceLabel,
pageAiDefaultChatOnlyProfileSpec,
pageAiDescriptorFieldOptions,
pageAiDescriptorFieldValue,
pageAiEditorTargetCandidates,
pageAiEnsureContextRefState,
pageAiFilteredHistoryRows,
@@ -125,7 +128,9 @@ export function createSidebarPageAiRenderRuntime(context) {
function pageAiCurrentAgentSelectionLabel() {
var agentId = pageAiCurrentAgentId();
if (agentId === 'reasonix') return 'Reasonix';
var descriptor = pageAiAgentDescriptor(agentId);
var descriptorLabel = String(descriptor && (descriptor.displayName || descriptor.label) || '').trim();
if (agentId === 'reasonix') return descriptorLabel || 'Reasonix';
var profile = pageAiCurrentProfileRecord();
var chatOnlySpec = pageAiChatOnlyProfileSpec(profile || pageAiCurrentProfile());
if (agentId === 'chat_only') {
@@ -134,8 +139,8 @@ export function createSidebarPageAiRenderRuntime(context) {
if (chatOnlySpec) {
return 'ChatOnly / ' + chatOnlySpec.label;
}
if (agentId === 'hermes') return 'Hermes / ' + pageAiProfileDisplayLabel(profile, pageAiCurrentProfile());
return pageAiAgentRecord(agentId).label;
if (agentId === 'hermes') return (descriptorLabel || 'Hermes') + ' / ' + pageAiProfileDisplayLabel(profile, pageAiCurrentProfile());
return descriptorLabel || pageAiAgentRecord(agentId).label;
}
function pageAiRenderAgentProfileOption(agentId, profile, label, detail) {
@@ -148,6 +153,59 @@ export function createSidebarPageAiRenderRuntime(context) {
'</button>';
}
function pageAiAgentDescriptorDetail(descriptor) {
if (!descriptor) return '';
var capabilities = pageAiNormalizeArray(descriptor.capabilities);
var toolCount = Number(descriptor.toolCount || pageAiNormalizeArray(descriptor.tools).length || 0);
var parts = [];
parts.push(descriptor.canWriteFiles === false ? '只读/聊天' : '可写授权文件');
if (capabilities.length) parts.push('capability ' + capabilities.length);
if (toolCount) parts.push('tool ' + toolCount);
return parts.join(' · ');
}
function pageAiRenderDescriptorField(field) {
var key = String(field && field.key || '').trim();
if (!key || field.secret === true) return '';
var label = String(field.label || key).trim();
var kind = String(field.kind || 'string').trim();
var value = pageAiDescriptorFieldValue(field);
var options = pageAiDescriptorFieldOptions(field);
var control = '';
if (kind === 'select' || options.length) {
control = '<select data-page-ai-descriptor-field="' + escapeHtml(key) + '" data-page-ai-descriptor-kind="select">' +
options.map(function(option) {
return '<option value="' + escapeHtml(option.value) + '"' + (String(value) === String(option.value) ? ' selected' : '') + '>' + escapeHtml(option.label) + '</option>';
}).join('') +
'</select>';
} else if (kind === 'boolean' || kind === 'toggle') {
control = '<input type="checkbox" data-page-ai-descriptor-field="' + escapeHtml(key) + '" data-page-ai-descriptor-kind="boolean"' + (value === true ? ' checked' : '') + ' />';
} else {
control = '<input type="text" value="' + escapeHtml(value == null ? '' : String(value)) + '" data-page-ai-descriptor-field="' + escapeHtml(key) + '" data-page-ai-descriptor-kind="' + escapeHtml(kind || 'string') + '" />';
}
return '' +
'<label class="wolai-page-ai-profile-select" data-page-ai-descriptor-field-row="' + escapeHtml(key) + '">' +
'<span>' + escapeHtml(label) + '</span>' +
control +
'</label>';
}
function pageAiRenderAgentDescriptorCard(agentId, fallbackTitle) {
var descriptor = pageAiAgentDescriptor(agentId) || pageAiAgentRecord(agentId);
var title = String(descriptor && (descriptor.displayName || descriptor.label) || fallbackTitle || agentId).trim();
var fields = pageAiNormalizeArray(descriptor && descriptor.fields).map(pageAiRenderDescriptorField).filter(Boolean).join('');
var disabled = pageAiNormalizeArray(descriptor && descriptor.disabledCapabilities);
var disabledText = disabled.length ? '<div class="wolai-page-ai-tool-meta">disabled: ' + escapeHtml(disabled.join(', ')) + '</div>' : '';
return '' +
'<section class="wolai-page-ai-memory-card" data-page-ai-agent-descriptor-card="' + escapeHtml(agentId) + '">' +
'<div class="wolai-page-ai-memory-head">' +
'<div><div class="wolai-page-ai-memory-title">' + escapeHtml(title) + '</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(pageAiAgentDescriptorDetail(descriptor) || 'descriptor fallback') + '</div></div>' +
'</div>' +
disabledText +
(fields ? '<div class="wolai-page-ai-settings-grid">' + fields + '</div>' : '<div class="wolai-page-ai-tool-meta">当前 agent 没有可配置字段。</div>') +
'</section>';
}
function pageAiCurrentModelLabel() {
var profile = pageAiCurrentProfileRecord();
var toolModel = pageAiMnoteToolModel();
@@ -277,6 +335,9 @@ export function createSidebarPageAiRenderRuntime(context) {
return pageAiRenderAgentProfileOption('hermes', profile, pageAiProfileDisplayLabel(profile, profileId), scope);
}).join('');
var reasonixActive = activeAgentId === 'reasonix';
var reasonixDescriptor = pageAiAgentDescriptor('reasonix');
var reasonixLabel = String(reasonixDescriptor && (reasonixDescriptor.displayName || reasonixDescriptor.label) || 'Reasonix').trim() || 'Reasonix';
var reasonixDetail = pageAiAgentDescriptorDetail(reasonixDescriptor) || '可读取并在授权目录内写文件';
agentPopover.innerHTML = '' +
'<div class="wolai-page-ai-context-popover-head">' +
'<strong>选择 Agent</strong>' +
@@ -294,8 +355,8 @@ export function createSidebarPageAiRenderRuntime(context) {
'<div class="wolai-page-ai-agent-popover-title">Reasonix</div>' +
'<div class="wolai-page-ai-agent-option-list">' +
'<button type="button" class="wolai-page-ai-agent-option' + (reasonixActive ? ' is-active' : '') + '" data-page-ai-agent-id="reasonix" aria-pressed="' + (reasonixActive ? 'true' : 'false') + '">' +
'<span class="wolai-page-ai-agent-option-label">Reasonix</span>' +
'<span class="wolai-page-ai-agent-option-detail">可读取并在授权目录内写文件</span>' +
'<span class="wolai-page-ai-agent-option-label">' + escapeHtml(reasonixLabel) + '</span>' +
'<span class="wolai-page-ai-agent-option-detail">' + escapeHtml(reasonixDetail) + '</span>' +
'</button>' +
'</div>' +
'</div>';
@@ -414,9 +475,11 @@ export function createSidebarPageAiRenderRuntime(context) {
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
if (agentPanel instanceof HTMLElement) {
agentPanel.innerHTML = '' +
(pageUiState.pageAiAgentDescriptorError ? '<div class="wolai-page-ai-inline-error">' + escapeHtml(pageUiState.pageAiAgentDescriptorError) + '</div>' : '') +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">授权区域</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(pageAiBuildAllowedRoots().length ? 'SQLite directory_grants' : '需要授权') + '</div></div>' +
'</section>' +
pageAiRenderAgentDescriptorCard(activeAgentId, pageAiCurrentAgentSelectionLabel()) +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">默认上下文</div><div class="wolai-page-ai-memory-scope">ContextRefs</div></div>' +
'</section>' +
@@ -512,7 +575,7 @@ export function createSidebarPageAiRenderRuntime(context) {
}
var hermesMemoryPanel = drawer.querySelector('[data-page-ai-hermes-panel]');
if (hermesMemoryPanel instanceof HTMLElement) {
hermesMemoryPanel.innerHTML = ['soul', 'user', 'memory'].map(function(section) {
hermesMemoryPanel.innerHTML = pageAiRenderAgentDescriptorCard('hermes', 'Hermes') + ['soul', 'user', 'memory'].map(function(section) {
var label = pageAiMemoryFileLabel(section);
var value = pageUiState.pageAiProfileMemoryDrafts[section] || '';
return '' +
@@ -713,6 +776,7 @@ export function createSidebarPageAiRenderRuntime(context) {
var reasonixPanel = drawer.querySelector('[data-page-ai-reasonix-panel]');
if (reasonixPanel instanceof HTMLElement) {
reasonixPanel.innerHTML = '' +
pageAiRenderAgentDescriptorCard('reasonix', 'Reasonix') +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head">' +
'<div><div class="wolai-page-ai-memory-title">Reasonix 专属设置</div><div class="wolai-page-ai-memory-scope">ACP runtime / memory</div></div>' +
@@ -723,7 +787,8 @@ export function createSidebarPageAiRenderRuntime(context) {
}
var chatOnlyPanel = drawer.querySelector('[data-page-ai-chat-only-panel]');
if (chatOnlyPanel instanceof HTMLElement) {
chatOnlyPanel.innerHTML = '<section class="wolai-page-ai-memory-card"><div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">Chat-only</div><div class="wolai-page-ai-memory-scope">默认不申请文件写权限</div></div></section>';
chatOnlyPanel.innerHTML = pageAiRenderAgentDescriptorCard('chat_only', 'Chat-only') +
'<section class="wolai-page-ai-memory-card"><div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">Chat-only</div><div class="wolai-page-ai-memory-scope">默认不申请文件写权限</div></div></section>';
}
}
@@ -25,11 +25,12 @@ export function createSidebarPageAiRuntime(context) {
// Page AI 请求合同:agentId 只负责路由;contextRefs 是用户勾选的上下文地址;
// allowedRoots 只展示 SQLite directory_grants 解析结果,服务端仍会按当前用户重算;
// runTargetSnapshot 必须来自 OpenEditorsSnapshot,避免运行中 UI 切换导致目标漂移。
var PAGE_AI_AGENT_REGISTRY = [
var PAGE_AI_AGENT_FALLBACK_REGISTRY = [
{ id: 'hermes', label: 'Hermes', acpRuntime: 'hermes', canWriteFiles: true },
{ id: 'reasonix', label: 'Reasonix', acpRuntime: 'reasonix', canWriteFiles: true },
{ id: 'chat_only', label: 'Chat-only', acpRuntime: 'hermes', canWriteFiles: false }
];
var PAGE_AI_DESCRIPTOR_SCHEMA = 'mnote.ai_agent_descriptor.v1';
var PAGE_AI_CHAT_ONLY_PROFILE_REGISTRY = [
{ profileId: 'shared_deepseek_chat', baseProfile: 'deepseek-chat', label: 'DeepSeek' },
{ profileId: 'shared_doubao_chat', baseProfile: 'doubao-chat', label: '豆包' },
@@ -151,6 +152,9 @@ export function createSidebarPageAiRuntime(context) {
pageAiAllowedRootsError: '',
pageAiAgentId: 'reasonix',
pageAiAgentPopoverOpen: false,
pageAiAgentDescriptors: [],
pageAiAgentDescriptorError: '',
pageAiAgentDescriptorsLoaded: false,
pageAiTools: [],
pageAiToolsError: '',
pageAiGatewayHealth: null,
@@ -354,14 +358,45 @@ export function createSidebarPageAiRuntime(context) {
};
}
function pageAiNormalizeAgentDescriptor(descriptor) {
if (!descriptor || typeof descriptor !== 'object') return null;
var agentId = String(descriptor.agentId || descriptor.id || '').trim();
if (!agentId) return null;
return Object.assign({}, descriptor, {
id: agentId,
agentId: agentId,
label: String(descriptor.displayName || descriptor.label || agentId).trim() || agentId,
acpRuntime: String(descriptor.acpRuntime || descriptor.acp_runtime || '').trim() || (agentId === 'reasonix' ? 'reasonix' : 'hermes'),
canWriteFiles: descriptor.canWriteFiles !== false,
capabilities: pageAiNormalizeArray(descriptor.capabilities),
fields: pageAiNormalizeArray(descriptor.fields),
tools: pageAiNormalizeArray(descriptor.tools),
capabilityPacks: pageAiNormalizeArray(descriptor.capabilityPacks),
schema: String(descriptor.schema || PAGE_AI_DESCRIPTOR_SCHEMA).trim()
});
}
function pageAiAgentRegistry() {
var descriptors = pageAiNormalizeArray(pageUiState.pageAiAgentDescriptors)
.map(pageAiNormalizeAgentDescriptor)
.filter(Boolean);
return descriptors.length ? descriptors : PAGE_AI_AGENT_FALLBACK_REGISTRY;
}
function pageAiAgentDescriptor(agentId) {
var normalized = String(agentId || '').trim();
if (!normalized) normalized = pageAiCurrentAgentId();
return pageAiAgentRegistry().find(function(agent) { return agent.id === normalized || agent.agentId === normalized; }) || null;
}
function pageAiNormalizeAgentId(agentId) {
var value = String(agentId || '').trim();
return PAGE_AI_AGENT_REGISTRY.some(function(agent) { return agent.id === value; }) ? value : 'reasonix';
return pageAiAgentRegistry().some(function(agent) { return agent.id === value; }) ? value : 'reasonix';
}
function pageAiAgentRecord(agentId) {
var normalized = pageAiNormalizeAgentId(agentId);
return PAGE_AI_AGENT_REGISTRY.find(function(agent) { return agent.id === normalized; }) || PAGE_AI_AGENT_REGISTRY[1];
return pageAiAgentRegistry().find(function(agent) { return agent.id === normalized; }) || pageAiAgentRegistry()[1] || PAGE_AI_AGENT_FALLBACK_REGISTRY[1];
}
function pageAiCurrentAgentId() {
@@ -460,6 +495,7 @@ export function createSidebarPageAiRuntime(context) {
const pageAiRenameBackendSession = (...args) => pageAiSessionRuntime.pageAiRenameBackendSession(...args);
const pageAiDeleteBackendSession = (...args) => pageAiSessionRuntime.pageAiDeleteBackendSession(...args);
const pageAiResumeBackendSession = (...args) => pageAiSessionRuntime.pageAiResumeBackendSession(...args);
const pageAiCheckActiveRun = (...args) => pageAiSessionRuntime.pageAiCheckActiveRun(...args);
const pageAiSkillRuntime = createSidebarPageAiSkillRuntime({
pageAiCurrentAgentId,
pageAiCurrentProfile,
@@ -561,6 +597,7 @@ export function createSidebarPageAiRuntime(context) {
documentRef: document,
escapeHtml,
localMarkdownRelativePathFromPageAiDocumentId,
pageAiAgentDescriptor,
pageAiAgentRecord,
pageAiAllSkillEntries,
pageAiBuildAllowedRoots,
@@ -573,6 +610,8 @@ export function createSidebarPageAiRuntime(context) {
pageAiCurrentSkillSource,
pageAiCurrentSkillSourceLabel,
pageAiDefaultChatOnlyProfileSpec,
pageAiDescriptorFieldOptions,
pageAiDescriptorFieldValue,
pageAiEditorTargetCandidates,
pageAiEnsureContextRefState,
pageAiFilteredHistoryRows,
@@ -611,6 +650,7 @@ export function createSidebarPageAiRuntime(context) {
var record = pageAiAgentRecord(next);
pageUiState.pageAiAgentId = next;
pageUiState.pageAiAcpRuntime = record.acpRuntime || 'reasonix';
if (record.tools) pageUiState.pageAiTools = record.tools;
pageUiState.pageAiAgentPopoverOpen = next === 'hermes';
if (next === 'hermes') pageAiSetActiveProfile(pageAiCurrentProfile());
if (next === 'chat_only') pageAiSetActiveProfile(pageAiNormalizeChatOnlyProfileId(pageAiCurrentProfile()));
@@ -692,6 +732,114 @@ export function createSidebarPageAiRuntime(context) {
} catch (_) {}
}
function pageAiDescriptorPreferenceValues() {
return pageUiState.pageAiSkillPreferences && typeof pageUiState.pageAiSkillPreferences === 'object'
? pageUiState.pageAiSkillPreferences
: {};
}
function pageAiDescriptorFieldValue(field) {
var key = String(field && field.key || '').trim();
var preferences = pageAiDescriptorPreferenceValues();
if (key && Object.prototype.hasOwnProperty.call(preferences, key)) return preferences[key];
return field && Object.prototype.hasOwnProperty.call(field, 'default') ? field.default : '';
}
function pageAiDescriptorFieldOptions(field) {
var source = String(field && field.optionsSource || '').trim();
if (source === 'hermes_profiles') {
return pageAiHermesProfileEntries().map(function(profile) {
var value = pageAiProfileValue(profile);
return {
value: value,
label: pageAiProfileDisplayLabel(profile, value)
};
}).filter(function(option) { return option.value; });
}
if (source === 'chat_only_profiles') {
return pageAiChatOnlyProfileEntries().map(function(profile) {
var value = pageAiProfileValue(profile);
return {
value: value,
label: profile.menuLabel || pageAiProfileDisplayLabel(profile, value)
};
}).filter(function(option) { return option.value; });
}
return pageAiNormalizeArray(field && field.options).map(function(option) {
if (option && typeof option === 'object') {
var value = String(option.value || option.id || option.key || '').trim();
return {
value: value,
label: String(option.label || option.title || value).trim() || value
};
}
var value = String(option || '').trim();
return { value: value, label: value };
}).filter(function(option) { return option.value; });
}
function pageAiSetDescriptorFieldValue(key, value) {
var normalizedKey = String(key || '').trim();
if (!normalizedKey) return;
pageUiState.pageAiSkillPreferences = Object.assign({}, pageAiDescriptorPreferenceValues(), {
[normalizedKey]: value
});
pageAiPersistRawAiPreference(normalizedKey, value);
if (normalizedKey === 'ai.agent.hermes.profile_id') {
pageAiSetActiveProfile(value);
void pageAiLoadAgentDescriptors();
void pageAiLoadSkills();
void pageAiLoadTools();
}
if (normalizedKey === 'ai.agent.chat_only.model_id' && pageAiCurrentAgentId() === 'chat_only') {
pageAiSetActiveProfile(pageAiNormalizeChatOnlyProfileId(value));
}
renderPageAiControls();
}
function pageAiApplyAgentDescriptorsPayload(payload) {
var descriptors = pageAiNormalizeArray(payload && payload.descriptors)
.map(pageAiNormalizeAgentDescriptor)
.filter(function(descriptor) { return descriptor.schema === PAGE_AI_DESCRIPTOR_SCHEMA; });
if (!descriptors.length) return false;
pageUiState.pageAiAgentDescriptors = descriptors;
pageUiState.pageAiAgentDescriptorsLoaded = true;
pageUiState.pageAiAgentDescriptorError = '';
if (payload && payload.preferenceValues && typeof payload.preferenceValues === 'object') {
pageUiState.pageAiSkillPreferences = Object.assign(
{},
pageAiDescriptorPreferenceValues(),
payload.preferenceValues
);
}
var current = pageAiCurrentAgentId();
var record = pageAiAgentRecord(current);
pageUiState.pageAiAgentId = record.id;
pageUiState.pageAiAcpRuntime = record.acpRuntime || pageUiState.pageAiAcpRuntime || 'reasonix';
if (record.tools) pageUiState.pageAiTools = record.tools;
return true;
}
async function pageAiLoadAgentDescriptors() {
try {
var url = new URL('/api/page-ai/agents/descriptors', window.location.origin);
url.searchParams.set('profile', pageAiCurrentProfile());
var response = await fetch(url.toString(), {
headers: { accept: 'application/json' },
cache: 'no-store'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'agent_descriptors_failed_' + response.status));
if (!pageAiApplyAgentDescriptorsPayload(payload)) throw new Error('agent_descriptors_empty');
} catch (error) {
pageUiState.pageAiAgentDescriptorError = error instanceof Error ? error.message : String(error);
pageUiState.pageAiAgentDescriptorsLoaded = false;
}
renderPageAiProviderButtons();
renderPageAiControls();
return pageUiState.pageAiAgentDescriptors;
}
async function pageAiLoadAiPreferences() {
try {
var url = new URL('/api/ui/preferences/effective', window.location.origin);
@@ -711,6 +859,7 @@ export function createSidebarPageAiRuntime(context) {
var agent = pageAiAgentRecord(defaultAgent);
pageUiState.pageAiAgentId = agent.id;
pageUiState.pageAiAcpRuntime = agent.acpRuntime || pageUiState.pageAiAcpRuntime || 'reasonix';
if (agent.tools) pageUiState.pageAiTools = agent.tools;
}
var hermesProfile = String(preferences['ai.agent.hermes.profile_id'] || '').trim();
if (hermesProfile) pageAiSetActiveProfile(hermesProfile);
@@ -1136,6 +1285,7 @@ export function createSidebarPageAiRuntime(context) {
if (pageUiState.pageAiCurrentRunId) {
document.documentElement.setAttribute('data-mnote-page-ai-run-id', pageUiState.pageAiCurrentRunId);
}
pageAiUpdateActiveRunSnapshotFromStatus(pageUiState.pageAiRunStatus, pageUiState.pageAiCurrentRunId);
}
function pageAiApplyRuntimeState(runtime) {
@@ -1157,6 +1307,74 @@ export function createSidebarPageAiRuntime(context) {
}
}
function pageAiActiveRunSnapshotKey() {
return pageAiStorageKey() + ':active-run';
}
function pageAiReadActiveRunSnapshot() {
try {
var raw = window.localStorage ? window.localStorage.getItem(pageAiActiveRunSnapshotKey()) : '';
return raw ? JSON.parse(raw) : null;
} catch (_) {
return null;
}
}
function pageAiWriteActiveRunSnapshot(snapshot) {
if (!snapshot || !snapshot.hostRunId) return;
try {
window.localStorage.setItem(pageAiActiveRunSnapshotKey(), JSON.stringify(Object.assign({
schema: 'mnote.page_ai_active_run_snapshot.v1',
createdAt: Date.now()
}, snapshot, {
updatedAt: Date.now()
})));
document.documentElement.setAttribute('data-mnote-page-ai-active-run-snapshot', 'true');
} catch (_) {}
}
function pageAiClearActiveRunSnapshot(runId) {
var existing = pageAiReadActiveRunSnapshot();
if (runId && existing && existing.hostRunId && existing.hostRunId !== runId) return;
try {
if (window.localStorage) window.localStorage.removeItem(pageAiActiveRunSnapshotKey());
document.documentElement.removeAttribute('data-mnote-page-ai-active-run-snapshot');
} catch (_) {}
}
function pageAiRunStatusIsTerminal(status) {
return ['completed', 'failed', 'aborted', 'cancelled', 'canceled'].indexOf(String(status || '').trim()) >= 0;
}
function pageAiUpdateActiveRunSnapshotFromStatus(status, runId) {
var id = String(runId || '').trim();
if (!id) return;
if (pageAiRunStatusIsTerminal(status)) {
pageAiClearActiveRunSnapshot(id);
return;
}
if (['running', 'tool_calling', 'queued', 'pending', 'interrupted', 'acp_pending'].indexOf(String(status || '').trim()) < 0) return;
var previous = pageAiReadActiveRunSnapshot();
var lastSeq = previous && previous.hostRunId === id ? String(previous.lastSeq || '000000000000000000') : '000000000000000000';
pageAiWriteActiveRunSnapshot({
hostRunId: id,
sessionId: pageUiState.pageAiActiveSessionId || '',
lastSeq: lastSeq,
status: status || 'running'
});
}
async function pageAiCheckAndResumeActiveRun() {
var snapshot = pageAiReadActiveRunSnapshot();
var sessionId = snapshot && snapshot.sessionId ? String(snapshot.sessionId || '').trim() : '';
var run = await pageAiCheckActiveRun(sessionId || undefined);
if (run && sessionId && pageUiState.pageAiActiveSessionId !== sessionId) {
pageAiSetActiveSession(sessionId);
}
if (run) await pageAiResumeActiveRunJournal(run);
return run;
}
function pageAiApplyQueuedRun(payload) {
if (!payload || payload.queued !== true) return false;
var queueId = String(payload.queueId || payload.queue_id || '').trim();
@@ -1540,6 +1758,13 @@ export function createSidebarPageAiRuntime(context) {
async function pageAiLoadTools() {
try {
var descriptor = pageAiAgentDescriptor(pageAiCurrentAgentId());
if (descriptor && pageUiState.pageAiAgentDescriptorsLoaded) {
pageUiState.pageAiTools = pageAiNormalizeArray(descriptor.tools);
pageUiState.pageAiToolsError = '';
renderPageAiControls();
return;
}
var response = await fetch('/api/hermes/client/tools?scope=mnote&profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
@@ -1646,12 +1871,14 @@ export function createSidebarPageAiRuntime(context) {
var active = pageAiProfileValue(pageUiState.pageAiProfiles.find(function(profile) { return profile.active; }));
pageAiSetActiveProfile(hasCurrent && current !== 'default' ? current : (active || pageAiProfileValue(personal) || pageAiProfileValue(pageUiState.pageAiProfiles[0]) || current));
pageUiState.pageAiProfileError = '';
void pageAiLoadAgentDescriptors();
void pageAiLoadGatewayHealth();
} catch (error) {
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
if (!pageUiState.pageAiProfiles.length) pageUiState.pageAiProfiles = [{ profileId: 'shared_lite', name: 'shared_lite', alias: 'Lite', kind: 'shared', readonly: true, canManageSkills: false }];
pageUiState.pageAiAcpRuntimes = pageAiNormalizeAcpRuntimes(pageUiState.pageAiAcpRuntimes);
pageAiSetActiveProfile(pageAiCurrentProfile());
void pageAiLoadAgentDescriptors();
}
renderPageAiProviderButtons();
renderPageAiControls();
@@ -1683,6 +1910,7 @@ export function createSidebarPageAiRuntime(context) {
pageAiPersistSessions();
await pageAiEnsureHermesSession(true);
await pageAiLoadProfileMemory();
await pageAiLoadAgentDescriptors();
await pageAiLoadSkills();
await pageAiLoadTools();
await pageAiLoadGatewayHealth();
@@ -1907,9 +2135,20 @@ export function createSidebarPageAiRuntime(context) {
drawer.hidden = false;
pageUiState.pageAiOpen = true;
updatePageAiTriggerState();
void pageAiCheckAndResumeActiveRun().catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
void pageAiLoadBackendSessions().then(function() {
return pageAiCheckAndResumeActiveRun();
}).catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
Promise.all([
pageAiLoadProfiles(),
pageAiLoadProfileMemory(),
pageAiLoadAgentDescriptors(),
pageAiLoadSkills(),
pageAiLoadTools(),
pageAiLoadGatewayHealth(),
@@ -1918,6 +2157,12 @@ export function createSidebarPageAiRuntime(context) {
pageAiLoadBackendSessions()
]).then(function() {
renderPageAiControls();
}).then(function() {
return pageAiCheckAndResumeActiveRun().catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
return null;
});
}).then(function() {
return pageAiRestoreHermesSession();
}).then(function() {
@@ -1983,6 +2228,27 @@ export function createSidebarPageAiRuntime(context) {
}
}
function pageAiEventSeqFromPayloadText(payloadText) {
try {
var payload = JSON.parse(payloadText || 'null');
return String((payload && payload.seq) || '').trim();
} catch (_) {
return '';
}
}
function pageAiTrackStreamingRunEvent(runId, eventName, payloadText) {
var seq = pageAiEventSeqFromPayloadText(payloadText);
if (!seq) return;
pageAiWriteActiveRunSnapshot({
hostRunId: runId,
sessionId: pageUiState.pageAiActiveSessionId || '',
lastSeq: seq,
status: pageAiRunStatusIsTerminal(eventName) ? pageUiState.pageAiRunStatus : (pageUiState.pageAiRunStatus || 'running')
});
document.documentElement.setAttribute('data-mnote-page-ai-active-run-last-seq', seq);
}
function pageAiEnsureStreamingAssistantMessage(runId) {
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
var existing = pageUiState.pageAiMessages.find(function(item) {
@@ -2318,6 +2584,90 @@ export function createSidebarPageAiRuntime(context) {
});
}
function pageAiJournalPayloadText(event) {
try {
return JSON.stringify(event && event.payload && typeof event.payload === 'object' ? event.payload : {});
} catch (_) {
return '{}';
}
}
function pageAiLastUserPrompt() {
var messages = pageAiNormalizeArray(pageUiState.pageAiMessages);
for (var index = messages.length - 1; index >= 0; index -= 1) {
if (messages[index] && messages[index].role === 'user') return String(messages[index].content || '');
}
return '';
}
function pageAiApplyRunJournalEvent(event, runId, runTraceId) {
if (!event || typeof event !== 'object') return;
var eventName = String(event.kind || event.eventType || event.event || '').trim();
var payload = event.payload && typeof event.payload === 'object' ? event.payload : {};
var payloadText = pageAiJournalPayloadText(event);
var prompt = pageAiLastUserPrompt();
if (eventName === 'message.delta') {
pageAiAppendStreamingAssistantDelta(runId, pageAiDecodeDeltaText(payloadText));
} else if (eventName === 'run.completed') {
pageAiFinishStreamingAssistantMessage(runId, String(payload.output || ''), prompt, []);
pageAiSetRunStatus('completed', runId);
} else if (eventName === 'run.failed') {
pageAiFinishStreamingAssistantMessage(runId, String(payload.message || payload.code || payload.error || 'AI run failed'), prompt, []);
pageAiSetRunStatus('failed', runId);
} else if (eventName === 'run.aborted' || eventName === 'run.cancelled' || eventName === 'run.canceled') {
pageUiState.pageAiStoppedRunIds[runId] = true;
pageAiSetRunStatus('aborted', runId);
} else if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
var toolItem = pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId);
if (eventName === 'tool.completed') {
var citations = pageAiCollectKnowledgeRagCitations(toolItem, payload);
if (citations.length) pageAiFinishStreamingAssistantMessage(runId, '', prompt, citations);
}
pageAiSetRunStatus('tool_calling', runId);
} else if (eventName === 'plan.updated') {
var planEntries = Array.isArray(payload.entries) ? payload.entries : [];
if (planEntries.length) {
pageUiState.pageAiMessages.push({ role: 'system', kind: 'plan', entries: planEntries, createdAt: Date.now(), updatedAt: Date.now() });
}
}
var seq = String(event.seq || '').trim();
if (seq) {
pageAiWriteActiveRunSnapshot({
hostRunId: runId,
sessionId: pageUiState.pageAiActiveSessionId || String(event.sessionId || ''),
lastSeq: seq,
status: pageUiState.pageAiRunStatus || 'running'
});
document.documentElement.setAttribute('data-mnote-page-ai-active-run-last-seq', seq);
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
}
async function pageAiResumeActiveRunJournal(run) {
if (!run || typeof run !== 'object') return;
var runId = String(run.hostRunId || run.runId || '').trim();
if (!runId) return;
var snapshot = pageAiReadActiveRunSnapshot();
var afterSeq = snapshot && snapshot.hostRunId === runId ? String(snapshot.lastSeq || '000000000000000000') : '000000000000000000';
var response = await fetch('/api/page-ai/runs/' + encodeURIComponent(runId) + '/events?afterSeq=' + encodeURIComponent(afterSeq), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'run_journal_failed_' + response.status));
}
var runTraceId = String((payload.run && payload.run.traceId) || (run && run.traceId) || '');
pageAiNormalizeArray(payload.events).forEach(function(event) {
pageAiApplyRunJournalEvent(event, runId, runTraceId);
});
var status = String((payload.run && payload.run.status) || run.status || '').trim();
if (pageAiRunStatusIsTerminal(status)) pageAiClearActiveRunSnapshot(runId);
document.documentElement.setAttribute('data-mnote-page-ai-run-journal-resumed', runId);
}
function pageAiLooksLikeBlockEdit(prompt) {
var text = searchText(prompt);
return ['新增', '添加', '插入', '删除', '删掉', '修改', '替换', '改成', '移动', '移到', 'move', 'replace', 'delete', 'insert'].some(function(word) {
@@ -2537,6 +2887,7 @@ export function createSidebarPageAiRuntime(context) {
var assistantText = '';
var autoCitations = [];
await streamPageAiResponse(eventResponse, function(eventName, payloadText) {
pageAiTrackStreamingRunEvent(runId, eventName, payloadText);
if (eventName === 'message.delta') {
if (pageUiState.pageAiStoppedRunIds[runId]) return;
assistantText = pageAiAppendStreamingAssistantDelta(runId, pageAiDecodeDeltaText(payloadText));
@@ -3002,6 +3353,16 @@ export function createSidebarPageAiRuntime(context) {
function handlePageAiChange(event, helpers) {
var closestAction = helpers && helpers.closestAction;
if (typeof closestAction !== 'function') return false;
var pageAiDescriptorField = closestAction(event.target, '[data-page-ai-descriptor-field]');
if (pageAiDescriptorField instanceof HTMLInputElement || pageAiDescriptorField instanceof HTMLSelectElement) {
var key = pageAiDescriptorField.getAttribute('data-page-ai-descriptor-field') || '';
var kind = pageAiDescriptorField.getAttribute('data-page-ai-descriptor-kind') || '';
var value = kind === 'boolean' && pageAiDescriptorField instanceof HTMLInputElement
? pageAiDescriptorField.checked
: pageAiDescriptorField.value;
pageAiSetDescriptorFieldValue(key, value);
return true;
}
var pageAiAcpRuntimeSelect = closestAction(event.target, '[data-page-ai-acp-runtime]');
if (pageAiAcpRuntimeSelect instanceof HTMLSelectElement) {
var next = String(pageAiAcpRuntimeSelect.value || 'reasonix').trim() || 'reasonix';
@@ -3074,6 +3435,13 @@ export function createSidebarPageAiRuntime(context) {
document.addEventListener('change', function(event) {
handlePageAiChange(event, helpers);
});
document.addEventListener('visibilitychange', function() {
if (document.visibilityState !== 'visible' || !pageUiState.pageAiOpen) return;
void pageAiCheckAndResumeActiveRun().catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
});
}
@@ -627,6 +627,37 @@ export function createSidebarPageAiSessionRuntime(context) {
renderPageAiControls();
}
async function pageAiCheckActiveRun(sessionId) {
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return null;
var response = await fetch('/api/page-ai/sessions/' + encodeURIComponent(sessionId) + '/active-run?' + pageAiBackendSessionQuery({}), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'active_run_failed_' + response.status));
}
if (!payload.active || !payload.run) return null;
var run = payload.run;
var hostRunId = String(run.hostRunId || run.runId || '').trim();
var status = String(run.status || '').trim();
var current = pageAiCurrentSession();
if (current && current.id === sessionId) {
current.runId = hostRunId;
current.status = status || current.status || 'running';
current.updatedAt = Date.now();
}
pageAiApplyRuntimeState(Object.assign({}, run.runtime || {}, {
status: status || 'running',
runId: hostRunId
}));
doc.documentElement.setAttribute('data-mnote-page-ai-active-run-checked', 'true');
if (hostRunId) doc.documentElement.setAttribute('data-mnote-page-ai-active-host-run-id', hostRunId);
pageAiPersistSessions();
renderPageAiControls();
return run;
}
return {
pageAiStorageKey,
pageAiBackendSessionQuery,
@@ -650,6 +681,7 @@ export function createSidebarPageAiSessionRuntime(context) {
pageAiStartNewSession,
pageAiRenameBackendSession,
pageAiDeleteBackendSession,
pageAiResumeBackendSession
pageAiResumeBackendSession,
pageAiCheckActiveRun
};
}
File diff suppressed because it is too large Load Diff
+17
View File
@@ -496,6 +496,23 @@ pub fn build_router(state: AppState) -> Router {
"/api/onlyoffice/bridge/capabilities",
get(onlyoffice_bridge::capabilities),
)
.route(
"/api/page-ai/agents/descriptors",
get(hermes_client::list_agent_descriptors),
)
.route("/api/page-ai/runs", post(hermes_client::create_page_ai_run))
.route(
"/api/page-ai/runs/{host_run_id}",
get(hermes_client::get_page_ai_run),
)
.route(
"/api/page-ai/runs/{host_run_id}/events",
get(hermes_client::list_page_ai_run_events),
)
.route(
"/api/page-ai/sessions/{session_id}/active-run",
get(hermes_client::get_page_ai_session_active_run),
)
.route(
"/api/onlyoffice/bridge/commands",
post(onlyoffice_bridge::enqueue_command),
@@ -229,6 +229,8 @@ mod tests {
include_str!("../../../browser/sidebar-workspace-runtime.js");
const SIDEBAR_PAGE_TREE_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-tree-runtime.js");
// Browser Page AI runtime is served as a module; keep this include explicit so
// resume/journal contract checks inspect the actual shipped JS.
const SIDEBAR_PAGE_AI_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-runtime.js");
const SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS: &str =
@@ -693,6 +695,9 @@ mod tests {
SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiLoadBackendSessionDetail")
);
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiSearchBackendSessions"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("function pageAiCheckActiveRun"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("/api/page-ai/sessions/"));
assert!(SIDEBAR_PAGE_AI_SESSION_RUNTIME_JS.contains("/active-run?"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-rename"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-delete"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-session-resume"));
@@ -776,6 +781,22 @@ mod tests {
assert!(SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS.contains("target=\"_blank\""));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function handlePageAiChange"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function installPageAiDelegates"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("document.addEventListener('visibilitychange'"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiCheckActiveRun(sessionId || undefined)"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiCheckAndResumeActiveRun"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiResumeActiveRunJournal"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiTrackStreamingRunEvent"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("void pageAiCheckAndResumeActiveRun().catch"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("mnote.page_ai_active_run_snapshot.v1"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-mnote-page-ai-active-run-last-seq"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("/api/page-ai/runs/"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("afterSeq="));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiCheckAndResumeActiveRun()"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("/api/page-ai/agents/descriptors"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiLoadAgentDescriptors"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("data-page-ai-descriptor-field"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("function pageAiRenderDescriptorField"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-agent-descriptor-card"));
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sidebarPageAi.installPageAiDelegates()"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("sidebarPageAi.handlePageAi"));
assert!(!SIDEBAR_TREE_RUNTIME_JS.contains("closestAction(e.target, '[data-page-ai-action"));