webnovel-writer.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. #!/usr/bin/env node
  2. import path from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. import { readFileSync } from 'node:fs'
  5. import { CacheManager } from '../src/cache/index.js'
  6. import { checkNodeVersion } from '../src/runtime/node-version.js'
  7. import { resolveRunContext } from '../src/runtime/locate.js'
  8. const __dirname = path.dirname(fileURLToPath(import.meta.url))
  9. // 版本门槛先行(M0 起的不变量;纯比较在 node-version.js,副作用留这里)
  10. const gate = checkNodeVersion(process.version)
  11. if (!gate.ok) {
  12. console.error(gate.message)
  13. process.exit(1)
  14. }
  15. const argv = process.argv.slice(2)
  16. const command = argv[0]
  17. if (command === '--version' || command === '-v') {
  18. const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
  19. console.log(pkg.version)
  20. process.exit(0)
  21. }
  22. if (!command || command === '--help') {
  23. console.log('用法:webnovel-writer <命令> [选项]')
  24. console.log('')
  25. console.log('精准读取接口(41 个,分布于 21 个命令;逐条清单见任务 prd.md AC2):')
  26. console.log(' read-chapter <章号> [--front-matter|--tail=N|--head=N|--摘要]')
  27. console.log(' read-chapters [--range=a-b --摘要|--recent=N --tail=M]')
  28. console.log(' list-chapters --章定位=推进 [--卷=N]')
  29. console.log(' read-thread <ID> [--履历|--收尾计划|--描述]')
  30. console.log(' list-threads [--悬了太久|--type=<t> [--status=<s>]|--strength=<强>]')
  31. console.log(' read-timeline [--current-and-prev|--current-volume|--卷=N|--在场=名]')
  32. console.log(' read-character <正名> [--front-matter|--section=<标题>]')
  33. console.log(' resolve-alias <别名>')
  34. console.log(' list-characters [--status=<状态>]')
  35. console.log(' read-worldview --section=<标题>')
  36. console.log(' read-secret <ID> [--基本信息|--内容]')
  37. console.log(' list-secrets [--reader-knows=false]')
  38. console.log(' read-outline [--总纲 [--section=<标题>|--结局]|--卷=N [--section=<标题>]]')
  39. console.log(' list-volumes')
  40. console.log(' grep-story <关键词> [--regex=<pattern>]')
  41. console.log(' report-overdue-threads | report-secret-accumulation | report-thread-activity --卷=N')
  42. console.log(' report-weak-hook-streak | report-book-stats | report-style-drift')
  43. console.log('')
  44. console.log('写章流程(M2,零 AI 脚本面):')
  45. console.log(' prepare-chapter <章号> 备料:写出 工作区/本章写作材料.md')
  46. console.log(' mechanical-check <章号> [--draft=<路径>] 机检:字数/禁词/禁句式/复读/新专名/信息差候选')
  47. console.log('')
  48. console.log('宿主通道(M5,AI 产物经文件回流;JSON 一律 --file/--payload 文件路径):')
  49. console.log(' review-input <章号> [--draft=<路径>] 组装两审输入 → 工作区/审稿输入.json')
  50. console.log(' save-review <章号> --file=<两审json> 两审报告校验合并 → 工作区/审稿.md')
  51. console.log(' persist-outline --file=<json> 细纲落盘({细纲})')
  52. console.log(' persist-book --file=<json> [--dir=<目录>] 建书落盘+登记({book,总纲,卷纲})')
  53. console.log(' persist-volume-review --file=<json> 卷复盘落盘({卷号,卷摘要,下卷卷纲,伏笔条目})')
  54. console.log(' persist-repair --file=<json> 修复回写({repairs},仅限检测失败清单内)')
  55. console.log(' finalize <章号> --payload=<json> 定稿原子 commit + 缓存刷新')
  56. console.log('')
  57. console.log('安装与多本书(M5):')
  58. console.log(' init [--hosts=a,b] [--force] 装出工作目录(AGENTS.md/.webnovel/平台壳)')
  59. console.log(' update [--hosts=a,b] [--force] 升级:哈希未变更新,手改跳过并列出')
  60. console.log(' list-books 书单(登记缺失自动扫描重建)')
  61. console.log(' switch-book <书名> 换书:改「当前」标记')
  62. console.log(' session-context 会话上下文注入文本(hook 与入口同源)')
  63. console.log('')
  64. console.log('状态机 / 例外流程(M3):')
  65. console.log(' next [--json] 继续:状态机判定下一步(--json 出完整 DTO)')
  66. console.log(' health-check 体检:悬了太久/条目活跃率/连续弱钩,报告落工作区(文体项随 M5.5)')
  67. console.log(' impact <关键词> 影响分析:哪些章建立在这个事实上(已发布/未发布)')
  68. console.log(' goto-chapter <章号> [--confirm] 回到第N章(先备份再回滚,作者不碰 git)')
  69. console.log(' relink --message=<一句话说明> 补登手改:定稿/大纲 未登记改动入档(fix(手改))')
  70. process.exit(0)
  71. }
  72. // 解析选项与位置参数:--key=value → {key:value},--flag → {flag:true}
  73. function parseArgs(rest) {
  74. const options = {}
  75. const positionalArgs = []
  76. for (const arg of rest) {
  77. if (arg.startsWith('--')) {
  78. const m = arg.match(/^--([^=]+)(?:=(.*))?$/)
  79. if (m) options[m[1]] = m[2] !== undefined ? m[2] : true
  80. } else {
  81. positionalArgs.push(arg)
  82. }
  83. }
  84. return { options, positionalArgs }
  85. }
  86. let cache
  87. try {
  88. const commandPath = path.join(__dirname, '../src/commands', `${command}.js`)
  89. const commandUrl = new URL(`file:///${commandPath.replace(/\\/g, '/')}`).href
  90. const mod = await import(commandUrl)
  91. const { options, positionalArgs } = parseArgs(argv.slice(1))
  92. const packageRoot = path.join(__dirname, '..')
  93. // 工作目录定位(story-repo-spec §2.0):书仓库直启 / 工作目录当前书 / 人话提示
  94. const plan = await resolveRunContext(process.cwd(), { scope: mod.scope || 'book' })
  95. if (plan.mode === 'error' || (plan.mode === 'workdir-no-book' && !mod.allowNoBook)) {
  96. console.error(plan.message)
  97. process.exitCode = 1
  98. } else {
  99. let ctx
  100. if (plan.mode === 'workdir') {
  101. ctx = { workdir: plan.workdir, packageRoot }
  102. } else if (plan.mode === 'workdir-no-book') {
  103. // 空工作目录里允许跑的命令(next → 状态机报序1 建书引导)
  104. ctx = { workdir: plan.workdir, packageRoot, repoPath: null, cache: null }
  105. } else {
  106. cache = new CacheManager(path.join(plan.repoPath, '.cache', 'index.db'))
  107. await cache.ensureReady(plan.repoPath)
  108. ctx = { repoPath: plan.repoPath, cache, workdir: plan.workdir ?? null, packageRoot }
  109. }
  110. const result = await mod.run(positionalArgs, options, ctx)
  111. if (result.ok) {
  112. if (result.output) console.log(result.output)
  113. process.exitCode = 0
  114. } else {
  115. console.error(result.error)
  116. process.exitCode = 1
  117. }
  118. }
  119. } catch (err) {
  120. if (err.code === 'ERR_MODULE_NOT_FOUND' || err.code === 'ENOENT') {
  121. console.error(`未知命令「${command}」。运行 webnovel-writer --help 查看可用命令。`)
  122. process.exitCode = 1
  123. } else {
  124. // 永不带栈崩溃(错误规范 §1)
  125. console.error(`执行命令「${command}」时出错:${err.message}`)
  126. process.exitCode = 1
  127. }
  128. } finally {
  129. if (cache) await cache.close() // 显式关闭,避免 Windows 文件锁
  130. }