Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2630,6 +2630,14 @@ const privateConversationSchema = z.object({
goal_active: z.boolean().optional(), monitor: z.object({todo_id: z.string(), status: z.string(),
cadence_minutes: z.number().int().nullable(), next_due_at: z.string().nullable(),
last_checked_at: z.string().nullable()}).nullable().optional(),
report: z.discriminatedUnion("available", [
z.object({available: z.literal(true), enabled: z.boolean(), configured: z.boolean(),
schedule: z.record(z.string(), z.unknown()).nullable().optional(), next_due_at: z.string().nullable().optional(),
retained_window_due_at: z.string().nullable().optional(), generation_id: z.string().nullable().optional(),
delivery_status: z.string().optional(), publication_verified: z.boolean().optional(),
destination: z.string().optional()}),
z.object({available: z.literal(false), error_code: z.string()}),
]).optional(),
goal_id: z.string().optional(), agent_id: z.string().optional(), session_id: z.string().optional()}),
z.object({available: z.literal(false), error_code: z.string()}),
]).optional(),
Expand Down Expand Up @@ -2695,6 +2703,21 @@ export async function changePrivateAgentTarget(bindingId: string, revision: numb
}));
}

const stewardReportPlanSchema = z.object({ok: z.literal(true), applied: z.boolean(), stop_status: z.string().optional(), plan: z.object({
schema_version: z.literal("loopx_steward_report_plan_v0"), plan_id: z.string().regex(/^[a-f0-9]{64}$/),
})});
export async function configureStewardReport(bindingId: string, operation: "enable" | "pause" | "cancel", schedule?: Record<string, unknown>) {
const path = "/api/chat/lark/private-conversations/managed-steward/report";
const send = (body: unknown) => requestJson<unknown>(path, {method: "POST",
headers: {"Content-Type": "application/json"}, body: JSON.stringify(body)});
const request = {binding_id: bindingId, operation, ...(operation === "enable" ? {schedule} : {})};
const preview = stewardReportPlanSchema.parse(await send({...request, execute: false}));
const applied = stewardReportPlanSchema.parse(await send({...request, execute: true, plan_id: preview.plan.plan_id}));
if (!applied.applied) throw new Error("The original report setting was not applied.");
if (applied.stop_status === "unconfirmed") throw new Error("汇报计划已取消;正在生成的任务是否停止尚未确认。Report cancelled; the active task's stop is unconfirmed.");
return applied;
}

