feat(page-ai): add resumable run journal descriptors
This commit is contained in:
@@ -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>,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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"));
|
||||
|
||||
Reference in New Issue
Block a user