666db0b939
Persist goal runs in MySQL, bind agent runs to checkpoints, expose awaiting-approval UX in chat, and add admin inspection routes with local verify scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
208 lines
6.5 KiB
JavaScript
208 lines
6.5 KiB
JavaScript
import { createGoalRunService, ensureGoalRunSchema } from './goal-run-service.mjs';
|
|
import { isGoalRunEnabledForUser } from './goal-run-intent.mjs';
|
|
|
|
function envFlag(value, fallback = false) {
|
|
const raw = String(value ?? '').trim().toLowerCase();
|
|
if (!raw) return fallback;
|
|
return ['1', 'true', 'yes', 'on'].includes(raw);
|
|
}
|
|
|
|
function parseUserIdList(raw) {
|
|
return String(raw ?? '')
|
|
.split(/[,;\s]+/)
|
|
.map((item) => item.trim())
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function parseJson(value, fallback = null) {
|
|
if (value == null) return fallback;
|
|
if (typeof value === 'object') return value;
|
|
try {
|
|
return JSON.parse(String(value));
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
function normalizeGoalListRow(row) {
|
|
return {
|
|
id: String(row.id),
|
|
userId: String(row.user_id),
|
|
username: row.username == null ? null : String(row.username),
|
|
title: String(row.title),
|
|
intentSummary: String(row.intent_summary),
|
|
status: String(row.status),
|
|
priority: Number(row.priority ?? 5),
|
|
sourceChannel: String(row.source_channel ?? 'h5'),
|
|
sourceSessionId: row.source_session_id == null ? null : String(row.source_session_id),
|
|
currentCheckpointId: row.current_checkpoint_id == null ? null : String(row.current_checkpoint_id),
|
|
checkpointCount: Number(row.checkpoint_count ?? 0),
|
|
activeAgentRunCount: Number(row.active_agent_run_count ?? 0),
|
|
createdAt: Number(row.created_at ?? 0),
|
|
updatedAt: Number(row.updated_at ?? 0),
|
|
completedAt: row.completed_at == null ? null : Number(row.completed_at),
|
|
};
|
|
}
|
|
|
|
export function createGoalRunAdminOpsService(pool, { env = process.env, now = () => Date.now() } = {}) {
|
|
if (!pool?.query) return null;
|
|
const goalRunService = createGoalRunService({ pool, now });
|
|
let schemaReady = false;
|
|
|
|
async function ensureSchema() {
|
|
if (schemaReady) return;
|
|
await ensureGoalRunSchema(pool);
|
|
schemaReady = true;
|
|
}
|
|
|
|
return {
|
|
getRuntime() {
|
|
const canaryUserIds = parseUserIdList(env.GOAL_RUN_CANARY_USER_IDS);
|
|
return {
|
|
enabled: envFlag(env.GOAL_RUN_ENABLED, false),
|
|
canaryUserIds,
|
|
canaryMode: canaryUserIds.length > 0,
|
|
};
|
|
},
|
|
|
|
async countByStatus() {
|
|
await ensureSchema();
|
|
const [rows] = await pool.query(
|
|
`SELECT status, COUNT(*) AS count
|
|
FROM h5_goal_runs
|
|
GROUP BY status`,
|
|
);
|
|
const counts = {};
|
|
for (const row of rows) {
|
|
counts[String(row.status)] = Number(row.count ?? 0);
|
|
}
|
|
return counts;
|
|
},
|
|
|
|
async listGoals({
|
|
status = null,
|
|
userId = null,
|
|
limit = 50,
|
|
offset = 0,
|
|
} = {}) {
|
|
await ensureSchema();
|
|
const safeLimit = Math.max(1, Math.min(100, Number(limit) || 50));
|
|
const safeOffset = Math.max(0, Number(offset) || 0);
|
|
const filters = [];
|
|
const params = [];
|
|
if (status) {
|
|
filters.push('g.status = ?');
|
|
params.push(String(status));
|
|
}
|
|
if (userId) {
|
|
filters.push('g.user_id = ?');
|
|
params.push(String(userId));
|
|
}
|
|
const where = filters.length ? `WHERE ${filters.join(' AND ')}` : '';
|
|
const [rows] = await pool.query(
|
|
`SELECT g.*,
|
|
u.username,
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM h5_goal_checkpoints c
|
|
WHERE c.goal_run_id = g.id
|
|
) AS checkpoint_count,
|
|
(
|
|
SELECT COUNT(*)
|
|
FROM h5_agent_runs r
|
|
WHERE r.goal_run_id = g.id
|
|
AND r.status NOT IN ('succeeded', 'failed')
|
|
) AS active_agent_run_count
|
|
FROM h5_goal_runs g
|
|
LEFT JOIN h5_users u ON u.id = g.user_id
|
|
${where}
|
|
ORDER BY g.updated_at DESC
|
|
LIMIT ? OFFSET ?`,
|
|
[...params, safeLimit, safeOffset],
|
|
);
|
|
return rows.map(normalizeGoalListRow);
|
|
},
|
|
|
|
async getGoalDetail(goalRunId) {
|
|
await ensureSchema();
|
|
const [rows] = await pool.query(
|
|
`SELECT g.*, u.username
|
|
FROM h5_goal_runs g
|
|
LEFT JOIN h5_users u ON u.id = g.user_id
|
|
WHERE g.id = ?
|
|
LIMIT 1`,
|
|
[String(goalRunId)],
|
|
);
|
|
const row = rows[0];
|
|
if (!row) return null;
|
|
const goal = await goalRunService.getGoalRun({
|
|
userId: String(row.user_id),
|
|
goalRunId: String(goalRunId),
|
|
});
|
|
if (!goal) return null;
|
|
const [agentRuns] = await pool.query(
|
|
`SELECT id, status, request_id, goal_checkpoint_id, created_at, updated_at, completed_at
|
|
FROM h5_agent_runs
|
|
WHERE goal_run_id = ?
|
|
ORDER BY created_at DESC
|
|
LIMIT 20`,
|
|
[String(goalRunId)],
|
|
);
|
|
return {
|
|
...goal,
|
|
username: row.username == null ? null : String(row.username),
|
|
context: parseJson(row.context_json, goal.context),
|
|
agentRuns: agentRuns.map((run) => ({
|
|
id: String(run.id),
|
|
status: String(run.status),
|
|
requestId: String(run.request_id),
|
|
goalCheckpointId: run.goal_checkpoint_id == null ? null : String(run.goal_checkpoint_id),
|
|
createdAt: Number(run.created_at ?? 0),
|
|
updatedAt: Number(run.updated_at ?? 0),
|
|
completedAt: run.completed_at == null ? null : Number(run.completed_at),
|
|
})),
|
|
canaryEnabled: isGoalRunEnabledForUser(String(row.user_id), env),
|
|
};
|
|
},
|
|
|
|
async approveCheckpoint({ goalRunId, checkpointId, feedback = null, reviewedBy = null } = {}) {
|
|
await ensureSchema();
|
|
const [rows] = await pool.query(
|
|
'SELECT user_id FROM h5_goal_runs WHERE id = ? LIMIT 1',
|
|
[String(goalRunId)],
|
|
);
|
|
const userId = rows[0]?.user_id == null ? null : String(rows[0].user_id);
|
|
if (!userId) {
|
|
const error = new Error('目标不存在');
|
|
error.code = 'GOAL_RUN_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
const note = feedback ?? (reviewedBy ? `admin:${reviewedBy}` : null);
|
|
return goalRunService.approveCheckpoint({
|
|
userId,
|
|
goalRunId,
|
|
checkpointId,
|
|
feedback: note,
|
|
});
|
|
},
|
|
|
|
async cancelGoal({ goalRunId, reviewedBy = null } = {}) {
|
|
await ensureSchema();
|
|
const [rows] = await pool.query(
|
|
'SELECT user_id FROM h5_goal_runs WHERE id = ? LIMIT 1',
|
|
[String(goalRunId)],
|
|
);
|
|
const userId = rows[0]?.user_id == null ? null : String(rows[0].user_id);
|
|
if (!userId) {
|
|
const error = new Error('目标不存在');
|
|
error.code = 'GOAL_RUN_NOT_FOUND';
|
|
throw error;
|
|
}
|
|
return goalRunService.cancelGoal({
|
|
userId,
|
|
goalRunId,
|
|
});
|
|
},
|
|
};
|
|
}
|