Merge branch 'master' into ji
冲突解决: - frontend-web/src/pages/AgentChatPage.tsx: 保留 HEAD 的 isLandingState + AgentWelcome 欢迎页扩展,丢弃 master 的旧版 InputBox 风格 - offline-backend-20260512/backend/config.yaml: 保留 HEAD 的对齐注释格式; master 引入的中文 bank_mission / bank_retain_mission 已自动合入 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
a892f55cb2
18 files changed
+1043
-96
No files matched your search
@@ -4,3 +4,6 @@
|
|||||||
|
|
||||||
VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
|
VITE_BACKEND_BASE_URL=http://127.0.0.1:8001
|
||||||
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
|
VITE_LANGGRAPH_BASE_URL=http://127.0.0.1:8001/api
|
||||||
|
|
||||||
|
# 轻应用「新建笔记」系列接口(与 DeerFlow Gateway 分离)
|
||||||
|
VITE_NOTE_API_BASE_URL=http://47.88.25.99:8000
|
||||||
@@ -21,11 +21,13 @@ import {
|
|||||||
History,
|
History,
|
||||||
Layout,
|
Layout,
|
||||||
Library,
|
Library,
|
||||||
|
List,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
ListTodo,
|
ListTodo,
|
||||||
MessageSquareCode,
|
MessageSquareCode,
|
||||||
MessageSquarePlus,
|
MessageSquarePlus,
|
||||||
Network,
|
Network,
|
||||||
|
NotebookPen,
|
||||||
Plus,
|
Plus,
|
||||||
Settings,
|
Settings,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
@@ -81,10 +83,23 @@ const menuItems: MenuItem[] = [
|
|||||||
icon: Grid3X3,
|
icon: Grid3X3,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
id: "light-app-new-note",
|
id: "light-app-note-management",
|
||||||
label: "新建笔记",
|
label: "笔记管理",
|
||||||
icon: Plus,
|
icon: NotebookPen,
|
||||||
path: "/page/strategy/light-app/new-note",
|
children: [
|
||||||
|
{
|
||||||
|
id: "light-app-new-note",
|
||||||
|
label: "新建笔记",
|
||||||
|
icon: Plus,
|
||||||
|
path: "/page/strategy/light-app/new-note",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "light-app-note-list",
|
||||||
|
label: "笔记列表",
|
||||||
|
icon: List,
|
||||||
|
path: "/page/strategy/light-app/notes",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{ id: "light-app-square", label: "应用广场", icon: Layout },
|
{ id: "light-app-square", label: "应用广场", icon: Layout },
|
||||||
{ id: "light-app-records", label: "运行记录", icon: History },
|
{ id: "light-app-records", label: "运行记录", icon: History },
|
||||||
@@ -208,6 +223,8 @@ const menuItems: MenuItem[] = [
|
|||||||
|
|
||||||
const defaultExpandedIds = new Set([
|
const defaultExpandedIds = new Set([
|
||||||
"qa-center",
|
"qa-center",
|
||||||
|
"light-app-center",
|
||||||
|
"light-app-note-management",
|
||||||
"task-management",
|
"task-management",
|
||||||
"xk-center-task",
|
"xk-center-task",
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -68,3 +68,13 @@ export function getLangGraphBaseURL(isMock?: boolean) {
|
|||||||
return "http://localhost:2026/api/langgraph";
|
return "http://localhost:2026/api/langgraph";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 轻应用「新建笔记」系列接口的基础地址(与 DeerFlow Gateway 分离)。 */
|
||||||
|
export function getNoteApiBaseURL(): string {
|
||||||
|
if (env.NEXT_PUBLIC_NOTE_API_BASE_URL) {
|
||||||
|
return new URL(env.NEXT_PUBLIC_NOTE_API_BASE_URL, getBaseOrigin())
|
||||||
|
.toString()
|
||||||
|
.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { getNoteApiBaseURL } from "@/core/config";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ensureNoteAuth,
|
||||||
|
getNoteAuthorizationHeader,
|
||||||
|
getStoredNoteAuth,
|
||||||
|
} from "./auth";
|
||||||
|
|
||||||
|
export type { NoteAuth } from "./auth";
|
||||||
|
|
||||||
|
export interface SearchSpace {
|
||||||
|
created_at: string;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
user_id: string;
|
||||||
|
citations_enabled: boolean;
|
||||||
|
qna_custom_instructions: string;
|
||||||
|
summary: string | null;
|
||||||
|
avatar_url: string | null;
|
||||||
|
sources_limit: number;
|
||||||
|
sources_used: number;
|
||||||
|
is_shared: boolean;
|
||||||
|
is_owner: boolean;
|
||||||
|
owner_email: string | null;
|
||||||
|
member_count?: number;
|
||||||
|
source_count?: number;
|
||||||
|
membership_role?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListSearchSpacesQuery {
|
||||||
|
owned_only?: boolean;
|
||||||
|
limit?: number;
|
||||||
|
order_by?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateSearchSpaceInput {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
citations_enabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function noteApiFetch(
|
||||||
|
path: string,
|
||||||
|
init: RequestInit = {},
|
||||||
|
): Promise<Response> {
|
||||||
|
const auth = getStoredNoteAuth() ?? (await ensureNoteAuth());
|
||||||
|
|
||||||
|
const headers = new Headers(init.headers);
|
||||||
|
headers.set("Authorization", getNoteAuthorizationHeader(auth));
|
||||||
|
|
||||||
|
return fetch(`${getNoteApiBaseURL()}${path}`, {
|
||||||
|
...init,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSearchSpace(
|
||||||
|
input: CreateSearchSpaceInput,
|
||||||
|
): Promise<SearchSpace> {
|
||||||
|
await ensureNoteAuth();
|
||||||
|
|
||||||
|
const response = await noteApiFetch("/api/v1/searchspaces", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: input.name,
|
||||||
|
description: input.description ?? "",
|
||||||
|
citations_enabled: input.citations_enabled ?? true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`创建笔记失败: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as SearchSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listSearchSpaces(
|
||||||
|
query: ListSearchSpacesQuery = {},
|
||||||
|
): Promise<SearchSpace[]> {
|
||||||
|
await ensureNoteAuth();
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
owned_only: String(query.owned_only ?? true),
|
||||||
|
limit: String(query.limit ?? 1000),
|
||||||
|
order_by: query.order_by ?? "created_at",
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await noteApiFetch(`/api/v1/searchspaces?${params.toString()}`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`获取笔记列表失败: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: unknown = await response.json();
|
||||||
|
return Array.isArray(data) ? (data as SearchSpace[]) : [];
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { getNoteApiBaseURL } from "@/core/config";
|
||||||
|
|
||||||
|
const NOTE_AUTH_STORAGE_KEY = "deerflow.note.auth";
|
||||||
|
|
||||||
|
const NOTE_LOGIN_USERNAME = "dhk@qq.com";
|
||||||
|
const NOTE_LOGIN_PASSWORD = "1qaz@WSX";
|
||||||
|
|
||||||
|
export interface NoteAuth {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBrowser(): boolean {
|
||||||
|
return typeof window !== "undefined";
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNoteApiBaseURL(): string {
|
||||||
|
const baseURL = getNoteApiBaseURL();
|
||||||
|
if (!baseURL) {
|
||||||
|
throw new Error("未配置 VITE_NOTE_API_BASE_URL");
|
||||||
|
}
|
||||||
|
return baseURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getStoredNoteAuth(): NoteAuth | null {
|
||||||
|
if (!isBrowser()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const raw = window.localStorage.getItem(NOTE_AUTH_STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as NoteAuth;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setStoredNoteAuth(auth: NoteAuth): void {
|
||||||
|
if (!isBrowser()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.localStorage.setItem(NOTE_AUTH_STORAGE_KEY, JSON.stringify(auth));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearStoredNoteAuth(): void {
|
||||||
|
if (!isBrowser()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.localStorage.removeItem(NOTE_AUTH_STORAGE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNoteAuthorizationHeader(auth: NoteAuth): string {
|
||||||
|
const token = auth.access_token.trim();
|
||||||
|
const tokenType = auth.token_type?.trim() || "Bearer";
|
||||||
|
return `${tokenType} ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginNoteApi(): Promise<NoteAuth> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("username", NOTE_LOGIN_USERNAME);
|
||||||
|
formData.append("password", NOTE_LOGIN_PASSWORD);
|
||||||
|
formData.append("grant_type", "password");
|
||||||
|
|
||||||
|
const response = await fetch(`${assertNoteApiBaseURL()}/auth/jwt/login`, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`笔记登录失败: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await response.json()) as Partial<NoteAuth>;
|
||||||
|
if (!data.access_token) {
|
||||||
|
throw new Error("笔记登录失败: 未返回 access_token");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth: NoteAuth = {
|
||||||
|
access_token: data.access_token,
|
||||||
|
token_type: data.token_type?.trim() || "bearer",
|
||||||
|
};
|
||||||
|
setStoredNoteAuth(auth);
|
||||||
|
return auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureNoteAuth(): Promise<NoteAuth> {
|
||||||
|
const existing = getStoredNoteAuth();
|
||||||
|
if (existing?.access_token) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
return loginNoteApi();
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ function readEnv(
|
|||||||
| "VITE_BACKEND_BASE_URL"
|
| "VITE_BACKEND_BASE_URL"
|
||||||
| "VITE_LANGGRAPH_BASE_URL"
|
| "VITE_LANGGRAPH_BASE_URL"
|
||||||
| "VITE_CANVAS_LANGGRAPH_BASE_URL"
|
| "VITE_CANVAS_LANGGRAPH_BASE_URL"
|
||||||
|
| "VITE_NOTE_API_BASE_URL"
|
||||||
| "VITE_STATIC_WEBSITE_ONLY"
|
| "VITE_STATIC_WEBSITE_ONLY"
|
||||||
| "VITE_GITHUB_OAUTH_TOKEN",
|
| "VITE_GITHUB_OAUTH_TOKEN",
|
||||||
): EnvValue {
|
): EnvValue {
|
||||||
@@ -18,6 +19,7 @@ export const env = {
|
|||||||
// Canvas page uses its own LangGraph base URL so it can be independently
|
// Canvas page uses its own LangGraph base URL so it can be independently
|
||||||
// routed (e.g. a separate canvas-agent server). Falls back to the main URL.
|
// routed (e.g. a separate canvas-agent server). Falls back to the main URL.
|
||||||
NEXT_PUBLIC_CANVAS_LANGGRAPH_BASE_URL: readEnv("VITE_CANVAS_LANGGRAPH_BASE_URL"),
|
NEXT_PUBLIC_CANVAS_LANGGRAPH_BASE_URL: readEnv("VITE_CANVAS_LANGGRAPH_BASE_URL"),
|
||||||
|
NEXT_PUBLIC_NOTE_API_BASE_URL: readEnv("VITE_NOTE_API_BASE_URL"),
|
||||||
NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readEnv("VITE_STATIC_WEBSITE_ONLY"),
|
NEXT_PUBLIC_STATIC_WEBSITE_ONLY: readEnv("VITE_STATIC_WEBSITE_ONLY"),
|
||||||
GITHUB_OAUTH_TOKEN: readEnv("VITE_GITHUB_OAUTH_TOKEN"),
|
GITHUB_OAUTH_TOKEN: readEnv("VITE_GITHUB_OAUTH_TOKEN"),
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { loginNoteApi, type NoteAuth } from "@/core/note/auth";
|
||||||
|
|
||||||
|
export function useNoteAuth() {
|
||||||
|
const [auth, setAuth] = useState<NoteAuth | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const refreshAuth = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const nextAuth = await loginNoteApi();
|
||||||
|
setAuth(nextAuth);
|
||||||
|
return nextAuth;
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : "笔记登录失败,请稍后重试";
|
||||||
|
setError(message);
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshAuth().catch(() => {
|
||||||
|
// 错误已在 refreshAuth 中写入 state
|
||||||
|
});
|
||||||
|
}, [refreshAuth]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
auth,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
refreshAuth,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -40,6 +40,8 @@ import { AIWritingTimeline } from './AIWritingTimeline';
|
|||||||
import { InterventionCard } from './InterventionCard';
|
import { InterventionCard } from './InterventionCard';
|
||||||
import { WritingSkeletonCard } from './WritingSkeletonCard';
|
import { WritingSkeletonCard } from './WritingSkeletonCard';
|
||||||
import { DraftStreamingView } from './DraftStreamingView';
|
import { DraftStreamingView } from './DraftStreamingView';
|
||||||
|
import { IntentResultCard } from './IntentResultCard';
|
||||||
|
import { IntentStreamingView } from './IntentStreamingView';
|
||||||
import { OutlineStreamingView } from './OutlineStreamingView';
|
import { OutlineStreamingView } from './OutlineStreamingView';
|
||||||
import { ReviewStreamingView } from './ReviewStreamingView';
|
import { ReviewStreamingView } from './ReviewStreamingView';
|
||||||
import { RevisionSummaryCard } from './RevisionSummaryCard';
|
import { RevisionSummaryCard } from './RevisionSummaryCard';
|
||||||
@@ -343,6 +345,17 @@ export function AIWritingPanel() {
|
|||||||
<AIWritingForm onSubmit={startWriting} />
|
<AIWritingForm onSubmit={startWriting} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 意图解析:流式 → 结果卡片承接。
|
||||||
|
放在 timeline 之上 —— 解析后 timeline 会从 step_done 起一直长,
|
||||||
|
如果把意图块塞在 timeline 下面就会随 timeline 增长被推走;
|
||||||
|
放上面则恒定锚定在内容最顶部,刷到底也能滚回顶部看清楚意图理解结果。 */}
|
||||||
|
{(state.intentStreamingText || state.intentThinkingText) && (
|
||||||
|
<IntentStreamingView text={state.intentStreamingText} thinkingText={state.intentThinkingText} />
|
||||||
|
)}
|
||||||
|
{state.parsedIntent && !state.intentStreamingText && (
|
||||||
|
<IntentResultCard intent={state.parsedIntent} articleTypes={articleTypes} />
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 运行中或完成后显示时间线 */}
|
{/* 运行中或完成后显示时间线 */}
|
||||||
{!isIdle && (
|
{!isIdle && (
|
||||||
<AIWritingTimeline
|
<AIWritingTimeline
|
||||||
@@ -394,7 +407,9 @@ export function AIWritingPanel() {
|
|||||||
{showSkeleton &&
|
{showSkeleton &&
|
||||||
state.status !== 'writing_draft' &&
|
state.status !== 'writing_draft' &&
|
||||||
!(state.status === 'writing_outline' && (state.outlineStreamingText || state.outlineThinkingText)) &&
|
!(state.status === 'writing_outline' && (state.outlineStreamingText || state.outlineThinkingText)) &&
|
||||||
!(state.status === 'reviewing' && state.streamingReviewItems.length > 0) && (
|
!(state.status === 'reviewing' && state.streamingReviewItems.length > 0) &&
|
||||||
|
/* 意图解析流式过程中(LLM 还在吐 JSON)不要显示 skeleton */
|
||||||
|
!state.intentStreamingText && !state.intentThinkingText && (
|
||||||
<div className="px-4 pb-4">
|
<div className="px-4 pb-4">
|
||||||
<WritingSkeletonCard status={state.status} researchProgress={state.researchProgress} />
|
<WritingSkeletonCard status={state.status} researchProgress={state.researchProgress} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Sparkles } from 'lucide-react';
|
||||||
|
|
||||||
|
import type { ParsedIntent } from '../../types/ai-writing';
|
||||||
|
import type { ArticleType } from '../../api/ai-writing-article-types';
|
||||||
|
|
||||||
|
interface IntentResultCardProps {
|
||||||
|
intent: ParsedIntent;
|
||||||
|
articleTypes: ArticleType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 意图解析完成后的结构化卡片。展示 LLM 把用户原始意图理解成的四项:
|
||||||
|
* 文章类型 / 目标读者 / 字数 / 精炼意图。在素材收集开始前作为"承上启下"的视觉锚点。
|
||||||
|
*/
|
||||||
|
export function IntentResultCard({ intent, articleTypes }: IntentResultCardProps) {
|
||||||
|
// article_type 后端给的是 key(如 "analysis"),UI 展示用 label(如 "分析报道")
|
||||||
|
const typeLabel =
|
||||||
|
articleTypes.find((t) => t.key === intent.articleType)?.label ?? intent.articleType;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 pb-2">
|
||||||
|
<div className="rounded-xl border border-blue-200 dark:border-blue-900 bg-blue-50/40 dark:bg-blue-950/20 overflow-hidden shadow-sm">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-blue-200/60 dark:border-blue-900/60 bg-blue-100/40 dark:bg-blue-900/30">
|
||||||
|
<Sparkles className="size-3.5 text-blue-500 shrink-0" />
|
||||||
|
<span className="text-xs font-medium text-blue-600 dark:text-blue-400">
|
||||||
|
意图解析完成
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="px-3 py-2.5 space-y-1.5 text-xs">
|
||||||
|
<Row label="文章类型" value={typeLabel} />
|
||||||
|
<Row label="目标读者" value={intent.targetAudience} />
|
||||||
|
<Row label="字数目标" value={`${intent.wordCountTarget} 字`} />
|
||||||
|
{intent.refinedIntent && (
|
||||||
|
<Row label="精炼意图" value={intent.refinedIntent} multiline />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Row({ label, value, multiline }: { label: string; value: string; multiline?: boolean }) {
|
||||||
|
return (
|
||||||
|
<div className={multiline ? 'flex flex-col gap-0.5' : 'flex items-baseline gap-2'}>
|
||||||
|
<span className="shrink-0 text-muted-foreground">{label}</span>
|
||||||
|
<span className={multiline ? 'text-foreground leading-relaxed' : 'text-foreground truncate'}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { Sparkles, ChevronDown, ChevronRight, Brain } from 'lucide-react';
|
||||||
|
|
||||||
|
interface IntentStreamingViewProps {
|
||||||
|
text: string;
|
||||||
|
thinkingText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 意图解析流式视图:与 OutlineStreamingView / DraftStreamingView 同样的打字机机制,
|
||||||
|
* 颜色用蓝色主题区分意图理解阶段。展示 LLM 边吐 JSON 边滚动的过程,结果由
|
||||||
|
* IntentResultCard 接力展示结构化字段。
|
||||||
|
*/
|
||||||
|
export function IntentStreamingView({ text, thinkingText }: IntentStreamingViewProps) {
|
||||||
|
const preRef = useRef<HTMLPreElement>(null);
|
||||||
|
const thinkRef = useRef<HTMLPreElement>(null);
|
||||||
|
const [thinkOpen, setThinkOpen] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preRef.current) {
|
||||||
|
preRef.current.scrollTop = preRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [text]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (thinkOpen && thinkRef.current) {
|
||||||
|
thinkRef.current.scrollTop = thinkRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [thinkingText, thinkOpen]);
|
||||||
|
|
||||||
|
if (!text && !thinkingText) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="px-4 pb-2 space-y-2">
|
||||||
|
{thinkingText && (
|
||||||
|
<div className="rounded-xl border border-amber-200 dark:border-amber-900 bg-amber-50/50 dark:bg-amber-950/20 overflow-hidden shadow-sm">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setThinkOpen((v) => !v)}
|
||||||
|
className="w-full flex items-center gap-2 px-3 py-2 border-b border-amber-200/60 dark:border-amber-900/60 bg-amber-100/40 dark:bg-amber-900/30 hover:bg-amber-100/70 dark:hover:bg-amber-900/50 transition-colors"
|
||||||
|
>
|
||||||
|
<Brain className="size-3.5 text-amber-500 shrink-0" />
|
||||||
|
<span className="text-xs font-medium text-amber-600 dark:text-amber-400">
|
||||||
|
思考过程
|
||||||
|
</span>
|
||||||
|
<span className="ml-auto text-amber-400">
|
||||||
|
{thinkOpen
|
||||||
|
? <ChevronDown className="size-3.5" />
|
||||||
|
: <ChevronRight className="size-3.5" />}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{thinkOpen && (
|
||||||
|
<pre
|
||||||
|
ref={thinkRef}
|
||||||
|
className="text-[11px] font-mono leading-relaxed p-3 text-amber-700/70 dark:text-amber-300/60 whitespace-pre-wrap max-h-40 overflow-y-auto italic"
|
||||||
|
>
|
||||||
|
{thinkingText}
|
||||||
|
{!text && (
|
||||||
|
<span className="inline-block w-0.5 h-3.5 bg-amber-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{text && (
|
||||||
|
<div className="rounded-xl border border-blue-200 dark:border-blue-900 bg-blue-50/50 dark:bg-blue-950/20 overflow-hidden shadow-sm">
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-blue-200/60 dark:border-blue-900/60 bg-blue-100/40 dark:bg-blue-900/30">
|
||||||
|
<Sparkles className="size-3.5 text-blue-500 shrink-0" />
|
||||||
|
<span className="text-xs font-medium text-blue-600 dark:text-blue-400">
|
||||||
|
意图解析中…
|
||||||
|
</span>
|
||||||
|
<span className="ml-auto inline-flex gap-1">
|
||||||
|
{[0, 1, 2].map((i) => (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className="size-1 rounded-full bg-blue-400 animate-bounce"
|
||||||
|
style={{ animationDelay: `${i * 150}ms` }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<pre
|
||||||
|
ref={preRef}
|
||||||
|
className="text-[11px] font-mono leading-relaxed p-3 text-muted-foreground whitespace-pre-wrap max-h-52 overflow-y-auto"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
<span className="inline-block w-0.5 h-3.5 bg-blue-400 ml-0.5 align-text-bottom animate-[blink_1s_step-end_infinite]" />
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<style>{`
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0; }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import type {
|
|||||||
AIOutline,
|
AIOutline,
|
||||||
BlockedSection,
|
BlockedSection,
|
||||||
CompletedIntervention,
|
CompletedIntervention,
|
||||||
|
ParsedIntent,
|
||||||
ProgressEvent,
|
ProgressEvent,
|
||||||
ProgressEventType,
|
ProgressEventType,
|
||||||
ReviewIssue,
|
ReviewIssue,
|
||||||
@@ -66,6 +67,9 @@ const INITIAL_STATE: AIWritingState = {
|
|||||||
streamingDraftSections: [],
|
streamingDraftSections: [],
|
||||||
outlineStreamingText: '',
|
outlineStreamingText: '',
|
||||||
outlineThinkingText: '',
|
outlineThinkingText: '',
|
||||||
|
intentStreamingText: '',
|
||||||
|
intentThinkingText: '',
|
||||||
|
parsedIntent: null,
|
||||||
researchProgress: '',
|
researchProgress: '',
|
||||||
streamingReviewItems: [],
|
streamingReviewItems: [],
|
||||||
revisionSummary: null,
|
revisionSummary: null,
|
||||||
@@ -146,6 +150,41 @@ function handleEvent(prev: AIWritingState, event: ProgressEvent): AIWritingState
|
|||||||
case 'step_done':
|
case 'step_done':
|
||||||
return base;
|
return base;
|
||||||
|
|
||||||
|
case 'intent_chunk': {
|
||||||
|
const text = (event as unknown as { text: string }).text ?? '';
|
||||||
|
const isThinking = (event as unknown as { is_thinking: boolean }).is_thinking ?? false;
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
intentStreamingText: isThinking ? prev.intentStreamingText : prev.intentStreamingText + text,
|
||||||
|
intentThinkingText: isThinking ? prev.intentThinkingText + text : prev.intentThinkingText,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'intent_ready': {
|
||||||
|
const ev = event as unknown as {
|
||||||
|
articleType?: string;
|
||||||
|
article_type?: string;
|
||||||
|
targetAudience?: string;
|
||||||
|
target_audience?: string;
|
||||||
|
wordCountTarget?: number;
|
||||||
|
word_count_target?: number;
|
||||||
|
refinedIntent?: string;
|
||||||
|
refined_intent?: string;
|
||||||
|
};
|
||||||
|
const parsed: ParsedIntent = {
|
||||||
|
articleType: ev.articleType ?? ev.article_type ?? '',
|
||||||
|
targetAudience: ev.targetAudience ?? ev.target_audience ?? '',
|
||||||
|
wordCountTarget: Number(ev.wordCountTarget ?? ev.word_count_target ?? 800),
|
||||||
|
refinedIntent: ev.refinedIntent ?? ev.refined_intent ?? '',
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
parsedIntent: parsed,
|
||||||
|
intentStreamingText: '',
|
||||||
|
intentThinkingText: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case 'research_progress':
|
case 'research_progress':
|
||||||
return { ...base, researchProgress: event.message ?? '' };
|
return { ...base, researchProgress: event.message ?? '' };
|
||||||
|
|
||||||
@@ -403,6 +442,7 @@ function customToProgressEvent(raw: unknown, sessionId: string): ProgressEvent |
|
|||||||
// 后端 _save_draft_to_repo 等 helper 不走 custom 通道,type 必为节点 emit 的 chunk 类型
|
// 后端 _save_draft_to_repo 等 helper 不走 custom 通道,type 必为节点 emit 的 chunk 类型
|
||||||
if (!type) return null;
|
if (!type) return null;
|
||||||
const allowed: ProgressEventType[] = [
|
const allowed: ProgressEventType[] = [
|
||||||
|
'intent_chunk', 'intent_ready',
|
||||||
'research_progress', 'outline_chunk', 'draft_chunk',
|
'research_progress', 'outline_chunk', 'draft_chunk',
|
||||||
'review_chunk', 'review_item_done',
|
'review_chunk', 'review_item_done',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ export type ProgressEventType =
|
|||||||
| 'connected'
|
| 'connected'
|
||||||
| 'step_start'
|
| 'step_start'
|
||||||
| 'step_done'
|
| 'step_done'
|
||||||
|
| 'intent_chunk'
|
||||||
|
| 'intent_ready'
|
||||||
| 'research_progress'
|
| 'research_progress'
|
||||||
| 'materials_ready'
|
| 'materials_ready'
|
||||||
| 'outline_chunk'
|
| 'outline_chunk'
|
||||||
@@ -213,6 +215,14 @@ export interface CompletedIntervention {
|
|||||||
|
|
||||||
// ── Hook 状态 ─────────────────────────────────────────────────────────────────
|
// ── Hook 状态 ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** 意图解析最终结构化字段,来自 intent_ready 事件。 */
|
||||||
|
export interface ParsedIntent {
|
||||||
|
articleType: string;
|
||||||
|
targetAudience: string;
|
||||||
|
wordCountTarget: number;
|
||||||
|
refinedIntent: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AIWritingState {
|
export interface AIWritingState {
|
||||||
sessionId: string | null;
|
sessionId: string | null;
|
||||||
status: WritingStatus;
|
status: WritingStatus;
|
||||||
@@ -234,6 +244,10 @@ export interface AIWritingState {
|
|||||||
// 流式大纲(正文 / 思考分开)
|
// 流式大纲(正文 / 思考分开)
|
||||||
outlineStreamingText: string;
|
outlineStreamingText: string;
|
||||||
outlineThinkingText: string;
|
outlineThinkingText: string;
|
||||||
|
// 意图解析(流式 + 结果)
|
||||||
|
intentStreamingText: string;
|
||||||
|
intentThinkingText: string;
|
||||||
|
parsedIntent: ParsedIntent | null;
|
||||||
// 素材收集实时进度(最新一条提示文案)
|
// 素材收集实时进度(最新一条提示文案)
|
||||||
researchProgress: string;
|
researchProgress: string;
|
||||||
// 流式审核(逐条到达)
|
// 流式审核(逐条到达)
|
||||||
|
|||||||
@@ -1,17 +1,43 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { ArrowRight, NotebookPen, Sparkles } from "lucide-react";
|
import { ArrowRight, Loader2, NotebookPen, Sparkles } from "lucide-react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
import { useTheme } from "@/contexts/ThemeContext";
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
import { createSearchSpace } from "@/core/note/api";
|
||||||
|
import { useNoteAuth } from "@/hooks/useNoteAuth";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export default function LightAppNewNotePage() {
|
export default function LightAppNewNotePage() {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
const { loading: authLoading, error: authError } = useNoteAuth();
|
||||||
const [titleInput, setTitleInput] = useState("");
|
const [titleInput, setTitleInput] = useState("");
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
|
||||||
const hasTitle = useMemo(() => titleInput.trim().length > 0, [titleInput]);
|
const hasTitle = useMemo(() => titleInput.trim().length > 0, [titleInput]);
|
||||||
|
const isBusy = authLoading || creating;
|
||||||
|
|
||||||
|
const handleCreate = async () => {
|
||||||
|
const name = titleInput.trim();
|
||||||
|
if (!name || isBusy || authError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCreating(true);
|
||||||
|
try {
|
||||||
|
const searchSpace = await createSearchSpace({ name });
|
||||||
|
toast.success(`笔记「${searchSpace.name}」创建成功`);
|
||||||
|
setTitleInput("");
|
||||||
|
} catch (error) {
|
||||||
|
const message =
|
||||||
|
error instanceof Error ? error.message : "创建笔记失败,请稍后重试";
|
||||||
|
toast.error(message);
|
||||||
|
} finally {
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -21,7 +47,6 @@ export default function LightAppNewNotePage() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-[720px] -translate-y-12 space-y-8">
|
<div className="w-full max-w-[720px] -translate-y-12 space-y-8">
|
||||||
{/* 固定标题:新建笔记 */}
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<NotebookPen
|
<NotebookPen
|
||||||
className={cn("h-5 w-5 shrink-0", isDark ? "text-slate-400" : "text-black/70")}
|
className={cn("h-5 w-5 shrink-0", isDark ? "text-slate-400" : "text-black/70")}
|
||||||
@@ -36,12 +61,18 @@ export default function LightAppNewNotePage() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 标题输入框 */}
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<input
|
<input
|
||||||
value={titleInput}
|
value={titleInput}
|
||||||
onChange={(e) => setTitleInput(e.target.value)}
|
onChange={(e) => setTitleInput(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleCreate();
|
||||||
|
}
|
||||||
|
}}
|
||||||
placeholder="新建笔记"
|
placeholder="新建笔记"
|
||||||
|
disabled={isBusy}
|
||||||
className={cn(
|
className={cn(
|
||||||
"min-w-0 flex-1 bg-transparent text-[42px] leading-tight font-normal tracking-tight outline-none",
|
"min-w-0 flex-1 bg-transparent text-[42px] leading-tight font-normal tracking-tight outline-none",
|
||||||
hasTitle
|
hasTitle
|
||||||
@@ -56,20 +87,25 @@ export default function LightAppNewNotePage() {
|
|||||||
{hasTitle && (
|
{hasTitle && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={() => void handleCreate()}
|
||||||
|
disabled={isBusy || !!authError}
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-colors",
|
"inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-70",
|
||||||
isDark
|
isDark
|
||||||
? "bg-blue-500 text-white hover:bg-blue-400"
|
? "bg-blue-500 text-white hover:bg-blue-400"
|
||||||
: "bg-sky-200 text-slate-700 hover:bg-sky-300",
|
: "bg-sky-200 text-slate-700 hover:bg-sky-300",
|
||||||
)}
|
)}
|
||||||
aria-label="进入笔记"
|
aria-label="创建笔记"
|
||||||
>
|
>
|
||||||
<ArrowRight className="h-4 w-4" />
|
{creating || authLoading ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 说明文字 */}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-11 items-center gap-2.5 rounded-xl px-4",
|
"flex h-11 items-center gap-2.5 rounded-xl px-4",
|
||||||
@@ -88,6 +124,7 @@ export default function LightAppNewNotePage() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
新建笔记开始使用
|
新建笔记开始使用
|
||||||
|
{authError ? `(${authError})` : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,371 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
BookOpen,
|
||||||
|
Grid3X3,
|
||||||
|
LayoutList,
|
||||||
|
Loader2,
|
||||||
|
MoreHorizontal,
|
||||||
|
Plus,
|
||||||
|
RefreshCw,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import { useTheme } from "@/contexts/ThemeContext";
|
||||||
|
import { listSearchSpaces, type SearchSpace } from "@/core/note/api";
|
||||||
|
import { useNoteAuth } from "@/hooks/useNoteAuth";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type ViewMode = "grid" | "list";
|
||||||
|
|
||||||
|
function formatCreatedAt(value: string): string {
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return date.toLocaleDateString("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSourceCount(note: SearchSpace): number {
|
||||||
|
return note.source_count ?? note.sources_used ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardThemes = [
|
||||||
|
"bg-[#e4f2f6]",
|
||||||
|
"bg-[#f0eaf3]",
|
||||||
|
"bg-[#edf4e6]",
|
||||||
|
"bg-[#f5eadf]",
|
||||||
|
"bg-[#e9edf8]",
|
||||||
|
];
|
||||||
|
|
||||||
|
function NoteIcon({ isDark }: { isDark: boolean }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-8 shrink-0 items-center justify-center rounded-sm shadow-sm",
|
||||||
|
isDark ? "bg-violet-400 text-slate-950" : "bg-[#4d3f74] text-white",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<BookOpen className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LightAppNoteListPage() {
|
||||||
|
const { theme } = useTheme();
|
||||||
|
const isDark = theme === "dark";
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { loading: authLoading, error: authError } = useNoteAuth();
|
||||||
|
const [notes, setNotes] = useState<SearchSpace[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>("grid");
|
||||||
|
|
||||||
|
const loadNotes = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const data = await listSearchSpaces({
|
||||||
|
owned_only: true,
|
||||||
|
limit: 1000,
|
||||||
|
order_by: "created_at",
|
||||||
|
});
|
||||||
|
setNotes(data);
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : "获取笔记列表失败,请稍后重试";
|
||||||
|
setError(message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (authLoading || authError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void loadNotes();
|
||||||
|
}, [authError, authLoading, loadNotes]);
|
||||||
|
|
||||||
|
const isBusy = authLoading || loading;
|
||||||
|
|
||||||
|
const createButtonClass = cn(
|
||||||
|
"group text-center transition-all hover:-translate-y-0.5",
|
||||||
|
isDark
|
||||||
|
? "border-slate-700 bg-slate-900/70 text-slate-300 hover:border-slate-500 hover:bg-slate-900"
|
||||||
|
: "border-slate-200 bg-white text-slate-600 shadow-sm hover:border-sky-200 hover:shadow-md",
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCreateButton = (layout: ViewMode) => {
|
||||||
|
if (layout === "grid") {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate("/page/strategy/light-app/new-note")}
|
||||||
|
className={cn(
|
||||||
|
createButtonClass,
|
||||||
|
"flex h-[132px] flex-col items-center justify-center rounded-2xl border border-dashed",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"mb-3 flex h-9 w-9 items-center justify-center rounded-full transition-colors",
|
||||||
|
isDark
|
||||||
|
? "bg-slate-800 text-sky-300 group-hover:bg-slate-700"
|
||||||
|
: "bg-sky-50 text-sky-500 group-hover:bg-sky-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-medium">新建笔记本</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => navigate("/page/strategy/light-app/new-note")}
|
||||||
|
className={cn(
|
||||||
|
createButtonClass,
|
||||||
|
"flex w-full items-center gap-4 rounded-2xl border border-dashed px-5 py-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-10 shrink-0 items-center justify-center rounded-full transition-colors",
|
||||||
|
isDark
|
||||||
|
? "bg-slate-800 text-sky-300 group-hover:bg-slate-700"
|
||||||
|
: "bg-sky-50 text-sky-500 group-hover:bg-sky-100",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Plus className="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
<span className="text-base font-medium">新建笔记本</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderNoteCard = (note: SearchSpace, index: number) => (
|
||||||
|
<article
|
||||||
|
key={note.id}
|
||||||
|
className={cn(
|
||||||
|
"group relative overflow-hidden rounded-2xl p-4 shadow-sm ring-1 ring-black/5 transition-all hover:-translate-y-0.5 hover:shadow-lg",
|
||||||
|
isDark
|
||||||
|
? "h-[132px] bg-slate-900 text-slate-100 ring-white/10"
|
||||||
|
: cn("h-[132px]", cardThemes[index % cardThemes.length]),
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"absolute right-3 top-3 rounded-full p-1.5 opacity-80 transition-colors",
|
||||||
|
isDark ? "bg-slate-800/80 text-slate-300" : "bg-white/70 text-slate-500",
|
||||||
|
)}
|
||||||
|
aria-label="更多"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<NoteIcon isDark={isDark} />
|
||||||
|
|
||||||
|
<h2 className="max-w-[130px] truncate text-xl font-semibold tracking-tight">
|
||||||
|
{note.name}
|
||||||
|
</h2>
|
||||||
|
<p className={cn("mt-1 text-xs", isDark ? "text-slate-400" : "text-slate-600")}>
|
||||||
|
{formatCreatedAt(note.created_at)} · {getSourceCount(note)} 个来源
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderNoteRow = (note: SearchSpace) => (
|
||||||
|
<article
|
||||||
|
key={note.id}
|
||||||
|
className={cn(
|
||||||
|
"group flex items-center gap-4 rounded-2xl px-5 py-4 shadow-sm ring-1 transition-all hover:-translate-y-0.5 hover:shadow-md",
|
||||||
|
isDark
|
||||||
|
? "bg-slate-900 text-slate-100 ring-white/10"
|
||||||
|
: "bg-white ring-black/5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NoteIcon isDark={isDark} />
|
||||||
|
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<h2 className="truncate text-lg font-semibold tracking-tight">{note.name}</h2>
|
||||||
|
<p className={cn("mt-1 text-sm", isDark ? "text-slate-400" : "text-slate-500")}>
|
||||||
|
{formatCreatedAt(note.created_at)} · {getSourceCount(note)} 个来源
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"shrink-0 rounded-full p-2 opacity-80 transition-colors",
|
||||||
|
isDark ? "text-slate-400 hover:bg-slate-800" : "text-slate-500 hover:bg-slate-100",
|
||||||
|
)}
|
||||||
|
aria-label="更多"
|
||||||
|
>
|
||||||
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-full w-full overflow-y-auto px-10 py-10",
|
||||||
|
isDark ? "bg-slate-950" : "bg-[#fdfcfc]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mx-auto w-full max-w-[1120px] space-y-7">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
"text-[32px] font-semibold tracking-tight",
|
||||||
|
isDark ? "text-slate-100" : "text-slate-950",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
我的笔记本
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex rounded-xl p-1",
|
||||||
|
isDark ? "bg-slate-900" : "bg-white shadow-sm ring-1 ring-black/5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewMode("grid")}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg p-2 transition-colors",
|
||||||
|
viewMode === "grid"
|
||||||
|
? isDark
|
||||||
|
? "bg-slate-800 text-slate-100"
|
||||||
|
: "bg-slate-100 text-slate-900"
|
||||||
|
: isDark
|
||||||
|
? "text-slate-500 hover:text-slate-300"
|
||||||
|
: "text-slate-400 hover:text-slate-600",
|
||||||
|
)}
|
||||||
|
aria-label="网格视图"
|
||||||
|
aria-pressed={viewMode === "grid"}
|
||||||
|
>
|
||||||
|
<Grid3X3 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setViewMode("list")}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg p-2 transition-colors",
|
||||||
|
viewMode === "list"
|
||||||
|
? isDark
|
||||||
|
? "bg-slate-800 text-slate-100"
|
||||||
|
: "bg-slate-100 text-slate-900"
|
||||||
|
: isDark
|
||||||
|
? "text-slate-500 hover:text-slate-300"
|
||||||
|
: "text-slate-400 hover:text-slate-600",
|
||||||
|
)}
|
||||||
|
aria-label="列表视图"
|
||||||
|
aria-pressed={viewMode === "list"}
|
||||||
|
>
|
||||||
|
<LayoutList className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void loadNotes()}
|
||||||
|
disabled={isBusy || !!authError}
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl p-2 shadow-sm ring-1 ring-black/5 transition-colors disabled:cursor-not-allowed disabled:opacity-60",
|
||||||
|
isDark
|
||||||
|
? "bg-slate-900 text-slate-300 hover:bg-slate-800"
|
||||||
|
: "bg-white text-slate-600 hover:bg-slate-50",
|
||||||
|
)}
|
||||||
|
aria-label="刷新"
|
||||||
|
>
|
||||||
|
<RefreshCw className={cn("h-4 w-4", loading && "animate-spin")} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{authError && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl border px-4 py-3 text-sm",
|
||||||
|
isDark
|
||||||
|
? "border-red-500/30 bg-red-500/10 text-red-300"
|
||||||
|
: "border-red-200 bg-red-50 text-red-600",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{authError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error && !authError && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-xl border px-4 py-3 text-sm",
|
||||||
|
isDark
|
||||||
|
? "border-red-500/30 bg-red-500/10 text-red-300"
|
||||||
|
: "border-red-200 bg-red-50 text-red-600",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isBusy && !authError && (
|
||||||
|
<div className="flex items-center justify-center py-24">
|
||||||
|
<Loader2
|
||||||
|
className={cn("h-6 w-6 animate-spin", isDark ? "text-slate-400" : "text-black/40")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isBusy && !authError && !error && viewMode === "grid" && (
|
||||||
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(180px,1fr))] gap-4">
|
||||||
|
{renderCreateButton("grid")}
|
||||||
|
{notes.map((note, index) => renderNoteCard(note, index))}
|
||||||
|
{notes.length === 0 && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex h-[132px] items-center justify-center rounded-2xl border text-sm",
|
||||||
|
isDark
|
||||||
|
? "border-slate-800 bg-slate-900/50 text-slate-500"
|
||||||
|
: "border-slate-100 bg-white/70 text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
暂无笔记
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isBusy && !authError && !error && viewMode === "list" && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{renderCreateButton("list")}
|
||||||
|
{notes.map((note) => renderNoteRow(note))}
|
||||||
|
{notes.length === 0 && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-2xl border px-6 py-10 text-center text-sm",
|
||||||
|
isDark
|
||||||
|
? "border-slate-800 bg-slate-900/50 text-slate-500"
|
||||||
|
: "border-slate-100 bg-white/70 text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
暂无笔记
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
|
|||||||
import AdminUserActivityPage from "./AdminUserActivityPage";
|
import AdminUserActivityPage from "./AdminUserActivityPage";
|
||||||
import TagsPage from "./TagsPage";
|
import TagsPage from "./TagsPage";
|
||||||
import LightAppNewNotePage from "./LightAppNewNotePage";
|
import LightAppNewNotePage from "./LightAppNewNotePage";
|
||||||
|
import LightAppNoteListPage from "./LightAppNoteListPage";
|
||||||
|
|
||||||
export default function StrategyRoutes() {
|
export default function StrategyRoutes() {
|
||||||
return (
|
return (
|
||||||
@@ -39,6 +40,7 @@ export default function StrategyRoutes() {
|
|||||||
|
|
||||||
{/* 轻应用 */}
|
{/* 轻应用 */}
|
||||||
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
|
<Route path="light-app/new-note" element={<LightAppNewNotePage />} />
|
||||||
|
<Route path="light-app/notes" element={<LightAppNoteListPage />} />
|
||||||
|
|
||||||
{/* 对话 */}
|
{/* 对话 */}
|
||||||
<Route path="chats" element={<ChatsPage />} />
|
<Route path="chats" element={<ChatsPage />} />
|
||||||
|
|||||||
@@ -144,8 +144,9 @@ checkpointer:
|
|||||||
# users/{user_id}/memory_config.json > .deer-flow/memory_user_overrides.json > 此处
|
# users/{user_id}/memory_config.json > .deer-flow/memory_user_overrides.json > 此处
|
||||||
memory:
|
memory:
|
||||||
enabled: true
|
enabled: true
|
||||||
provider: hindsight # 'builtin' | 'hindsight'(外部 provider 名)
|
provider:
|
||||||
# builtin 始终启用,这里只决定 external 选哪个
|
hindsight # 'builtin' | 'hindsight'(外部 provider 名)
|
||||||
|
# builtin 始终启用,这里只决定 external 选哪个
|
||||||
builtin:
|
builtin:
|
||||||
enabled: true
|
enabled: true
|
||||||
memory_char_limit: 2200
|
memory_char_limit: 2200
|
||||||
@@ -159,10 +160,10 @@ memory:
|
|||||||
# 注意:写成 $VAR 但环境变量未设置会导致配置加载失败
|
# 注意:写成 $VAR 但环境变量未设置会导致配置加载失败
|
||||||
bank_id_template: "deerflow-user-{user_id}"
|
bank_id_template: "deerflow-user-{user_id}"
|
||||||
work_bank_id_template: "deerflow-work-{user_id}-{agent_id}"
|
work_bank_id_template: "deerflow-work-{user_id}-{agent_id}"
|
||||||
memory_mode: context # 'context' | 'tools' | 'hybrid'
|
memory_mode: context # 'context' | 'tools' | 'hybrid'
|
||||||
bank_mission: ""
|
bank_mission: "你是一个中文记忆助手,负责从对话中提炼和存储重要信息。所有记忆内容必须使用中文记录。"
|
||||||
bank_retain_mission: ""
|
bank_retain_mission: "请从以下对话中提炼重要信息,包括用户的偏好、需求、背景知识和关键事实。所有提炼的内容必须用中文表达。"
|
||||||
recall_budget: mid # 'low' | 'mid' | 'high'
|
recall_budget: mid # 'low' | 'mid' | 'high'
|
||||||
recall_max_tokens: 4096
|
recall_max_tokens: 4096
|
||||||
recall_max_input_chars: 800
|
recall_max_input_chars: 800
|
||||||
auto_recall: true
|
auto_recall: true
|
||||||
@@ -188,10 +189,10 @@ memory:
|
|||||||
max_tokens: 2000
|
max_tokens: 2000
|
||||||
include_builtin: true
|
include_builtin: true
|
||||||
include_hindsight: true
|
include_hindsight: true
|
||||||
context_tag: "memory-context" # 包裹 Hindsight recall 的标签名
|
context_tag: "memory-context" # 包裹 Hindsight recall 的标签名
|
||||||
|
|
||||||
security:
|
security:
|
||||||
scan_content: true # 注入/外渗模式扫描
|
scan_content: true # 注入/外渗模式扫描
|
||||||
block_invisible_unicode: true
|
block_invisible_unicode: true
|
||||||
# streaming_scrubber:预留字段,代码尚未实现 —— 待版本支持后取消注释。
|
# streaming_scrubber:预留字段,代码尚未实现 —— 待版本支持后取消注释。
|
||||||
# streaming_scrubber: true # SSE 流前清洗 <memory-context>
|
# streaming_scrubber: true # SSE 流前清洗 <memory-context>
|
||||||
@@ -200,6 +201,6 @@ memory:
|
|||||||
# 注意:这三项必须嵌套在 ai_writing: 段下;写到顶层会被 _ai_writing_config()
|
# 注意:这三项必须嵌套在 ai_writing: 段下;写到顶层会被 _ai_writing_config()
|
||||||
# (app/gateway/routers/ai_writing.py:_ai_writing_config) 直接忽略。
|
# (app/gateway/routers/ai_writing.py:_ai_writing_config) 直接忽略。
|
||||||
ai_writing:
|
ai_writing:
|
||||||
keyword_model: deepseek-chat # 检索词生成专用模型,留空回退写作主模型
|
keyword_model: deepseek-chat # 检索词生成专用模型,留空回退写作主模型
|
||||||
rank_model: deepseek-chat # 素材排序专用模型,留空回退写作主模型
|
rank_model: deepseek-chat # 素材排序专用模型,留空回退写作主模型
|
||||||
max_materials: 20 # 合并去重后的素材总量上限,范围 [1, 100],默认 20
|
max_materials: 20 # 合并去重后的素材总量上限,范围 [1, 100],默认 20
|
||||||
+70
-25
@@ -6,8 +6,16 @@ import re
|
|||||||
|
|
||||||
from langchain_core.runnables import RunnableConfig
|
from langchain_core.runnables import RunnableConfig
|
||||||
|
|
||||||
|
from langchain_core.messages import HumanMessage, SystemMessage
|
||||||
|
|
||||||
from ..state import AIWritingState
|
from ..state import AIWritingState
|
||||||
from ._utils import llm_json
|
from ._utils import (
|
||||||
|
ThinkTagParser,
|
||||||
|
_content_to_text,
|
||||||
|
get_model,
|
||||||
|
parse_json,
|
||||||
|
safe_get_stream_writer,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -138,42 +146,79 @@ async def intent_parser_node(state: AIWritingState, config: RunnableConfig) -> d
|
|||||||
if fallback_type not in valid_keys:
|
if fallback_type not in valid_keys:
|
||||||
fallback_type = default_key
|
fallback_type = default_key
|
||||||
|
|
||||||
result = await llm_json(
|
fallback_result = {
|
||||||
system,
|
"article_type": fallback_type,
|
||||||
user,
|
"target_audience": state.get("target_audience", "普通读者"),
|
||||||
fallback={
|
"word_count_target": state.get("word_count_target", 800),
|
||||||
"article_type": fallback_type,
|
"refined_intent": state["user_intent"],
|
||||||
"target_audience": state.get("target_audience", "普通读者"),
|
}
|
||||||
"word_count_target": state.get("word_count_target", 800),
|
|
||||||
"refined_intent": state["user_intent"],
|
|
||||||
},
|
|
||||||
config=config,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 意图解析模型调用失败时以非致命 notice 提示用户(llm_json 已用 fallback 兜底,
|
# 流式调用:与 writer_outline / writer_draft 同套机制 —— LLM 逐字吐出 JSON,
|
||||||
# 流程仍可继续,但用户需知道结构化配置可能不准)。
|
# 通过 ``intent_chunk`` 推给前端打字机;完成后 ``parse_json`` 解析结构化结果,
|
||||||
|
# 再通过 ``intent_ready`` 推一份结构化字段给前端结果卡片。
|
||||||
|
writer = safe_get_stream_writer()
|
||||||
|
llm_error: str | None = None
|
||||||
|
full_text = ""
|
||||||
|
parser = ThinkTagParser()
|
||||||
|
try:
|
||||||
|
model = get_model(config)
|
||||||
|
async for chunk in model.astream(
|
||||||
|
[SystemMessage(content=system), HumanMessage(content=user)]
|
||||||
|
):
|
||||||
|
delta = _content_to_text(chunk.content)
|
||||||
|
if not delta:
|
||||||
|
continue
|
||||||
|
for is_thinking, text in parser.feed(delta):
|
||||||
|
if not is_thinking:
|
||||||
|
full_text += text
|
||||||
|
writer({
|
||||||
|
"type": "intent_chunk",
|
||||||
|
"text": text,
|
||||||
|
"is_thinking": is_thinking,
|
||||||
|
})
|
||||||
|
except Exception as exc:
|
||||||
|
# 模型 / 网络挂掉 —— 不阻断流程,用 fallback 继续。
|
||||||
|
logger.error("intent_parser streaming failed: %s", exc, exc_info=True)
|
||||||
|
llm_error = f"{type(exc).__name__}: {exc}"
|
||||||
|
|
||||||
|
result = parse_json(full_text, fallback_result) if full_text else fallback_result
|
||||||
|
|
||||||
|
# 意图解析模型调用失败时以非致命 notice 提示用户(fallback 已兜底,流程仍可继续)
|
||||||
progress_events: list[dict] = []
|
progress_events: list[dict] = []
|
||||||
if result.get("_llm_error"):
|
if llm_error:
|
||||||
progress_events.append({
|
progress_events.append({
|
||||||
"type": "notice",
|
"type": "notice",
|
||||||
"agent_name": "系统",
|
"agent_name": "系统",
|
||||||
"title": "模型错误",
|
"title": "模型错误",
|
||||||
"detail": f"意图解析失败,已按默认配置兜底。原因:{result['_llm_error']}",
|
"detail": f"意图解析失败,已按默认配置兜底。原因:{llm_error}",
|
||||||
})
|
})
|
||||||
progress_events.append({"type": "step_done", "agent_name": "系统", "message": "意图解析完成"})
|
|
||||||
|
|
||||||
updates: dict = {"status": "researching", "progress_events": progress_events}
|
|
||||||
|
|
||||||
# 校验 LLM 返回的 article_type 是否在有效范围内,不合法时回退到默认值
|
# 校验 LLM 返回的 article_type 是否在有效范围内,不合法时回退到默认值
|
||||||
raw_type = result.get("article_type", "")
|
raw_type = result.get("article_type", "")
|
||||||
updates["article_type"] = raw_type if raw_type in valid_keys else fallback_type
|
final_article_type = raw_type if raw_type in valid_keys else fallback_type
|
||||||
|
final_target_audience = result.get("target_audience") or fallback_result["target_audience"]
|
||||||
if result.get("target_audience"):
|
|
||||||
updates["target_audience"] = result["target_audience"]
|
|
||||||
word_count = _coerce_word_count(result.get("word_count_target"))
|
word_count = _coerce_word_count(result.get("word_count_target"))
|
||||||
|
final_word_count = word_count if word_count is not None else fallback_result["word_count_target"]
|
||||||
|
final_refined_intent = result.get("refined_intent") or state["user_intent"]
|
||||||
|
|
||||||
|
# 推送结构化结果给前端 —— 结果卡片显示文章类型 / 目标读者 / 字数 / 精炼意图
|
||||||
|
writer({
|
||||||
|
"type": "intent_ready",
|
||||||
|
"article_type": final_article_type,
|
||||||
|
"target_audience": final_target_audience,
|
||||||
|
"word_count_target": final_word_count,
|
||||||
|
"refined_intent": final_refined_intent,
|
||||||
|
})
|
||||||
|
|
||||||
|
progress_events.append({"type": "step_done", "agent_name": "系统", "message": "意图解析完成"})
|
||||||
|
|
||||||
|
updates: dict = {"status": "researching", "progress_events": progress_events}
|
||||||
|
updates["article_type"] = final_article_type
|
||||||
|
if result.get("target_audience"):
|
||||||
|
updates["target_audience"] = final_target_audience
|
||||||
if word_count is not None:
|
if word_count is not None:
|
||||||
updates["word_count_target"] = word_count
|
updates["word_count_target"] = final_word_count
|
||||||
if result.get("refined_intent"):
|
if result.get("refined_intent"):
|
||||||
updates["user_intent"] = result["refined_intent"]
|
updates["user_intent"] = final_refined_intent
|
||||||
|
|
||||||
return updates
|
return updates
|
||||||
@@ -1,51 +1,53 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "deer-flow"
|
name = "deer-flow"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "LangGraph-based AI agent system with sandbox execution capabilities"
|
description = "LangGraph-based AI agent system with sandbox execution capabilities"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deerflow-harness",
|
"deerflow-harness",
|
||||||
"fastapi>=0.115.0",
|
"fastapi>=0.115.0",
|
||||||
"httpx>=0.28.0",
|
"httpx>=0.28.0",
|
||||||
"python-multipart>=0.0.26",
|
"python-multipart>=0.0.26",
|
||||||
"sse-starlette>=2.1.0",
|
"sse-starlette>=2.1.0",
|
||||||
"uvicorn[standard]>=0.34.0",
|
"uvicorn[standard]>=0.34.0",
|
||||||
"lark-oapi>=1.4.0",
|
"lark-oapi>=1.4.0",
|
||||||
"slack-sdk>=3.33.0",
|
"slack-sdk>=3.33.0",
|
||||||
"python-telegram-bot>=21.0",
|
"python-telegram-bot>=21.0",
|
||||||
"langgraph-sdk>=0.1.51",
|
"langgraph-sdk>=0.1.51",
|
||||||
"markdown-to-mrkdwn>=0.3.1",
|
"markdown-to-mrkdwn>=0.3.1",
|
||||||
"wecom-aibot-python-sdk>=0.1.6",
|
"wecom-aibot-python-sdk>=0.1.6",
|
||||||
"dingtalk-stream>=0.24.3",
|
"dingtalk-stream>=0.24.3",
|
||||||
"bcrypt>=4.0.0",
|
"bcrypt>=4.0.0",
|
||||||
"pyjwt>=2.9.0",
|
"pyjwt>=2.9.0",
|
||||||
"email-validator>=2.0.0",
|
"email-validator>=2.0.0",
|
||||||
"croniter>=6.2.2",
|
"croniter>=6.2.2",
|
||||||
]
|
"hindsight-client>=0.4.22",
|
||||||
|
"asyncmy>=0.2.11",
|
||||||
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
postgres = ["deerflow-harness[postgres]"]
|
postgres = ["deerflow-harness[postgres]"]
|
||||||
mysql = ["deerflow-harness[mysql]"]
|
mysql = ["deerflow-harness[mysql]"]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"prompt-toolkit>=3.0.0",
|
"prompt-toolkit>=3.0.0",
|
||||||
"pytest>=9.0.3",
|
"pytest>=9.0.3",
|
||||||
"pytest-asyncio>=1.3.0",
|
"pytest-asyncio>=1.3.0",
|
||||||
"ruff>=0.14.11",
|
"ruff>=0.14.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
markers = [
|
markers = [
|
||||||
"no_auto_user: disable the conftest autouse contextvar fixture for this test",
|
"no_auto_user: disable the conftest autouse contextvar fixture for this test",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
index-url = "https://pypi.org/simple"
|
index-url = "https://pypi.org/simple"
|
||||||
|
|
||||||
[tool.uv.workspace]
|
[tool.uv.workspace]
|
||||||
members = ["packages/harness"]
|
members = ["packages/harness"]
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
deerflow-harness = { workspace = true }
|
deerflow-harness = { workspace = true }
|
||||||
Reference in new issue
Block a user