- 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 单测)
120 lines
4.6 KiB
Bash
Executable File
120 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# build.sh —— PAPER 服务编译硬化(复制自 bioflow_service/build.sh,白名单改造)。
|
||
# 白名单(明文):app/main.py(uvicorn 入口)、app/config.py(运维可读面)、
|
||
# app/schemas.py(常量面)、app/db.py(aiosqlite 协程取消热区——Cython 协程断流断言
|
||
# abort 是既有教训,明文保留)、app/mcp.py(MCP 工具层,mcp 依赖 pydantic + async 工具)、
|
||
# services/paper/{llm,search,write,outline,complete}.py(长 LLM/httpx/aiosqlite 调用,
|
||
# 断流崩溃高危点,明文走 CPython 原生协程)+ 全部 __init__.py。
|
||
# 仅纯同步逻辑(cite.py 等)可编译。
|
||
# 用法:build.sh <source-root> <output-dir> [python]
|
||
set -euo pipefail
|
||
umask 027
|
||
|
||
SOURCE_ROOT="${1:?Usage: $0 <source-root> <output-dir> [python]}"
|
||
OUTPUT_DIR="${2:?Usage: $0 <source-root> <output-dir> [python]}"
|
||
PYTHON="${3:-python3}"
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
VISIBILITY_TOOL="${ELF_VISIBILITY_TOOL:-$SCRIPT_DIR/../tooling/compile/tools/elf_visibility.py}"
|
||
|
||
SOURCE_ROOT="$(realpath "$SOURCE_ROOT")"
|
||
OUTPUT_DIR="$(realpath -m "$OUTPUT_DIR")"
|
||
|
||
[[ -d "$SOURCE_ROOT" ]] || { echo "Paper service source missing: $SOURCE_ROOT" >&2; exit 2; }
|
||
[[ -x "$PYTHON" ]] || { echo "Python is not executable: $PYTHON" >&2; exit 2; }
|
||
[[ -f "$VISIBILITY_TOOL" ]] || { echo "ELF visibility tool missing: $VISIBILITY_TOOL" >&2; exit 2; }
|
||
for command_name in gcc strip rsync; do command -v "$command_name" >/dev/null; done
|
||
|
||
case "$OUTPUT_DIR" in
|
||
"/"|"$(realpath -m "$HOME")"|"$SOURCE_ROOT")
|
||
echo "Unsafe output directory: $OUTPUT_DIR" >&2; exit 2 ;;
|
||
esac
|
||
[[ ! -e "$OUTPUT_DIR" ]] || { echo "Output exists; refusing to overwrite: $OUTPUT_DIR" >&2; exit 2; }
|
||
|
||
echo "[PAPER BUILD 1/5] Copying source (excluding tests)"
|
||
mkdir -p "$OUTPUT_DIR"
|
||
rsync -a --exclude '__pycache__' --exclude 'test_*.py' --exclude 'data' --exclude '.env' \
|
||
--exclude 'build.sh' --exclude 'restart.sh' --exclude 'pytest.ini' \
|
||
"$SOURCE_ROOT/" "$OUTPUT_DIR/"
|
||
|
||
WHITELIST_PY=(
|
||
"app/main.py"
|
||
"app/config.py"
|
||
"app/schemas.py"
|
||
"app/db.py"
|
||
"app/mcp.py"
|
||
"app/services/paper/llm.py"
|
||
"app/services/paper/search.py"
|
||
"app/services/paper/write.py"
|
||
"app/services/paper/outline.py"
|
||
"app/services/paper/complete.py"
|
||
)
|
||
_is_whitelisted() {
|
||
local rel="$1"
|
||
[[ "$(basename "$rel")" == "__init__.py" ]] && return 0
|
||
for w in "${WHITELIST_PY[@]}"; do
|
||
case "$rel" in
|
||
$w) return 0 ;;
|
||
esac
|
||
done
|
||
return 1
|
||
}
|
||
|
||
echo "[PAPER BUILD 2/5] Discovering Python modules"
|
||
PY_FILES=()
|
||
while IFS= read -r -d '' path; do PY_FILES+=("$path"); done \
|
||
< <(find "$OUTPUT_DIR" -type f -name '*.py' -print0)
|
||
FILTERED=()
|
||
for py_file in "${PY_FILES[@]}"; do
|
||
rel="${py_file#"$OUTPUT_DIR"/}"
|
||
if _is_whitelisted "$rel"; then
|
||
echo " [whitelist] $rel (明文保留)"
|
||
continue
|
||
fi
|
||
FILTERED+=("$py_file")
|
||
done
|
||
PY_FILES=("${FILTERED[@]}")
|
||
TOTAL="${#PY_FILES[@]}"
|
||
(( TOTAL > 0 )) || { echo "No paper service modules selected" >&2; exit 3; }
|
||
|
||
INCLUDE_PY="$("$PYTHON" -c 'import sysconfig; print(sysconfig.get_config_var("INCLUDEPY") or "")')"
|
||
EXT_SUFFIX="$("$PYTHON" -c 'import sysconfig; print(sysconfig.get_config_var("EXT_SUFFIX") or ".so")')"
|
||
|
||
echo "[PAPER BUILD 3/5] Cython preflight ($TOTAL modules)"
|
||
COUNT=0; FAILURES=0
|
||
for py_file in "${PY_FILES[@]}"; do
|
||
COUNT=$((COUNT+1))
|
||
directory="$(dirname "$py_file")"
|
||
module="$(basename "$py_file" .py)"
|
||
c_file="$directory/$module.c"
|
||
echo " [$COUNT/$TOTAL] ${py_file#"$OUTPUT_DIR"/}"
|
||
if ! "$PYTHON" -m cython -3 \
|
||
--directive always_allow_keywords=true,annotation_typing=False \
|
||
--no-docstrings "$py_file" -o "$c_file" 2>/dev/null; then
|
||
FAILURES=$((FAILURES+1))
|
||
fi
|
||
done
|
||
(( FAILURES == 0 )) || { echo "Paper service Cython failed: $FAILURES" >&2; exit 3; }
|
||
|
||
echo "[PAPER BUILD 4/5] GCC hidden visibility + strip"
|
||
COUNT=0
|
||
for py_file in "${PY_FILES[@]}"; do
|
||
COUNT=$((COUNT+1))
|
||
directory="$(dirname "$py_file")"
|
||
module="$(basename "$py_file" .py)"
|
||
c_file="$directory/$module.c"
|
||
so_file="$directory/$module$EXT_SUFFIX"
|
||
gcc -shared -pthread -fPIC -O2 -fvisibility=hidden \
|
||
-I"$INCLUDE_PY" -o "$so_file" "$c_file"
|
||
strip --strip-unneeded "$so_file"
|
||
"$PYTHON" -B "$VISIBILITY_TOOL" --hide-non-pyinit --check "$so_file" >/dev/null
|
||
rm -f -- "$c_file" "$py_file"
|
||
done
|
||
|
||
echo "[PAPER BUILD 5/5] Final checks"
|
||
SO_COUNT="$(find "$OUTPUT_DIR" -type f -name '*.so' | wc -l)"
|
||
[[ "$SO_COUNT" -eq "$TOTAL" ]] || { echo "count mismatch: $TOTAL vs $SO_COUNT" >&2; exit 4; }
|
||
if find "$OUTPUT_DIR" -type f -name '*.c' -print -quit | grep -q .; then
|
||
echo "Generated C source remains" >&2; exit 4
|
||
fi
|
||
echo "Paper service compile+harden complete: modules=$SO_COUNT output=$OUTPUT_DIR"
|