Files
lzf_0626 f72a545c39 refactor: 品牌统一为「南方财富」(项目方定)
背景:品牌名此前三处不一致 —— 后端 Agent 自称「奶龙基金」(customer_service_rules
的防诈骗/转人工话术、risk_agent 与 risk_analysis_service 的系统提示词)、前端全站
「南方财富」、闲聊提示词与知识素材「南方科技」。docs/36 已把它登记为"上报项目方后
待定",现按项目方决定统一为「南方财富」。

改动:
- app/core/customer_service_rules.py:P0 防诈骗话术与 P2 转人工话术里的品牌名
- app/service/agent/implementations/customer_service.py:COMPANY 常量,
  以及那处引用实测样本的注释(改为不绑定具体品牌名,免得下次改名又过时)
- app/service/agent/implementations/risk_agent.py:docstring、自我介绍、system prompt
- app/service/risk_analysis_service.py:SYSTEM_PROMPT
- app/worker/risk_scan_scheduler.py:--help 描述
- app/static/index.html(旧联调页 4 处)、portal/employee-risk/dashboard/index.html
- tests/unit/api/test_customer_service_test_page.py:同步断言(它断言的正是页面里的品牌名)
- tools/publish_chitchat_prompt.py:SYSTEM_PROMPT 改品牌;并修掉"存在即跳过"的检查
  —— 原来只判当前版本有没有这一行,于是改了文案也发不出去(脚本打印"无需发布"直接
  退出),没有任何提示。改为比对 system_prompt/user_prompt_template 内容。

闲聊提示词已重发为 release 308 / v5,生效内容为"你是南方财富的智能客服助手…"。

刻意未动:
- fin_product.fund_manager = "南方基金" —— 它被 market_quote_sync_service 与
  product_history_sync_service 当过滤条件使用,改名会让同步链路查不到产品
- knowledge_search_service.py 注释里引用的知识库实际标题「南方科技有限公司…」
- docs/客服docs 下的历史素材与 docs/ 下的过程记录(属历史留痕)

⚠️ Milvus 里的知识条目仍写「奶龙基金」(RAG-*/NF-*)与「南方科技」(PROD-*),
属知识数据,需重灌才能统一;本轮不动。

同时:
- docs/40 把品牌条目标为已处理,并补上知识库缺口的现状
- 新增 docs/42-场内基金知识条目草稿.md:按 fin_product 的 20 只产品生成,
  含通用交易规则与产品清单;费率等缺失字段一律标"以交易页面为准",未编造数字。
  **该文件是草稿,未入库**,待审核后走 POST /api/v1/knowledge/documents 灌库。
2026-09-13 19:17:56 +08:00

156 lines
8.9 KiB
HTML

