@yeaft/webchat-agent 1.0.649 → 1.0.650
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/connection/index.js +3 -0
- package/connection/message-router.js +6 -1
- package/index.js +1 -1
- package/local-runtime/server/db/connection.js +5 -2
- package/local-runtime/server/db/yeaft-project-db.js +2 -0
- package/local-runtime/server/db/yeaft-session-db.js +4 -1
- package/local-runtime/server/handlers/agent-output.js +48 -8
- package/local-runtime/server/handlers/client-conversation.js +14 -2
- package/local-runtime/server/handlers/client-scheduled-tasks.js +121 -0
- package/local-runtime/server/session-catalog.js +1 -0
- package/local-runtime/server/ws-client.js +1 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +421 -281
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/sessions/scheduled-task-definition.js +74 -0
- package/yeaft/sessions/scheduled-task-scheduler.js +257 -0
- package/yeaft/sessions/session-crud.js +5 -0
- package/yeaft/sessions/session-store.js +18 -10
- package/yeaft/web-bridge.js +100 -20
package/connection/index.js
CHANGED
|
@@ -4,8 +4,10 @@ import { sendToServer, parseMessage } from './buffer.js';
|
|
|
4
4
|
import { startAgentHeartbeat, stopAgentHeartbeat, scheduleReconnect } from './heartbeat.js';
|
|
5
5
|
import { handleMessage } from './message-router.js';
|
|
6
6
|
import { cleanupTerminalsForDisconnect } from '../terminal.js';
|
|
7
|
+
import { invalidateYeaftProjectContexts } from '../yeaft/web-bridge.js';
|
|
7
8
|
|
|
8
9
|
export function resetConnectionTransport() {
|
|
10
|
+
invalidateYeaftProjectContexts();
|
|
9
11
|
ctx.sessionKey = null;
|
|
10
12
|
ctx.serverEncryptionRequired = true;
|
|
11
13
|
ctx.serverCapabilities = new Set();
|
|
@@ -106,6 +108,7 @@ export function connect(WebSocketImpl = WebSocket) {
|
|
|
106
108
|
socket.on('close', (code, reason) => {
|
|
107
109
|
if (socket !== ctx.ws) return;
|
|
108
110
|
console.log(`Disconnected from server: ${code} ${reason}`);
|
|
111
|
+
invalidateYeaftProjectContexts();
|
|
109
112
|
ctx.sessionKey = null;
|
|
110
113
|
ctx.pendingAuthTempId = null;
|
|
111
114
|
stopAgentHeartbeat();
|
|
@@ -30,7 +30,7 @@ import { getLlmConfig, updateLlmConfig, getYeaftSettings, updateYeaftSettings, g
|
|
|
30
30
|
import { loadConfig } from '../yeaft/config.js';
|
|
31
31
|
import { discoverLlmModels } from '../llm-model-discovery.js';
|
|
32
32
|
import { fetchModelsDev } from '../yeaft/llm/models-dev.js';
|
|
33
|
-
import { handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, refreshLiveSessionConfig, handleYeaftLoadHistory, handleYeaftLoadHistoryOutline, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftProjectContextSync, handleYeaftProjectMutation, handleYeaftCreateSession, handleYeaftCopySession, handleYeaftMergeSessions, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, handleYeaftPluginCatalog, handleYeaftManagedSkill, ensureSessionLoaded, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, broadcastYeaftVpSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
|
|
33
|
+
import { invalidateYeaftProjectContexts, startYeaftScheduledTasks, handleYeaftScheduledTask, handleYeaftSessionSend, handleYeaftAskUserAnswer, handleYeaftSubAgentPrompt, handleYeaftTaskCancel, handleYeaftModeSwitch, handleYeaftModelSwitch, resetYeaftSession, refreshLiveSessionConfig, handleYeaftLoadHistory, handleYeaftLoadHistoryOutline, handleYeaftSearchHistory, handleYeaftLoadHistoryWindow, handleYeaftLoadMoreHistory, handleYeaftAbortThread, handleYeaftAbortAll, handleYeaftAbortTurn, handleYeaftVpSubscribe, handleYeaftVpCreate, handleYeaftVpUpdate, handleYeaftVpDelete, handleYeaftVpRead, handleYeaftListSessions, handleYeaftProjectContextSync, handleYeaftProjectMutation, handleYeaftCreateSession, handleYeaftCopySession, handleYeaftMergeSessions, handleYeaftRenameSession, handleYeaftUpdateSession, handleYeaftUpdateSessionConfig, handleYeaftArchiveSession, handleYeaftDeleteSession, handleYeaftSessionAddMember, handleYeaftSessionRemoveMember, handleYeaftSessionSetDefaultVp, handleYeaftScanWorkdirSessions, handleYeaftRestoreSession, handleYeaftDreamTrigger, handleYeaftFetchToolStats, handleYeaftFetchDebugHistory, handleYeaftMcpList, handleYeaftMcpAdd, handleYeaftMcpRemove, handleYeaftMcpReload, handleYeaftPluginCatalog, handleYeaftManagedSkill, ensureSessionLoaded, broadcastLanguageChange, broadcastYeaftSessionSnapshotEager, broadcastYeaftVpSnapshotEager, preloadYeaftSkillSlashCommands } from '../yeaft/web-bridge.js';
|
|
34
34
|
import { startYeaftStatusRefresh, forceRefreshYeaftStatus } from '../yeaft/status-cache.js';
|
|
35
35
|
import { handleWorkCenterRequest } from '../yeaft/work-center/bridge.js';
|
|
36
36
|
import { handlePersonRequest } from '../yeaft/person/bridge.js';
|
|
@@ -175,6 +175,7 @@ export async function handleMessage(msg) {
|
|
|
175
175
|
if (await handleBrowserRuntimeMessage(msg)) return;
|
|
176
176
|
switch (msg.type) {
|
|
177
177
|
case 'registered':
|
|
178
|
+
invalidateYeaftProjectContexts();
|
|
178
179
|
applyRegisteredTransport(msg);
|
|
179
180
|
|
|
180
181
|
// 只保存基本配置。instanceId 是本地服务实例身份;agentName 只用于展示。
|
|
@@ -210,6 +211,7 @@ export async function handleMessage(msg) {
|
|
|
210
211
|
// The callee already wraps its FS scan + emit in try/catch and
|
|
211
212
|
// logs via console.warn — no second guard needed here.
|
|
212
213
|
broadcastYeaftSessionSnapshotEager();
|
|
214
|
+
startYeaftScheduledTasks();
|
|
213
215
|
// Stock VPs are bundled Agent data. Seed + publish them during register so
|
|
214
216
|
// a first-install user can open the create dialog immediately instead of
|
|
215
217
|
// depending on a later Session runtime or modal subscription race.
|
|
@@ -847,6 +849,9 @@ export async function handleMessage(msg) {
|
|
|
847
849
|
handleYeaftRestoreSession(msg);
|
|
848
850
|
break;
|
|
849
851
|
// Phase 2: session_send is just group_chat (N≥1 fan-out already works).
|
|
852
|
+
case 'yeaft_scheduled_task':
|
|
853
|
+
handleYeaftScheduledTask(msg);
|
|
854
|
+
break;
|
|
850
855
|
case 'yeaft_session_send':
|
|
851
856
|
handleYeaftSessionSend(msg);
|
|
852
857
|
break;
|
package/index.js
CHANGED
|
@@ -178,7 +178,7 @@ async function detectCapabilities() {
|
|
|
178
178
|
// agent build can speak plaintext WS frames. New servers see this and
|
|
179
179
|
// flip `agent.encryptOutbound = false`, stopping outbound encryption
|
|
180
180
|
// to this peer. Old servers ignore the unknown capability token.
|
|
181
|
-
const capabilities = ['background_tasks', 'file_editor', 'ping_session', 'plaintext-ok', 'workbench_session_routes', 'workbench_request_correlation', 'workbench_terminal_cleanup_fence', 'workbench_file_content_chunks', 'workbench_video_stream', 'session_history_search', 'session_history_outline', 'session_history_window_prefetch', 'session_fork_from_turn', 'session_fork_while_running', 'session_merge', 'file_reference_resolution', 'response_image_preview', 'yeaft_plugins', 'yeaft_managed_skills', 'settings_request_correlation', 'work_center_feature_settings'];
|
|
181
|
+
const capabilities = ['scheduled_tasks', 'background_tasks', 'file_editor', 'ping_session', 'plaintext-ok', 'workbench_session_routes', 'workbench_request_correlation', 'workbench_terminal_cleanup_fence', 'workbench_file_content_chunks', 'workbench_video_stream', 'session_history_search', 'session_history_outline', 'session_history_window_prefetch', 'session_fork_from_turn', 'session_fork_while_running', 'session_merge', 'file_reference_resolution', 'response_image_preview', 'yeaft_plugins', 'yeaft_managed_skills', 'settings_request_correlation', 'work_center_feature_settings'];
|
|
182
182
|
capabilities.push('digital_person', getAgentUpgradeCapability());
|
|
183
183
|
if (ctx.CONFIG?.workCenterEnabled === true) {
|
|
184
184
|
capabilities.push('work_center', 'work_center_message_v2', 'work_center_workbench');
|
|
@@ -404,6 +404,7 @@ const yeaftSessionsTable = `
|
|
|
404
404
|
default_vp_id TEXT,
|
|
405
405
|
work_dir TEXT,
|
|
406
406
|
config_json TEXT,
|
|
407
|
+
task_json TEXT,
|
|
407
408
|
announcement TEXT,
|
|
408
409
|
created_at INTEGER,
|
|
409
410
|
updated_at INTEGER NOT NULL,
|
|
@@ -519,6 +520,7 @@ const yeaftMigrations = [
|
|
|
519
520
|
`ALTER TABLE yeaft_sessions ADD COLUMN is_pinned INTEGER DEFAULT 0`,
|
|
520
521
|
`ALTER TABLE yeaft_sessions ADD COLUMN sort_order INTEGER`,
|
|
521
522
|
`ALTER TABLE yeaft_sessions ADD COLUMN metadata_updated_at INTEGER`,
|
|
523
|
+
`ALTER TABLE yeaft_sessions ADD COLUMN task_json TEXT`,
|
|
522
524
|
];
|
|
523
525
|
for (const migration of yeaftMigrations) {
|
|
524
526
|
try { db.exec(migration); } catch (_) { /* column exists */ }
|
|
@@ -1408,8 +1410,8 @@ export const stmts = {
|
|
|
1408
1410
|
upsertYeaftSession: db.prepare(`
|
|
1409
1411
|
INSERT INTO yeaft_sessions
|
|
1410
1412
|
(id, user_id, agent_id, name, roster_json, default_vp_id, work_dir,
|
|
1411
|
-
config_json, announcement, created_at, updated_at, is_archived)
|
|
1412
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1413
|
+
config_json, announcement, created_at, updated_at, is_archived, task_json)
|
|
1414
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1413
1415
|
ON CONFLICT(user_id, agent_id, id) DO UPDATE SET
|
|
1414
1416
|
user_id = COALESCE(excluded.user_id, user_id),
|
|
1415
1417
|
name = excluded.name,
|
|
@@ -1417,6 +1419,7 @@ export const stmts = {
|
|
|
1417
1419
|
default_vp_id = excluded.default_vp_id,
|
|
1418
1420
|
work_dir = excluded.work_dir,
|
|
1419
1421
|
config_json = excluded.config_json,
|
|
1422
|
+
task_json = excluded.task_json,
|
|
1420
1423
|
announcement = excluded.announcement,
|
|
1421
1424
|
created_at = COALESCE(yeaft_sessions.created_at, excluded.created_at),
|
|
1422
1425
|
updated_at = excluded.updated_at,
|
|
@@ -267,6 +267,8 @@ export const yeaftProjectDb = {
|
|
|
267
267
|
for (const rawSessionId of Array.isArray(row.sessionIds) ? row.sessionIds : []) {
|
|
268
268
|
const sessionId = typeof rawSessionId === 'string' ? rawSessionId.trim() : '';
|
|
269
269
|
if (!sessionId || !sessionExists(sessionId)) continue;
|
|
270
|
+
// Legacy discovery is not authority over an existing Server membership.
|
|
271
|
+
if (stmts.getYeaftProjectForSession.get(ownerId, targetAgentId, sessionId)) continue;
|
|
270
272
|
stmts.deleteYeaftProjectSessionMembership.run(ownerId, targetAgentId, sessionId);
|
|
271
273
|
stmts.insertYeaftProjectSessionMembership.run(
|
|
272
274
|
ownerId,
|
|
@@ -39,7 +39,9 @@ function sessionOrderKey(agentId, sessionId) {
|
|
|
39
39
|
|
|
40
40
|
function mapRow(row) {
|
|
41
41
|
if (!row) return row;
|
|
42
|
+
const task = safeJsonParse(row.task_json, null);
|
|
42
43
|
return {
|
|
44
|
+
...(task && typeof task === 'object' && !Array.isArray(task) ? { task } : {}),
|
|
43
45
|
id: row.id,
|
|
44
46
|
userId: row.user_id || null,
|
|
45
47
|
agentId: row.agent_id,
|
|
@@ -72,7 +74,7 @@ export const yeaftSessionDb = {
|
|
|
72
74
|
* @param {string} userId
|
|
73
75
|
* @param {string} agentId
|
|
74
76
|
* @param {object} session Shape from `web-bridge.snapshotSessions()`:
|
|
75
|
-
* { id, name, roster[], defaultVpId, workDir, config, announcement, createdAt }
|
|
77
|
+
* { id, name, roster[], defaultVpId, workDir, config, announcement, createdAt, task? }
|
|
76
78
|
*/
|
|
77
79
|
upsertFromSnapshot(userId, agentId, session) {
|
|
78
80
|
if (!session || !session.id || !agentId) return;
|
|
@@ -90,6 +92,7 @@ export const yeaftSessionDb = {
|
|
|
90
92
|
session.createdAt || now,
|
|
91
93
|
now,
|
|
92
94
|
0,
|
|
95
|
+
safeJsonStringify(session.task && typeof session.task === 'object' && !Array.isArray(session.task) ? session.task : null),
|
|
93
96
|
);
|
|
94
97
|
if (session.metadataUpdatedAt) {
|
|
95
98
|
stmts.touchYeaftSessionMetadata.run(
|
|
@@ -6,6 +6,7 @@ import { advanceYeaftDebugRequestChunk, consumeYeaftDebugRequest, webClients, pr
|
|
|
6
6
|
import { CONFIG } from '../config.js';
|
|
7
7
|
import { yeaftAssetStore } from '../yeaft-asset-store.js';
|
|
8
8
|
import { recordPerfTraceEvent } from '../perf-trace.js';
|
|
9
|
+
import { syncProjectContextsToAgents } from './client-conversation.js';
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
/** Keep Skill detail read-only and browser-safe, including replies from newer Agents. */
|
|
@@ -69,7 +70,7 @@ function reconcileAuthoritativeSessionSnapshot(ownerId, agentId, sessions) {
|
|
|
69
70
|
return rows;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
function syncYeaftSessionMetadata(agentId, agent, event) {
|
|
73
|
+
async function syncYeaftSessionMetadata(agentId, agent, event) {
|
|
73
74
|
if (!event || typeof event !== 'object') return event;
|
|
74
75
|
const ownerId = agent?.ownerId || null;
|
|
75
76
|
|
|
@@ -80,6 +81,8 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
|
|
|
80
81
|
.map(session => [session.id, { ...session }]));
|
|
81
82
|
try {
|
|
82
83
|
if (ownerId) {
|
|
84
|
+
// Catalog discovery can precede the one-time legacy Project import.
|
|
85
|
+
// Existing memberships are protected by importLegacyProjects itself.
|
|
83
86
|
reconcileAuthoritativeSessionSnapshot(ownerId, agentId, rows);
|
|
84
87
|
yeaftProjectDb.importLegacyProjects(
|
|
85
88
|
ownerId,
|
|
@@ -87,6 +90,7 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
|
|
|
87
90
|
event.projects,
|
|
88
91
|
sessionId => rows.some(row => row?.id === sessionId),
|
|
89
92
|
);
|
|
93
|
+
await syncProjectContextsToAgents(ownerId, agentId);
|
|
90
94
|
}
|
|
91
95
|
} catch (e) {
|
|
92
96
|
console.warn(`[Server] yeaft session persist failed for agent ${agentId}:`, e?.message || e);
|
|
@@ -99,12 +103,30 @@ function syncYeaftSessionMetadata(agentId, agent, event) {
|
|
|
99
103
|
const sessionId = event.sessionId;
|
|
100
104
|
if (event.ok && op === 'list' && Array.isArray(event.sessions)) {
|
|
101
105
|
try {
|
|
102
|
-
if (ownerId)
|
|
106
|
+
if (ownerId) {
|
|
107
|
+
reconcileAuthoritativeSessionSnapshot(ownerId, agentId, event.sessions);
|
|
108
|
+
await syncProjectContextsToAgents(ownerId, agentId);
|
|
109
|
+
}
|
|
103
110
|
} catch (e) {
|
|
104
111
|
console.warn(`[Server] yeaft session list persist failed for agent ${agentId}:`, e?.message || e);
|
|
105
112
|
}
|
|
106
113
|
return { ...event, sessions: decorateYeaftSessionsWithPinned(agentId, event.sessions) };
|
|
107
114
|
}
|
|
115
|
+
// Task state is Agent-authored. Persist successful CRUD rows as well as full
|
|
116
|
+
// snapshots so reloads after an acknowledgement cannot show stale run state.
|
|
117
|
+
if (event.ok && ownerId && event.session?.id
|
|
118
|
+
&& (['update_task', 'run_task'].includes(op)
|
|
119
|
+
|| (event.session.task && op !== 'copy' && op !== 'merge'))) {
|
|
120
|
+
try {
|
|
121
|
+
const existing = yeaftSessionDb.getForAgent(ownerId, agentId, event.session.id);
|
|
122
|
+
yeaftSessionDb.upsertFromSnapshot(ownerId, agentId, { ...existing, ...event.session });
|
|
123
|
+
agent.yeaftSessions ??= new Map();
|
|
124
|
+
agent.yeaftSessions.set(event.session.id, { ...existing, ...event.session });
|
|
125
|
+
if (op === 'create') await syncProjectContextsToAgents(ownerId, agentId);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.warn(`[Server] task Session persist failed for agent ${agentId}:`, e?.message || e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
108
130
|
// A fork/merge is a new Session on the same Agent, not a new Project. Resolve
|
|
109
131
|
// membership here (never from browser/Agent project metadata) before the
|
|
110
132
|
// successful acknowledgement can focus the new Session in the browser.
|
|
@@ -628,7 +650,7 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
628
650
|
case 'session_output': {
|
|
629
651
|
if (isRetiredDreamEvent(msg.event)) break;
|
|
630
652
|
const data = hydrateInlinePreviewData(msg.data);
|
|
631
|
-
let event = syncYeaftSessionMetadata(agentId, agent, msg.event);
|
|
653
|
+
let event = await syncYeaftSessionMetadata(agentId, agent, msg.event);
|
|
632
654
|
if ((event?.type === 'session_list_updated' || event?.type === 'session_crud_result') && agent.ownerId) {
|
|
633
655
|
event = {
|
|
634
656
|
...event,
|
|
@@ -636,7 +658,12 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
636
658
|
projectsAuthoritative: true,
|
|
637
659
|
};
|
|
638
660
|
}
|
|
639
|
-
|
|
661
|
+
// Task notifications are live owner events, never request-client events.
|
|
662
|
+
// Do not widen this boundary in skip-auth mode when an owner is known.
|
|
663
|
+
const taskNotification = event?.type === 'scheduled_task_notification';
|
|
664
|
+
const taskResult = event?.type === 'session_crud_result'
|
|
665
|
+
&& (['update_task', 'run_task'].includes(event.op) || event.session?.task);
|
|
666
|
+
let catalogChanged = !!(taskResult && event.ok && event.session?.id);
|
|
640
667
|
if (event?.type === 'yeaft_status') {
|
|
641
668
|
agent.yeaftStatus = event;
|
|
642
669
|
await broadcastAgentList();
|
|
@@ -655,6 +682,7 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
655
682
|
defaultVpId: event.defaultVpId != null ? event.defaultVpId : (existing.defaultVpId || null),
|
|
656
683
|
workDir: existing.workDir || '',
|
|
657
684
|
config: existing.config || {},
|
|
685
|
+
...(existing.task ? { task: existing.task } : {}),
|
|
658
686
|
announcement: typeof event.announcement === 'string'
|
|
659
687
|
? event.announcement
|
|
660
688
|
: (existing.announcement || ''),
|
|
@@ -685,7 +713,8 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
685
713
|
}
|
|
686
714
|
// History request completions are client-scoped; live Session output stays
|
|
687
715
|
// broadcast to every authenticated tab owned by this user.
|
|
688
|
-
const hasRequestedClient =
|
|
716
|
+
const hasRequestedClient = !taskNotification
|
|
717
|
+
&& typeof msg._requestClientId === 'string' && !!msg._requestClientId;
|
|
689
718
|
const requestedClient = hasRequestedClient ? webClients.get(msg._requestClientId) : null;
|
|
690
719
|
const outputClients = hasRequestedClient
|
|
691
720
|
? (requestedClient ? [[msg._requestClientId, requestedClient]] : [])
|
|
@@ -715,7 +744,10 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
715
744
|
// are non-empty in practice, but we don't want the relay silently
|
|
716
745
|
// eating a legitimate "" or 0 if one ever shows up.
|
|
717
746
|
for (const [cId, c] of outputClients) {
|
|
718
|
-
|
|
747
|
+
const ownerMatches = taskNotification || taskResult
|
|
748
|
+
? !!agent.ownerId && c.userId === agent.ownerId
|
|
749
|
+
: CONFIG.skipAuth || c.userId === agent.ownerId;
|
|
750
|
+
if (c.authenticated && ownerMatches) {
|
|
719
751
|
await sendToWebClient(c, {
|
|
720
752
|
type: 'yeaft_output',
|
|
721
753
|
conversationId: msg.conversationId,
|
|
@@ -977,7 +1009,7 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
977
1009
|
// Use one batch read (`getByAgent`) and a Set lookup instead of an
|
|
978
1010
|
// N+1 `get(id)` per row — relays this size hot path on every
|
|
979
1011
|
// snapshot per connected client.
|
|
980
|
-
const syncedEvent = syncYeaftSessionMetadata(agentId, agent, msg);
|
|
1012
|
+
const syncedEvent = await syncYeaftSessionMetadata(agentId, agent, msg);
|
|
981
1013
|
const decoratedSessions = syncedEvent.sessions;
|
|
982
1014
|
await broadcastSessionCatalog(agent.ownerId);
|
|
983
1015
|
// Relay verbatim to web (agentId stamped so the web sessions store
|
|
@@ -1020,6 +1052,7 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
1020
1052
|
: (existing?.defaultVpId || null),
|
|
1021
1053
|
workDir: existing?.workDir || '',
|
|
1022
1054
|
config: existing?.config || {},
|
|
1055
|
+
...(existing?.task ? { task: existing.task } : {}),
|
|
1023
1056
|
announcement: typeof msg.announcement === 'string'
|
|
1024
1057
|
? msg.announcement
|
|
1025
1058
|
: (existing?.announcement || ''),
|
|
@@ -1042,12 +1075,19 @@ export async function handleAgentOutput(agentId, agent, msg) {
|
|
|
1042
1075
|
}
|
|
1043
1076
|
|
|
1044
1077
|
case 'session_crud_result': {
|
|
1078
|
+
// Task-capable Agents may use the top-level CRUD alias. Normalize it to
|
|
1079
|
+
// the same requester-scoped envelope as nested task acknowledgements.
|
|
1080
|
+
if (['update_task', 'run_task'].includes(msg.op) || msg.session?.task) {
|
|
1081
|
+
const { type: _type, _requestClientId, ...event } = msg;
|
|
1082
|
+
await handleAgentOutput(agentId, agent, { type: 'yeaft_output', _requestClientId, event });
|
|
1083
|
+
break;
|
|
1084
|
+
}
|
|
1045
1085
|
// Surface op result to the web (the only client that cares about
|
|
1046
1086
|
// requestId routing). Keep the server shadow table in sync for list
|
|
1047
1087
|
// probes too: entering Yeaft asks each connected agent for its opened
|
|
1048
1088
|
// sessions, and that response must carry persisted server-side pin
|
|
1049
1089
|
// state before it hits the web store.
|
|
1050
|
-
const syncedMsg = syncYeaftSessionMetadata(agentId, agent, msg);
|
|
1090
|
+
const syncedMsg = await syncYeaftSessionMetadata(agentId, agent, msg);
|
|
1051
1091
|
const outboundMsg = agent.ownerId
|
|
1052
1092
|
? { ...syncedMsg, projects: yeaftProjectDb.listForAgent(agent.ownerId, agentId), projectsAuthoritative: true }
|
|
1053
1093
|
: { ...syncedMsg, projects: [], projectsAuthoritative: true };
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
verifyConversationOwnership, verifyAgentOwnership, resolveAgentAccessError
|
|
23
23
|
} from '../ws-utils.js';
|
|
24
24
|
import { routeSessionPin } from './session-pin-router.js';
|
|
25
|
+
import { handleClientScheduledTasks } from './client-scheduled-tasks.js';
|
|
25
26
|
import { recordPerfTraceEvent } from '../perf-trace.js';
|
|
26
27
|
import {
|
|
27
28
|
chatCatalogKey,
|
|
@@ -119,9 +120,10 @@ async function sendVpSnapshotError(client, msg, error) {
|
|
|
119
120
|
});
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
export async function syncProjectContextsToAgents(userId) {
|
|
123
|
+
export async function syncProjectContextsToAgents(userId, targetAgentId = null) {
|
|
123
124
|
try {
|
|
124
|
-
const sessionsByAgent = groupOnlineYeaftSessions(yeaftSessionDb.getByUser(userId)
|
|
125
|
+
const sessionsByAgent = groupOnlineYeaftSessions(yeaftSessionDb.getByUser(userId)
|
|
126
|
+
.filter(row => !targetAgentId || row.agentId === targetAgentId));
|
|
125
127
|
await Promise.all(Object.entries(sessionsByAgent).map(async ([agentId, sessions]) => {
|
|
126
128
|
try {
|
|
127
129
|
const contexts = sessions.map(session => ({
|
|
@@ -303,6 +305,7 @@ function skippedYeaftDreamResult(msg, reason) {
|
|
|
303
305
|
* update_conversation_settings, ask_user_answer, btw_question
|
|
304
306
|
*/
|
|
305
307
|
export async function handleClientConversation(clientId, client, msg, checkAgentAccess) {
|
|
308
|
+
if (await handleClientScheduledTasks(clientId, client, msg)) return true;
|
|
306
309
|
switch (msg.type) {
|
|
307
310
|
case 'get_agents':
|
|
308
311
|
// 前端可能附带 conversationIds(server 重启后恢复场景)
|
|
@@ -1736,6 +1739,15 @@ export async function handleClientConversation(clientId, client, msg, checkAgent
|
|
|
1736
1739
|
if (typeof msg.type === 'string' && (msg.type.startsWith('yeaft_') || msg.type.startsWith('unify_'))) {
|
|
1737
1740
|
const relayType = msg.type.startsWith('unify_') ? `yeaft_${msg.type.slice('unify_'.length)}` : msg.type;
|
|
1738
1741
|
const relayAgentId = msg.agentId || client.currentAgent;
|
|
1742
|
+
// Internal authority messages are generated from Server SQLite only.
|
|
1743
|
+
// Generic relay must not turn a browser assertion into trusted context.
|
|
1744
|
+
if (relayType === 'yeaft_project_context_sync') {
|
|
1745
|
+
await sendToWebClient(client, {
|
|
1746
|
+
type: 'error', code: 'server_only_message', message: 'Project context synchronization is Server-only.',
|
|
1747
|
+
requestId: msg.requestId ?? null, agentId: relayAgentId || null,
|
|
1748
|
+
});
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1739
1751
|
const isSessionSend = relayType === 'yeaft_session_send' || relayType === 'yeaft_session_chat';
|
|
1740
1752
|
if (msg.perfTraceId) {
|
|
1741
1753
|
recordPerfTraceEvent({
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { agents } from '../context.js';
|
|
2
|
+
import { yeaftProjectDb, yeaftSessionDb } from '../database.js';
|
|
3
|
+
import { forwardToAgent, resolveAgentAccessError, sendToWebClient } from '../ws-utils.js';
|
|
4
|
+
|
|
5
|
+
const CREATE_TYPES = new Set(['yeaft_create_session', 'yeaft_create_group', 'unify_create_group', 'unify_create_session']);
|
|
6
|
+
const TASK_TYPES = new Set(['yeaft_scheduled_task', 'unify_scheduled_task']);
|
|
7
|
+
const CREATE_FIELDS = ['name', 'roster', 'defaultVpId', 'workDir', 'config'];
|
|
8
|
+
|
|
9
|
+
function isRecord(value) {
|
|
10
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function validIdentity(value) {
|
|
14
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 256
|
|
15
|
+
&& value === value.trim() && !/[\u0000-\u001f\u007f]/.test(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Only user-authored configuration crosses this boundary. Runtime state is Agent-owned. */
|
|
19
|
+
function projectTaskInput(source) {
|
|
20
|
+
if (!isRecord(source)) throw new Error('task must be an object');
|
|
21
|
+
if (Buffer.byteLength(JSON.stringify(source)) > 96 * 1024) throw new Error('Task input is too large');
|
|
22
|
+
const task = {};
|
|
23
|
+
if (Object.hasOwn(source, 'prompt')) {
|
|
24
|
+
if (typeof source.prompt !== 'string' || !source.prompt.trim() || source.prompt.length > 65536) {
|
|
25
|
+
throw new Error('Task prompt must be non-empty text of at most 65536 characters');
|
|
26
|
+
}
|
|
27
|
+
task.prompt = source.prompt;
|
|
28
|
+
}
|
|
29
|
+
for (const key of ['enabled', 'notify']) {
|
|
30
|
+
if (!Object.hasOwn(source, key)) continue;
|
|
31
|
+
if (typeof source[key] !== 'boolean') throw new Error(`${key} must be a boolean`);
|
|
32
|
+
task[key] = source[key];
|
|
33
|
+
}
|
|
34
|
+
if (Object.hasOwn(source, 'trigger')) {
|
|
35
|
+
const trigger = source.trigger;
|
|
36
|
+
if (!isRecord(trigger) || !['manual', 'schedule'].includes(trigger.type)) throw new Error('Invalid task trigger');
|
|
37
|
+
task.trigger = { type: trigger.type };
|
|
38
|
+
if (trigger.type === 'schedule') {
|
|
39
|
+
if (!Number.isSafeInteger(trigger.scheduledFor) || trigger.scheduledFor < 0) throw new Error('Invalid scheduledFor');
|
|
40
|
+
task.trigger.scheduledFor = trigger.scheduledFor;
|
|
41
|
+
if (Object.hasOwn(trigger, 'recurrence')) {
|
|
42
|
+
if (trigger.recurrence !== null && !isRecord(trigger.recurrence)) throw new Error('Invalid recurrence');
|
|
43
|
+
// Calendar-rule validation belongs to the Agent; never forward shared input objects.
|
|
44
|
+
const recurrence = JSON.stringify(trigger.recurrence);
|
|
45
|
+
if (recurrence.length > 4096) throw new Error('Recurrence is too large');
|
|
46
|
+
task.trigger.recurrence = JSON.parse(recurrence);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!Object.keys(task).length) throw new Error('No editable task fields supplied');
|
|
51
|
+
return task;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Task creation shares Session CRUD, but must never fall through to an older Agent. */
|
|
55
|
+
export async function handleClientScheduledTasks(clientId, client, msg) {
|
|
56
|
+
const isCreate = CREATE_TYPES.has(msg.type) && isRecord(msg.payload) && Object.hasOwn(msg.payload, 'task');
|
|
57
|
+
if (!isCreate && !TASK_TYPES.has(msg.type)) return false;
|
|
58
|
+
const agentId = msg.agentId;
|
|
59
|
+
const sessionId = isCreate ? null : msg.sessionId;
|
|
60
|
+
const requestId = validIdentity(msg.requestId) ? msg.requestId : null;
|
|
61
|
+
const op = isCreate ? 'create' : (msg.op === 'update' ? 'update_task' : 'run_task');
|
|
62
|
+
const reject = async (code, message) => {
|
|
63
|
+
await sendToWebClient(client, {
|
|
64
|
+
type: 'yeaft_output', agentId: typeof agentId === 'string' ? agentId : null,
|
|
65
|
+
...(sessionId != null ? { sessionId } : {}), requestId,
|
|
66
|
+
event: { type: 'session_crud_result', op, requestId, ok: false, error: { code, message } },
|
|
67
|
+
});
|
|
68
|
+
return true;
|
|
69
|
+
};
|
|
70
|
+
// No currentAgent fallback: a task action must name its full durable identity.
|
|
71
|
+
if (!validIdentity(agentId) || (!isCreate && !validIdentity(sessionId))) {
|
|
72
|
+
return reject('invalid_identity', 'An explicit Agent and Session identity is required');
|
|
73
|
+
}
|
|
74
|
+
if (!requestId || (!isCreate && !['update', 'run'].includes(msg.op))) {
|
|
75
|
+
return reject('invalid_request', 'A requestId and valid operation are required');
|
|
76
|
+
}
|
|
77
|
+
const accessError = resolveAgentAccessError(agentId, client.userId, client.role);
|
|
78
|
+
if (!client.authenticated || accessError) {
|
|
79
|
+
const denied = !client.authenticated || accessError === 'Agent access denied';
|
|
80
|
+
return reject(denied ? 'agent_access_denied' : 'agent_offline', denied ? 'Agent access denied' : 'Agent is offline');
|
|
81
|
+
}
|
|
82
|
+
const agent = agents.get(agentId);
|
|
83
|
+
if (!agent?.capabilities?.includes('scheduled_tasks')) {
|
|
84
|
+
return reject('scheduled_tasks_unsupported', 'This Agent does not support scheduled tasks; upgrade it first');
|
|
85
|
+
}
|
|
86
|
+
if (!isCreate && !yeaftSessionDb.getForAgent(client.userId, agentId, sessionId)) {
|
|
87
|
+
return reject('session_access_denied', 'Session not found or access denied');
|
|
88
|
+
}
|
|
89
|
+
const outbound = {
|
|
90
|
+
type: isCreate ? 'yeaft_create_session' : 'yeaft_scheduled_task',
|
|
91
|
+
requestId, _requestClientId: clientId,
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
if (isCreate) {
|
|
95
|
+
outbound.payload = Object.fromEntries(CREATE_FIELDS.filter(key => Object.hasOwn(msg.payload, key)).map(key => [key, msg.payload[key]]));
|
|
96
|
+
outbound.payload.task = projectTaskInput(msg.payload.task);
|
|
97
|
+
} else {
|
|
98
|
+
outbound.op = msg.op;
|
|
99
|
+
outbound.sessionId = sessionId;
|
|
100
|
+
if (msg.op === 'update') {
|
|
101
|
+
if (typeof msg.expectedRevision !== 'string' || !msg.expectedRevision) {
|
|
102
|
+
return reject('task_conflict', 'A task revision is required; refresh before editing');
|
|
103
|
+
}
|
|
104
|
+
outbound.expectedRevision = msg.expectedRevision;
|
|
105
|
+
outbound.task = projectTaskInput(msg.task);
|
|
106
|
+
} else {
|
|
107
|
+
if (Object.hasOwn(msg, 'task')) throw new Error('run does not accept task configuration');
|
|
108
|
+
// Never relay a browser-supplied Project; this authority comes only from SQLite.
|
|
109
|
+
outbound.projectContext = yeaftProjectDb.contextForSession(client.userId, agentId, sessionId);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return reject('invalid_task', error.message);
|
|
114
|
+
}
|
|
115
|
+
// Override private routing instead of accepting any browser-provided identity.
|
|
116
|
+
// A socket may disappear between the access check and the write.
|
|
117
|
+
try {
|
|
118
|
+
if (await forwardToAgent(agentId, outbound)) return true;
|
|
119
|
+
} catch { /* Report a correlated failure instead of leaving the CRUD promise pending. */ }
|
|
120
|
+
return reject('agent_offline', 'Agent delivery could not be confirmed; refresh before retrying');
|
|
121
|
+
}
|
|
@@ -88,6 +88,7 @@ export function projectSessionCatalog({
|
|
|
88
88
|
rows.push({
|
|
89
89
|
catalogKey,
|
|
90
90
|
runtimeProvider: 'yeaft',
|
|
91
|
+
...(session.task ? { task: session.task } : {}),
|
|
91
92
|
routeRef: { runtimeProvider: 'yeaft', agentId: session.agentId, sessionId: session.id },
|
|
92
93
|
title: session.name || session.id,
|
|
93
94
|
workDir: session.workDir || '',
|
|
@@ -144,6 +144,7 @@ export function handleWebConnection(ws, url, req = {}) {
|
|
|
144
144
|
acceptPlaintext: true,
|
|
145
145
|
yeaftSessionInventoryComplete: true,
|
|
146
146
|
sessionCatalogProtocol: 1,
|
|
147
|
+
scheduledTaskProtocol: 1,
|
|
147
148
|
workbenchRouteProtocol: WORKBENCH_ROUTE_PROTOCOL,
|
|
148
149
|
workCenterWorkbenchProtocol: WORK_CENTER_WORKBENCH_PROTOCOL,
|
|
149
150
|
browserRuntimeProtocol: BROWSER_RUNTIME_PROTOCOL,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.650"}
|