cube(chat): apps.json 成 SSOT,注进 chatbot system prompt
deploy cube / build-and-deploy (push) Successful in 1m22s
deploy cube / build-and-deploy (push) Successful in 1m22s
前端 apps.ts 之前是 source of truth,后端 chatbot 只能用硬编码的 "werewolf / articulate / karaoke / music / simpleasm 等" 句式糊 弄 + 靠训练知识猜。改成 apps.json 当 SSOT: - 前端 apps.ts 改为 import data from './apps.json' - 后端 include_str! 同一份 → 解析渲染 markdown bullet 列表 注进 system prompt,附带 slug / status / desc / url - prompt 显式约束:只能基于列表事实回答,不存在的 app 直说没有 - 兜底:JSON 解析失败把 raw 文本喂 LLM,不让 chatbot 因为 ssot 坏掉 500 - 10 个 cargo test(多覆盖 render_apps_list / prompt 含 apps)
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"slug": "cube",
|
||||||
|
"name": "cube",
|
||||||
|
"description": "你正在看的这个门户。cube 平台本身的入口。",
|
||||||
|
"url": "https://cube.famzheng.me",
|
||||||
|
"status": "live"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "portfolio",
|
||||||
|
"name": "portfolio",
|
||||||
|
"description": "投资组合追踪。从 oci 迁移中(原 portfolio.oci.euphon.net)。",
|
||||||
|
"url": "https://portfolio.famzheng.me",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "repo-vis",
|
||||||
|
"name": "repo-vis",
|
||||||
|
"description": "git 仓库可视化。从 oci 迁移中。",
|
||||||
|
"url": "https://repo-vis.famzheng.me",
|
||||||
|
"status": "pending"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "simpleasm",
|
||||||
|
"name": "simpleasm",
|
||||||
|
"description": "汇编教学小游戏。",
|
||||||
|
"url": "https://asm.famzheng.me",
|
||||||
|
"status": "live"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "music",
|
||||||
|
"name": "music",
|
||||||
|
"description": "听歌 + 练琴 曲目管理。243 首曲库(从 oci 旧 guitar 迁过来)+ 自动抓 yopu 吉他/功能谱 + LLM 灵感推荐。",
|
||||||
|
"url": "https://music.famzheng.me",
|
||||||
|
"status": "live"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "pyroblem",
|
||||||
|
"name": "pyroblem",
|
||||||
|
"description": "详情待补。",
|
||||||
|
"url": "https://pyroblem.famzheng.me",
|
||||||
|
"status": "tbd"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "werewolf",
|
||||||
|
"name": "werewolf",
|
||||||
|
"description": "狼人杀单机发牌器。一台手机轮流传,30 个角色、4x 偏好加权、配置历史本地记忆。从 partiverse 移植。",
|
||||||
|
"url": "https://werewolf.famzheng.me",
|
||||||
|
"status": "live"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "articulate",
|
||||||
|
"name": "articulate",
|
||||||
|
"description": "中英猜词派对游戏(Articulate)。15 个主题词库 + 3 档难度 + 已看词跨场记忆。从 partiverse 移植。",
|
||||||
|
"url": "https://articulate.famzheng.me",
|
||||||
|
"status": "live"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "karaoke",
|
||||||
|
"name": "karaoke",
|
||||||
|
"description": "卡拉OK 点歌单本地管理。增删改排 + YouTube 一键搜,10 秒撤销。从 partiverse 移植。",
|
||||||
|
"url": "https://karaoke.famzheng.me",
|
||||||
|
"status": "live"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"slug": "notes",
|
||||||
|
"name": "notes",
|
||||||
|
"description": "录音 → ASR 转写 → LLM 生成会议纪要。Sidebar + content;passphrase 鉴权。",
|
||||||
|
"url": "https://notes.famzheng.me",
|
||||||
|
"status": "live"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
// apps 列表的 SSOT 是 apps.json — 后端 chat.rs 也 include_str! 同一份,注入到
|
||||||
|
// chatbot 的 system prompt 里。改 apps 只改 apps.json。
|
||||||
|
|
||||||
|
import data from './apps.json'
|
||||||
|
|
||||||
export type AppStatus = 'live' | 'pending' | 'tbd'
|
export type AppStatus = 'live' | 'pending' | 'tbd'
|
||||||
|
|
||||||
export interface App {
|
export interface App {
|
||||||
@@ -8,75 +13,4 @@ export interface App {
|
|||||||
status: AppStatus
|
status: AppStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apps: App[] = [
|
export const apps: App[] = data as App[]
|
||||||
{
|
|
||||||
slug: 'cube',
|
|
||||||
name: 'cube',
|
|
||||||
description: '你正在看的这个门户。cube 平台本身的入口。',
|
|
||||||
url: 'https://cube.famzheng.me',
|
|
||||||
status: 'live',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'portfolio',
|
|
||||||
name: 'portfolio',
|
|
||||||
description: '投资组合追踪。从 oci 迁移中(原 portfolio.oci.euphon.net)。',
|
|
||||||
url: 'https://portfolio.famzheng.me',
|
|
||||||
status: 'pending',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'repo-vis',
|
|
||||||
name: 'repo-vis',
|
|
||||||
description: 'git 仓库可视化。从 oci 迁移中。',
|
|
||||||
url: 'https://repo-vis.famzheng.me',
|
|
||||||
status: 'pending',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'simpleasm',
|
|
||||||
name: 'simpleasm',
|
|
||||||
description: '汇编教学小游戏。',
|
|
||||||
url: 'https://asm.famzheng.me',
|
|
||||||
status: 'live',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'music',
|
|
||||||
name: 'music',
|
|
||||||
description: '听歌 + 练琴 曲目管理。243 首曲库(从 oci 旧 guitar 迁过来)+ 自动抓 yopu 吉他/功能谱 + LLM 灵感推荐。',
|
|
||||||
url: 'https://music.famzheng.me',
|
|
||||||
status: 'live',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'pyroblem',
|
|
||||||
name: 'pyroblem',
|
|
||||||
description: '详情待补。',
|
|
||||||
url: 'https://pyroblem.famzheng.me',
|
|
||||||
status: 'tbd',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'werewolf',
|
|
||||||
name: 'werewolf',
|
|
||||||
description: '狼人杀单机发牌器。一台手机轮流传,30 个角色、4x 偏好加权、配置历史本地记忆。从 partiverse 移植。',
|
|
||||||
url: 'https://werewolf.famzheng.me',
|
|
||||||
status: 'live',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'articulate',
|
|
||||||
name: 'articulate',
|
|
||||||
description: '中英猜词派对游戏(Articulate)。15 个主题词库 + 3 档难度 + 已看词跨场记忆。从 partiverse 移植。',
|
|
||||||
url: 'https://articulate.famzheng.me',
|
|
||||||
status: 'live',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'karaoke',
|
|
||||||
name: 'karaoke',
|
|
||||||
description: '卡拉OK 点歌单本地管理。增删改排 + YouTube 一键搜,10 秒撤销。从 partiverse 移植。',
|
|
||||||
url: 'https://karaoke.famzheng.me',
|
|
||||||
status: 'live',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
slug: 'notes',
|
|
||||||
name: 'notes',
|
|
||||||
description: '录音 → ASR 转写 → LLM 生成会议纪要。Sidebar + content;passphrase 鉴权。',
|
|
||||||
url: 'https://notes.famzheng.me',
|
|
||||||
status: 'live',
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|||||||
+65
-4
@@ -84,17 +84,56 @@ impl IntoResponse for ChatError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// apps.json 是 SSOT — 前端 apps.ts 也 import 同一份。
|
||||||
|
const APPS_JSON: &str = include_str!("../frontend/src/apps.json");
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct AppInfo {
|
||||||
|
slug: String,
|
||||||
|
description: String,
|
||||||
|
url: String,
|
||||||
|
status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 把 apps.json 渲染成 markdown bullet list 注进 system prompt。
|
||||||
|
/// 解析失败兜底成 raw JSON(让 LLM 自己 grok),不让 chatbot 因为 ssot 坏掉而 500。
|
||||||
|
pub fn render_apps_list() -> String {
|
||||||
|
match serde_json::from_str::<Vec<AppInfo>>(APPS_JSON) {
|
||||||
|
Ok(apps) => apps
|
||||||
|
.iter()
|
||||||
|
.map(|a| {
|
||||||
|
format!(
|
||||||
|
"- **{slug}** ({status}) — {desc} <{url}>",
|
||||||
|
slug = a.slug,
|
||||||
|
status = a.status,
|
||||||
|
desc = a.description,
|
||||||
|
url = a.url,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n"),
|
||||||
|
Err(_) => APPS_JSON.trim().to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn system_prompt(repo: &str) -> String {
|
pub fn system_prompt(repo: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
"你是 cube 平台(cube.famzheng.me,Fam 的小 app 平台)入口页上的聊天助手。\n\
|
"你是 cube 平台(cube.famzheng.me,Fam 的小 app 平台)入口页上的聊天助手。\n\
|
||||||
|
\n\
|
||||||
|
当前 cube 上线的 app 列表(status: live=可用 / pending=迁移中 / tbd=待定):\n\
|
||||||
|
{apps}\n\
|
||||||
|
\n\
|
||||||
你可以做两件事:\n\
|
你可以做两件事:\n\
|
||||||
1. 回答用户关于 cube 上各个 app(werewolf / articulate / karaoke / music / simpleasm 等)的问题,简短直接\n\
|
1. 回答用户关于上面这些 app 的问题,简短直接。**回答时只能基于上面列表的事实**,\n\
|
||||||
2. 当用户表达 bug / feature request / feedback / 想反馈给 Fam 时,调用 `create_issue` 工具,\n\
|
不要凭训练知识瞎猜不存在的 app 或功能。如果用户问的 app 不在列表里,直说没有。\n\
|
||||||
把它整理成 issue 创建到 `{repo}`。标题要简洁明确(不超过 60 个字符),body 要包含足够上下文\n\
|
2. 当用户表达 bug / feature request / feedback / 想反馈给 Fam 时,调用 `create_issue`\n\
|
||||||
(重现步骤、期望行为、用户原话等)。\n\
|
工具,把它整理成 issue 创建到 `{repo}`。标题简洁明确(≤60 字符),body 包含足够上下文\n\
|
||||||
|
(涉及哪个 app / 重现步骤 / 期望行为 / 用户原话等)。\n\
|
||||||
\n\
|
\n\
|
||||||
不要主动鼓励用户提 issue —— 只在他明确表达想反馈时才创建。\n\
|
不要主动鼓励用户提 issue —— 只在他明确表达想反馈时才创建。\n\
|
||||||
同一次对话只创建一个 issue。Reply 要简短,不写长段散文。",
|
同一次对话只创建一个 issue。Reply 要简短,不写长段散文。",
|
||||||
|
apps = render_apps_list(),
|
||||||
|
repo = repo,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +351,28 @@ mod tests {
|
|||||||
assert!(p.contains("create_issue"));
|
assert!(p.contains("create_issue"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_apps_list_parses_ssot() {
|
||||||
|
let list = render_apps_list();
|
||||||
|
// 兜底分支会返回 raw JSON(含 `{`),正常解析后是 markdown bullet(每行以 `- ` 起头)
|
||||||
|
assert!(list.starts_with("- "), "render output should be markdown bullets, got: {list}");
|
||||||
|
// 抽查几个已知 slug 在里面
|
||||||
|
for slug in ["cube", "werewolf", "articulate", "karaoke", "music"] {
|
||||||
|
assert!(list.contains(slug), "apps list 应该含 {slug}: {list}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn system_prompt_includes_apps_list() {
|
||||||
|
let p = system_prompt("fam/cube");
|
||||||
|
// 至少一个 app 的 slug 出现在 prompt 里,说明 list 真的被注进去了
|
||||||
|
assert!(p.contains("werewolf"));
|
||||||
|
assert!(p.contains("live"));
|
||||||
|
// 防回退:旧 prompt 写死了 "werewolf / articulate / karaoke / music / simpleasm",
|
||||||
|
// 现在不再用那种枚举句式 —— 列表是数据驱动的
|
||||||
|
assert!(!p.contains("werewolf / articulate / karaoke / music / simpleasm"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tool_schema_shape() {
|
fn tool_schema_shape() {
|
||||||
let s = create_issue_tool_schema();
|
let s = create_issue_tool_schema();
|
||||||
|
|||||||
Reference in New Issue
Block a user