<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>南方财富智能助手联调</title>
<style>
:root { --ink: #17212b; --muted: #607080; --line: #d9e1e7; --canvas: #f4f7f8; --surface: #ffffff; --brand: #008a7a; --brand-dark: #00695f; --accent: #e96f45; --visitor: #e8f6f1; --agent: #f2f5f7; --danger: #b43b2c; }
* { box-sizing: border-box; }
body { margin: 0; min-height: 100vh; background: var(--canvas); color: var(--ink); font-family: "Microsoft YaHei", "PingFang SC", sans-serif; }
main { width: min(100%, 980px); min-height: 100vh; margin: 0 auto; padding: 24px; display: grid; grid-template-rows: auto auto minmax(360px, 1fr) auto; gap: 14px; }
header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding-bottom: 14px; border-bottom: 1px solid var(--line); }
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark { width: 38px; height: 38px; display: grid; place-items: center; background: var(--brand); color: #ffffff; border-radius: 8px; font-weight: 700; }
h1 { margin: 0; font-size: 20px; font-weight: 700; letter-spacing: 0; }
.status { margin: 2px 0 0; color: var(--muted); font-size: 13px; }
.role { color: var(--brand-dark); font-weight: 700; }
.samples { display: flex; flex-wrap: wrap; gap: 8px; }
.sample { min-height: 34px; border: 1px solid var(--line); background: var(--surface); color: var(--ink); padding: 7px 10px; border-radius: 6px; cursor: pointer; font: inherit; font-size: 13px; }
.sample:hover, .sample:focus-visible { border-color: var(--brand); color: var(--brand-dark); outline: none; }
.chat { overflow-y: auto; display: flex; flex-direction: column; gap: 12px; padding: 18px; background: var(--surface); border: 1px solid var(--line); border-radius: 8px; }
.message { display: flex; gap: 10px; max-width: min(82%, 620px); }
.message.user { align-self: flex-end; flex-direction: row-reverse; }
.avatar { flex: 0 0 30px; height: 30px; display: grid; place-items: center; border-radius: 50%; background: var(--brand); color: #ffffff; font-size: 12px; font-weight: 700; }
.message.user .avatar { background: var(--accent); }
.bubble { margin: 0; padding: 10px 12px; background: var(--agent); border-radius: 6px; font-size: 14px; line-height: 1.6; white-space: pre-wrap; overflow-wrap: anywhere; }
.message.user .bubble { background: var(--visitor); }
.pending .bubble { color: var(--muted); }
.error .bubble { background: #fff0ed; color: var(--danger); }
form { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; }
input { min-width: 0; height: 44px; border: 1px solid var(--line); border-radius: 6px; padding: 0 12px; background: var(--surface); color: var(--ink); font: inherit; }
input:focus { border-color: var(--brand); outline: 2px solid #bde6df; }
button[type="submit"] { min-width: 76px; height: 44px; border: 0; border-radius: 6px; background: var(--brand); color: #ffffff; cursor: pointer; font: inherit; }
button[type="submit"]:disabled { cursor: wait; opacity: 0.6; }
@media (max-width: 620px) { main { padding: 14px; } header { align-items: flex-start; flex-direction: column; } .message { max-width: 92%; } }
</style>
</head>
<body>
<main>
<header>
<div class="brand">
<div class="brand-mark" aria-hidden="true">龙</div>
<div><h1>南方财富智能助手</h1><p class="status">公开知识与客服分流联调页</p></div>
</div>
<p id="connection-status" class="status"><span class="role">访客</span>:正在建立测试连接</p>
</header>
<section class="samples" aria-label="测试场景">
<button class="sample" type="button" data-message="你好">闲聊</button>
<button class="sample" type="button" data-message="基金赎回到账规则是什么">政策</button>
<button class="sample" type="button" data-message="我的持仓收益是多少">账户入口</button>
<button class="sample" type="button" data-message="验证码已经发给别人了">安全风险</button>
<button class="sample" type="button" data-message="帮我推荐一只收益最高的基金">合规拒答</button>
<button class="sample" type="button" data-message="我要转人工客服">人工服务</button>
</section>
<section id="chat" class="chat" aria-live="polite"></section>
<form id="composer">
<input id="message" type="text" maxlength="2000" autocomplete="off" placeholder="输入要测试的问题" aria-label="客服测试消息">
<button id="send" type="submit">发送</button>
</form>
</main>
<script>
let accessToken = "";
const sessionId = `visitor-${crypto.randomUUID()}`;
const chat = document.querySelector("#chat");
const input = document.querySelector("#message");
const composer = document.querySelector("#composer");
const sendButton = document.querySelector("#send");
const connectionStatus = document.querySelector("#connection-status");
function appendMessage(role, text, state = "") {
const wrapper = document.createElement("article");
wrapper.className = `message ${role} ${state}`;
const avatar = document.createElement("span");
avatar.className = "avatar";
avatar.textContent = role === "user" ? "我" : "龙";
const bubble = document.createElement("p");
bubble.className = "bubble";
bubble.textContent = text;
wrapper.append(avatar, bubble);
chat.append(wrapper);
chat.scrollTop = chat.scrollHeight;
return { wrapper, bubble };
}
async function ensureVisitorToken() {
if (accessToken) return;
const response = await fetch("/api/v1/visitor-tokens", { method: "POST" });
if (!response.ok) throw new Error("访客令牌获取失败");
accessToken = (await response.json()).access_token;
connectionStatus.innerHTML = '<span class="role">访客</span>:测试连接已建立';
}
function delay(milliseconds) {
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
}
async function pollRun(statusUrl, messageView) {
for (let attempt = 0; attempt < 40; attempt += 1) {
const response = await fetch(statusUrl, { headers: { Authorization: `Bearer ${accessToken}` } });
if (!response.ok) throw new Error("客服运行状态读取失败");
const run = (await response.json()).data;
if (run.status === "succeeded") {
messageView.bubble.textContent = run.result?.content || "客服未返回可显示内容。";
messageView.wrapper.classList.remove("pending");
chat.scrollTop = chat.scrollHeight;
return;
}
if (run.status === "failed" || run.status === "cancelled") {
messageView.bubble.textContent = `本次客服运行未完成:${run.error_code || run.status}`;
messageView.wrapper.className = "message error";
return;
}
await delay(500);
}
messageView.bubble.textContent = "客服请求仍在处理中,请确认 Worker 已启动后重试。";
messageView.wrapper.className = "message error";
}
async function sendMessage(message) {
const normalized = message.trim();
if (!normalized) return;
appendMessage("user", normalized);
input.value = "";
sendButton.disabled = true;
const pending = appendMessage("agent", "南方财富智能助手正在处理您的问题…", "pending");
try {
await ensureVisitorToken();
const response = await fetch("/api/v1/agent-runs", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}` },
body: JSON.stringify({ agent_type: "customer_service", message: normalized, session_id: sessionId, idempotency_key: crypto.randomUUID().replaceAll("-", "") }),
});
if (!response.ok) throw new Error("客服请求创建失败");
await pollRun((await response.json()).data.status_url, pending);
} catch (error) {
pending.bubble.textContent = error instanceof Error ? error.message : "客服测试请求失败";
pending.wrapper.className = "message error";
} finally {
sendButton.disabled = false;
input.focus();
}
}
composer.addEventListener("submit", (event) => {
event.preventDefault();
void sendMessage(input.value);
});
document.querySelectorAll(".sample").forEach((button) => {
button.addEventListener("click", () => void sendMessage(button.dataset.message || ""));
});
appendMessage("agent", "您好呀,我是南方财富智能助手。这里可测试公开问题、账户入口、安全提示和人工服务路径。");
void ensureVisitorToken().catch(() => { connectionStatus.textContent = "访客:连接未建立,发送消息时将自动重试"; });
</script>
</body>
</html>