Merge branch 'likun' into master
This commit is contained in:
commit
faca2c9a2d
21 files changed
+617
-19
No files matched your search
@@ -2,9 +2,13 @@
|
|||||||
.vscode/
|
.vscode/
|
||||||
frontend-web/.vscode/
|
frontend-web/.vscode/
|
||||||
|
|
||||||
|
# Local runtime logs and pid files
|
||||||
|
.runtime/
|
||||||
|
|
||||||
# Frontend local/runtime artifacts
|
# Frontend local/runtime artifacts
|
||||||
frontend-web/.env
|
frontend-web/.env
|
||||||
frontend-web/node_modules/
|
frontend-web/node_modules/
|
||||||
|
frontend-web/.pnpm-store/
|
||||||
frontend-web/tsconfig.tsbuildinfo
|
frontend-web/tsconfig.tsbuildinfo
|
||||||
frontend-web/vite-dev.log
|
frontend-web/vite-dev.log
|
||||||
frontend-web/vite-dev.err.log
|
frontend-web/vite-dev.err.log
|
||||||
@@ -51,6 +55,8 @@ offline-backend-20260512/deerflow-backend-offline-image-20260505.tar
|
|||||||
offline-backend-20260512/extensions_config.json
|
offline-backend-20260512/extensions_config.json
|
||||||
offline-backend-20260512/skills/
|
offline-backend-20260512/skills/
|
||||||
offline-backend-20260512/wheelhouse/
|
offline-backend-20260512/wheelhouse/
|
||||||
|
offline-backend-20260512/backend/.deer-flow/data/*.db
|
||||||
|
offline-backend-20260512/backend/.deer-flow/data/*.db-*
|
||||||
offline-backend-20260512/start_backend.sh
|
offline-backend-20260512/start_backend.sh
|
||||||
offline-backend-20260512/stop_backend.sh
|
offline-backend-20260512/stop_backend.sh
|
||||||
offline-backend-20260512/check_backend.sh
|
offline-backend-20260512/check_backend.sh
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
import type { BaseStream } from "@langchain/langgraph-sdk/react";
|
||||||
|
import { SendIcon } from "lucide-react";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Conversation,
|
Conversation,
|
||||||
ConversationContent,
|
ConversationContent,
|
||||||
} from "@/components/ai-elements/conversation";
|
} from "@/components/ai-elements/conversation";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { useI18n } from "@/core/i18n/hooks";
|
import { useI18n } from "@/core/i18n/hooks";
|
||||||
import {
|
import {
|
||||||
extractContentFromMessage,
|
extractContentFromMessage,
|
||||||
@@ -40,12 +44,14 @@ export function MessageList({
|
|||||||
thread,
|
thread,
|
||||||
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
|
paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM,
|
||||||
tokenUsageEnabled = false,
|
tokenUsageEnabled = false,
|
||||||
|
onClarificationSubmit,
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
threadId: string;
|
threadId: string;
|
||||||
thread: BaseStream<AgentThreadState>;
|
thread: BaseStream<AgentThreadState>;
|
||||||
paddingBottom?: number;
|
paddingBottom?: number;
|
||||||
tokenUsageEnabled?: boolean;
|
tokenUsageEnabled?: boolean;
|
||||||
|
onClarificationSubmit?: (answer: string) => void | Promise<void>;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
|
||||||
@@ -77,9 +83,11 @@ export function MessageList({
|
|||||||
if (message && hasContent(message)) {
|
if (message && hasContent(message)) {
|
||||||
return (
|
return (
|
||||||
<div key={group.id} className="w-full">
|
<div key={group.id} className="w-full">
|
||||||
<MarkdownContent
|
<ClarificationCard
|
||||||
content={extractContentFromMessage(message)}
|
fallbackContent={extractContentFromMessage(message)}
|
||||||
isLoading={thread.isLoading}
|
isLoading={thread.isLoading}
|
||||||
|
message={message}
|
||||||
|
onSubmit={onClarificationSubmit}
|
||||||
rehypePlugins={rehypePlugins}
|
rehypePlugins={rehypePlugins}
|
||||||
/>
|
/>
|
||||||
<MessageTokenUsageList
|
<MessageTokenUsageList
|
||||||
@@ -240,3 +248,148 @@ export function MessageList({
|
|||||||
</Conversation>
|
</Conversation>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClarificationOption = {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ClarificationPayload = {
|
||||||
|
question?: string;
|
||||||
|
context?: string | null;
|
||||||
|
clarification_type?: string;
|
||||||
|
options?: ClarificationOption[];
|
||||||
|
allow_custom?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getClarificationPayload(message: unknown): ClarificationPayload | 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 clarification = Reflect.get(additionalKwargs, "clarification");
|
||||||
|
if (typeof clarification !== "object" || clarification === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return clarification as ClarificationPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClarificationCard({
|
||||||
|
fallbackContent,
|
||||||
|
isLoading,
|
||||||
|
message,
|
||||||
|
onSubmit,
|
||||||
|
rehypePlugins,
|
||||||
|
}: {
|
||||||
|
fallbackContent: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
message: unknown;
|
||||||
|
onSubmit?: (answer: string) => void | Promise<void>;
|
||||||
|
rehypePlugins: ReturnType<typeof useRehypeSplitWordsIntoSpans>;
|
||||||
|
}) {
|
||||||
|
const payload = getClarificationPayload(message);
|
||||||
|
const [customAnswer, setCustomAnswer] = useState("");
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const options = useMemo(
|
||||||
|
() =>
|
||||||
|
(payload?.options ?? []).filter(
|
||||||
|
(option) => typeof option.label === "string" && option.label.trim(),
|
||||||
|
),
|
||||||
|
[payload?.options],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return (
|
||||||
|
<MarkdownContent
|
||||||
|
content={fallbackContent}
|
||||||
|
isLoading={isLoading}
|
||||||
|
rehypePlugins={rehypePlugins}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabled = isLoading || submitting || !onSubmit;
|
||||||
|
|
||||||
|
async function submitAnswer(answer: string) {
|
||||||
|
const trimmed = answer.trim();
|
||||||
|
if (!trimmed || disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
await onSubmit?.(trimmed);
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border-border bg-card text-card-foreground w-full rounded-lg border p-4 shadow-sm">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-muted-foreground text-xs font-medium">
|
||||||
|
需要你的确认
|
||||||
|
</div>
|
||||||
|
{payload.context ? (
|
||||||
|
<p className="text-muted-foreground text-sm leading-6">
|
||||||
|
{payload.context}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<div className="text-sm leading-6 font-medium">
|
||||||
|
{payload.question || fallbackContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{options.length > 0 ? (
|
||||||
|
<div className="mt-4 grid gap-2">
|
||||||
|
{options.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.id || option.label}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-auto justify-start whitespace-normal px-3 py-2 text-left"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => void submitAnswer(`我选择:${option.label}`)}
|
||||||
|
>
|
||||||
|
<span className="flex min-w-0 flex-col items-start gap-1">
|
||||||
|
<span>{option.label}</span>
|
||||||
|
{option.description ? (
|
||||||
|
<span className="text-muted-foreground text-xs font-normal leading-5">
|
||||||
|
{option.description}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{payload.allow_custom !== false ? (
|
||||||
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
|
<Textarea
|
||||||
|
value={customAnswer}
|
||||||
|
onChange={(event) => setCustomAnswer(event.currentTarget.value)}
|
||||||
|
placeholder="如果以上选项都不合适,可以在这里输入你的回答"
|
||||||
|
disabled={disabled}
|
||||||
|
className="min-h-20 resize-none"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
disabled={disabled || !customAnswer.trim()}
|
||||||
|
onClick={() => void submitAnswer(customAnswer)}
|
||||||
|
>
|
||||||
|
<SendIcon className="h-4 w-4" />
|
||||||
|
发送回答
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -98,6 +98,14 @@ export default function AgentChatPage() {
|
|||||||
[sendMessage, threadId, agent_id],
|
[sendMessage, threadId, agent_id],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleClarificationSubmit = useCallback(
|
||||||
|
async (answer: string) => {
|
||||||
|
setHasStartedConversation(true);
|
||||||
|
await sendMessage(threadId, { text: answer, files: [] }, { agent_id });
|
||||||
|
},
|
||||||
|
[sendMessage, threadId, agent_id],
|
||||||
|
);
|
||||||
|
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [thread]);
|
}, [thread]);
|
||||||
@@ -160,6 +168,7 @@ export default function AgentChatPage() {
|
|||||||
thread={thread}
|
thread={thread}
|
||||||
paddingBottom={messageListPaddingBottom}
|
paddingBottom={messageListPaddingBottom}
|
||||||
tokenUsageEnabled={tokenUsageEnabled}
|
tokenUsageEnabled={tokenUsageEnabled}
|
||||||
|
onClarificationSubmit={handleClarificationSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,13 @@ export default function ChatPage() {
|
|||||||
},
|
},
|
||||||
[sendMessage, threadId],
|
[sendMessage, threadId],
|
||||||
);
|
);
|
||||||
|
const handleClarificationSubmit = useCallback(
|
||||||
|
async (answer: string) => {
|
||||||
|
setHasStartedConversation(true);
|
||||||
|
await sendMessage(threadId, { text: answer, files: [] });
|
||||||
|
},
|
||||||
|
[sendMessage, threadId],
|
||||||
|
);
|
||||||
const handleStop = useCallback(async () => {
|
const handleStop = useCallback(async () => {
|
||||||
await thread.stop();
|
await thread.stop();
|
||||||
}, [thread]);
|
}, [thread]);
|
||||||
@@ -137,6 +144,7 @@ export default function ChatPage() {
|
|||||||
thread={thread}
|
thread={thread}
|
||||||
paddingBottom={messageListPaddingBottom}
|
paddingBottom={messageListPaddingBottom}
|
||||||
tokenUsageEnabled={tokenUsageEnabled}
|
tokenUsageEnabled={tokenUsageEnabled}
|
||||||
|
onClarificationSubmit={handleClarificationSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
<div className="absolute right-0 bottom-0 left-0 z-30 flex justify-center px-4">
|
||||||
|
|||||||
@@ -465,6 +465,7 @@ export default function NewAgentPage() {
|
|||||||
className={cn("size-full", showSaveHint ? "pt-4" : "pt-10")}
|
className={cn("size-full", showSaveHint ? "pt-4" : "pt-10")}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
thread={thread}
|
thread={thread}
|
||||||
|
onClarificationSubmit={handleChatSubmit}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ static/browser_history/*.gif
|
|||||||
|
|
||||||
log/
|
log/
|
||||||
log/*
|
log/*
|
||||||
|
.deer-flow/users/
|
||||||
|
|
||||||
# Virtual environments
|
# Virtual environments
|
||||||
.venv
|
.venv
|
||||||
|
|||||||
+3
-1
@@ -442,6 +442,8 @@ You are {agent_name}, an open-source super agent.
|
|||||||
- ✅ If you identify the need for clarification in your thinking, you MUST call the tool IMMEDIATELY
|
- ✅ If you identify the need for clarification in your thinking, you MUST call the tool IMMEDIATELY
|
||||||
- ✅ After calling ask_clarification, execution will be interrupted automatically
|
- ✅ After calling ask_clarification, execution will be interrupted automatically
|
||||||
- ✅ Wait for user response - do NOT continue with assumptions
|
- ✅ Wait for user response - do NOT continue with assumptions
|
||||||
|
- ✅ When practical, provide 3-4 clear, mutually distinct options so the user can choose quickly
|
||||||
|
- ✅ Keep options concise and actionable; the user can still provide a custom answer if none fit
|
||||||
|
|
||||||
**How to Use:**
|
**How to Use:**
|
||||||
```python
|
```python
|
||||||
@@ -449,7 +451,7 @@ ask_clarification(
|
|||||||
question="Your specific question here?",
|
question="Your specific question here?",
|
||||||
clarification_type="missing_info", # or other type
|
clarification_type="missing_info", # or other type
|
||||||
context="Why you need this information", # optional but recommended
|
context="Why you need this information", # optional but recommended
|
||||||
options=["option1", "option2"] # optional, for choices
|
options=["option1", "option2", "option3"] # use 3-4 options when practical
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+55
-16
@@ -67,21 +67,7 @@ class ClarificationMiddleware(AgentMiddleware[ClarificationMiddlewareState]):
|
|||||||
question = args.get("question", "")
|
question = args.get("question", "")
|
||||||
clarification_type = args.get("clarification_type", "missing_info")
|
clarification_type = args.get("clarification_type", "missing_info")
|
||||||
context = args.get("context")
|
context = args.get("context")
|
||||||
options = args.get("options", [])
|
options = self._normalize_options(args.get("options", []))
|
||||||
|
|
||||||
# Some models (e.g. Qwen3-Max) serialize array parameters as JSON strings
|
|
||||||
# instead of native arrays. Deserialize and normalize so `options`
|
|
||||||
# is always a list for the rendering logic below.
|
|
||||||
if isinstance(options, str):
|
|
||||||
try:
|
|
||||||
options = json.loads(options)
|
|
||||||
except (json.JSONDecodeError, TypeError):
|
|
||||||
options = [options]
|
|
||||||
|
|
||||||
if options is None:
|
|
||||||
options = []
|
|
||||||
elif not isinstance(options, list):
|
|
||||||
options = [options]
|
|
||||||
|
|
||||||
# Type-specific icons
|
# Type-specific icons
|
||||||
type_icons = {
|
type_icons = {
|
||||||
@@ -110,10 +96,62 @@ class ClarificationMiddleware(AgentMiddleware[ClarificationMiddlewareState]):
|
|||||||
if options and len(options) > 0:
|
if options and len(options) > 0:
|
||||||
message_parts.append("") # blank line for spacing
|
message_parts.append("") # blank line for spacing
|
||||||
for i, option in enumerate(options, 1):
|
for i, option in enumerate(options, 1):
|
||||||
message_parts.append(f" {i}. {option}")
|
label = option.get("label") or option.get("id") or ""
|
||||||
|
message_parts.append(f" {i}. {label}")
|
||||||
|
|
||||||
return "\n".join(message_parts)
|
return "\n".join(message_parts)
|
||||||
|
|
||||||
|
def _normalize_options(self, raw_options: object) -> list[dict[str, str]]:
|
||||||
|
"""Normalize model-produced options into a stable UI-friendly shape."""
|
||||||
|
options = raw_options
|
||||||
|
|
||||||
|
# Some models serialize array parameters as JSON strings instead of native arrays.
|
||||||
|
if isinstance(options, str):
|
||||||
|
try:
|
||||||
|
options = json.loads(options)
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
options = [options]
|
||||||
|
|
||||||
|
if options is None:
|
||||||
|
values: list[object] = []
|
||||||
|
elif isinstance(options, list):
|
||||||
|
values = options
|
||||||
|
else:
|
||||||
|
values = [options]
|
||||||
|
|
||||||
|
normalized: list[dict[str, str]] = []
|
||||||
|
for index, value in enumerate(values, 1):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
raw_label = value.get("label") or value.get("title") or value.get("name") or value.get("value") or value.get("id")
|
||||||
|
label = str(raw_label).strip() if raw_label is not None else ""
|
||||||
|
if not label:
|
||||||
|
continue
|
||||||
|
raw_id = value.get("id") or value.get("value") or f"option-{index}"
|
||||||
|
option = {
|
||||||
|
"id": str(raw_id).strip() or f"option-{index}",
|
||||||
|
"label": label,
|
||||||
|
}
|
||||||
|
description = value.get("description")
|
||||||
|
if description is not None and str(description).strip():
|
||||||
|
option["description"] = str(description).strip()
|
||||||
|
normalized.append(option)
|
||||||
|
else:
|
||||||
|
label = str(value).strip()
|
||||||
|
if label:
|
||||||
|
normalized.append({"id": f"option-{index}", "label": label})
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
def _build_payload(self, args: dict) -> dict:
|
||||||
|
"""Build structured clarification data for rich clients."""
|
||||||
|
return {
|
||||||
|
"question": str(args.get("question", "")).strip(),
|
||||||
|
"clarification_type": args.get("clarification_type", "missing_info"),
|
||||||
|
"context": args.get("context"),
|
||||||
|
"options": self._normalize_options(args.get("options", [])),
|
||||||
|
"allow_custom": True,
|
||||||
|
}
|
||||||
|
|
||||||
def _handle_clarification(self, request: ToolCallRequest) -> Command:
|
def _handle_clarification(self, request: ToolCallRequest) -> Command:
|
||||||
"""Handle clarification request and return command to interrupt execution.
|
"""Handle clarification request and return command to interrupt execution.
|
||||||
|
|
||||||
@@ -143,6 +181,7 @@ class ClarificationMiddleware(AgentMiddleware[ClarificationMiddlewareState]):
|
|||||||
content=formatted_message,
|
content=formatted_message,
|
||||||
tool_call_id=tool_call_id,
|
tool_call_id=tool_call_id,
|
||||||
name="ask_clarification",
|
name="ask_clarification",
|
||||||
|
additional_kwargs={"clarification": self._build_payload(args)},
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return a Command that:
|
# Return a Command that:
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
---
|
||||||
|
name: intent-clarification-policy
|
||||||
|
description: Use this skill to analyze a higher-level task directive before planning. It helps infer the superior intent, assess whether the task is clear enough to proceed, and ask concise clarification questions when key purpose, audience, effect, boundary, coordination, or evaluation details are missing.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Intent Clarification Policy
|
||||||
|
|
||||||
|
Use this skill before drafting a plan for a higher-level task directive, action requirement, special work arrangement, cognitive-domain task, public opinion/information/psychological/influence-related task, or cross-department mission.
|
||||||
|
|
||||||
|
This skill does not draft the full plan. It prepares the intent analysis and decides whether clarification is needed first.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Restate the task briefly to avoid misreading.
|
||||||
|
2. Assess clarity across seven dimensions.
|
||||||
|
3. Assign a clarity level: A, B, C, or D.
|
||||||
|
4. If level A or B, produce a concise intent analysis.
|
||||||
|
5. If level C or D, call `ask_clarification` before planning.
|
||||||
|
|
||||||
|
## Clarity Dimensions
|
||||||
|
|
||||||
|
Check whether the task clearly states:
|
||||||
|
|
||||||
|
- Goal: what higher-level purpose or end state is expected.
|
||||||
|
- Object: who or what the task is directed toward.
|
||||||
|
- Effect: what cognitive, attitude, behavior, posture, or environment change is desired.
|
||||||
|
- Time window: when results are expected.
|
||||||
|
- Boundary: legal, policy, ethical, authorization, escalation, geographic, and resource limits.
|
||||||
|
- Coordination: who leads, supports, approves, or assesses.
|
||||||
|
- Evaluation: how completion and effect will be judged.
|
||||||
|
|
||||||
|
## Clarity Levels
|
||||||
|
|
||||||
|
- **A - Highly clear**: goal, object, effect, time, and boundary are mostly clear. Proceed to intent analysis.
|
||||||
|
- **B - Basically clear**: core purpose is clear, but some boundary or metric details are missing. Provide tentative analysis and list a few follow-up questions.
|
||||||
|
- **C - Clearly vague**: only a broad direction is given; object, effect, or boundary is missing. Ask clarification first.
|
||||||
|
- **D - Not enough context**: original task text or background is absent. Ask the user to provide the task material.
|
||||||
|
|
||||||
|
## Intent Analysis Mode
|
||||||
|
|
||||||
|
Use this mode for level A or B tasks.
|
||||||
|
|
||||||
|
Start with:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
上级任务可概括为:
|
||||||
|
在【时间/阶段】内,围绕【背景/事件】,面向【目标对象】,通过【主要行动方向】,达成【期望效果】,服务于【上级目标】。
|
||||||
|
```
|
||||||
|
|
||||||
|
Then analyze only the points supported by the task text:
|
||||||
|
|
||||||
|
- Superior purpose: why the task was assigned and what higher objective it serves.
|
||||||
|
- Task nature: shaping, deterrence, stabilization, protection, weakening, exposure, support, or assessment.
|
||||||
|
- Target objects: primary objects, secondary objects, protected groups, and possible unintended affected groups.
|
||||||
|
- Expected effects: desired changes in cognition, attitude, judgment, behavior, posture, or environment.
|
||||||
|
- Action boundaries: authorization, policy, legal, ethical, timing, geography, and resource constraints.
|
||||||
|
- Evaluation method: indicators, feedback channels, acceptance criteria, or effect assessment.
|
||||||
|
|
||||||
|
If the task is cognitive-domain related, identify one or more task types:
|
||||||
|
|
||||||
|
- Shaping: influence the narrative or perception environment in advance.
|
||||||
|
- Deterrence: affect an opponent's cost-benefit judgment.
|
||||||
|
- Stabilization: reduce panic, misunderstanding, uncertainty, or loss of trust.
|
||||||
|
- Protection: resist false information, psychological pressure, or cognitive manipulation.
|
||||||
|
- Weakening: reduce an opponent's credibility, mobilization, or influence capacity.
|
||||||
|
- Exposure: reveal facts, attribute responsibility, or strengthen credibility.
|
||||||
|
- Support: support military, diplomatic, public affairs, cyber, or civil action.
|
||||||
|
- Assessment: monitor the cognitive environment or action effect.
|
||||||
|
|
||||||
|
Summarize inferred intent with:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## 上级意图推断
|
||||||
|
|
||||||
|
上级希望在【时间窗口】内,围绕【事件/议题/背景】,通过【本级行动方向】,影响【主要对象】对【关键问题】的理解、判断或行为,使其从【当前状态】转向【期望状态】,从而支撑【上级战略/战役/行动/业务目标】。
|
||||||
|
|
||||||
|
本任务的核心不是单纯完成【具体动作】,而是通过该动作形成【认知效果/行为效果/环境效果】。
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clarification Mode
|
||||||
|
|
||||||
|
Use this mode for level C or D tasks. Do not draft a full plan yet.
|
||||||
|
|
||||||
|
Diagnose missing information in these categories:
|
||||||
|
|
||||||
|
- Purpose unclear: higher-level objective or desired end state is missing.
|
||||||
|
- Object unclear: primary audience, affected group, area, platform, or stakeholder is missing.
|
||||||
|
- Effect unclear: expected cognitive, attitude, behavior, or environment change is missing.
|
||||||
|
- Means unclear: allowed and prohibited action methods are missing.
|
||||||
|
- Boundary unclear: legal, policy, ethical, authorization, or escalation limits are missing.
|
||||||
|
- Coordination unclear: lead, support, approval, and assessment roles are missing.
|
||||||
|
- Evaluation unclear: completion criteria and effect metrics are missing.
|
||||||
|
|
||||||
|
Ask the minimum number of questions needed to proceed. Prefer one main question with 2-4 options. If several key fields are missing, ask for the most blocking one first.
|
||||||
|
|
||||||
|
Use `ask_clarification`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ask_clarification(
|
||||||
|
question="一个用户可以快速回答的具体问题。",
|
||||||
|
clarification_type="missing_info",
|
||||||
|
context="简要说明为什么这个信息会影响后续计划拟制。",
|
||||||
|
options=["选项一", "选项二", "选项三"],
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Choose `clarification_type`:
|
||||||
|
|
||||||
|
- `missing_info`: required task material or key detail is absent.
|
||||||
|
- `ambiguous_requirement`: the directive has multiple plausible interpretations.
|
||||||
|
- `approach_choice`: several valid task directions exist and user preference matters.
|
||||||
|
- `risk_confirmation`: the action may be sensitive, irreversible, escalatory, or policy-relevant.
|
||||||
|
- `suggestion`: you recommend a framing and need user approval.
|
||||||
|
|
||||||
|
## High-Value Questions
|
||||||
|
|
||||||
|
Use or adapt these questions when needed:
|
||||||
|
|
||||||
|
- 上级下发该任务主要服务于什么目标?
|
||||||
|
- 任务完成后,上级希望看到什么状态变化?
|
||||||
|
- 本任务主要面向哪些对象?
|
||||||
|
- 是否有不能影响、不能触碰或需要特别保护的对象?
|
||||||
|
- 希望目标对象在认知、态度、判断或行为上发生什么变化?
|
||||||
|
- 该效果是短期即时效果,还是长期环境塑造效果?
|
||||||
|
- 本级在任务中是主责、支援、协同、评估,还是审批角色?
|
||||||
|
- 有哪些法律、政策、授权、伦理或升级风险边界?
|
||||||
|
- 如何判断任务完成,如何评估实际效果?
|
||||||
|
|
||||||
|
## Style Rules
|
||||||
|
|
||||||
|
- Use Chinese when the user writes Chinese.
|
||||||
|
- Be concise and structured.
|
||||||
|
- Do not invent unsupported intent; label uncertain points as assumptions.
|
||||||
|
- Do not proceed to full planning when clarity is C or D.
|
||||||
|
- For sensitive influence, public opinion, psychological, or cognitive-domain work, keep analysis lawful, ethical, defensive, and policy-bounded.
|
||||||
Generated
+2
@@ -745,6 +745,7 @@ version = "0.1.0"
|
|||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
|
{ name = "croniter" },
|
||||||
{ name = "deerflow-harness" },
|
{ name = "deerflow-harness" },
|
||||||
{ name = "dingtalk-stream" },
|
{ name = "dingtalk-stream" },
|
||||||
{ name = "email-validator" },
|
{ name = "email-validator" },
|
||||||
@@ -778,6 +779,7 @@ dev = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "bcrypt", specifier = ">=4.0.0" },
|
{ name = "bcrypt", specifier = ">=4.0.0" },
|
||||||
|
{ name = "croniter", specifier = ">=6.2.2" },
|
||||||
{ name = "deerflow-harness", editable = "packages/harness" },
|
{ name = "deerflow-harness", editable = "packages/harness" },
|
||||||
{ name = "deerflow-harness", extras = ["postgres"], marker = "extra == 'postgres'", editable = "packages/harness" },
|
{ name = "deerflow-harness", extras = ["postgres"], marker = "extra == 'postgres'", editable = "packages/harness" },
|
||||||
{ name = "dingtalk-stream", specifier = ">=0.24.3" },
|
{ name = "dingtalk-stream", specifier = ">=0.24.3" },
|
||||||
|
|||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
BACKEND_DIR="${ROOT_DIR}/offline-backend-20260512/backend"
|
||||||
|
FRONTEND_DIR="${ROOT_DIR}/frontend-web"
|
||||||
|
RUNTIME_DIR="${ROOT_DIR}/.runtime"
|
||||||
|
LOG_DIR="${RUNTIME_DIR}/logs"
|
||||||
|
PID_DIR="${RUNTIME_DIR}/pids"
|
||||||
|
|
||||||
|
BACKEND_HOST="${BACKEND_HOST:-0.0.0.0}"
|
||||||
|
BACKEND_PORT="${BACKEND_PORT:-8001}"
|
||||||
|
FRONTEND_HOST="${FRONTEND_HOST:-0.0.0.0}"
|
||||||
|
FRONTEND_PORT="${FRONTEND_PORT:-5174}"
|
||||||
|
|
||||||
|
detect_host_ip() {
|
||||||
|
if command -v hostname >/dev/null 2>&1; then
|
||||||
|
hostname -I 2>/dev/null | awk '{print $1}'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
HOST_IP="${HOST_IP:-$(detect_host_ip)}"
|
||||||
|
HOST_IP="${HOST_IP:-127.0.0.1}"
|
||||||
|
|
||||||
|
BACKEND_PUBLIC_BASE_URL="${BACKEND_PUBLIC_BASE_URL:-http://${HOST_IP}:${BACKEND_PORT}}"
|
||||||
|
FRONTEND_PUBLIC_BASE_URL="${FRONTEND_PUBLIC_BASE_URL:-http://${HOST_IP}:${FRONTEND_PORT}}"
|
||||||
|
|
||||||
|
GATEWAY_CORS_ORIGINS="${GATEWAY_CORS_ORIGINS:-http://localhost:${FRONTEND_PORT},http://127.0.0.1:${FRONTEND_PORT},http://${HOST_IP}:${FRONTEND_PORT}}"
|
||||||
|
|
||||||
|
mkdir -p "${LOG_DIR}" "${PID_DIR}"
|
||||||
|
|
||||||
|
is_running() {
|
||||||
|
local pid_file="$1"
|
||||||
|
[[ -f "${pid_file}" ]] || return 1
|
||||||
|
|
||||||
|
local pid
|
||||||
|
pid="$(cat "${pid_file}" 2>/dev/null || true)"
|
||||||
|
[[ -n "${pid}" ]] || return 1
|
||||||
|
kill -0 "${pid}" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_by_pid_file() {
|
||||||
|
local name="$1"
|
||||||
|
local pid_file="$2"
|
||||||
|
|
||||||
|
if ! [[ -f "${pid_file}" ]]; then
|
||||||
|
echo "${name} is not running (no pid file)."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pid
|
||||||
|
pid="$(cat "${pid_file}" 2>/dev/null || true)"
|
||||||
|
if [[ -z "${pid}" ]] || ! kill -0 "${pid}" >/dev/null 2>&1; then
|
||||||
|
echo "${name} is not running (stale pid file removed)."
|
||||||
|
rm -f "${pid_file}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Stopping ${name} (pid ${pid})..."
|
||||||
|
# Services are started with setsid, so the pid is also the process group id.
|
||||||
|
# Stop the whole group to avoid leaving Vite/esbuild/uvicorn child processes.
|
||||||
|
kill "-${pid}" >/dev/null 2>&1 || true
|
||||||
|
kill "${pid}" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
for _ in {1..30}; do
|
||||||
|
if ! kill -0 "${pid}" >/dev/null 2>&1; then
|
||||||
|
rm -f "${pid_file}"
|
||||||
|
echo "${name} stopped."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 0.2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "${name} did not stop gracefully; killing..."
|
||||||
|
kill -9 "-${pid}" >/dev/null 2>&1 || true
|
||||||
|
kill -9 "${pid}" >/dev/null 2>&1 || true
|
||||||
|
rm -f "${pid_file}"
|
||||||
|
echo "${name} stopped."
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_by_pattern() {
|
||||||
|
local name="$1"
|
||||||
|
local pattern="$2"
|
||||||
|
|
||||||
|
if ! command -v pgrep >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pids
|
||||||
|
pids="$(pgrep -f "${pattern}" || true)"
|
||||||
|
if [[ -z "${pids}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Stopping ${name} processes matching: ${pattern}"
|
||||||
|
while IFS= read -r pid; do
|
||||||
|
[[ -n "${pid}" ]] || continue
|
||||||
|
[[ "${pid}" != "$$" ]] || continue
|
||||||
|
kill "${pid}" >/dev/null 2>&1 || true
|
||||||
|
done <<<"${pids}"
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
"${SCRIPT_DIR}/stop-all.sh"
|
||||||
|
"${SCRIPT_DIR}/start-all.sh"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
"${SCRIPT_DIR}/stop-backend.sh"
|
||||||
|
"${SCRIPT_DIR}/start-backend.sh"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
"${SCRIPT_DIR}/stop-frontend.sh"
|
||||||
|
"${SCRIPT_DIR}/start-frontend.sh"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
"${SCRIPT_DIR}/start-backend.sh"
|
||||||
|
"${SCRIPT_DIR}/start-frontend.sh"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
|
||||||
|
|
||||||
|
PID_FILE="${PID_DIR}/backend.pid"
|
||||||
|
LOG_FILE="${LOG_DIR}/backend.log"
|
||||||
|
|
||||||
|
if is_running "${PID_FILE}"; then
|
||||||
|
echo "Backend is already running (pid $(cat "${PID_FILE}"))."
|
||||||
|
echo "URL: http://${HOST_IP}:${BACKEND_PORT}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "${PID_FILE}"
|
||||||
|
|
||||||
|
echo "Starting backend..."
|
||||||
|
echo "Log: ${LOG_FILE}"
|
||||||
|
|
||||||
|
setsid bash -c '
|
||||||
|
cd "$0" || exit 1
|
||||||
|
exec env \
|
||||||
|
GATEWAY_CORS_ORIGINS="$1" \
|
||||||
|
PYTHONPATH=. \
|
||||||
|
uv run uvicorn app.gateway.app:app \
|
||||||
|
--host "$2" \
|
||||||
|
--port "$3" \
|
||||||
|
--reload
|
||||||
|
' "${BACKEND_DIR}" "${GATEWAY_CORS_ORIGINS}" "${BACKEND_HOST}" "${BACKEND_PORT}" \
|
||||||
|
>"${LOG_FILE}" 2>&1 &
|
||||||
|
|
||||||
|
echo "$!" >"${PID_FILE}"
|
||||||
|
echo "Backend started (pid $(cat "${PID_FILE}"))."
|
||||||
|
echo "URL: http://${HOST_IP}:${BACKEND_PORT}"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
|
||||||
|
|
||||||
|
PID_FILE="${PID_DIR}/frontend.pid"
|
||||||
|
LOG_FILE="${LOG_DIR}/frontend.log"
|
||||||
|
|
||||||
|
if is_running "${PID_FILE}"; then
|
||||||
|
echo "Frontend is already running (pid $(cat "${PID_FILE}"))."
|
||||||
|
echo "URL: ${FRONTEND_PUBLIC_BASE_URL}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "${PID_FILE}"
|
||||||
|
|
||||||
|
echo "Starting frontend..."
|
||||||
|
echo "Log: ${LOG_FILE}"
|
||||||
|
|
||||||
|
setsid bash -c '
|
||||||
|
cd "$0" || exit 1
|
||||||
|
exec env \
|
||||||
|
VITE_BACKEND_BASE_URL="$1" \
|
||||||
|
VITE_LANGGRAPH_BASE_URL="$1/api" \
|
||||||
|
pnpm exec vite \
|
||||||
|
--host "$2" \
|
||||||
|
--port "$3"
|
||||||
|
' "${FRONTEND_DIR}" "${BACKEND_PUBLIC_BASE_URL}" "${FRONTEND_HOST}" "${FRONTEND_PORT}" \
|
||||||
|
>"${LOG_FILE}" 2>&1 &
|
||||||
|
|
||||||
|
echo "$!" >"${PID_FILE}"
|
||||||
|
echo "Frontend started (pid $(cat "${PID_FILE}"))."
|
||||||
|
echo "URL: ${FRONTEND_PUBLIC_BASE_URL}"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
|
||||||
|
|
||||||
|
print_status() {
|
||||||
|
local name="$1"
|
||||||
|
local pid_file="$2"
|
||||||
|
local url="$3"
|
||||||
|
local pattern="$4"
|
||||||
|
|
||||||
|
if is_running "${pid_file}"; then
|
||||||
|
echo "${name}: running (pid $(cat "${pid_file}"))"
|
||||||
|
echo " ${url}"
|
||||||
|
elif command -v pgrep >/dev/null 2>&1 && pgrep -f "${pattern}" >/dev/null 2>&1; then
|
||||||
|
echo "${name}: running (unmanaged by scripts)"
|
||||||
|
echo " ${url}"
|
||||||
|
else
|
||||||
|
echo "${name}: stopped"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
print_status "Backend" "${PID_DIR}/backend.pid" "http://${HOST_IP}:${BACKEND_PORT}" "${BACKEND_DIR}.*uvicorn app.gateway.app:app.*--port ${BACKEND_PORT}"
|
||||||
|
print_status "Frontend" "${PID_DIR}/frontend.pid" "${FRONTEND_PUBLIC_BASE_URL}" "${FRONTEND_DIR}.*vite.*--port ${FRONTEND_PORT}"
|
||||||
|
echo "Logs: ${LOG_DIR}"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
"${SCRIPT_DIR}/stop-frontend.sh"
|
||||||
|
"${SCRIPT_DIR}/stop-backend.sh"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
|
||||||
|
|
||||||
|
stop_by_pid_file "Backend" "${PID_DIR}/backend.pid"
|
||||||
|
stop_by_pattern "Backend" "${BACKEND_DIR}.*uvicorn app.gateway.app:app.*--port ${BACKEND_PORT}"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/env.sh"
|
||||||
|
|
||||||
|
stop_by_pid_file "Frontend" "${PID_DIR}/frontend.pid"
|
||||||
|
stop_by_pattern "Frontend" "${FRONTEND_DIR}.*vite.*--port ${FRONTEND_PORT}"
|
||||||
Reference in new issue
Block a user