forked from admin/PlantAI-rag-new
- paper_writer_service(8097):FastAPI + MCP streamable-http 单端点,9 工具
(propose_paper_outline/write_paper_section/get_paper_progress/search_refs/
revise_paper_section/revise_paper/get_paper/complete_paper/cancel_paper),
SQLite 三表(papers/paper_sections/citations,WAL),GB/T 7714 引用协议
- BFF:services/paper/{mcp_client,agent}.py(裸 JSON-RPC 客户端 + 规则状态机)、
routers/paper.py、schemas.py 4 个 Paper*Block + ArtifactBlock 加 paper_id/citation_count、
config.py 开关、main.py 条件注册、docgen.py 抽 insert_artifact 公共 helper
- 前端:intent.js paperIntent 纯函数、Composer docgen 分支 sub-route、sendPaper/轮询、
PaperBriefCard/PaperOutlineCard/PaperProgressCard、DocPane cite-chip
- deploy/paper_restart.sh + toggle_paper.sh;compile_sdk.sh 白名单追加 paper 文件
- tests/paper 26 条(工具逻辑 + MCP 协议 + BFF 状态机 + 块 schema + 前端 intent 单测)
122 lines
7.2 KiB
JavaScript
122 lines
7.2 KiB
JavaScript
// intent.js —— 前端意图路由(P3/dev3 归属;纯函数,无 Vue 依赖,可直接 node 直测)
|
||
// 唯一事实源:docs/team/cases_intent.md(与后端 services/docgen/intent.py 同词典同规则同用例表)
|
||
// 判定顺序(优先级从高到低,命中即返回;漏判安全:不命中一律 qa):
|
||
// 1. export:hasActiveArtifact 且含 EXPORT_WORD 且含 FORMAT_WORD
|
||
// 2. edit: hasActiveArtifact 且含 EDIT_WORD
|
||
// 3. docgen:含 WRITE_VERB 且含 DOC_TYPE(排除词优先,防误判;
|
||
// 缺陷 #52a 细化:纯总结类动词句的「总结」自身不作 DOC_TYPE 证据,
|
||
// 须命中其他文档类型词如「论文」才进 docgen)
|
||
// 4. docgen:hasAttachments 且含 WRITE_VERB(附件兜底信号)
|
||
// 5. qa
|
||
|
||
// —— 导出意图词典 ——
|
||
const EXPORT_WORD = ['导出', '转成', '转为', '转换成', '下载成', '输出为', '存为', '转pdf', '转word'];
|
||
// FORMAT_WORD 英文不区分大小写;"doc" 是 "docx" 的子串,判定时逐一 contains 即可
|
||
const FORMAT_WORD = ['pdf', 'docx', 'doc', 'word'];
|
||
|
||
// —— 编辑意图词典(仅当存在活动文档时生效)——
|
||
const EDIT_WORD = ['改', '修改', '调整', '删', '删除', '加一段', '补充', '润色', '缩写', '扩写', '重写', '改写', '优化', '加个', '增加一段'];
|
||
|
||
// —— 写文档意图词典 ——
|
||
// 缺陷 #52a:补「总结/概括/归纳」——「帮我总结论文」+附件走 docgen(规则 4 附件兜底)。
|
||
// 纯总结类动词句(无其他写动词、无附件)须命中「论文」等其他文档类型词才进
|
||
// docgen(规则 3 细化,详见 WRITE_VERB_SUMMARIZE_EXCLUDE 注释)——
|
||
// 无附件「总结一下植物光合作用的过程」仍走 QA(漏判安全)。
|
||
// 后端同词典:sdk/app/services/docgen/intent.py 的 WRITE_VERB(由 Dev-B1 同步)。
|
||
const WRITE_VERB = ['写', '撰写', '拟', '起草', '编制', '生成', '编写', '拟定', '草拟', '制定', '总结', '概括', '归纳'];
|
||
const DOC_TYPE = [
|
||
'论文', '报告', '通知', '通告', '公告', '请示', '批复', '函', '申请书', '申请',
|
||
'申报书', '标书', '合同', '协议', '方案', '计划', '总结', '汇报', '讲话稿', '致辞',
|
||
'证明', '纪要', '简报', '材料', '说明书', '指南', '文档', '申报材料', '工作总结',
|
||
'调研报告', '可行性报告', '项目书', '策划书', '演讲稿', '新闻稿', '文案', '简历',
|
||
'制度', '规定', '办法', '细则', '章程', '声明', '邀请函', '感谢信', '慰问信',
|
||
'介绍信', '推荐信', '倡议书', '述职报告', '开题报告', '结题报告', '实验报告',
|
||
'分析报告', '会议纪要',
|
||
];
|
||
// 明确排除(不属于 DOC_TYPE,防误判):介绍、内容、代码、诗、作业、问题、建议、想法
|
||
const DOC_TYPE_EXCLUDE = ['介绍', '内容', '代码', '诗', '作业', '问题', '建议', '想法'];
|
||
// 缺陷 #52a:「总结/概括/归纳」入 WRITE_VERB 后,「总结」本身会命中 DOC_TYPE
|
||
// (如「总结一下植物光合作用的过程」无附件也被规则 3 拉进 docgen)。
|
||
// 防护语义(TL 裁决):句子的写动词仅由总结类新动词构成时,「总结」自身
|
||
// 不作 DOC_TYPE 证据——必须有其他文档类型词(如「总结这篇论文」的「论文」)
|
||
// 才进 docgen;带附件场景由规则 4 兜底,不受影响。
|
||
// 「写一篇总结」「写一份工作总结」等既有写类动词 + 总结类文档词路径不受影响。
|
||
const WRITE_VERB_SUMMARIZE_EXCLUDE = ['总结', '概括', '归纳'];
|
||
const WRITE_VERB_NON_SUMMARIZE = WRITE_VERB.filter((w) => !WRITE_VERB_SUMMARIZE_EXCLUDE.includes(w));
|
||
const DOC_TYPE_NON_SUMMARIZE = DOC_TYPE.filter((w) => !WRITE_VERB_SUMMARIZE_EXCLUDE.includes(w));
|
||
|
||
/** 文本是否含词典中任一词(子串判定;英文不区分大小写——中文不受影响,与后端一致) */
|
||
export function hasAnyWord(text, words) {
|
||
const s = String(text == null ? '' : text).toLowerCase();
|
||
for (const w of words) {
|
||
if (s.includes(String(w).toLowerCase())) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/** 文本是否含任一排除词(排除词优先于 DOC_TYPE 命中——防误判) */
|
||
export function hasAnyExcludeWord(text) {
|
||
return hasAnyWord(text, DOC_TYPE_EXCLUDE);
|
||
}
|
||
|
||
/** 缺陷 #52a:文本含总结类新动词 → 规则 3 需额外校验 */
|
||
export function hasAnySummarizeVerb(text) {
|
||
return hasAnyWord(text, WRITE_VERB_SUMMARIZE_EXCLUDE);
|
||
}
|
||
|
||
/** 文本含除总结类新动词外的其他写类动词(写/撰写/生成/制定…) */
|
||
export function hasNonSummarizeWriteVerb(text) {
|
||
return hasAnyWord(text, WRITE_VERB_NON_SUMMARIZE);
|
||
}
|
||
|
||
/** 文本命中非总结类的文档类型词(「总结」自身不作证据) */
|
||
export function hasNonSummarizeDocType(text) {
|
||
return hasAnyWord(text, DOC_TYPE_NON_SUMMARIZE);
|
||
}
|
||
|
||
/** 导出文档类型词词典(任务书约定的导出类型词;判定走 EXPORT_WORD+FORMAT_WORD,此处导出供组件复用) */
|
||
export const EXPORT_DOC_TYPE_WORDS = [
|
||
'论文', '报告', '通知', '通告', '公告', '请示', '批复', '函', '申请书', '申报书',
|
||
'标书', '合同', '协议', '方案', '计划', '总结', '汇报', '讲话稿', '致辞',
|
||
'证明', '纪要', '简报', '材料', '说明书', '指南',
|
||
];
|
||
|
||
/**
|
||
* 意图路由(唯一事实源 docs/team/cases_intent.md 的 route 签名)
|
||
* @param {string} text 用户输入文本
|
||
* @param {boolean} hasAttachments 本轮是否带附件(资料 chips)
|
||
* @param {boolean} hasActiveArtifact 是否存在活动文档(文档 TAB 当前打开的 artifact)
|
||
* @returns {'qa'|'docgen'|'edit'|'export'}
|
||
*/
|
||
export function route(text, hasAttachments, hasActiveArtifact) {
|
||
const s = String(text == null ? '' : text);
|
||
// 1. 导出意图(需活动文档)
|
||
if (hasActiveArtifact && hasAnyWord(s, EXPORT_WORD) && hasAnyWord(s, FORMAT_WORD)) return 'export';
|
||
// 2. 编辑意图(需活动文档)
|
||
if (hasActiveArtifact && hasAnyWord(s, EDIT_WORD)) return 'edit';
|
||
// 3. 写文档意图:写类动词 + 文档类型词(排除词防误判优先)
|
||
// 缺陷 #52a:纯总结类动词句(无其他写动词)须命中非总结类文档类型词
|
||
// (如「论文」)才进 docgen——「总结一下植物光合作用的过程」仍走 QA
|
||
if (hasAnyWord(s, WRITE_VERB) && !hasAnyExcludeWord(s) && hasAnyWord(s, DOC_TYPE)) {
|
||
const summarizeOnly = hasAnySummarizeVerb(s) && !hasNonSummarizeWriteVerb(s);
|
||
if (!summarizeOnly || hasNonSummarizeDocType(s)) return 'docgen';
|
||
}
|
||
// 4. 附件兜底:拖入文件 + 写类动词,即使无文档类型词也视为生成意图
|
||
if (hasAttachments && hasAnyWord(s, WRITE_VERB)) return 'docgen';
|
||
// 5. 漏判安全:一律 qa
|
||
return 'qa';
|
||
}
|
||
|
||
// —— 论文撰写意图(paper-writer):在 docgen 分支内做 sub-route ——
|
||
// 「论文/综述」主题词 + 写类动词 → 论文撰写(结构化引用体系),
|
||
// 其余 docgen(报告/通知等)仍走原 sendDocgen。纯函数,node 直测。
|
||
const PAPER_TYPE_WORDS = ['论文', '综述'];
|
||
|
||
export function paperIntent(text) {
|
||
const s = String(text == null ? '' : text).trim();
|
||
if (!s) return false;
|
||
return PAPER_TYPE_WORDS.some((w) => s.includes(w)) && hasAnyWord(s, WRITE_VERB);
|
||
}
|
||
|
||
export default route;
|