ai写作默认模型

This commit is contained in:
董宏凯 committed 2026-05-27 20:28:00 +08:00
1 parent 8bdbf6ab15
commit dab6ecbf9a
8 files changed
+111 -158

No files matched your search

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { PenLine } from 'lucide-react';
import { Button } from '@/components/ui/button';
@@ -10,8 +10,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { getBackendBaseURL } from '@/core/config';
import { apiFetch } from '@/core/api';
import { useModels } from '@/core/models/hooks';
import type { AIWritingRequest } from '../../types/ai-writing';
import { useArticleTypes } from '../../hooks/useArticleTypes';
@@ -23,11 +22,6 @@ interface AIWritingFormProps {
hideAdvancedFields?: boolean;
}
interface ModelOption {
name: string;
displayName: string;
}
const INPUT_CLASS =
'flex h-12 w-full rounded-xl border border-[#e8dedd] bg-white px-4 py-2 text-sm shadow-sm ' +
'placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 ' +
@@ -52,10 +46,24 @@ export function AIWritingForm({
const [wordCountInput, setWordCountInput] = useState('800');
const [strictMode, setStrictMode] = useState(false);
const [modelName, setModelName] = useState('');
const [models, setModels] = useState<ModelOption[]>([]);
const { models, isLoading: modelsLoading } = useModels();
const { articleTypes, loading: typesLoading, refresh: refreshTypes } = useArticleTypes();
// 首次 /api/models 返回后默认选第一项;配置变更导致当前值不在列表时也回退到首项。
useEffect(() => {
if (models.length === 0) return;
const stillExists = models.some((m) => m.name === modelName);
if (!modelName || !stillExists) {
setModelName(models[0].name);
}
}, [models, modelName]);
const selectedModel = useMemo(
() => models.find((m) => m.name === modelName) ?? models[0],
[models, modelName],
);
// 初始化时加载文章类型
useEffect(() => {
refreshTypes();
@@ -81,25 +89,6 @@ export function AIWritingForm({
}
}
useEffect(() => {
apiFetch(`${getBackendBaseURL()}/api/models`)
.then((r) => r.json())
.then((data) => {
const list: ModelOption[] = (data.models ?? []).map(
(m: { name: string; display_name?: string }) => ({
name: m.name,
displayName: m.display_name || m.name,
}),
);
setModels(list);
if (list.length > 0 && !modelName) {
setModelName(list[0].name);
}
})
.catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
function handleWordCountBlur() {
const num = parseInt(wordCountInput, 10);
if (isNaN(num) || num < 100) {
@@ -122,7 +111,7 @@ export function AIWritingForm({
wordCountTarget: wordCount,
keywordCount: DEFAULT_KEYWORD_COUNT,
writingMode: hideAdvancedFields ? 'loose' : strictMode ? 'strict' : 'loose',
modelName: modelName || undefined,
modelName: selectedModel?.name || modelName || undefined,
});
}
@@ -192,17 +181,25 @@ export function AIWritingForm({
<div className="mt-4 space-y-2">
<label className={FIELD_LABEL_CLASS}>写作模型</label>
<Select
value={modelName}
value={selectedModel?.name ?? ''}
onValueChange={setModelName}
disabled={disabled || models.length === 0}
disabled={disabled || modelsLoading || models.length === 0}
>
<SelectTrigger className={FIELD_TRIGGER_CLASS}>
<SelectValue placeholder={models.length > 0 ? '选择模型…' : '模型加载中…'} />
<SelectValue
placeholder={
modelsLoading
? '模型加载中…'
: models.length > 0
? '选择模型…'
: '暂无可用模型'
}
/>
</SelectTrigger>
<SelectContent>
{models.map((m) => (
<SelectItem key={m.name} value={m.name} className="text-sm">
{m.displayName}
{m.display_name || m.name}
</SelectItem>
))}
</SelectContent>
@@ -27,7 +27,7 @@ export interface StreamRecommendResult {
* `done` → picks parsed and validated, ready to populate the dialog as
* default-checked selections.
* `asking` → recommender failed to emit a usable [RECOMMEND_READY] block,
* caller should apply its fallback list (typically the 6 roundtable-* seats).
* caller should pick from the live candidate pool as fallback.
* `error` → upstream / network failure.
*/
status: "done" | "asking" | "error";
@@ -62,20 +62,6 @@ export interface RecommendAgentsDialogProps {
const MIN_SELECTED = 2;
const MAX_SELECTED = 8;
/**
* Default selection when the recommender fails or returns < 2 valid picks.
* Intersected with the live candidate pool at runtime so missing built-ins
* don't break the dialog.
*/
const FALLBACK_AGENT_IDS = [
"roundtable-intelligence",
"roundtable-environment",
"roundtable-solution-design",
"roundtable-risk-review",
"roundtable-execution-plan",
"roundtable-summary",
];
type DialogPhase =
| "idle"
| "loading_agents"
@@ -319,12 +305,17 @@ export function RecommendAgentsDialog({
return;
}
// Fallback: status === "asking" or picks too few. Use the 6 built-in
// roundtable seats, intersected against the live pool.
const fallback = intersectInOrder(FALLBACK_AGENT_IDS, candidates);
const seed = fallback.length >= MIN_SELECTED
? fallback
: candidates.slice(0, MIN_SELECTED).map((c) => c.agent_id);
// Fallback: status === "asking" or picks too few — pick from live pool.
const seed = candidates.slice(0, MIN_SELECTED).map((c) => c.agent_id);
if (seed.length < MIN_SELECTED) {
setState((prev) => ({
...prev,
phase: "error",
streamPhase: null,
errorMessage: `候选智能体不足 ${MIN_SELECTED} 个,无法进入圆桌研讨。请先发布更多智能体。`,
}));
return;
}
setSelectedIds(new Set(seed));
const { rationale } = parseRecommendStreamContent(result.content || "");
setState((prev) => ({
@@ -334,7 +325,7 @@ export function RecommendAgentsDialog({
recommendedIds: seed,
reasons,
streamPhase: null,
warning: "智能推荐未生成有效结果,已为你勾选默认的圆桌席位,可手动调整。",
warning: "智能推荐未生成有效结果,已为你勾选候选池中的前几位智能体,可手动调整。",
}));
} catch (err) {
if (myGen !== generationRef.current) return;
@@ -15,7 +15,7 @@
*
* 状态全部来自 `useStep2Orchestration` —— 本组件不调任何后端、不持久化。
*/
import { useCallback, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
Cpu,
FileIcon,
@@ -89,8 +89,8 @@ export interface Step2PanelProps {
// ─── Handlers ─────────────────────────────────────────────────
onPauseRoundtable: () => void;
onResumeRoundtable: () => void;
onRecordInjectMessage: () => void;
onSubmitClarification: (answer: string) => void;
onInjectToRoundtable: () => void;
onSavePersona: (id: number) => void;
onProceedToStep3: () => void;
onTriggerToast: (msg: string) => void;
@@ -123,8 +123,8 @@ export function Step2Panel(props: Step2PanelProps) {
setSelectedAgentId,
onPauseRoundtable,
onResumeRoundtable,
onRecordInjectMessage,
onSubmitClarification,
onInjectToRoundtable,
onSavePersona,
onProceedToStep3,
onTriggerToast,
@@ -134,14 +134,24 @@ export function Step2Panel(props: Step2PanelProps) {
const { atBottom: step2AtBottom, scrollToBottom: scrollStep2ToBottom } =
useAutoScroll(step2ScrollRef, [step2RoundtableDialogues]);
// 干预研审 → 暂停会商联动反馈:点击「干预研审」瞬间让右上角按钮高亮闪烁,
// 让用户清楚看到状态已切换到「恢复会商」。
// 干预研审 → 暂停会商:与右上角按钮共用 onPauseRoundtable;乐观 UI 避免等父 state 刷新。
const [pauseFlash, setPauseFlash] = useState(false);
const [optimisticPaused, setOptimisticPaused] = useState(false);
const showPausedUi = !isRoundTableRunning || optimisticPaused;
useEffect(() => {
if (!isRoundTableRunning) {
setOptimisticPaused(false);
}
}, [isRoundTableRunning]);
const handleInject = useCallback(() => {
onInjectToRoundtable();
setOptimisticPaused(true);
onPauseRoundtable();
onRecordInjectMessage();
setPauseFlash(true);
window.setTimeout(() => setPauseFlash(false), 1600);
}, [onInjectToRoundtable]);
}, [onPauseRoundtable, onRecordInjectMessage]);
const isSandboxStreaming =
isOrchestrating ||
@@ -186,16 +196,7 @@ export function Step2Panel(props: Step2PanelProps) {
</option>
))}
</select>
{isRoundTableRunning ? (
<button
type="button"
onClick={onPauseRoundtable}
className="bg-amber-100 hover:bg-amber-200 text-amber-800 text-[10px] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-amber-200/40 shadow-sm"
>
<Pause className="w-3.5 h-3.5" />
暂停会商
</button>
) : (
{showPausedUi ? (
<button
type="button"
onClick={onResumeRoundtable}
@@ -208,6 +209,15 @@ export function Step2Panel(props: Step2PanelProps) {
<Play className="w-3.5 h-3.5 fill-emerald-800" />
恢复会商
</button>
) : (
<button
type="button"
onClick={onPauseRoundtable}
className="bg-amber-100 hover:bg-amber-200 text-amber-800 text-[10px] font-bold px-2.5 py-1 rounded-xl flex items-center gap-1 transition border border-amber-200/40 shadow-sm"
>
<Pause className="w-3.5 h-3.5" />
暂停会商
</button>
)}
</div>
</div>
@@ -342,8 +352,9 @@ export function Step2Panel(props: Step2PanelProps) {
/>
<button
type="button"
onClick={handleInject}
title="发送干预指令并暂停会商;点击右上角「恢复会商」继续执行"
title="暂停会商并记录干预指令;点击右上角「恢复会商」继续执行"
className="bg-primary hover:bg-[var(--brand-900)] text-white text-xs font-bold px-4 py-2.5 rounded-xl transition shrink-0 cursor-pointer active:scale-95 shadow-md shadow-primary/10"
>
干预研审
@@ -26,7 +26,6 @@ import {
type ThreadIdMap,
} from "@/roundtable-planning/api/multi-agent";
import {
DEFAULT_SELECTED_AGENTS,
MAX_CYCLES,
buildAgentNameToRole,
buildRoleTemplates,
@@ -99,18 +98,8 @@ export type Step2Snapshot = {
seatStubMessages: Record<string, StubAiMessage[]>;
};
/**
* 6 个默认席位的初始 Persona 配置。model 留空 → 运行时 `agentConfigs[seat]?.model
* || selectedModel || undefined`,即「席位覆盖优先,否则跟随全局模型」。
*/
const DEFAULT_AGENT_CONFIGS: Record<number, AgentConfig> = {
2: { model: "", weight: 90, temp: 0.2, prompt: "你负责解析核心任务、资源极限和用户目标偏好,监督全局研商收敛。" },
3: { model: "", weight: 85, temp: 0.3, prompt: "你负责搜集电力、通信系统、历史极端部署条件情报,支持备灾架构体系。" },
4: { model: "", weight: 88, temp: 0.1, prompt: "你评估服务器机房热载荷、算力并发承载极限,预报运行瓶颈与风险时钟。" },
5: { model: "", weight: 92, temp: 0.4, prompt: "你负责双全栈主备建模设计,高内聚低耦合,规避不可预知停机崩溃。" },
6: { model: "", weight: 95, temp: 0.1, prompt: "你作为安全主管,严厉把控预算开销与合规通过率防线,识别计划漏洞。" },
7: { model: "", weight: 80, temp: 0.4, prompt: "你负责对72小时里程碑执行路径规划、重排和资源调配演练序列。" },
};
/** 席位 Persona 配置;初始为空,用户在 Persona 抽屉保存后写入。 */
const DEFAULT_AGENT_CONFIGS: Record<number, AgentConfig> = {};
export interface UseStep2OrchestrationOptions {
/** 当前页面 currentStep,===2 触发自动 init。 */
@@ -175,9 +164,11 @@ export interface UseStep2OrchestrationReturn {
pauseRoundtable: () => void;
/** 与 Step2Panel「恢复会商」按钮相同 —— 恢复自动研讨(含待处理的干预指令)。 */
resumeRoundtable: () => void;
/** 记录干预指令(不暂停);通常由 Step2Panel 在 pauseRoundtable 之后调用。 */
recordInjectMessage: () => void;
/** 中断所有 in-flight SSE(给新建任务 / 加载草稿用)。 */
abortAllInFlight: () => void;
/** 用户「干预研审」—— 先暂停会商,干预指令在恢复后执行。 */
/** @deprecated 请用 pauseRoundtable + recordInjectMessage;保留兼容组合调用。 */
handleInjectToRoundtable: () => void;
/** 总控澄清回复 —— 把回答拼到 prompt 里再起一轮编排。 */
submitClarification: (rawAnswer: string) => void;
@@ -251,9 +242,7 @@ export function useStep2Orchestration(
Record<string, StubAiMessage[]>
>({});
const seatStubMessagesRef = useRef<Record<string, StubAiMessage[]>>({});
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>(
DEFAULT_SELECTED_AGENTS,
);
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>([]);
const [threadIds, setThreadIds] = useState<ThreadIdMap | null>(null);
const [coordinatorName, setCoordinatorName] = useState<string | null>(null);
const [isInitializing, setIsInitializing] = useState<boolean>(false);
@@ -1151,6 +1140,7 @@ export function useStep2Orchestration(
const pauseRoundtable = useCallback(() => {
setIsRoundTableRunning(false);
orchestrationCancelRef.current = true;
stopOrchestration();
triggerToast("⏸️ 会商已暂停");
}, [stopOrchestration, triggerToast]);
@@ -1160,29 +1150,29 @@ export function useStep2Orchestration(
triggerToast("▶️ 恢复大模型并行博弈与推理决策");
}, [triggerToast]);
// ─── Public handlers ─────────────────────────────────────────────
const handleInjectToRoundtable = useCallback(() => {
if (!customAgentInput.trim()) return;
const recordInjectMessage = useCallback(() => {
const userText = customAgentInput.trim();
if (!userText) return;
if (!threadIdsRef.current || !coordinatorNameRef.current) {
triggerToast("⚠️ 圆桌尚未就绪,请稍候");
triggerToast("⚠️ 圆桌尚未就绪,无法记录干预指令");
return;
}
const userText = customAgentInput.trim();
setCustomAgentInput("");
// 与右上角「暂停会商」完全相同的挂起逻辑。
pauseRoundtable();
appendDialogue({
sender: "用户 (插话干预)",
avatarType: "human",
content: `💡 (人工指令介入) ${userText}`,
confidence: "最高指令",
});
pendingInjectPromptRef.current = `用户人工干预新指令:${userText}\n\n请结合当前讨论状态,决定是否继续派活或直接给出更新后的结论。`;
}, [appendDialogue, customAgentInput, pauseRoundtable, triggerToast]);
triggerToast("干预指令已记录,点击「恢复会商」后执行");
}, [appendDialogue, customAgentInput, triggerToast]);
const handleInjectToRoundtable = useCallback(() => {
pauseRoundtable();
recordInjectMessage();
}, [pauseRoundtable, recordInjectMessage]);
const submitClarification = useCallback(
(rawAnswer: string) => {
@@ -1376,6 +1366,7 @@ export function useStep2Orchestration(
stopOrchestration,
pauseRoundtable,
resumeRoundtable,
recordInjectMessage,
abortAllInFlight,
handleInjectToRoundtable,
submitClarification,
@@ -24,23 +24,7 @@ export type RoleEntry = {
/** 用户在推荐弹窗选定的一个研讨席位。 */
export type SelectedAgent = { agent_id: string; name: string };
/**
* 6 个内置 roundtable 席位的展示元数据。用户保留默认席位时,
* 头像 / 配色 / 中文显示名都按这里走;非内置(用户自建)走 FALLBACK_AVATAR_TYPES。
*/
export const BUILTIN_AGENT_META: Record<
string,
{ avatarType: string; displayName: string }
> = {
"roundtable-intelligence": { avatarType: "purple", displayName: "情报收集" },
"roundtable-environment": { avatarType: "emerald", displayName: "环境评估" },
"roundtable-solution-design": { avatarType: "human", displayName: "方案设计" },
"roundtable-risk-review": { avatarType: "amber", displayName: "风险审查" },
"roundtable-execution-plan": { avatarType: "pink", displayName: "执行规划" },
"roundtable-summary": { avatarType: "blue", displayName: "总结陈述" },
};
/** 非内置 agent 的头像配色轮换池。 */
/** 研讨席位的头像配色轮换池。 */
export const FALLBACK_AVATAR_TYPES = [
"blue",
"purple",
@@ -49,28 +33,12 @@ export const FALLBACK_AVATAR_TYPES = [
"pink",
] as const;
/**
* 用户未确认推荐弹窗时,Step 2 默认使用的 6 个内置席位。
* 保留默认时页面与原硬编码行为完全一致。
*/
export const DEFAULT_SELECTED_AGENTS: SelectedAgent[] = [
{ agent_id: "roundtable-summary", name: "总结陈述" },
{ agent_id: "roundtable-intelligence", name: "情报收集" },
{ agent_id: "roundtable-environment", name: "环境评估" },
{ agent_id: "roundtable-solution-design", name: "方案设计" },
{ agent_id: "roundtable-risk-review", name: "风险审查" },
{ agent_id: "roundtable-execution-plan", name: "执行规划" },
];
export function getAvatarTypeFor(agentId: string, index: number): string {
return (
BUILTIN_AGENT_META[agentId]?.avatarType ??
FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length]
);
export function getAvatarTypeFor(_agentId: string, index: number): string {
return FALLBACK_AVATAR_TYPES[index % FALLBACK_AVATAR_TYPES.length];
}
export function getDisplayNameFor(agent: SelectedAgent): string {
return BUILTIN_AGENT_META[agent.agent_id]?.displayName ?? agent.name;
return agent.name;
}
/**
@@ -62,7 +62,6 @@ import {
import { useStep2Orchestration } from "@/roundtable-planning/hooks/useStep2Orchestration";
import { useDraftPersistence } from "@/roundtable-planning/hooks/useDraftPersistence";
import {
BUILTIN_AGENT_META,
getAvatarTypeFor,
getDisplayNameFor,
makeRolesFromTemplates,
@@ -260,8 +259,7 @@ export default function RoundtablePlanningPage() {
(agents: Array<{ agent_id: string; name: string }>) => {
const normalized: SelectedAgent[] = agents.map(({ agent_id, name }) => ({
agent_id,
// 内置 agent 走 BUILTIN_AGENT_META;非内置用候选池传过来的 name。
name: BUILTIN_AGENT_META[agent_id]?.displayName ?? name ?? agent_id,
name: name ?? agent_id,
}));
step2.setSelectedAgents(normalized);
step2.resetInitGate();
@@ -498,12 +496,12 @@ export default function RoundtablePlanningPage() {
openArtifactPreview={step2.openArtifactPreview}
onPauseRoundtable={step2.pauseRoundtable}
onResumeRoundtable={step2.resumeRoundtable}
onRecordInjectMessage={step2.recordInjectMessage}
setClarificationDraft={step2.setClarificationDraft}
setCustomAgentInput={step2.setCustomAgentInput}
setAgentConfigs={step2.setAgentConfigs}
setSelectedAgentId={step2.setSelectedAgentId}
onSubmitClarification={step2.submitClarification}
onInjectToRoundtable={step2.handleInjectToRoundtable}
onSavePersona={step2.handleSaveAgentAndRecalculate}
onProceedToStep3={() => {
if (!step2.hasConsensus) {
@@ -895,9 +893,7 @@ function LeftSidebar({
...selectedAgents.map((agent, i) => ({
id: agent.agent_id,
name: getDisplayNameFor(agent),
role: BUILTIN_AGENT_META[agent.agent_id]
? `${BUILTIN_AGENT_META[agent.agent_id].displayName} 席位`
: "圆桌席位",
role: "圆桌席位",
icon: getAvatar(getAvatarTypeFor(agent.agent_id, i), "h-7 w-7"),
})),
].map((contrib, idx) => (
@@ -406,7 +406,7 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
> 以下两个示例覆盖**不同任务类型**,目的是教你「思路要呼应任务本身」,**不要**把任何一个示例的具体职能链当模板。
### 示例 A:合规改造任务(职能型候选池)
### 示例 A:合规改造任务(领域候选池)
**用户输入**:
```
@@ -415,12 +415,11 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
关键假设:现有系统具备基础数字化能力;允许预定窗口停机
候选智能体池:
- roundtable-intelligence (情报收集):检索同类历史案例、行业最佳实践与外部情报
- roundtable-environment (环境评估):评估硬件容量与系统量化承载力
- roundtable-solution-design (方案设计):产出主备方案与关键路径
- roundtable-risk-review (风险审查):合规红线检查与收敛建议
- roundtable-execution-plan (执行规划):里程碑与资源排布
- roundtable-summary (总结陈述):归纳冲突标记与盖印
- compliance-research (合规调研):检索同类历史案例、行业最佳实践与监管口径
- infra-capacity (基础设施评估):评估硬件容量与系统量化承载力
- solution-architect (方案架构):产出主备方案与关键路径
- risk-auditor (风险审查):合规红线检查与收敛建议
- execution-planner (执行规划):里程碑与资源排布
- my-cost-calc (成本测算):基于预算和资源单价计算总成本
```
@@ -431,10 +430,10 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
```json
{
"picks": [
{"agent_id": "roundtable-intelligence", "reason": "检索同类等保 2.0 改造案例与监管口径,直接支撑本次合规对标"},
{"agent_id": "roundtable-solution-design", "reason": "在 6 个月工期约束下产出主备改造方案与关键路径"},
{"agent_id": "roundtable-risk-review", "reason": "对照等保 2.0 合规红线审查方案,避免后期返工"},
{"agent_id": "roundtable-execution-plan", "reason": "把改造拆成可在 6 个月内交付的里程碑与资源排布"},
{"agent_id": "compliance-research", "reason": "检索同类等保 2.0 改造案例与监管口径,直接支撑本次合规对标"},
{"agent_id": "solution-architect", "reason": "在 6 个月工期约束下产出主备改造方案与关键路径"},
{"agent_id": "risk-auditor", "reason": "对照等保 2.0 合规红线审查方案,避免后期返工"},
{"agent_id": "execution-planner", "reason": "把改造拆成可在 6 个月内交付的里程碑与资源排布"},
{"agent_id": "my-cost-calc", "reason": "校验整体方案落在 500 万元预算硬约束内"}
]
}
@@ -453,7 +452,7 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
- channel-planner (渠道规划师):按预算和受众设计跨平台投放组合
- audience-insight (受众洞察):画像、关键词、情绪点研究
- creative-copy (创意文案):产出落地页与社媒文案变体
- roundtable-risk-review (风险审查):合规红线检查与收敛建议
- ad-compliance (广告合规):审查文案与素材合规风险
```
**你的输出**:
@@ -471,8 +470,8 @@ RECOMMENDER_AGENT_SOUL = """# 智能体推荐专员
}
```
> **关键观察**:示例 A 选了 `roundtable-intelligence / solution-design / execution-plan` 等"职能型"智能体,示例 B **完全没用任何 `roundtable-*` 内置**,而是从用户自建的领域智能体里挑。
> 这是因为:**候选池里的智能体能不能匹配任务,只看 description 和任务的实际语义,不看 id 是不是 `roundtable-` 开头**。如果候选池里有"广告设计智能体"且任务是广告设计,**你必须挑它**,而不是套用示例 A 的职能链。
> **关键观察**:两个示例都从候选池里按任务语义挑选,**不要**套用固定职能链(如"调研→设计→风险→执行")。
> 候选池里的智能体能不能匹配任务,只看 description 和任务的实际语义。如果候选池里有"广告设计智能体"且任务是广告设计,**你必须挑它**,而不是机械复制示例 A 的链路。
"""