feat: 无界 ticket 登录、AI 写作配置确认与定时任务弹窗修复

无界 onlineTicket 对接 MaxKey 与 consumer 接口并用 sub 登录;新增 setup_writing 写作配置确认流;定时任务创建弹窗限制多行高度并支持滚动。
This commit is contained in:
fengxiangdebaozi committed 2026-06-03 16:20:45 +08:00
1 parent ab876fc7d8
commit ad4b27b7ca
25 files changed
+980 -54

No files matched your search

@@ -60,6 +60,8 @@ export function MessageList({
onClarificationSubmit,
onSetupAgentApprove,
onSetupAgentReject,
onSetupWritingApprove,
onSetupWritingReject,
}: {
className?: string;
threadId: string;
@@ -70,6 +72,8 @@ export function MessageList({
onClarificationSubmit?: (answer: string) => void | Promise<void>;
onSetupAgentApprove?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
onSetupAgentReject?: (payload: SetupAgentApprovalPayload) => void | Promise<void>;
onSetupWritingApprove?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
onSetupWritingReject?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
}) {
const baseRehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
const rehypePlugins = useMemo(
@@ -77,6 +81,7 @@ export function MessageList({
[baseRehypePlugins, extraRehypePlugins],
);
const updateSubtask = useUpdateSubtask();
const { t } = useI18n();
const messages = thread.messages;
// Stamp a wall-clock time onto every question/answer the first time it is
@@ -307,6 +312,23 @@ export function MessageList({
/>
</div>
) : null;
} else if (group.type === "assistant:setup-writing-approval") {
const message = group.messages[0];
return message ? (
<div key={group.id} className="w-full">
<SetupWritingApprovalCard
disabled={thread.isLoading}
message={message}
onApprove={onSetupWritingApprove}
onReject={onSetupWritingReject}
/>
<MessageTokenUsageList
enabled={tokenUsageEnabled}
isLoading={thread.isLoading}
messages={group.messages}
/>
</div>
) : null;
} else if (group.type === "assistant:present-files") {
const files: string[] = [];
for (const message of group.messages) {
@@ -485,6 +507,16 @@ export type SetupAgentApprovalPayload = {
skills?: string[];
};
export type SetupWritingApprovalPayload = {
user_intent?: string;
article_type?: string;
word_count_target?: number | null;
target_audience?: string;
writing_mode?: string;
material_source?: string;
outline?: string;
};
function findPreviousHumanMessageId(
messages: Message[],
beforeMessageId?: string,
@@ -635,6 +667,155 @@ function SetupAgentApprovalCard({
);
}
function getSetupWritingApprovalPayload(
message: unknown,
): SetupWritingApprovalPayload | null {
if (typeof message !== "object" || message === null) {
return null;
}
const additionalKwargs = Reflect.get(message, "additional_kwargs");
if (typeof additionalKwargs !== "object" || additionalKwargs === null) {
return null;
}
const approval = Reflect.get(additionalKwargs, "setup_writing_approval");
if (typeof approval !== "object" || approval === null) {
return null;
}
return approval as SetupWritingApprovalPayload;
}
const WRITING_MODE_LABELS: Record<string, string> = {
strict: "严格依据素材",
loose: "可结合通用知识",
};
const MATERIAL_SOURCE_LABELS: Record<string, string> = {
knowledge_base: "知识库",
skill: "技能检索",
};
function SetupWritingApprovalCard({
disabled,
message,
onApprove,
onReject,
}: {
disabled: boolean;
message: unknown;
onApprove?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
onReject?: (payload: SetupWritingApprovalPayload) => void | Promise<void>;
}) {
const payload = getSetupWritingApprovalPayload(message);
const [submitting, setSubmitting] = useState<"approve" | "reject" | null>(
null,
);
if (!payload) {
return null;
}
const approvalPayload = payload;
const busy = disabled || submitting !== null;
async function run(action: "approve" | "reject") {
if (busy) {
return;
}
setSubmitting(action);
try {
if (action === "approve") {
await onApprove?.(approvalPayload);
} else {
await onReject?.(approvalPayload);
}
} finally {
setSubmitting(null);
}
}
const rows: Array<{ label: string; value: string }> = [];
if (approvalPayload.article_type) {
rows.push({ label: "文章类型", value: approvalPayload.article_type });
}
if (typeof approvalPayload.word_count_target === "number") {
rows.push({ label: "目标字数", value: `${approvalPayload.word_count_target} 字` });
}
if (approvalPayload.target_audience) {
rows.push({ label: "目标读者", value: approvalPayload.target_audience });
}
if (approvalPayload.writing_mode) {
rows.push({
label: "写作模式",
value: WRITING_MODE_LABELS[approvalPayload.writing_mode] ?? approvalPayload.writing_mode,
});
}
if (approvalPayload.material_source) {
rows.push({
label: "素材来源",
value: MATERIAL_SOURCE_LABELS[approvalPayload.material_source] ?? approvalPayload.material_source,
});
}
return (
<div className="border-border bg-card text-card-foreground w-full rounded-lg border p-4 shadow-sm">
<div className="space-y-3">
<div>
<div className="text-muted-foreground text-xs font-medium">写作配置建议</div>
<div className="text-sm font-medium">确认后将应用到左侧写作表单。</div>
</div>
{approvalPayload.user_intent ? (
<div className="space-y-1">
<div className="text-muted-foreground text-xs">写作意图</div>
<p className="text-sm leading-6">{approvalPayload.user_intent}</p>
</div>
) : null}
{rows.length > 0 ? (
<div className="flex flex-wrap gap-x-6 gap-y-1.5">
{rows.map((row) => (
<div key={row.label} className="space-y-0.5">
<div className="text-muted-foreground text-xs">{row.label}</div>
<p className="text-sm font-medium">{row.value}</p>
</div>
))}
</div>
) : null}
{approvalPayload.outline ? (
<div className="space-y-1">
<div className="text-muted-foreground text-xs">已有大纲</div>
<pre className="bg-muted max-h-52 overflow-auto rounded-md p-3 text-xs whitespace-pre-wrap">
{approvalPayload.outline}
</pre>
</div>
) : null}
</div>
<div className="mt-4 flex justify-end gap-2">
<Button
type="button"
variant="outline"
size="sm"
disabled={busy || !onReject}
onClick={() => void run("reject")}
>
<XIcon className="h-4 w-4" />
忽略
</Button>
<Button
type="button"
size="sm"
disabled={busy || !onApprove}
onClick={() => void run("approve")}
>
<CheckIcon className="h-4 w-4" />
应用到表单
</Button>
</div>
</div>
);
}
function getClarificationPayload(message: unknown): ClarificationPayload | null {
if (typeof message !== "object" || message === null) {
return null;