export async function changeOwnerNotifications(bindingId: string, bindingRevision: number, revision: number, enabled: boolean) {
return privateConversationsSchema.parse(await requestJson<unknown>("/api/chat/lark/private-conversations/owner-notifications", {
method: "POST", headers: {"Content-Type": "application/json"},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ export function withReportScheduleTimezone(
return result;
}

export function PeriodicReportScheduleField({ id, value, timezone, onChange }: Readonly<{
export function PeriodicReportScheduleField({ id, value, timezone, onChange, calendarRequired = false }: Readonly<{
id: string;
value: unknown;
timezone: string;
onChange?: (value: Schedule | null) => void;
calendarRequired?: boolean;
}>) {
const { locale } = useWorkspaceI18n();
const zh = locale === "zh-CN";
Expand Down Expand Up @@ -53,11 +54,11 @@ export function PeriodicReportScheduleField({ id, value, timezone, onChange }: R
}
return <div className="personal-report-schedule">
<label className="is-boolean" htmlFor={id}>
<span>{zh ? "按日历汇报" : "Calendar reports"}</span>
<input id={id} type="checkbox" role="switch" checked={Boolean(schedule)} disabled={!onChange}
<span>{calendarRequired ? (zh ? "按日历汇报(必选)" : "Calendar reports (required)") : (zh ? "按日历汇报" : "Calendar reports")}</span>
<input id={id} type="checkbox" role="switch" checked={Boolean(schedule)} disabled={!onChange || calendarRequired}
onChange={(event) => event.target.checked ? change({}) : onChange?.(null)} />
</label>
{!schedule ? <p>{zh ? "未设置日历计划;保持阶段结束时汇报。" : "No calendar schedule; report at validated stage boundaries."}</p> : <>
{!schedule ? <p>{calendarRequired ? (zh ? "请配置必选的汇报日历。" : "Configure the required report calendar.") : (zh ? "未设置日历计划;保持阶段结束时汇报。" : "No calendar schedule; report at validated stage boundaries.")}</p> : <>
{!supported ? <p role="alert">{zh ? "此计划需在 JSON 模式中编辑;当前内容已保留。" : "Edit this schedule in JSON mode; its current value is preserved."}</p> : <>
<label htmlFor={`${id}-frequency`}><span>{zh ? "频率" : "Frequency"}</span>
<select id={`${id}-frequency`} value={rule.FREQ} disabled={!onChange} onChange={(event) => change({ FREQ: event.target.value })}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useEffect, useRef, useState} from "react";
import {connectPrivateConversation, disconnectPrivateConversation, fetchPrivateConversations,
changePrivateAgentTarget, changeOwnerNotifications, prepareManagedSteward, configureStewardMonitor, fetchChatProjects, fetchChatCapabilities, fetchLarkGroupChats, type PrivateConversation,
changePrivateAgentTarget, changeOwnerNotifications, prepareManagedSteward, configureStewardMonitor, configureStewardReport, fetchChatProjects, fetchChatCapabilities, fetchLarkGroupChats, type PrivateConversation,
discoverBoundChatSession, type BoundConversationTarget,
type ChatProject, type LarkApp, type PrivateConversationStopRecovery} from "../../data/chat";
import {useWorkspaceI18n} from "./i18n";
Expand Down Expand Up @@ -165,7 +165,8 @@ export function PrivateConversationPanel({apps, onOpenOriginal}: {apps: LarkApp[
{row.context_kind === "steward" && row.audience === "owner" && row.executor_endpoint_id === "codex"
? <StewardManagedMode state={row.managed_steward} zh={zh} busy={busy} available={row.context_available}
onPrepare={() => void act(() => prepareManagedSteward(row.binding_id))}
onMonitor={(operation, cadence) => void act(() => configureStewardMonitor(row.binding_id, operation, cadence))}/> : null}
onMonitor={(operation, cadence) => void act(() => configureStewardMonitor(row.binding_id, operation, cadence))}
onReport={(operation, schedule) => void act(() => configureStewardReport(row.binding_id, operation, schedule))}/> : null}
{row.context_kind === "steward" && row.goal_scope !== "all_registered" ? <button disabled={busy || !row.context_available} onClick={() => void act(() => connectPrivateConversation(row.app_ref, row.project_ref, row.executor_endpoint_id, "steward"))} type="button">{zh ? "授权全部已注册工作" : "Authorize all registered work"}</button> : null}
<button disabled={busy} onClick={() => void act(() => disconnectPrivateConversation(row.binding_id, revision))} type="button">{zh ? "解绑" : "Disconnect"}</button>
</article>)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type {PrivateConversation} from "../../data/chat";
import {useEffect, useState} from "react";
import {StewardReport} from "./steward-report";

/** Identity preparation is deliberately distinct from enabling scheduled work. */
export function StewardManagedMode({state, zh, busy, available, onPrepare, onMonitor}: {
export function StewardManagedMode({state, zh, busy, available, onPrepare, onMonitor, onReport}: {
state: PrivateConversation["managed_steward"]; zh: boolean; busy: boolean;
available: boolean; onPrepare: () => void;
onMonitor: (operation: "enable" | "pause", cadenceMinutes: number) => void;
onReport: (operation: "enable" | "pause" | "cancel", schedule?: Record<string, unknown>) => void;
}) {
const initial = state?.available ? state.monitor?.cadence_minutes : null;
const monitorId = state?.available ? state.monitor?.todo_id : null;
Expand All @@ -20,7 +22,7 @@ export function StewardManagedMode({state, zh, busy, available, onPrepare, onMon
? (zh ? "授权 Goal 状态监控已启用。" : "Authorized Goal monitoring is enabled.")
: state.monitor ? (zh ? "状态监控已暂停。" : "Goal monitoring is paused.")
: (zh ? "管家身份已准备,状态监控尚未启用。" : "The steward identity is prepared; monitoring is not enabled.")}</p>
<p>{zh ? "状态检查复用原生监控,不调用模型。每日汇报尚未启用。" : "State checks use the native monitor without model calls. Daily reports are not enabled."}</p>
<p>{zh ? "状态检查复用原生监控,不调用模型。" : "State checks use the native monitor without model calls."}</p>
{state.goal_active === false ? <p role="status">{zh ? "管家的工作 Goal 已停止;恢复原 Goal 后才能继续监控。" : "The steward Goal is stopped. Resume that original Goal before monitoring."}</p> : null}
{state.monitor ? <p>{state.scheduling_enabled ? (zh ? "下次检查:" : "Next check: ") : (zh ? "保留的到期时间:" : "Retained deadline: ")}{state.monitor.next_due_at ? new Date(state.monitor.next_due_at).toLocaleString(zh ? "zh-CN" : "en-US") : (zh ? "未排期" : "Unscheduled")}
<br/>{zh ? "上次检查:" : "Last check: "}{state.monitor.last_checked_at ? new Date(state.monitor.last_checked_at).toLocaleString() : (zh ? "尚未完成首次检查" : "No completed check yet")}</p> : null}
Expand All @@ -32,6 +34,7 @@ export function StewardManagedMode({state, zh, busy, available, onPrepare, onMon
<div className="personal-cadence-actions"><button className="is-primary" style={{minHeight: 44}} type="button" disabled={busy || !available || state.goal_active === false || !Number.isInteger(cadence) || cadence < 30 || cadence > 1440}
onClick={() => onMonitor("enable", cadence)}>{state.scheduling_enabled ? (zh ? "更新周期" : "Update interval") : (zh ? "启用状态监控" : "Enable monitoring")}</button>
{state.monitor?.status === "open" ? <button style={{minHeight: 44}} type="button" disabled={busy || !available} onClick={() => onMonitor("pause", initial ?? 30)}>{zh ? "暂停状态监控" : "Pause monitoring"}</button> : null}</div>
<StewardReport state={state.report} zh={zh} busy={busy} available={available} goalActive={state.goal_active !== false} onChange={onReport}/>
</>
: <>
<p>{zh ? "为当前管家准备一个可见的 LoopX 工作身份,保留原对话和权限。此步骤不会启动定时任务或模型。" : "Prepare one visible LoopX identity for this steward, retaining its original conversation and permissions. This step starts neither scheduled tasks nor a model."}</p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.personal-steward-report { display: grid; gap: 16px; min-width: 0; margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--pw-line); }
.personal-steward-report h4 { margin: 0; font-size: var(--text-heading); line-height: var(--leading-heading); }
.personal-steward-report > p { margin: 0; color: var(--pw-muted); font-size: var(--text-body); line-height: var(--leading-body); overflow-wrap: anywhere; }
.personal-steward-report > [role="status"] { color: var(--pw-text); font-weight: 600; }
.personal-steward-report .personal-cadence-actions > button { min-height: 44px; }
.personal-steward-report .personal-capability-fields > label { grid-column: 1 / -1; }
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import {useEffect, useId, useState} from "react";
import type {PrivateConversation} from "../../data/chat";
import {PeriodicReportScheduleField, withReportScheduleTimezone} from "./periodic-report-schedule-field";
import "./steward-report.css";

type Managed = Extract<NonNullable<PrivateConversation["managed_steward"]>, {available: true}>;

export function StewardReport({state, zh, busy, available, goalActive, onChange}: {
state: Managed["report"]; zh: boolean; busy: boolean; available: boolean; goalActive: boolean;
onChange: (operation: "enable" | "pause" | "cancel", schedule?: Record<string, unknown>) => void;
}) {
const original = state?.available ? state.schedule : null;
const scheduleId = useId();
const defaultZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
const [zone, setZone] = useState(String(original?.timezone ?? defaultZone));
const [schedule, setSchedule] = useState<Record<string, unknown> | null>(original ?? {
schema_version: "periodic_report_schedule_v0", schedule_id: "steward-report",
timezone: defaultZone, rrule: "FREQ=DAILY;BYHOUR=9;BYMINUTE=0",
start_date: new Intl.DateTimeFormat("sv-SE", {timeZone: defaultZone}).format(new Date()),
});
const signature = JSON.stringify(original);
useEffect(() => {if (original) {setSchedule(original); setZone(String(original.timezone));}}, [signature]);
if (!state) return null;
const disabled = busy || !available;
let validZone = true;
try {new Intl.DateTimeFormat("en", {timeZone: zone}).format();} catch {validZone = false;}
const stamp = (value?: string | null) => value ? new Date(value).toLocaleString(zh ? "zh-CN" : "en-US") : (zh ? "暂无" : "None");
return <section className="personal-steward-report">
<h4>{zh ? "定期详细汇报" : "Detailed periodic reports"}</h4>
{!state.available ? <p role="status">{zh ? "汇报状态暂不可读;原计划和回执已保留。刷新连接后重试。" : "Report status is unavailable. The original schedule and receipts are retained; refresh the connection."}</p> : <>
<p role="status">{state.enabled ? (zh ? "已启用 · 完整中文正文回此私聊" : "Enabled · Full Chinese report returns to this private Chat")
: state.configured ? (zh ? "已暂停 · 原窗口和回执保留" : "Paused · Original windows and receipts retained")
: (zh ? "尚未启用" : "Not enabled")}</p>
<p>{zh ? "汇总全部获准本机 Goal,包括停止项和读取缺口。每天汇报前24小时,生成与实际送达分别核验;状态监控照常运行。" : "Covers every authorized local Goal, including stopped Goals and read gaps. Daily reports cover the previous 24 hours; generation and delivery are verified separately. Monitoring continues."}</p>
{state.configured ? <p>{zh ? "下次计划:" : "Next deadline: "}{stamp(state.next_due_at)}<br/>
{zh ? "最近窗口:" : "Latest window: "}{stamp(state.retained_window_due_at)} · {state.delivery_status === "delivered"
? (zh ? "原私聊全文已核验送达" : "Full private reply verified")
: state.delivery_status === "generation_failed" ? (zh ? "本期生成失败,证据保留;取消失败窗口后,后续到期可继续" : "Generation failed; evidence retained. Cancel this failed window to continue at a later deadline.")
: state.delivery_status === "generation_cancelled" ? (zh ? "失败窗口已取消,未完成或送达;原证据保留" : "Failed window cancelled, not completed or delivered; evidence retained")
: state.delivery_status === "unconfirmed" ? (zh ? "发送结果待确认,不重复发送" : "Send unconfirmed; no blind resend")
: state.generation_id ? (zh ? "已生成,投递待核验" : "Generated; delivery pending") : (zh ? "尚未生成" : "Not generated")}</p> : null}
<div className="personal-capability-fields"><label>{zh ? "汇报时区" : "Report timezone"}<input value={zone} aria-invalid={!validZone} disabled={disabled} onChange={event => {
const value = event.target.value; setZone(value);
if (schedule) setSchedule(withReportScheduleTimezone({schedule}, "timezone", value).schedule as Record<string, unknown>);
}}/></label>
{!validZone ? <p role="alert">{zh ? "请输入有效时区,例如 Asia/Shanghai。" : "Enter a valid timezone, such as Asia/Shanghai."}</p> : null}
<PeriodicReportScheduleField id={scheduleId} value={schedule} timezone={zone} calendarRequired
onChange={disabled ? undefined : setSchedule}/></div>
<p>{zh ? "暂停只停止后续生成;取消还会请求停止正在生成的本期汇报。两者均保留已生成报告和投递回执,停用期间不再发送。" : "Pause stops future generation. Cancel also requests a stop for an active report. Both retain generated reports and receipts, and stop further delivery while disabled."}</p>
<div className="personal-cadence-actions"><button className="is-primary" type="button" disabled={disabled || !goalActive || !schedule || !zone.trim() || !validZone}
onClick={() => schedule && onChange("enable", schedule)}>{state.enabled ? (zh ? "更新汇报计划" : "Update report schedule") : (zh ? "启用详细汇报" : "Enable detailed reports")}</button>
{state.enabled ? <button type="button" disabled={disabled} onClick={() => onChange("pause")}>{zh ? "暂停汇报" : "Pause reports"}</button> : null}
{state.configured ? <button type="button" disabled={disabled} onClick={() => onChange("cancel")}>{state.delivery_status === "generation_failed" ? (zh ? "取消失败窗口" : "Cancel failed window") : (zh ? "取消汇报" : "Cancel reports")}</button> : null}
</div>
</>}
</section>;
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading