| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- #!/usr/bin/env bash
- # =============================================================================
- # inkos-write-next-atomic.sh
- #
- # 用原子命令组装 inkos write next 的完整管线,绝不直接调用 inkos write next。
- #
- # 使用的原子命令:
- # inkos plan chapter – 预检:生成章节意图(无 LLM 调用)
- # inkos compose chapter – 预检:编译上下文/规则栈/追踪产物(无 LLM 调用)
- # inkos draft – 撰写章节草稿(LLM)
- # inkos audit – 连续性审计(LLM)
- # inkos revise – 修复问题 / 润色文笔(LLM)
- #
- # 管线:
- # plan → compose → draft → [audit → revise]×N → polish → 完成
- # =============================================================================
- set -euo pipefail
- # ---------------------------------------------------------------------------
- # LLM 环境变量(确保 inkos 使用正确的 API 端点和模型)
- # ---------------------------------------------------------------------------
- export INKOS_LLM_MODEL="${INKOS_LLM_MODEL:-deepseek-v3.2-exp}"
- export INKOS_LLM_BASE_URL="${INKOS_LLM_BASE_URL:-https://platform.shuyanai.com/v1}"
- # ---------------------------------------------------------------------------
- # 用法说明
- # ---------------------------------------------------------------------------
- usage() {
- cat <<'EOF'
- 用法:inkos-write-next-atomic.sh [book-id] [创作引导] [选项]
- 用原子命令组装 inkos 写下一章的完整管线:
- plan → [确认意图] → compose → draft → [确认草稿] → audit → revise×N → polish
- 参数:
- book-id 作品 ID(可选,仅一个作品时自动检测)
- 创作引导 章节创作引导文本(可选,不提供则交互式输入)
- 选项:
- -h, --help 显示此帮助信息
- 交互式检查点:
- 第 1 步后 用 less 查看意图文件,可选择重新输入引导或继续
- 第 3 步后 确认草稿,可选择输入提示词用 rewrite 重写
- 环境变量:
- INKOS_LLM_MODEL 模型(默认 deepseek-v4-flash)
- INKOS_LLM_BASE_URL API 端点(默认 https://platform.shuyanai.com/v1)
- EOF
- exit 0
- }
- # 检查 --help / -h
- for arg in "$@"; do
- if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
- usage
- fi
- done
- # ---------------------------------------------------------------------------
- # 辅助函数
- # ---------------------------------------------------------------------------
- RED='\033[0;31m'
- GREEN='\033[0;32m'
- YELLOW='\033[0;33m'
- CYAN='\033[0;36m'
- BOLD='\033[1m'
- NC='\033[0m'
- banner() { echo -e "${CYAN}${BOLD}==>${NC} ${BOLD}$*${NC}"; }
- ok() { echo -e " ${GREEN}✓${NC} $*"; }
- warn() { echo -e " ${YELLOW}⚠${NC} $*"; }
- fail() { echo -e " ${RED}✗${NC} $*"; }
- stage() { echo -e "\n${CYAN}━━━ $* ━━━${NC}"; }
- info() { echo -e " $*"; }
- die() {
- echo -e "${RED}${BOLD}致命错误:${NC} $*" >&2
- exit 1
- }
- require_cmd() {
- command -v "$1" >/dev/null 2>&1 || die "缺少命令:$1,请先安装。"
- }
- # 解析 JSON 字段(取首个匹配值)
- json_field() {
- local json="$1" key="$2"
- echo "$json" | jq -r ".${key} // empty" 2>/dev/null
- }
- # spinner:在后台任务运行时显示旋转动画和耗时
- spinner() {
- local pid="$1" msg="$2" spin='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' i=0
- local start="${3:-$SECONDS}"
- while kill -0 "$pid" 2>/dev/null; do
- local elapsed=$((SECONDS - start))
- printf "\r ${spin:$i:1} %s (已耗时 %ds)..." "$msg" "$elapsed" >&2
- i=$(( (i + 1) % ${#spin} ))
- sleep 0.2
- done
- wait "$pid"
- }
- # run_with_spinner <描述> <命令> [参数...]
- # 在后台运行命令,显示 spinner,捕获 stdout 到临时文件,保持原始退出码
- run_with_spinner() {
- local msg="$1" start_ts="$SECONDS"
- shift
- local tmpfile
- tmpfile=$(mktemp)
- # 后台运行命令,stdout → tmpfile,stderr → /dev/null
- "$@" > "$tmpfile" 2>/dev/null &
- local pid=$!
- spinner "$pid" "$msg" "$start_ts"
- local rc=$?
- # 清掉 spinner 行,输出结果
- local elapsed=$((SECONDS - start_ts))
- printf "\r\033[K ${GREEN}✓${NC} %s (耗时 %ds)\n" "$msg" "$elapsed" >&2
- cat "$tmpfile"
- rm -f "$tmpfile"
- return $rc
- }
- # ---------------------------------------------------------------------------
- # 自动检测项目中的作品 ID
- # ---------------------------------------------------------------------------
- detect_book_id() {
- local books_dir
- books_dir="$(findProjectRoot)/books"
- if [[ ! -d "$books_dir" ]]; then
- die "未找到 books/ 目录。请在 inkos 项目内运行此脚本。"
- fi
- local books
- books=($(ls -1 "$books_dir" 2>/dev/null || true))
- if [[ ${#books[@]} -eq 0 ]]; then
- die "项目中还没有作品。请先用 inkos book create --title ... 创建。"
- fi
- if [[ ${#books[@]} -eq 1 ]]; then
- echo "${books[0]}"
- return
- fi
- # 多作品时让用户选择
- echo "" >&2
- info "检测到多个作品:" >&2
- for i in "${!books[@]}"; do
- info " [$((i+1))] ${books[$i]}" >&2
- done
- echo "" >&2
- read -r -p "请选择作品 [1-${#books[@]}]:" choice || true
- if [[ ! "$choice" =~ ^[0-9]+$ ]] || (( choice < 1 || choice > ${#books[@]} )); then
- die "选择无效。"
- fi
- echo "${books[$((choice-1))]}"
- }
- findProjectRoot() {
- local dir="$PWD"
- while [[ "$dir" != "/" ]]; do
- if [[ -f "$dir/inkos.json" ]]; then
- echo "$dir"
- return
- fi
- dir="$(dirname "$dir")"
- done
- die "当前目录及父目录均未找到 inkos.json。请在 inkos 项目内运行此脚本。"
- }
- # ---------------------------------------------------------------------------
- # 章节号提取
- # ---------------------------------------------------------------------------
- get_latest_chapter() {
- local book_id="$1" root
- root="$(findProjectRoot)"
- local index="$root/books/$book_id/chapters/index.json"
- if [[ ! -f "$index" ]]; then
- echo "1"
- return
- fi
- jq -r 'if length == 0 then 1 else (max_by(.number) | .number + 1) end' "$index" 2>/dev/null || echo "1"
- }
- get_last_written_chapter() {
- local book_id="$1" root
- root="$(findProjectRoot)"
- local index="$root/books/$book_id/chapters/index.json"
- if [[ ! -f "$index" ]]; then
- echo "0"
- return
- fi
- jq -r 'max_by(.number) | .number // 0' "$index" 2>/dev/null || echo "0"
- }
- # ---------------------------------------------------------------------------
- # 前置检查
- # ---------------------------------------------------------------------------
- require_cmd inkos
- require_cmd jq
- require_cmd less
- echo ""
- echo -e "${BOLD}${CYAN}╔══════════════════════════════════════╗${NC}"
- echo -e "${BOLD}${CYAN}║ InkOS 原子写下一章(原子管线) ║${NC}"
- echo -e "${BOLD}${CYAN}╚══════════════════════════════════════╝${NC}"
- echo ""
- ROOT="$(findProjectRoot)"
- BOOK_ID="${1:-$(detect_book_id)}"
- NEXT_CH="$(get_latest_chapter "$BOOK_ID")"
- LAST_CH="$(get_last_written_chapter "$BOOK_ID")"
- info "项目 :${ROOT}"
- info "作品 :${BOOK_ID}"
- info "下一章 :第 ${NEXT_CH} 章"
- if (( LAST_CH > 0 )); then
- info "最新章 :第 ${LAST_CH} 章"
- fi
- # 可选章节引导
- CONTEXT="${2:-}"
- if [[ -z "$CONTEXT" ]]; then
- echo ""
- read -r -p "章节创作引导(可选,回车跳过):" CONTEXT || true
- fi
- # 用数组安全传参(处理空格等特殊字符)
- CONTEXT_ARGS=()
- if [[ -n "$CONTEXT" ]]; then
- CONTEXT_ARGS=(--context "$CONTEXT")
- fi
- # ---------------------------------------------------------------------------
- # 第 1 步:plan chapter(循环,允许用户查看意图后重新输入引导)
- # ---------------------------------------------------------------------------
- while true; do
- stage "第 1/5 步:规划章节意图"
- PLAN_OUTPUT=""
- PLAN_OUTPUT=$(run_with_spinner "正在规划章节意图" inkos plan chapter "$BOOK_ID" "${CONTEXT_ARGS[@]}" --json) || {
- fail "章节意图规划失败"
- echo "$PLAN_OUTPUT"
- exit 1
- }
- if [[ -z "$(json_field "$PLAN_OUTPUT" chapterNumber)" ]]; then
- fail "章节意图 JSON 解析失败(可能 stdout/stderr 混合)"
- exit 1
- fi
- PLAN_GOAL="$(json_field "$PLAN_OUTPUT" goal)"
- PLAN_CH="$(json_field "$PLAN_OUTPUT" chapterNumber)"
- PLAN_INTENT_FILE="$(json_field "$PLAN_OUTPUT" intentPath)"
- ok "已生成第 ${PLAN_CH} 章意图"
- if [[ -n "$PLAN_GOAL" ]]; then
- info "章节目标:${PLAN_GOAL}"
- fi
- if [[ -n "$PLAN_INTENT_FILE" ]]; then
- info "意图文件:${PLAN_INTENT_FILE}"
- fi
- # ---- 检查点:查看意图文件 ----
- echo ""
- INTENT_FULL_PATH="$ROOT/books/$BOOK_ID/$PLAN_INTENT_FILE"
- if [[ -n "$PLAN_INTENT_FILE" && -f "$INTENT_FULL_PATH" ]]; then
- info "正在用 less 显示意图文件(按 q 退出,↑↓ 滚动,/ 搜索)……"
- echo ""
- less -R "$INTENT_FULL_PATH" || true
- else
- warn "意图文件不存在(${INTENT_FULL_PATH}),跳过预览。"
- fi
- echo ""
- info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- info " c / 回车 — 继续下一步(compile)"
- info " r — 重新输入创作引导,重新 plan"
- info " q — 退出"
- info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- read -r -p "请选择 [c/r/q](默认 c):" PLAN_CHOICE || true
- case "${PLAN_CHOICE:-c}" in
- c|C|"")
- break
- ;;
- r|R)
- echo ""
- read -r -p "请输入新的创作引导:" CONTEXT || true
- CONTEXT_ARGS=()
- if [[ -n "$CONTEXT" ]]; then
- CONTEXT_ARGS=(--context "$CONTEXT")
- fi
- ;;
- q|Q)
- info "用户退出。"
- exit 0
- ;;
- *)
- warn "无效输入,默认继续。"
- break
- ;;
- esac
- done
- # ---------------------------------------------------------------------------
- # 第 2 步:compose chapter(无 LLM 调用 — 编译上下文/规则栈/追踪产物)
- # ---------------------------------------------------------------------------
- stage "第 2/5 步:编译章节上下文"
- COMPOSE_OUTPUT=""
- COMPOSE_OUTPUT=$(inkos compose chapter "$BOOK_ID" "${CONTEXT_ARGS[@]}" --json 2>/dev/null) || {
- fail "章节上下文编译失败"
- echo "$COMPOSE_OUTPUT"
- exit 1
- }
- if [[ -z "$(json_field "$COMPOSE_OUTPUT" contextPath)" ]]; then
- fail "章节上下文 JSON 解析失败(可能 stdout/stderr 混合)"
- exit 1
- fi
- COMPOSE_CH="$(json_field "$COMPOSE_OUTPUT" chapterNumber)"
- COMPOSE_CTX="$(json_field "$COMPOSE_OUTPUT" contextPath)"
- COMPOSE_RULES="$(json_field "$COMPOSE_OUTPUT" ruleStackPath)"
- COMPOSE_TRACE="$(json_field "$COMPOSE_OUTPUT" tracePath)"
- ok "已编译第 ${COMPOSE_CH} 章上下文"
- info "上下文 :${COMPOSE_CTX}"
- info "规则栈 :${COMPOSE_RULES}"
- info "追踪文件 :${COMPOSE_TRACE}"
- # ---------------------------------------------------------------------------
- # 第 3 步:draft(LLM — 撰写章节草稿,然后循环确认 / rewrite 重写)
- # ---------------------------------------------------------------------------
- stage "第 3/5 步:撰写章节草稿"
- DRAFT_OUTPUT=""
- DRAFT_OUTPUT=$(run_with_spinner "正在生成草稿" inkos draft "$BOOK_ID" "${CONTEXT_ARGS[@]}" --json) || {
- fail "草稿撰写失败"
- echo "$DRAFT_OUTPUT"
- exit 1
- }
- DRAFT_CH="$(json_field "$DRAFT_OUTPUT" chapterNumber)"
- DRAFT_TITLE="$(json_field "$DRAFT_OUTPUT" title)"
- DRAFT_WORDS="$(json_field "$DRAFT_OUTPUT" wordCount)"
- DRAFT_FILE="$(json_field "$DRAFT_OUTPUT" filePath)"
- if [[ -z "$DRAFT_CH" || -z "$DRAFT_FILE" ]]; then
- fail "草稿 JSON 解析失败(缺少 chapterNumber 或 filePath)"
- exit 1
- fi
- # ---- 确认循环:less 预览 → [c/r/q] → rewrite 后回到预览 ----
- while true; do
- info "第 ${DRAFT_CH} 章:${DRAFT_TITLE}"
- info "字数:${DRAFT_WORDS}"
- info "文件:${DRAFT_FILE}"
- # 检查字数警告
- LEN_WARNINGS="$(json_field "$DRAFT_OUTPUT" lengthWarnings)"
- if [[ -n "$LEN_WARNINGS" && "$LEN_WARNINGS" != "null" ]]; then
- warn "字数警告:"
- echo "$DRAFT_OUTPUT" | jq -r '.lengthWarnings[]? | " - \(.)"' 2>/dev/null || true
- fi
- echo ""
- if [[ -n "$DRAFT_FILE" && -f "$DRAFT_FILE" ]]; then
- info "正在用 less 显示草稿(按 q 退出,↑↓ 滚动,/ 搜索)……"
- echo ""
- less -R "$DRAFT_FILE" || true
- else
- warn "草稿文件不存在(${DRAFT_FILE}),跳过预览。"
- fi
- echo ""
- info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- info " c / 回车 — 继续下一步(审计)"
- info " r — 输入提示词,用 rewrite 重写本章"
- info " q — 退出"
- info "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- read -r -p "请选择 [c/r/q](默认 c):" DRAFT_CHOICE || true
- case "${DRAFT_CHOICE:-c}" in
- c|C|"")
- break
- ;;
- r|R)
- echo ""
- read -r -p "请输入重写提示词:" REWRITE_BRIEF || true
- if [[ -z "$REWRITE_BRIEF" ]]; then
- warn "未输入提示词,跳过重写。"
- continue
- fi
- REWRITE_OUTPUT=""
- REWRITE_OUTPUT=$(run_with_spinner "正在 rewrite 重写" inkos revise "$BOOK_ID" "$DRAFT_CH" --mode rewrite --brief "$REWRITE_BRIEF" --json) || {
- warn "重写失败,将使用当前草稿继续。"
- echo "$REWRITE_OUTPUT"
- continue
- }
- REWRITE_APPLIED="$(json_field "$REWRITE_OUTPUT" applied)"
- REWRITE_WORDS="$(json_field "$REWRITE_OUTPUT" wordCount)"
- if [[ "$REWRITE_APPLIED" == "true" ]]; then
- ok "重写已应用 —— ${REWRITE_WORDS} 字"
- DRAFT_TITLE="$(json_field "$REWRITE_OUTPUT" title)"
- DRAFT_WORDS="$REWRITE_WORDS"
- # 重写后回到预览,让用户再次确认
- else
- warn "重写未应用 —— $(json_field "$REWRITE_OUTPUT" skippedReason)"
- continue
- fi
- ;;
- q|Q)
- info "用户退出。"
- exit 0
- ;;
- *)
- warn "无效输入,默认继续。"
- break
- ;;
- esac
- done
- # ---------------------------------------------------------------------------
- # 第 4 步:审计 + 修订循环
- # ---------------------------------------------------------------------------
- MAX_REVISE_ROUNDS=3
- AUDIT_PASSED=false
- REVISED=false
- ROUND=0
- stage "第 4/5 步:审计 + 修订循环"
- while true; do
- ROUND=$((ROUND + 1))
- # ---- 审计 ----
- AUDIT_OUTPUT=""
- AUDIT_OUTPUT=$(run_with_spinner "正在审计(第 ${ROUND} 轮)" inkos audit "$BOOK_ID" "$DRAFT_CH" --json) || {
- fail "审计失败"
- echo "$AUDIT_OUTPUT"
- exit 1
- }
- AUDIT_PASSED_FLAG="$(json_field "$AUDIT_OUTPUT" passed)"
- AUDIT_SUMMARY="$(json_field "$AUDIT_OUTPUT" summary)"
- if [[ "$AUDIT_PASSED_FLAG" == "true" ]]; then
- ok "审计通过 —— ${AUDIT_SUMMARY}"
- AUDIT_PASSED=true
- break
- fi
- warn "审计未通过(第 ${ROUND} 轮)—— ${AUDIT_SUMMARY}"
- # 打印问题列表
- ISSUE_COUNT=$(echo "$AUDIT_OUTPUT" | jq '.issues | length' 2>/dev/null || echo "0")
- if (( ISSUE_COUNT > 0 )); then
- echo "$AUDIT_OUTPUT" | jq -r '.issues[] | " [\(.severity)] \(.category): \(.description)"' 2>/dev/null
- fi
- # 检查是否还有修订轮次
- if (( ROUND >= MAX_REVISE_ROUNDS )); then
- warn "已达最大修订轮次(${MAX_REVISE_ROUNDS})。章节将保留为草稿状态。"
- break
- fi
- echo ""
- read -r -p "是否现在修订?[Y/n]:" DO_REVISE || true
- if [[ "$DO_REVISE" =~ ^[Nn] ]]; then
- warn "跳过修订。章节将保留为草稿状态。"
- break
- fi
- # 选择修订模式
- echo ""
- info "修订模式:"
- info " 1) spot-fix – 定点修复,最小改动(默认)"
- info " 2) rewrite – 全章改写,保留剧情走向"
- info " 3) rework – 深层重构"
- info " 4) anti-detect – 去 AI 痕迹"
- read -r -p "请选择模式 [1-4](默认 1):" MODE_CHOICE || true
- case "${MODE_CHOICE:-1}" in
- 1) REVISE_MODE="spot-fix" ;;
- 2) REVISE_MODE="rewrite" ;;
- 3) REVISE_MODE="rework" ;;
- 4) REVISE_MODE="anti-detect" ;;
- *) REVISE_MODE="spot-fix" ;;
- esac
- # ---- 修订 ----
- REVISE_OUTPUT=""
- REVISE_OUTPUT=$(run_with_spinner "正在修订 [$REVISE_MODE]" inkos revise "$BOOK_ID" "$DRAFT_CH" --mode "$REVISE_MODE" --json) || {
- fail "修订失败"
- echo "$REVISE_OUTPUT"
- continue
- }
- REVISE_APPLIED="$(json_field "$REVISE_OUTPUT" applied)"
- REVISE_WORDS="$(json_field "$REVISE_OUTPUT" wordCount)"
- REVISE_STATUS="$(json_field "$REVISE_OUTPUT" status)"
- REVISE_SKIPPED="$(json_field "$REVISE_OUTPUT" skippedReason)"
- if [[ "$REVISE_APPLIED" == "true" ]]; then
- ok "修订已应用 —— ${REVISE_WORDS} 字,状态:${REVISE_STATUS}"
- REVISED=true
- echo "$REVISE_OUTPUT" | jq -r '.fixedIssues[]? | " 已修复:\(.)"' 2>/dev/null
- else
- warn "修订未应用 —— ${REVISE_SKIPPED:-无需修改}"
- fi
- done
- # ---------------------------------------------------------------------------
- # 第 5 步:润色(可选,仅审计通过后执行)
- # ---------------------------------------------------------------------------
- stage "第 5/5 步:文笔润色"
- if [[ "$AUDIT_PASSED" == "true" ]]; then
- echo ""
- read -r -p "是否进行文笔润色?[Y/n]:" DO_POLISH || true
- if [[ ! "$DO_POLISH" =~ ^[Nn] ]]; then
- POLISH_OUTPUT=""
- POLISH_OUTPUT=$(run_with_spinner "正在润色" inkos revise "$BOOK_ID" "$DRAFT_CH" --mode polish --json) || {
- warn "润色失败(非致命错误)"
- echo "$POLISH_OUTPUT"
- }
- POLISH_APPLIED="$(json_field "$POLISH_OUTPUT" applied)"
- POLISH_WORDS="$(json_field "$POLISH_OUTPUT" wordCount)"
- if [[ "$POLISH_APPLIED" == "true" ]]; then
- ok "润色已应用 —— ${POLISH_WORDS} 字"
- else
- info "润色跳过(无需修改)"
- fi
- else
- info "跳过润色。"
- fi
- else
- warn "审计未通过,跳过润色。"
- fi
- # ---------------------------------------------------------------------------
- # 完成
- # ---------------------------------------------------------------------------
- echo ""
- echo -e "${GREEN}${BOLD}╔══════════════════════════════════════╗${NC}"
- echo -e "${GREEN}${BOLD}║ 写入下一章完成 ║${NC}"
- echo -e "${GREEN}${BOLD}╚══════════════════════════════════════╝${NC}"
- echo ""
- info "作品 :${BOOK_ID}"
- info "章节 :第 ${DRAFT_CH} 章 — ${DRAFT_TITLE}"
- info "字数 :${DRAFT_WORDS}"
- info "文件 :${DRAFT_FILE}"
- info "审计 :$( [[ "$AUDIT_PASSED" == "true" ]] && echo "${GREEN}通过${NC}" || echo "${YELLOW}未通过${NC}" )"
- info "已修订 :$( [[ "$REVISED" == "true" ]] && echo "${GREEN}是${NC}" || echo "否" )"
- echo ""
- banner "后续操作:"
- info " inkos review approve ${BOOK_ID} ${DRAFT_CH}"
- info " inkos export ${BOOK_ID}"
- info " inkos status"
- echo ""
|