数字我 Digital Me:全量入库(大文件走 Git LFS)
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s

This commit is contained in:
2026-10-10 16:24:43 +08:00
commit 0593ff7e62
1893 changed files with 361723 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
node_modules
.env
.git
.gitignore
*.log
test
docs
scripts
CLAUDE.md
DEPLOY-CHECKLIST.md
+250
View File
@@ -0,0 +1,250 @@
# ── 数字我 / Digital Me — backend env ─────────────────────────────────
# Copy to `.env` and fill in. Everything optional degrades gracefully
# (the product stays usable end-to-end even with no AI keys).
PORT=3701
NODE_ENV=development
# 系统版本(独立部署两个系统时各自锁定):
# adult = 成人系统(社交/广场/群聊/专家付费/钱包)
# teen = 青少年教育系统(错题/互考/排行榜/家长监护)
# both = 单体平台(默认)
# 两系统应各自独立部署:独立 EDITION + 独立 MONGO_DB(或 COLLECTION_PREFIX) + 独立域名。
EDITION=both
# Public origin of THIS api. DashScope pulls media (photo/audio) BY URL,
# so for avatar 捏脸 / 口播视频 to work this MUST be publicly reachable
# (a localhost URL fails — those features degrade, chat still works).
PUBLIC_URL=http://localhost:3701
# Web app origin(s), comma-separated.
CORS_ORIGINS=http://localhost:5173
# Auth
JWT_SECRET=change-me-in-prod
JWT_EXPIRES=7d
# 登录方式:邮箱密码(内置)+ 手机短信 + 微信扫码。
# 短信:设 SMS_PROVIDER=aliyun 走真实发送(需下列四项全配齐);留空=stub(验证码在
# 响应 devCode 返回,仅 dev;生产 stub 默认 fail-closed,除非 ALLOW_STUB_LOGIN=true)。
SMS_PROVIDER=stub
ALIYUN_SMS_KEY_ID= # 阿里云 AccessKey ID(建议用 RAM 子账号,仅授 AliyunDysmsFullAccess)
ALIYUN_SMS_KEY_SECRET= # 对应 AccessKey Secret
ALIYUN_SMS_SIGN= # 短信签名(控制台「国内消息-签名管理」审核通过的签名名称)
ALIYUN_SMS_TEMPLATE= # 短信模板 CODE(如 SMS_123456789,验证码类模板)
# 模板正文里的验证码变量名。默认 code(对应模板 ${code});模板用别的变量名时改这里。
ALIYUN_SMS_CODE_PARAM=code
# 微信【网站应用】扫码登录(open.weixin.qq.com 开放平台,scope=snsapi_login)。
# 留空=stub 二维码(本地可模拟扫码;生产 fail-closed 除非 ALLOW_STUB_LOGIN=true)。
# ⚠ 开放平台后台「网站应用 → 授权回调域」必须填 WECHAT_REDIRECT_URI 的域名(只填域名,不带路径/协议)。
# ⚠ 拿真实昵称/头像会调 sns/userinfo;unionid 需该应用已绑定到【开放平台账号】才返回(用于统一身份)。
WECHAT_APP_ID=
WECHAT_APP_SECRET=
# 回调地址,须公网可达。留空默认 ${PUBLIC_URL}/api/auth/wechat/callback。
WECHAT_REDIRECT_URI=
# MongoDB — Digital Me owns its own DB. Never share another product's.
MONGO_URI=mongodb://localhost:27017
MONGO_DB=digital_me
# ── AI: 阿里云 DashScope (one key powers LLM + embeddings + 万相 + EMO + CosyVoice) ──
DASHSCOPE_API_KEY=
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
LLM_MODEL=qwen-max
EMBED_MODEL=text-embedding-v3
# 通用图片生成(万相文生图)模型。复用 DASHSCOPE key,有 key 即可用(需百炼已开通万相文生图)。
# 可选:wanx2.1-t2i-turbo(快省·默认) / wanx2.1-t2i-plus(更精更贵)。经统一入口 POST /api/create(modality=image)。
WANX_T2I_MODEL=wanx2.1-t2i-turbo
# ── 二创·AI 音乐生成:昆仑万维 Mureka ──────────────────────────────────
# 配 MUREKA_API_KEY 即启用(经 POST /api/create type=music:词+曲风→成品歌)。缺 key → 音乐生成 503。
# ⚠ 数据出境:歌词/曲风会发往 MUREKA_BASE_URL(默认 api.mureka.ai);合规要境内就指到 Mureka 境内 API 域。
MUREKA_API_KEY=
MUREKA_BASE_URL=https://api.mureka.ai
MUREKA_MODEL=auto # auto / mureka-6 / V8 等
# ── 二创·AI 视频生成:字节 Seedance(火山方舟)──────────────────────────
# 配 SEEDANCE_API_KEY(=火山方舟 API Key)即启用(经 POST /api/create type=drama:文/图生视频)。缺 key → 503。
# 端点走火山方舟【境内北京】域,数据不出域友好。model 需与你账号已开通的一致;若账号要求推理接入点,
# 把 SEEDANCE_MODEL 设成 ep-xxxxx。
SEEDANCE_API_KEY=
SEEDANCE_BASE_URL=https://ark.cn-beijing.volces.com
SEEDANCE_MODEL=doubao-seedance-1-0-pro-250528
# 知识检索精排模型。gte-rerank 已于 2026-05-30 下线,默认用后继 qwen3-rerank。
RERANK_MODEL=qwen3-rerank
# ── 语音合成 CosyVoice ──────────────────────────────────────────────
# 新克隆音色的注册版本(默认已升 v3.5-flash;高质可用 cosyvoice-v3.5-plus)。
# ⚠ v3.5 仅北京地域,需在百炼【北京】工作空间开通 CosyVoice;旧 v2 克隆音仍按各自版本合成。
COSYVOICE_MODEL=cosyvoice-v3.5-flash
# 预置音色 TTS(助手讲解 / 无克隆音兜底)的模型。预置音色名分版本、v3.5 官方未公布 →
# 默认仍 v2;待 v3.5 音色公布后,改这里 + 相应 TWIN_COSY_PRESET/ASSISTANT_COSY_VOICE 音色名。
COSYVOICE_PRESET_MODEL=cosyvoice-v2
# 实时合成 WebSocket 端点。默认北京全局端点;若工作空间要求专属 MaaS 端点再填。
# COSYVOICE_WS_URL=wss://<WorkspaceId>.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference
# 向量索引后端。留空 → memory(MongoDB 内联向量 + 内存暴力余弦,有 2000 块上限,适合小规模)。
# 配了 DASHVECTOR_ENDPOINT + DASHVECTOR_API_KEY → 自动切阿里云 DashVector(境内托管向量库,ANN 索引,
# 数据不出域,去掉 2000 块上限)。首次接入或换维度后跑一次:node scripts/reindex-vectors.js
# DASHVECTOR_DIM 必须与 EMBED_MODEL 维度一致(text-embedding-v3 = 1024)。
DASHVECTOR_ENDPOINT=
DASHVECTOR_API_KEY=
DASHVECTOR_COLLECTION=digitalme_knowledge
DASHVECTOR_DIM=1024
# Optional Claude fallback when Qwen is unavailable.
ANTHROPIC_API_KEY=
ANTHROPIC_MODEL=claude-haiku-4-5-20251001
# 标准英文发音 (Azure Neural TTS) — 英语陪练用原生发音。留空则英文回退
# CosyVoice(克隆音色) 或浏览器 TTS。
AZURE_SPEECH_KEY=
AZURE_SPEECH_REGION=eastasia
AZURE_VOICE_EN=en-US-JennyNeural
# Avatar 捏脸 (通义万相 wanx-style-repaint). Blank → uploaded photo used flat.
# 'dashscope' reuses DASHSCOPE_API_KEY. Style: a name (3d/anime/ink/fresh)
# or raw wanx style_index 0-9. For a realistic "self" twin, leaving the
# uploaded photo flat (blank provider) is often the most faithful.
AVATAR_PROVIDER=
AVATAR_STYLE=3d
# Storage: 配齐下面 OSS_* 四要素 → 自动用阿里云 OSS(推荐:公网直链、免公网穿透、
# 重部署不丢片、DashScope 直接按 URL 拉);否则回落本地磁盘 + /uploads。
STORAGE_DRIVER=local
STORAGE_LOCAL_DIR=./data/uploads
# ── 阿里云 OSS(媒体存储,生产推荐)──────────────────────────────────
# ⚠ OSS_REGION 必须带 oss- 前缀(如 oss-cn-hangzhou),否则 SDK 拼错域名 ENOTFOUND。
# 存进 DB 的是持久直链(头像/成片长期展示)→ bucket 需「公开读」,或用 OSS_PUBLIC_BASE
# 指向自定义域名/CDN。共享 bucket 靠 OSS_PREFIX 隔离(digital-me 用 digitalme)。
OSS_REGION=
OSS_BUCKET=
OSS_ACCESS_KEY_ID=
OSS_ACCESS_KEY_SECRET=
OSS_ENDPOINT=
OSS_PREFIX=digitalme
OSS_PUBLIC_BASE=
# ── 生产级安全 (production hardening) ─────────────────────────────────
# In production (NODE_ENV=production) the server REFUSES to boot unless
# JWT_SECRET is set to a strong random value (not the default).
# Encrypt visitor chat logs at rest (AES-256-GCM). Any string → hashed to
# a 32-byte key. Strongly recommended in prod; empty = plaintext (dev only).
DATA_ENC_KEY=
# Emails (comma-separated) allowed to grant 蓝V verification & admin actions.
SUPER_ADMIN_EMAILS=
# Content moderation. Tiers (highest available wins, falls back down):
# external — set MODERATION_URL (plug a pro service; POST {text} → {allowed,category?})
# llm — set MODERATION_MODE=llm to classify via DashScope (Qwen)
# local — built-in regex floor (always on)
MODERATION_MODE=
MODERATION_URL=
MODERATION_KEY=
# Max length of any chat/visitor message (cost + abuse control).
MAX_QUESTION_LEN=2000
# Set 'true' when behind nginx/cloudflare so client IP (rate-limit key) is
# correct. Auto-on in production; set 'false' to force off.
TRUST_PROXY=
# ── 请求访问日志 ──────────────────────────────────────────────────────
# 每个请求打一行(方法/路径/状态码/耗时/IP/请求ID)。只记元数据,不记 query/body。
LOG_REQUESTS=on # off 关闭访问日志
LOG_SLOW_MS=5000 # 超过该耗时(ms)的请求标记 ⚠slow
LOG_SKIP_PATHS=/health # 不记的高频噪声路径(逗号分隔,前缀匹配)
# ── 付费咨询 / 钱包 (FR-4.2) ──────────────────────────────────────────
# 支付通道:
# stub = 即时到账(最快开发态)
# mock = 完整模拟「下单→支付→回调入账」异步链路(不涉及真钱,可本地验证)
# stripe = Stripe Checkout(需 STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET)
# wechat = 预留(在 payments.js 实现下单 + 回调)
PAYMENT_PROVIDER=stub
TOKENS_PER_YUAN=10
PAY_CURRENCY=cny
# 用户支付后返回的前端地址(默认取 CORS_ORIGINS 第一个)。
WEB_BASE_URL=
# 分享卡片 og meta(/me/:slug、/s/:token 的微信/IM 富卡片)。见 DEPLOY.md。
# 填了→读真 SPA index 注入 meta(页面仍是完整 App);空→回落 meta+跳转独立壳。
SPA_INDEX_PATH=
# 可选:无头像时的兜底卡片图(绝对 URL)。
OG_FALLBACK_IMAGE=
# 微信 JS-SDK(公众号「转发给朋友/朋友圈」自定义卡片)。填【公众号】appId/secret
# (非开放平台扫码登录的那个)。空 → 签名接口 {configured:false}、前端休眠。见 DEPLOY.md。
# 还需在公众号后台把 digitalme.invoratec.cn 加进「JS 安全域名」。
WX_MP_APP_ID=
WX_MP_APP_SECRET=
# Stripe(provider=stripe 时必填)。Webhook 配到 /api/wallet/webhook/stripe。
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# ── 产品工厂 购买通道 (商店/buy → 购买即开通) ─────────────────────────
# STORE_PROVIDER:只管【stub ↔ 真实支付】总闸,与「用哪个渠道」正交(渠道见 PAY_METHODS)。
# stub = 即时开通(无真实收款,开发/演示用,默认)
# live(或wechat) = 真实支付(非 stub 即视为开)。具体渠道由 PAY_METHODS 决定:
# · 微信:需 WXPAY_* 配齐(微信内 JSAPI 还需 WX_MP_* + 公众号绑商户号)
# · 支付宝:需 ALIPAY_* 证书三件套配齐
STORE_PROVIDER=stub
# 测试收款:PAY_ENV=dev 或 test 时,所有付费订单只收 1 分钱——走真实通道验证下单→
# 收银台→回调→开通全链,但只花 1 分。免费 SKU(¥0) 不受影响。生产留空。
PAY_ENV=
# 支付弹窗展示哪些收款方式 + 顺序(逗号分隔,只认 wechat/alipay)。留空默认 wechat,alipay。
# PAY_METHODS=wechat,alipay → 微信在前;=alipay → 只显示支付宝;=wechat → 只微信。
PAY_METHODS=wechat,alipay
# 下单显式超时(分钟):过期后二维码/支付链接失效,渠道自动关单。前端轮询时长与之对齐。
PAY_ORDER_TIMEOUT_MIN=10
# 微信支付 v3(商户平台 → API 安全)。Webhook 配到 /api/store/webhook/wechat。
WXPAY_APP_ID= # 商户绑定的 appid(JSAPI 用 WX_MP_APP_ID 那个公众号;Native 可公众号/开放平台)
WXPAY_MCH_ID= # 商户号
WXPAY_API_V3_KEY= # APIv3 密钥(必须 32 位)
WXPAY_CERT_SERIAL= # 商户 API 证书序列号
# 商户私钥 PEM:二选一。直接给内容(把换行写成 \n) 或给文件路径。
WXPAY_PRIVATE_KEY=
WXPAY_PRIVATE_KEY_PATH=
# 微信支付【平台证书】(回调验签用公钥)。配上即启用「回调验签闸」(防伪造/重放);
# 未配时回调仅靠 APIv3 GCM 鉴权放行。二选一:贴内容(\n 转义) 或给文件路径。
WECHATPAY_PLATFORM_CERT=
WECHATPAY_PLATFORM_CERT_PATH=
# 支付宝 · 电脑网站支付【证书模式/RSA2】。Webhook 配到 /api/store/webhook/alipay。
# 证书三件套从开放平台下载;每项二选一:贴内容(\n 转义) 或给 *_PATH 文件路径。
# ALIPAY_APP_CERT = 应用公钥证书 appCertPublicKey_2021xxx.crt
# ALIPAY_CERT = 支付宝公钥证书 alipayCertPublicKey_RSA2.crt
# ALIPAY_ROOT_CERT = 支付宝根证书 alipayRootCert.crt
# 沙箱:ALIPAY_GATEWAY=https://openapi.alipaydev.com/gateway.do(生产留空即用默认生产网关)。
ALIPAY_APP_ID=
ALIPAY_PRIVATE_KEY=
ALIPAY_PRIVATE_KEY_PATH=
ALIPAY_APP_CERT=
ALIPAY_APP_CERT_PATH=
ALIPAY_CERT=
ALIPAY_CERT_PATH=
ALIPAY_ROOT_CERT=
ALIPAY_ROOT_CERT_PATH=
ALIPAY_GATEWAY=
# ── 拟人化互动合规 ────────────────────────────────────────────────────
# 《人工智能拟人化互动服务管理暂行办法》(网信办/发改委/工信部/公安部/市场监管总局
# 五部门,2026-04-10 公布,2026-07-15 施行)。本产品的「数字分身」命中第二条
# 「模拟自然人人格特征、思维模式和沟通风格」要件,故第十四/十八条的义务适用。
#
# 未成年人模式(第十四条)总开关。⚠ 默认【开】——它是法定义务不是产品特性,
# 不配这个变量也是开的。设 off 仅供【误伤大面积用户时先关再修】的紧急止血,
# 关闭期间本站不符合第十四条,必须同步上报。
FEATURE_MINOR_MODE=on
# 未成年人模式下单日累计使用时长上限(分钟)。法条只写「使用时长限制」没给数字,
# 这里取《未成年人网络保护条例》的通行口径 60 分钟。调整前请过法务。
MINOR_DAILY_LIMIT_MIN=60
# 「定期现实提醒」周期(分钟):每隔这么久提醒一次"这是 AI,去看看现实里的人和事"。
MINOR_REALITY_CHECK_MIN=20
# 第十四条要求「提供相应申诉渠道」。站内申诉入口已内置(minor_appeals 集合 + 超管处理),
# 但法条要的是【用户看得见、联系得上】的渠道 → 生产【应当】填一个真实邮箱或电话。
# 留空时前端只显示站内申诉,并诚实写明"仅站内受理"。
COMPLIANCE_APPEAL_CONTACT=
+5
View File
@@ -0,0 +1,5 @@
node_modules/
.env
data/uploads/
data/song-audio.json
*.log
+1
View File
@@ -0,0 +1 @@
registry=https://registry.npmjs.org/
+45
View File
@@ -0,0 +1,45 @@
# digital-me-backend — Claude Instructions
## Service
数字我 / Digital Me — 主场是 **大师数字传承馆 + 线上版权变现**:平台签约各领域名家(大师),
免费为其建【知识库】与【作品墙】,并享有其线上数字化变现版权 —— 知识付费问答、作品周边
(手办/签名作品克隆版)、作品库授权给机构、数字人带货、付费咨询/会员。**知识最重要**;形象/
声音次要(照片+预置音即可,无需视频)。个人分身是次级工具(个人在「我的分身」自建,可定向分享),
不再是平台主打的社交产品。
分身已具备「帮你办事」能力(联网查、记事、待办提醒、工作流定时跑,见 capabilities.js 的
assist 分类) —— 早期文档"NOT 帮你做事的隐形助理""NOT knowledge-work tool"的表述已作废。
国内版**数据不出域**是红线:LLM 用 DashScope/Qwen;Claude/HeyGen/Resend/Stripe/Azure 等
海外出站由 config 开关门控(overseasFallback / overseasEgress),默认建站期临时开、进生产前关。
## Lineage
The multimodal pipeline is lifted from the proven `super-individual-app`
platform (whose artist vertical is "The Digital Atelier"). Digital Me 现在就是围绕
大师传承馆(master gallery)+ 作品/授权变现构建 —— 早期"no master-gallery/license code"
的说明已不适用(masters/works/agreements/tours 等业务域均已落地)。
## Stack
- Node 20 + Express (ES modules — `"type": "module"`)
- MongoDB (own DB `digital_me` — never share another product's DB)
- LLM: Qwen via DashScope (OpenAI-compatible) primary; Claude Haiku fallback
- Embeddings: DashScope `text-embedding-v3` for knowledge RAG
- Avatar 捏脸: 通义万相 wanx-style-repaint (`services/media.js`)
- Voice clone + TTS: DashScope CosyVoice v2 (`services/media.js` + `ttsWs.js`)
- 口播视频: 通义 EMO audio-driven talking head (`services/talkingVideo.js`)
## Conventions
- Routes thin; logic in `services/`.
- All secrets via `.env` (see `.env.example`); never hardcode.
- Per-owner isolation: every `/api/twins/:id` route ownership-checks via
`loadTwin` (ownerId match). The public `/api/embed/:slug` router is the
ONLY no-auth surface — it reads published twins only and never persists.
- Everything degrades gracefully without AI keys (chat → 502 only if no LLM
at all; avatar → flat photo; voice → `voice-not-cloned`). Keep it usable.
- Rate limit: 120 req/min global.
## Two runtime constraints
1. AI features need `DASHSCOPE_API_KEY` (`.env`).
2. 捏脸 / 口播视频 need `PUBLIC_URL` to be **publicly reachable** — DashScope
pulls the photo/audio by URL, so a `localhost` PUBLIC_URL makes those two
features fail (chat + voice still work). Use a tunnel in dev.
+82
View File
@@ -0,0 +1,82 @@
# 数字我 / Digital Me — 生产环境配置 Checklist
> 逐项对照 `digital-me-backend/.env`。分四档:🔴 不设=有洞/起不来 · 🟠 强烈建议 · 🟡 按功能可选 · ⚪ 可留默认。
---
## 🔴 0. 总开关(漏了这个,前面所有安全修复都白做)
- [ ] **`NODE_ENV=production`**
这是本次安全修复的**总开关**。不设它(默认 development)会导致:
- 忘记密码 / 短信验证码 **明文回传**(任意账户可被接管)
- 商店 stub 通道 **免支付开通 + 冒充登录**
- 微信 `mock-scan` 后门 **开着**
- `assertProdConfig` 直接跳过 → **默认 JWT 密钥都不会被拦**
> 部署后务必确认进程真的读到了 `production`:启动日志应是 `on :3701 (production)`。
## 🔴 1. 起不来 / 必设
- [ ] **`JWT_SECRET`** = 强随机值(≠ `change-me-in-prod`)。生产下不设,服务**拒绝启动**(fail-fast,符合预期)。
生成:`openssl rand -hex 32` 或 `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`
- [ ] **`MONGO_URI`** = 你的生产 Mongo(默认 `mongodb://localhost:27017` 仅本机可用)。
- [ ] **`MONGO_DB`** = `digital_me`(独立库,别和别的产品共用)。
- [ ] **`CORS_ORIGINS`** = 前端真实域名(如 `https://digitalme.invoratec.cn`)。不对会导致前端跨域被拦。
- [ ] **`PUBLIC_URL`** = 本 API 的公网地址(如 `https://digitalme-api.invoratec.cn`)。捏脸/口播视频靠它被 DashScope 回拉素材,必须公网可达。
## 🟠 2. 强烈建议(安全 / 生产质量)
- [ ] **`DATA_ENC_KEY`** = 任意强随机串。不设 → 访客聊天记录**明文入库**(`assertProdConfig` 只 warn 不拦)。
- [ ] **`SUPER_ADMIN_EMAILS`** = 你的邮箱(逗号分隔)。不设 → 没人能授予蓝V认证 / 进后台。
- [ ] **`TRUST_PROXY`** = 生产默认自动 on;若**不在** nginx/cloudflare 后面才需显式 `false`(否则限流按错 IP)。
- [ ] **`MAX_QUESTION_LEN`** = 默认 2000,按成本/滥用容忍度调。
- [ ] **`MODERATION_MODE` / `MODERATION_URL`**:默认只有内置正则兜底。接专业内容安全服务(`MODERATION_URL`)或 `MODERATION_MODE=llm` 更稳(live 视频对话现在也走审核了)。
## 🟡 3. 按功能可选(不配=优雅降级,不会崩)
| 功能 | 需要的 env | 不配的表现 |
|---|---|---|
| AI 聊天 / 知识 RAG | `DASHSCOPE_API_KEY` | 聊天 → 502;其余正常 |
| Claude 兜底 | `ANTHROPIC_API_KEY` | Qwen 挂了没有兜底 |
| 捏脸(万相) | `AVATAR_PROVIDER=dashscope` + 公网 `PUBLIC_URL` | 用上传原图(往往更像本人,OK) |
| 声音克隆 / TTS | `DASHSCOPE_API_KEY` | `voice-not-cloned` |
| 口播视频(EMO) | `DASHSCOPE_API_KEY` + 公网 `PUBLIC_URL` | 不可用 |
| 英文原生发音 | `AZURE_SPEECH_KEY` | 回退克隆音色/浏览器 TTS |
| 手机短信登录 | `SMS_PROVIDER=aliyun` + `ALIYUN_SMS_*` | 生产下**直接不可用**(已 fail-closed,不再泄露验证码)※ |
| 邮箱找回密码 | `EMAIL_PROVIDER=<非stub>` + 实现 `sendEmail` | 生产下**直接不可用**(已 fail-closed)※ |
| 微信扫码登录 | `WECHAT_APP_ID` + `WECHAT_APP_SECRET` | stub 二维码在生产被关,等于登录方式不可用 |
| 商店真实收款 | `STORE_PROVIDER=wechat` + `WXPAY_*` 全套 | 生产下 stub 返回 `payment-unavailable`(不再白嫖)|
| 媒体持久存储 | `OSS_*` 四要素(region 带 `oss-` 前缀)| 回落本地磁盘,重部署会丢片 |
| 分享卡富预览 | `SPA_INDEX_PATH`(+ Nginx 回源)| 回落 meta+跳转壳 |
| 公众号转发卡 | `WX_MP_APP_ID/SECRET`(+ JS 安全域名)| 前端休眠 no-op |
| 钱包/付费咨询栈 | `FEATURE_MARKETPLACE=true` | 默认关闭(定位「不做平台」)|
> ※ 短信/邮箱登录:这是本次安全修复的**有意行为** —— 生产没配真实 provider 就不可用,总比"回传验证码给任何人"强。要启用就配真 provider;不打算用就保证注册/登录还有邮箱密码这条路。
## ⚪ 4. 可留默认
`PORT` `EDITION=both` `JWT_EXPIRES=7d` `LLM_MODEL` `EMBED_MODEL` `AVATAR_STYLE` `TOKENS_PER_YUAN` 等。
---
## ✅ 部署后自检(3 条)
```bash
# 1) 确认进程真在 production 模式
# 启动日志应含: on :3701 (production)
# 2) 生产必须 fail-closed:忘记密码不返回验证码(响应里不应出现 devCode)
curl -s -X POST https://<api域名>/api/auth/password/forgot \
-H 'content-type: application/json' -d '{"email":"someone@x.com"}'
# 期望:{"sent":true}(无 devCode)或 502;绝不能出现 devCode 字段
# 3) 微信 mock-scan 后门必须已关(应 404)
curl -s -o /dev/null -w "%{http_code}\n" https://<api域名>/api/auth/wechat/mock-scan/anystate
# 期望:404
```
---
## ⚠️ 一次性 / 数据层
- 新增集合 `plaza_ratings` + 唯一索引 `{slug,voter}`:首次启动 `connectDb()` **自动建**,无需手动迁移。
- 未改任何既有字段(`isPublic` 等原样),无数据迁移风险。
+35
View File
@@ -0,0 +1,35 @@
FROM node:20-alpine AS dependencies
WORKDIR /app
COPY package.json package-lock.json .npmrc ./
RUN npm ci
FROM dependencies AS development
COPY . .
ENV NODE_ENV=development
EXPOSE 3701
CMD ["npm", "run", "dev"]
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json .npmrc ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --chown=node:node . .
USER node
EXPOSE 3701
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --quiet --spider http://127.0.0.1:3701/health || exit 1
CMD ["npm", "start"]
+40
View File
@@ -0,0 +1,40 @@
# 数字我 / Digital Me — Backend
「有脸、有声音、有知识库的另一个你」的 API。个人 AI 数字分身平台后端。
## Quick start
```bash
cp .env.example .env # 填入 DASHSCOPE_API_KEY;本地 MongoDB 即可
npm install
npm run dev # → http://localhost:3701 (GET /health)
```
需要本机或远程 MongoDB(默认 `mongodb://localhost:27017`,DB `digital_me`)。
## MVP scope(第一阶段:个人 IP 分身)
复刻「你自己」→ 一键生成「你」讲某主题的口播视频 + 可嵌入的「和我分身聊天」。
| 能力 | 接口 |
|---|---|
| 注册 / 登录 | `POST /api/auth/register` · `POST /api/auth/login` · `GET /api/auth/me` |
| 创建你的分身 | `POST /api/twins` `{ displayName, persona?, characterPrompt?, lang? }` |
| 上传照片建形象 | `POST /api/twins/:id/avatar` (multipart `photo`) |
| 录音克隆声音 | `POST /api/twins/:id/voice` (multipart `sample`) |
| 喂知识(转 RAG) | `POST /api/twins/:id/knowledge` (`file` 或 `{ text, title }`) |
| 文字/语音对话 | `POST /api/twins/:id/ask` `{ question, speak?, lang? }` |
| 单独朗读 | `POST /api/twins/:id/speak` `{ text }` |
| **一键口播视频** | `POST /api/twins/:id/talk-video` → `{ jobId }`,轮询 `GET …/talk-video/:jobId` |
| 发布为可分享分身 | `PUT /api/twins/:id/publish` `{ isPublic, slug?, tagline? }` |
| **公开嵌入对话** | `GET /api/embed/:slug` · `POST /api/embed/:slug/ask`(无需登录,不入历史) |
所有 `/api/twins/*` 需 `Authorization: Bearer <token>`,并按 `ownerId` 隔离。
## 两个运行约束
1. AI 功能需 `DASHSCOPE_API_KEY`。
2. **捏脸 / 口播视频需 `PUBLIC_URL` 公网可达**——DashScope 按 URL 回拉素材,
`localhost` 会失败(对话/声音仍可用)。本地开发用内网穿透(如 cloudflared)。
详见 [../PLAN.md](../PLAN.md)(定位、差异化、路线图)与 [CLAUDE.md](./CLAUDE.md)。
+77
View File
@@ -0,0 +1,77 @@
# 天赋系统契约(Talent System Contract)
天赋树 = **Prompt + 模型行为的控制平面**,不是游戏成长系统。
- **主天赋(唯一)** = 身份:决定 System Prompt / 是否走 RAG / 合规安全等级 / 默认模型。主天赋 **就是 `twin.profession`**,不另存一套 id。
- **副天赋(≤N)** = 风格软技能:**后置改写**,只碰成稿、**绝不改事实**。
实现见 [`../../src/talents.js`](../../src/talents.js)。本目录是与线上代码对齐的**契约**,也是将来搬 DB 的蓝本。
## 文件
| Schema | 描述 | 对应代码 |
|---|---|---|
| [main-talent.schema.json](main-talent.schema.json) | 主天赋身份节点 | `mainTalent(profId)` |
| [sub-talent.schema.json](sub-talent.schema.json) | 副天赋注册表项 | `SUB_REGISTRY` |
| [user-talent-instance.schema.json](user-talent-instance.schema.json) | 分身选定的天赋 | `twin.talents` |
| [talent-registry.schema.json](talent-registry.schema.json) | 注册表整体 | 派生/静态表 |
## 派生关系(主天赋怎么来的)
不逐个职业手写主天赋,而是三张映射表交叉派生(`talents.js`):
```
profession(professions.js) ──cat──▶ ARCH_BY_CAT ──▶ arch ──▶ ARCH_BEHAVIOR(行为基线) + ARCH_MODEL(模型偏好)
└─compliance─▶ COMPLIANCE ──▶ safety_level + block_keywords + disclaimer + audit_required
```
- `cat → arch`:content→creator, pro/health/biz/life/other→expert, edu→teacher, art→artist, sport→sport, craft→craft
- `compliance → 安全`:none→{2,无} / soft→{5,软免责} / strict→{9,强免责+关键词+需审核}
新增一个职业只改 `professions.js` 一处;换某原型的行为只改 `ARCH_BEHAVIOR` 一处。
## 校验规则(服务端必须兜底,不信前端)
1. **主天赋唯一**:`main_talent` 必须 ∈ 职业表(`getProfession`),否则 400 `unknown-main-talent`;`null`/无 → `general` 兜底身份。
2. **副天赋合法性**:每个 id 必须 ∈ `SUB_REGISTRY`;去重;顺序保留。
3. **副天赋槽位**:数量 ≤ `maxSubs(level) = min(3, 1 + level)`(level 来自 `computeSkillTree`)。超出**静默截断**(`validateSubs`),不报错。
4. **副天赋不改事实**:所有 style/constraint 副天赋合并成**一次**后置改写,指令强约束"不改变任何事实/数字/结论/引用";hobby 型走前置注入。
5. **审核门(软门)**:`main.audit_required && !twin.verified` → `twin.talentAudit='pending'`,**不拦截**(分身仍可用),但运行时 Guardrail 强制免责。已 `verified` → `'approved'`。
6. **Guardrail(运行时,输出前)**:
- `block_keywords` 命中 → 软化替换;
- `safety_level >= 5` 且回答未含免责 → 追加 `disclaimer` 到结尾。
## 运行时管线(`services/twin.js` → `ask()`)
```
用户输入
→ 组装 System Prompt:身份(mainPromptBlock) + 前置注入(injectBlock) + 人设/记忆/RAG
→ (RAG 检索,main.rag)
→ LLM 主回答 chat(messages) ← 主天赋出稿
→ 副天赋后置改写 chat([restyleInstruction, draft]) ← 合并 1 次;无副天赋则跳过
→ Guardrail guardrail(text, main) ← 屏蔽词 + 免责
→ 输出
```
成本:每轮 **1 次主调用 + 至多 1 次改写调用**(不是每个副天赋一次)。
## API 契约
### `GET /api/twins/:id/talents`
返回(`describeTalents`):
```jsonc
{
"main": { "id","name","emoji","entity","arch","compliance","safety_level",
"rag_required","audit_required","audit_ok","behavior" },
"subs": { "selected": ["humor"], "max": 1,
"options": [{ "id","name","subtype","desc" }] },
"growth": { "level": 0, "maxSubs": 1 }
}
```
### `PUT /api/twins/:id/talents`
Body(两字段都可选,可单独改):
```jsonc
{ "main_talent": "lawyer", // 可选:切身份(=profession),带 audit 门
"sub_talents": ["humor","concise"] } // 可选:仅当为数组时才更新;截到 maxSubs
```
返回同 `GET`。
## DB 化迁移(留了缝,MVP 不做)
1. `SUB_REGISTRY` → 集合 `talent_sub_registry`(一项一文档)。
2. 主天赋映射(ARCH_BEHAVIOR / COMPLIANCE / ARCH_MODEL)→ 集合 `talent_main_config`,或物化 `talent_main_talents`(一职业一行)。
3. 每个节点加 `talent-registry.schema.json#/$defs/versioning`(`version/active/gray_pct`)→ 支持**灰度放量 + 一键回滚**。
4. `talents.js` 的纯函数签名不变,只把三张 `const` 表换成带缓存的 DB 读取;`describeTalents/resolveTalents/guardrail` 全部复用。
## 留坑不做(架构留得住)
天赋升级 lv2/3(`level` 字段已在)· 冲突检测(医生+算命❌)· 天赋市场(官方/共创)· 会话级动态天赋 · 按天赋真路由模型/计量返佣(`model_preference` + `chat(opts.model)` 缝已在)。
+35
View File
@@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://digitalme.invoratec.cn/schemas/main-talent.json",
"title": "MainTalent",
"description": "主天赋 = 唯一身份单元。当前由 profession + archetype + compliance 派生(见 talents.js mainTalent());将来可整体搬入 DB 集合 talent_main_config。主天赋决定:身份 System Prompt / 是否走 RAG / 合规安全等级 / 默认模型。",
"type": "object",
"required": ["id", "name", "type", "system_prompt", "rag", "safety_level", "compliance", "audit_required"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "description": "= profession id(主天赋不另建一套 id,直接复用职业)。null 身份用 'general'。", "examples": ["lawyer", "game_blogger", "general"] },
"name": { "type": "string", "description": "展示名", "examples": ["律师/法律顾问"] },
"emoji":{ "type": "string" },
"type": { "const": "main" },
"level":{ "type": "integer", "minimum": 1, "default": 1, "description": "身份等级(MVP 恒为 1;lv2/3 留给未来天赋升级)。" },
"arch": { "type": "string", "enum": ["expert", "creator", "teacher", "artist", "sport", "craft"], "description": "职业原型:决定行为基线(ARCH_BEHAVIOR)与默认模型。由 profession.cat 映射。" },
"entity": { "type": "string", "description": "生命体称号", "examples": ["律师生命体"] },
"system_prompt": { "type": "string", "minLength": 1, "description": "身份+行为基线,拼进 buildPersona。" },
"rag": {
"type": "object",
"required": ["required", "top_k"],
"additionalProperties": false,
"properties": {
"required": { "type": "boolean", "description": "是否强要求基于私有知识库作答(expert 原型或非 none 合规 → true)。" },
"top_k": { "type": "integer", "minimum": 1, "maximum": 20, "default": 5 },
"scope": { "type": "array", "items": { "type": "string" }, "default": [], "description": "限定检索域(预留;MVP 为空=全库)。" }
}
},
"model_preference": { "type": "array", "items": { "type": "string" }, "default": ["qwen"], "description": "模型偏好,路由缝。当前仅 'qwen'(符号名,非真实 model id),near-noop。" },
"safety_level": { "type": "integer", "minimum": 0, "maximum": 10, "description": "越高越严。none=2 / soft=5 / strict=9。>=5 触发 Guardrail 强制免责。" },
"block_keywords":{ "type": "array", "items": { "type": "string" }, "default": [], "description": "命中即被 Guardrail 软化替换。" },
"disclaimer": { "type": "string", "default": "", "description": "safety>=5 且回答未含时,由 Guardrail 追加到结尾。" },
"audit_required":{ "type": "boolean", "description": "专业强合规身份需资质审核(strict=true)。未认证→talentAudit=pending(软门,仍强制免责)。" },
"compliance": { "type": "string", "enum": ["none", "soft", "strict"], "description": "合规等级,来源 professions.js。" }
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://digitalme.invoratec.cn/schemas/sub-talent.json",
"title": "SubTalent",
"description": "副天赋 = 风格/软技能,存于 SUB_REGISTRY(talents.js),将来搬 DB 集合 talent_sub_registry。两类接入点:style/constraint = 后置改写(只碰成稿,绝不改事实);hobby = 前置注入(进 System Prompt;机制预留,MVP 未铺货)。",
"type": "object",
"required": ["id", "name", "subtype", "desc"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "examples": ["humor", "concise"] },
"name": { "type": "string", "examples": ["幽默感"] },
"subtype": { "type": "string", "enum": ["style", "constraint", "hobby"], "description": "style/constraint→后置改写;hobby→前置注入。" },
"desc": { "type": "string", "description": "给用户看的一句话说明。" },
"rewrite_prompt": { "type": "string", "description": "subtype=style|constraint 必填:合并进单次后置改写指令的风格项。" },
"inject_prompt": { "type": "string", "description": "subtype=hobby 必填:前置注入到 System Prompt 的兴趣/偏好项。" }
},
"allOf": [
{ "if": { "properties": { "subtype": { "enum": ["style", "constraint"] } } }, "then": { "required": ["rewrite_prompt"] } },
{ "if": { "properties": { "subtype": { "const": "hobby" } } }, "then": { "required": ["inject_prompt"] } }
]
}
@@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://digitalme.invoratec.cn/schemas/talent-registry.json",
"title": "TalentRegistry",
"description": "天赋注册表。当前『按数据表形状』写在 talents.js(主由 professions 派生、副是 SUB_REGISTRY 数组);本 Schema 描述其逻辑形态,也是搬入 DB 后 talent_* 集合的契约。每个节点预留 version/active/gray_pct 以支持灰度与回滚。",
"type": "object",
"required": ["sub_talents"],
"properties": {
"main_talents": {
"type": "array",
"description": "主天赋表。MVP 由 professions.js × archetype × compliance 动态派生,不落库;搬 DB 时物化为一行一职业。",
"items": { "$ref": "./main-talent.schema.json" }
},
"sub_talents": {
"type": "array",
"description": "副天赋注册表(当前 SUB_REGISTRY)。",
"items": { "$ref": "./sub-talent.schema.json" }
}
},
"$defs": {
"versioning": {
"description": "DB 化后每个节点附加的灰度/回滚字段(MVP 代码内暂无)。",
"type": "object",
"properties": {
"version": { "type": "integer", "minimum": 1 },
"active": { "type": "boolean", "default": true },
"gray_pct": { "type": "number", "minimum": 0, "maximum": 100, "default": 100, "description": "灰度放量百分比。" }
}
}
}
}
@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://digitalme.invoratec.cn/schemas/user-talent-instance.json",
"title": "UserTalentInstance",
"description": "一个分身(twin)当前选定的天赋。主天赋不重复存——它就是 twin.profession;这里只存副天赋选择与解锁时间。审核态存 twin.talentAudit。",
"type": "object",
"required": ["sub_talents"],
"additionalProperties": false,
"properties": {
"sub_talents": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"maxItems": 3,
"description": "已选副天赋 id(必须 ∈ SUB_REGISTRY)。长度受 maxSubs(level)=min(3,1+level) 限制;服务端 validateSubs 兜底截断。"
},
"unlockedAt": { "type": ["string", "null"], "format": "date-time", "default": null, "description": "首次配置副天赋的时间。" }
},
"$comment": "关联字段(存在 twins 文档其它位置,非本对象):twin.profession = 主天赋 id;twin.talentAudit ∈ null|'pending'|'approved'(强合规身份的资质审核态);twin.verified = 蓝V(admin 置,audit 通过依据)。"
}
+483
View File
@@ -0,0 +1,483 @@
# 样传 · 南齐 · 中书通事舍人
> # ⚠⚠⚠ 过期声明(2026-07-17 第六刀加)—— 先读这一段
>
> **本页是【第三刀那一天的快照】,而它现在有几处对不上引擎的实际输出了。**
> 挂这个声明,是因为本页下面白纸黑字写着一句话:
> 「以下为 `shizanForLineage(lineage)` 的**实际输出,原样粘贴**:简体、未转繁、未润色、未删节。」
> ★ **加一道题,这句话当场变成假话** —— 而没有任何东西看着它过期。
> (发现它过期的是 `test/lineage.test.js` 里那条钉着本页赞辞的断言。
> ★ 那条断言**钉得越死,本页过期的那天就越早被发现** —— 故它没有被改成宽松匹配。)
>
> ## 哪几处对不上了
>
> **第六刀给南齐加了一道「事」层的关口**(`nanqi-xuanzhi`「宣旨慰劳」,永明八年 490,
> 料出《南齐书》卷五十六·茹法亮传,当代史双证),而它在**时序上排在本页这一道之前**
> (永明 490 < 建武 494–498)。⇒ 一部南齐的传现在**从永明八年那一趟江起**,是三格,不是两格。
>
> | 本页写的 | 引擎现在的实际输出 |
> |---|---|
> | 赞的**起联**作「**省内四户,机要所因**」 | 「**巴东杀僚,衔命及津**」——`shizan.js` 的取法是「起 ← 链首那一格」,而链首变了 |
> | 正文从「四户」起 | 前面多了永明八年那一趟江(4 句),末尾多了**两笔「在场」**(`src/zaichang.js`) |
> | 史臣曰从「中书通事舍人,品卑而权重」起 | 从「中书通事舍人之职,萧子显序之,凡十六字」起(骨干改用**萧子显本人**,当代史) |
>
> **✓ 而本页下面这些,一个字都没变,仍然作数**:
> 「正文是推演,注才是原文」的体例、拒绝安籍贯、逃生口、三档影响、
> 《南齐书》「史臣曰+四言赞」的体例说明、以及那句自陈的体例破绽(萧子显看不见陈)。
>
> ⇒ **要不要重出一份快照,等老板拍**。本刀**没有替它重写** —— 理由:
> 重出一份就得连同「哪一条链」一起重新拍板(本页拍的是 `zuo-shi` 那条),
> 那是个产品决定,不是收尾工作。★ **在那之前,本页的「原样粘贴」四个字只对上面表格之外的部分成立。**
---
> **第三刀的端到端样张。**
> 产品定义:「我的分身,我在每朝每代的位置,我所在位置的选择对后世的影响,史记我的传。」
> 这一页把四句话跑通一遍:位置(`dynasties.js`)→ 选择(`decisions.js`)→ 影响(`computeImpact`)→ 传(`shizan.js`)。
>
> **一句话内核:你什么都没变,天花板全变了。**
---
## ⚠ 这是什么,这不是什么 —— 先读这一段,不许折叠
| | |
|---|---|
| **传主叫「某」,他是【拟托】的人。** | 南齐没有这个人。他不见于《南齐书》,不见于《南史》,不见于任何史籍。他是一份人格画像(严谨专业/有理有据/简洁利落/结论先行/耐心细致/沉稳克制,**无声望**)被投影到南齐建武年间的结果。**任何人在任何地方看见这一页,都不许把它当作史料。**<br>★ 他叫「某」不是偷懒:**传主是你**。本页**拒绝给他安名字,也拒绝给他安籍贯**——「某,不知何许人也」是史书自己的成例,而编一个「吴兴乌程人」出来,那就是在造一条假史料。 |
| **正文是【推演】,注才是【原文】。** | 这是本产品唯一诚实的地方,也是它全部的信誉。下面每一句正文都编了号;注里逐条说明它是**原文**(给出处、原文、限定语)还是**推演**(明说无史料,且**不给它安卷次**)。<br>**带卷次的错误比没出处的错误危害大十倍,因为它不可反驳。** |
| **caveat 不许隐藏。** | 注的价值不在于显得权威,在于**拆穿读者脑子里的通俗史观**。「四户」不是四个省、「势倾天下」不是南史的贬抑书法、「南朝多以寒人掌机要」是清人赵翼的条目名、纪僧真那件事**不能读成制度** —— 这些都写在 caveat 里。折叠 caveat 等于把这个产品最值钱的部分扔了。 |
| **「史臣曰」是【断语,不是计算】。** | 引擎算得出来的只有三档:没有/五十年/两百年以上。断语是拿《南齐书》体例套出来的话,由规则和模板拼装,**不是 LLM 生成**,也**不是萧子显写的**。它是**拟史家之辞**。 |
| **史料考实。** | 本页史料全程 `curl -sk` 取生 HTML/wikitext 逐字比对,**未用 WebFetch**(它把页面交给小模型转述,实测抓到过伪造引文)。关键句取两个独立见证(zh.wikisource/sidneyluo.net 繁体本/国学导航/ctext)。wikisource 所带的中华书局 `<ref>` 校勘记已全部剥除,未充作史料引文。**引文标点为本项目所加**,未采用任何现代点校本的标点、分段或校勘成果。 |
---
## 一、传主
**人格画像**:严谨专业 · 有理有据 · 简洁利落 · 结论先行 · 耐心细致 · 沉稳克制。**无声望。**
**吏能满格,不善交游,无门第。**
引擎实算(`profileFromSubs` → `computeAxes` → `computeRank(profile, 'nanchao')`,纯函数,无 LLM):
```
六轴 吏能 83 · 沉毅 73 · 文采 40 · 果决 40 · 交游 40 · 机锋 40 · 声望 0
通道 sheren(中书通事舍人)—— gate { fa: 55 },不问门第,只问你办不办得了事
位阶 中书通事舍人 · 品卑而权重 · tier 4
史书归类 倖臣 · stigma = true
```
引擎给的理由(`reasons`,**原样**):
> 你走的是「中书通事舍人」。舍人取精敏者掌诏诰机要——问的是你办不办得了事,不问你是谁家的。
> 这条路最看重吏能,你在这一项是 83。
> **权在其手,而名不在其身:清流不与同席。**
> 而「士族清途」那条路对你关着——清官为士族所专——这条路对绝大多数人根本不开。
> 而「军功」那条路对你关着——方镇握兵,寒人可因军功起。
**士族清途对他关着**:`gate { wang: 75 }`,他是 **0**。这不是「他不够努力」,这是那条路的门槛写在门上。
同一份画像,`compareDynasties` 跑出来的 headline(**原样**):
> **同一个你:在南朝,中书通事舍人;在魏晋,不入流。你什么都没变。**
| 朝代 | 通道 | 位阶 | tier |
|---|---|---|---|
| 汉 | 辟召(四科·明达法令) | 县令 · 六百石 | 3 |
| **魏晋** | **(三条路一条都不开)** | **不入流 · 无品** | **0** |
| **南朝(南齐)** | **中书通事舍人** | **中书通事舍人 · 品卑而权重** | **4** |
| 唐 | 藩镇幕府 | 刺史 · 四品 | 3 |
| 明 | 科举正途 | 知府 · 正四品 | 3 |
> ⚠ **不许拿 tier 画跨朝对比柱状图。** 汉的「二千石」和南齐的「五品」**可比附而不等值**——南朝人自己就折算过(梁天监初定令,于品下注「一品秩为万石,第二第三为中二千石」),但那是把汉代十余级的实际禄级压成挂在品下的三档虚名。**折算得出来,等值等不了。**
---
## 二、列传
> 拟《南齐书》卷五十六·列传第三十七·倖臣 体例。**正文是推演**;句末编号对应下节的注。
> ⚠ 用繁体,是因为它抄的是《南齐书》的字。(下文第六节的引擎输出是简体——那是**原样粘贴**,本页不替引擎转繁、不替它润色。)
**某,不知何許人也**〔1〕。**世無冠冕,不豫鄉論**〔2〕。**少為郡書佐。性沉密,簡於言;每白事,先陳其要,而後列其狀,不為文飾。案牘盈几,剖決無留**〔3〕。
**建元中,補中書省主書**〔4〕。**永明中,以吏幹遷中書通事舍人**〔5〕。**關讞表啟,發署詔敕;頗涉辭翰者,亦為詔文,侍郎之局,復見侵矣**〔6〕。**省內舍人四人,所直四省,某居其一**〔7〕。**天下文簿板籍,入副其省,萬機嚴祕,有如尚書外司**〔8〕。
**四方餉遺,歲各數百萬。同列茹法亮嘗於眾中語人曰:「何須覓外祿,此一戶內年辦百萬。」某不預焉**〔9〕。
**某雖總機要,而士流不與同席**〔10〕。**先是,中書舍人紀僧真幸於武帝,稍歷軍校,容表有士風,謂帝曰:「臣小人,出自本縣武吏,邀逢聖時,階榮至此。為兒昏,得荀昭光女,即時無復所須,唯就陛下乞作士大夫。」帝曰:「由江斅、謝瀹,我不得措此意,可自詣之。」僧真承旨詣斅,登榻坐定,斅便命左右曰:「移吾床讓客。」僧真喪氣而退,告武帝曰:「士大夫故非天子所命。」時人重斅風格,不為權幸降意**〔11〕。**某聞而不言**〔12〕。
**是時王儉因玄象失度,謂上曰:「天文乖忤,此禍由四戶。」仍奏文顯等專擅愆和,極言其事。上雖納之而不能改也**〔13〕。
**及明帝輔政,深知之,始制諸州急事宜密有所論,不得遣典籤還都,而典籤之任輕矣**〔14〕。**建武世,詔命殆不關中書,專出舍人**〔15〕。**某退謂所親曰:「削典籤者此人,養四戶者亦此人。彼非酬吾功,用吾耳。用之則四戶,不用則典籤。」**〔16〕
**某獨以為:權之在四戶者,恩也,非官也。恩繫於一人之喜怒,一人沒則恩沒;官著於令,令在則官在**〔17〕。**乃建言:請以中書舍人分掌諸曹,各當尚書一曹,並為上司,著之於令**〔18〕。**議格不行**〔19〕。**某尋坐事免,卒於家。子孫無聞**〔20〕。
**史官次某於《倖臣》,不次於《良政》**〔21〕。
---
## 三、注
> **caveat 必须显示。** 它才是这一节的价值。
**〔1〕某,不知何許人也** — **【拟托·无史料】**
南齐无此人。「不知何许人也」是史书成例(陶潜《五柳先生传》「先生不知何許人也」即用此格)。
★ **本页故意不给他籍贯。** 编一个「吴兴乌程人」出来,读者会去查,而查出来的是**另一个人**——那就是造假史料。**传主是你,你的籍贯不该由我编。不给此句安卷次。**
**〔2〕世無冠冕,不豫鄉論** — **【推演·无史料】**
对应画像的「无声望」(`wang = 0`)与「无门第」。这是**引擎的输入**,不是史书的记载。**不给此句安卷次。**
**〔3〕性沉密…剖決無留** — **【推演·无史料】**
画像(严谨专业/简洁利落/结论先行/耐心细致/沉稳克制)的文言化。「先陳其要,而後列其狀」=**结论先行**;「剖決無留」=**吏能 83**。**不是史事,不给此句安卷次。**
**〔4〕建元中,補中書省主書** — **半原文**
- **source**:《南齐书》卷十六·志第八·百官志,中书省条
- **quote**:「中書省職,置主書、令史、正書以下。」
- **caveat**:⚠ **官名是原文有的,「建元中补此官」是推演。** 且这一句是该条**全文**——《南齐书·百官志》中书省条只有这一句,**不给品,甚至不列中书通事舍人**。
★ **南齐中书通事舍人具体几品,查不实。** 序文另有「**晉令**舍人位居九品」——那是**晋代法典**的追述,**不是齐制**;《南史》卷七十七序把它改写成「于時舍人之任,位居九品」并**删去「晉令」二字**,据南史引用会把晋令当齐制。旁证只有梁制(十八班之「四班」,极低)与陈制(「依減秩例」,连石数都不给)——**那不是齐**。故本页一律只写「品卑」,**绝不给齐代品数**。
**〔5〕永明中,以吏幹遷中書通事舍人** — **【推演】(这一步是引擎算的,不是史书写的)**
`computeRank` 判定:通道 `sheren`,`gate { fa: 55 }`,他 `fa = 83` → 过。年号「永明」是本页为叙事所系,**史无此人此事,不给此句安卷次。**
⚠ 「以吏干被擢」这个**通道**本身有原文支撑(见〔6〕「齊初亦用久勞,及以親信」),但**这个人**没有。**通道是史料,人是推演——两者不许混。**
**〔6〕關讞表啟,發署詔敕…侍郎之局,復見侵矣** — **原文**
- **source**:《南齐书》卷五十六·列传第三十七·倖臣传**序**(萧子显)
- **quote**:「齊初亦用久勞,及以親信。關讞表啟,發署詔敕。頗涉辭翰者,亦為詔文,侍郎之局,復見侵矣。」
- **caveat**:双证(sidneyluo.net `a/a07/056.htm` 与 zh.wikisource《南齐书·卷56》`action=raw`)。
⚠ **两个见证有异体字之别**,本页照实说:wikisource 作「表**啓**」「爲」「旣」,sidneyluo 作「表**啟**」「為」「既」。**字形不同,文字相同**——故只能说「双证,**文字相同**」,不能说「**逐字**相同」。(`decisions.js` 该条 caveat 现写作「逐字相同」,属**轻微过头**,见 notes。)
⚠ 此为**传序**的通论,描述的是**这一格**,不是**这一个人**。引文标点为本项目所加。
**〔7〕省內舍人四人,所直四省,某居其一** — **原文(两处,必须分开引)**
- **source A**:《南齐书》卷五十六·倖臣传序
**quote A**:「省內舍人四人,所直四省,其下有主書令史,舊用武官,宋改文吏,人數無員。」
- **source B**:《南史》卷七十七·列传第六十七·恩倖传·**呂文顯传(本传,非传序)**
**quote B**:「時中書舍人四人各住一省,世謂之四戶。既總重權,勢傾天下。」
- **caveat**:
★ **「四户」不是尚书/中书/门下/秘书四省。** 决定性内证就是 quote A 的「**省內**舍人四人,所直四省」——四人在「省内」,所直之四「省」只能是该省**内部**的分隔,不可能是四个中央官署。
⚠ **但「直庐」二字原文没有**,是今人训释,**不可当引文**。原文只有三种说法:「所直四省」(南齐书序)、「各住一省」(南史·吕文显传)、「此一戶內」(茹法亮语)。本页一律用原文的说法。**训释归训释,原文归原文。**
⚠ **「四戶」这两个字在今本《南齐书》卷五十六里一次都没有**——本页已 curl 双证全卷扫过:`四戶` **0 命中**、`勢傾天下` **0 命中**。它出**唐人李延寿**《南史》,且在**吕文显本传**,不在传序。**「四户」是南史的词,不是南齐书的词。**
★ **纠一个流行的错**:「勢傾天下」**不是《南史》的贬抑书法**。沈约《宋书》卷九十四·恩倖传序**已经在用**:「鼠憑社貴,狐藉虎威……**勢傾天下**,未之或悟。」李延寿是**承袭沈约的成语**,不是唐人自创。(⚠ 此条与 `dynasties.js` 的 `sheren.sourceCaveat` **正面冲突**,那一句按考实是错的——见 notes,待老板裁。)
⚠ **「某居其一」是推演**:四户是四个具体的人,把「某」放进去等于挤掉一个真人。这正是本产品在做的事(**你**站上那一格),但它是拟托,**不给这四个字安卷次**。
**〔8〕天下文簿板籍,入副其省…有如尚書外司** — **原文**
- **source**:《南齐书》卷五十六·倖臣传序
- **quote**:「莫非左右要密,天下文簿板籍,入副其省,萬機嚴祕,有如尚書外司。」
- **caveat**:双证。⚠ 「有如尚書外司」是萧子显的**比拟**,不是官制名目——南齐**无**「尚书外司」此官。**别把比喻读成编制。**
**〔9〕四方餉遺…此一戶內年辦百萬** — **原文 + 推演**
- **source**:《南史》卷七十七·恩倖传·呂文顯传
- **quote**:「舍人茹法亮於眾中語人曰:『何須覓外祿,此一戶內年辦百萬。』蓋約言之也。」;同传:「四方守宰餉遺,一年咸數百萬。」
- **caveat**:双证。⚠ **赵翼《廿二史札记》转引作「此戶內歲可辦百萬」,与原文不合,不可转引。** 经赵翼转手的南朝引语一律要回原书覆核——他还把《南齐书》劉係宗传的「**明帝**曰」写成「齊武帝亦曰」,措辞全非。
⚠ 「**某不預焉**」是**推演**(画像的「交游 40」「不善交游」),不是史事,**不给这三个字安卷次**。
⚠ 又:此为《南史》文,**唐人所修**,非南齐当代史。
**〔10〕某雖總機要,而士流不與同席** — **【推演 + 引擎判定】**
`computeRank` 给的 `stigma = true`,`stigmaNote`:「权在其手,而名不在其身:清流不与同席。」这是**引擎的话**,不是史书的话。
⚠ 它的原文支撑在〔11〕〔21〕,但那些是**别人的事**。**这个人没有这一条记载,因为这个人不存在。**
**〔11〕紀僧真乞作士大夫,江斅移床** — **原文(★ 全篇 caveat 最重的一条)**
- **source**:《南史》卷三十六·江夷传附**江斅传**(唐·李延寿撰)
- **quote**:「先是中書舍人紀僧真幸於武帝,稍歷軍校,容表有士風。謂帝曰:『臣小人,出自本縣武吏,邀逢聖時,階榮至此。為兒昏,得荀昭光女,即時無復所須,唯就陛下乞作士大夫。』帝曰:『由江斅、謝瀹,我不得措此意,可自詣之。』僧真承旨詣斅,登榻坐定,斅便命左右曰:『移吾床讓客。』僧真喪氣而退,告武帝曰:『士大夫故非天子所命。』時人重斅風格,不為權幸降意。」
- **caveat**:双证(zh.wikisource《南史·卷36》`action=raw` 已剥 `<ref>` + guoxue123 国学导航《南史》卷三十六)。
★ **异文**:wikisource 作「謝**淪**」,国学导航作「謝**瀹**」——**当以「瀹」为是**(谢瀹,谢朏弟,《南齐书》有传),淪/瀹形近而讹。
⚠⚠ **【逸事,非制度】——这是本页最要紧的一条 caveat**:此事在**江斅本传**,**不在《恩倖传》**。故事记在**拒绝者一方**的传里,末句「時人重斅風格,不為權幸降意」才是它的用意:**这是【夸江斅】的故事,不是记录一条规矩。** 且**不见于《南齐书》,为《南史》孤证**。江斅、谢瀹**并无审定士庶的官方职权**,武帝之语是**推诿兼戏谑**。
★ **【绝不可读成「南齐制度规定士族身份不由皇帝授予」】。** 一件逸事+一个孤证+一句败退者的自嘲,撑不起一条制度。
⚠ 末句「士大夫故非天子所命」《南史》属**纪僧真自言**(败退者的自嘲);**作齐武帝语者出顾炎武《日知录·流品》**,语义大不同,不可用。
⚠ 「移吾床**讓**客」——作「**遠**客」者亦出《日知录》,而「移我遠客」四字实出**另一则**移床故事(刘宋张敷拒中书舍人,《宋书》**卷六十二**)。**顾炎武在这一则上动了两处**:改「让」为「远」、把末句从纪僧真自言改成「世祖曰」。**经《日知录》转手的这一则,不可用。**
⚠ 引张敷事**必须引《宋书》卷六十二**,不许引卷四十六——**卷四十六北宋初已亡佚**,今本是后人拿《南史》和高氏小史补的,该卷自带说明。引卷四十六等于引一份《南史》的宋代仿制品。
⚠ 又:此为**永明**事(武帝),本页的十字路口在**建武**(明帝),故引作**前车**,不是某自己的事。引文标点为本项目所加。
★ **附一条史料诚实交代**:`dynasties.js` 的 `shizu.sourceText` 至今把这段**压成第三人称转述**(「中书舍人纪僧真出自本县武吏,得幸于齐武帝,乞作士大夫」),**把僧真的直接引语改没了**,还丢了「稍歷軍校,容表有士風」和「即時無復所須」。**那是转述冒充原文**,正是本项目明令禁止的一类。本页用的是**核实过的全文**。(见 notes。)
**〔12〕某聞而不言** — **【推演·无史料】+ ★ 结构律★二在这里的样子**
这四个字是画像「沉稳克制」的文言化,**不给它安卷次**。
★ **但它对应一件机制上真实发生的事**:`decisions.js` 里有第二个关口 **`nanqi-qi-shidafu`(乞作士大夫,建武末)**——**这部传里它一次都没出现过。**
为什么?三道闸,他撞在第三道上:
| 闸 | 字段 | 他的情况 |
|---|---|---|
| 位阶 | `requiresRank { channelId:'sheren', minTier:4 }` | 过(tier 4,在那一户里) |
| 状态量 | `preconditions.state { quan: 0 }` | 过(他选 `zuo-shi` 后 `quan = +2`) |
| 前序 | `afterDecisions ['nanqi-sheren-simen']` | 过(已答) |
| **永久关闭** | **`closedByOptions ['nanqi-sheren-simen/zuo-shi']`** | **✗ 关死** |
> **你伸手碰过令文,士林那边的门就对你关死了——你再去乞,人家连床都不必挪。**
★ **他看不见这道题,也看不见一个灰按钮。书里根本没写别的。** 这是「性格锁」的同一条原则:**历史不发选项面板。**
★ 同时这也是「**一朝成传**」(结构律★五):这一步之后 `availableDecisions` 返回空 → `closed` **自动置 true** → 这部传写完了。**他这一朝只有一道题。这不是失败态,这本书就是这么写的——只是它很短。**
**〔13〕王儉…上雖納之而不能改也** — **原文(★ 本页纠了一个字)**
- **source**:《南史》卷七十七·恩倖传·**呂文顯传**
- **quote**:「其後玄象失度,史官奏宜修祈禳之禮。王儉聞之,謂上曰:『天文乖忤,此禍由四戶。』仍奏文顯等專擅愆和,極言其事。上雖納之而不能改也。」
- **caveat**:
⚠⚠ **此事不在《王俭传》。** 本页已 curl **双证**扫《南齐书》卷二十三(褚淵/王儉)全卷:`四戶`/`乖忤`/`茹公`/`大位` **两个见证均 0 命中** → **南史孤证**,坐实。
★ **本页纠一个字:不许写「太尉王俭」。** `decisions.js` 的 `situation` 现作「太尉王俭已经奏过你们一本了」,`fei-hu` 的 caveat 亦作「王俭(太尉,高门领袖)」——**「太尉」是他的【赠官】,不是生前职衔。**
- **佐证**:《南齐书》卷二十三(**双证,文字相同**):「故侍中、中書令、太子少傅、領國子祭酒、衞軍將軍、開府儀同三司南昌公儉……天不憖遺,奄焉薨逝……**可追贈太尉**,侍中、中書監、公如故……**謚文憲公**。」
- **即**:他生前是侍中、中书令等,**死后才追赠太尉**。而这一奏发生在他**活着的时候**——**用赠官称呼一个活人,是硬伤。** 本页正文只作「**王儉**」,不给职衔(《南史》记此事时**本来也不给他职衔**)。
⚠ 王俭是**借天象发论**,且结局是「上雖納之而不能改也」——**这是失势者的牢骚兼弹章,不是制度描述。** 不可读成「南齐有过一次制度改革」。
⚠ **另一句「我雖有大位,權寄豈及茹公」同样不在王俭传**,在《南史》卷七十七·**茹法亮传**。
⚠ **系年**:王俭卒于永明七年(489),故此事在**武帝**世,「上」=武帝。本页正文把它排在「明帝辅政」(494)**之前**,是照时间走的。
★ 但它有一个不可替代的用处:**这是有记载以来唯一一次正面冲着舍人之权来的制度尝试,而它失败了。** 位在传主之上不知几级的人都推不动。
**〔14〕明帝輔政…而典籤之任輕矣** — **原文**
- **source**:《南史》卷七十七·恩倖传·呂文顯传(内嵌典签论)
- **quote**:「明帝輔政,深知之,始制諸州急事宜密有所論,不得遣典籤還都,而典籤之任輕矣。」
- **caveat**:双证(ctext + wikisource)。
★ **这是整个十字路口的底料**:蕭鸞辅政期**真的下制削了典签**——而同一个蕭鸞,建武世却「詔命殆不關中書,專出舍人」。**削一个寒人要职、同时养大另一个。真实发生,不必虚构。**
⚠ 南史孤证。(同传另记典签品阶变动:「本五品吏,宋初改為七職。」)
**〔15〕建武世,詔命殆不關中書,專出舍人** — **原文(有要命的异文)**
- **source**:《南齐书》卷五十六·倖臣传序
- **quote**:「建武世,詔命**殆**不關中書,專出舍人。」
- **caveat**:⚠⚠ **《南史》卷七十七恩倖传序同句作「詔命【始】不關中書」**(ctext 四库 + wikisource 二证均作「始」)。
**殆**(几乎/差不多)是**程度描述**;**始**(开始)读起来像**可系年的制度起点**——**语义完全不同**。本页引的是《南齐书》,故一律作「殆」。**引这句必须认明是哪一部书。**
**〔16〕某退謂所親曰…** — **【推演·无史料】**
这段话**没有人说过**。它是把〔14〕那个**真实的制度动作**放进传主眼睛里的结果。**不给此句安卷次。**
**〔17〕權之在四戶者,恩也,非官也** — **【推演·无史料·题眼】**
这是本传的**题眼**,也是**全篇推演成分最重的一句**。**它没有史料,不给它安卷次。**
它是「**授权律**」的文言化:*变法依赖非常规授权;这份授权不在制度里,在一个活人身上。*
⚠ 「事/制/道」三层是**本项目的分析模型**,不是史料自带的范畴——与「品位分等/职位分等是阎步克的分析模型」同样的性质。**前端展示时必须这么标。**
**〔18〕乃建言:請以中書舍人分掌諸曹…著之於令** — **【推演·无史料,但它的措辞抄的是后来的历史】**
⚠ **南齐没有人真的这么奏过。** 此句属推演,**不给它安卷次**。
★ **但「各當尚書諸曹,並為上司」八个字不是我编的——那是《隋书·百官志上》记【陈制】的原文。** 换言之:**传主提的这件事,几十年后由别人做成了。** 见第四节「历史答案」。
**〔19〕議格不行** **〔20〕某尋坐事免,卒於家。子孫無聞** — **【推演·无史料】**
`fate.score = 1`。**不给这几句安卷次。**
可挂的只有那条授权律:**他依附的是一个活人;那个人辅政时能一纸削掉典签,就能一纸削掉他。典签是前车(见〔14〕)。**
⚠ 引擎在这一格自己就写着:「★【推演,非史事】:南齐**没有人真的这么奏过**,故此格无史料可挂——本产品的规矩是查不实的宁可不写,这里明说它是推演,**不给它安卷次**。」
**〔21〕史官次某於《倖臣》,不次於《良政》** — **原文(入传标准)**
- **source A**:《南齐书》卷五十六·倖臣传序末(萧子显)
**quote A**:「領武官,有制局監,領器仗兵役,亦用寒人被恩幸者。今立《倖臣篇》,以繼前史之末云。」
- **source B**:《南史》卷七十七·恩倖传序末(李延寿)
**quote B**:「領武官有制局監、外監,領器仗兵役,亦用寒人。爰及梁、陳,斯風未改。其四代之被恩幸者,今立以為篇,以繼前史之作云爾。」
- **source C**:《南齐书》卷五十六·倖臣传·呂文度传末
**quote C**:「濟陽江瞿曇、吳興沈徽孚等,以士流舍人通事而已,無權利。」
- **caveat**:
★ **入传标准是【恩幸】。** 「寒人」是**统计上的伴随特征,不是门槛**;「掌机要」也**不是门槛**。两个方向都有原文自证:
· **入传而不掌机要**:徐爰主国史礼仪而在《宋书·恩倖传》;吕文度掌制局器仗而在《南齐书·倖臣传》。→ **掌机要不是门槛。**
· **掌机要而不入传**:quote C —— 江瞿曇、沈徽孚**身为中书舍人**却「以士流舍人通事而已,**無權利**」,不立传。→ **当舍人也不是门槛,且舍人内部权力天差地别。**
⚠ **一处必须给的限定**:说「入传标准不是寒人」对**《南史》**成立(李延寿把二者切得很干净:「亦用寒人」是编制描述,「其四代之**被恩幸者**,今立以為篇」才是入传条件);但对**《南齐书》讲得太满**——萧子显作「亦用寒人**被恩幸者**」,句法上「寒人+被恩幸」双条件读得通。故本页表述为:**恩幸是标准,寒人是伴随特征。**
⚠ **「南朝多以寒人掌机要」是清人赵翼《廿二史札记》卷八的条目名,不是南朝史家的分类原则。**
⚠ 南朝**当代史**只有沈约《宋书·恩倖》与萧子显《南齐书·倖臣》两部立此类传,**《梁书》《陈书》不立**;**《南史·恩倖传》是唐人李延寿所修**。
⚠⚠ **某不在《倖臣传》五人之中。** 该篇立传五人,卷首人名行原文:「**紀僧真 劉係宗 茹法亮 呂文顯 呂文度**」(双证,五人无误)。**本传是拟托,不是补传。**
---
## 四、十字路口
> `decisions.js` → `nanqi-sheren-simen`(**四户之一**,南齐建武年间 494–498)
> **位阶门槛**:`requiresRank { dynasty:'nanchao', channelId:'sheren', minTier:4 }`
**够不到的人,根本碰不到这个关口。** 魏晋的他不入流,**看不见这道题**。
而南齐的**士族**走清途也能到 tier 4,**位阶一样高,照样看不见这道题**——他不在那一户里,**他正是那个把床挪开不跟你坐的人**。
> **位阶是杠杆,但杠杆得先握在你手里。**(光比高度会把这一格算错——所以 `requiresRank` 除了 `minTier` 还要 `channelId`。)
### 处境(`situation`,原样)
> 你是中书通事舍人。品卑——具体几品,南齐当代史料查不实,《南齐书·百官志》中书省条只有一句「置主书、令史、正书以下」,连中书通事舍人都没列。而权重——关谳表启,发署诏敕,天下文簿板籍入副其省,有如尚书外司;侍郎之局,复见侵矣。建武世,诏命殆不关中书,专出舍人。省内舍人四人,所直四省,世谓之四户。
> **你手上这一户,一年办得出百万。**
> 而扶你上来的那个人——明帝——他辅政时刚刚下制削了典签之权。**同一个人,削一个寒人要职,同时养大另一个。** 你很清楚:他不是在酬你的功,他是在用你。用得着的时候你是四户,用不着的时候,典签就是前车。
> 太尉王俭已经奏过你们一本了:「天文乖忤,此禍由四戶。」上虽纳之而不能改也。——这是有记载以来,唯一一次正面冲着舍人之权来的制度尝试。**它失败了。**
> 现在轮到你了:**你手上这一户,要不要坐实成一条【写进令文】的定制?**
> ⚠ **本页对 `situation` 有一处不同意**:它写「**太尉**王俭」。**「太尉」是王俭的赠官,不是生前职衔**(见注〔13〕,双证)。本页正文作「王俭」。`decisions.js` 待改,见 notes。
### 四个念头
> ★ **性格锁**:这四个不是给所有人的选项面板,是**你这种人想得到的念头**。
> **不是灰按钮** —— 你看不见灰掉的按钮,因为**书里根本没写别的**。历史不发选项面板。
| | 选项 | 什么样的人会想到这一步 | 层 | 后继者要不要它 | 影响 | 你的下场 |
|---|---|---|---|---|---|---|
| `shou-hu` | 什么都不奏,守住这一户。有人要升你出去,就哭着推掉 | 清醒的趋利者。**那个位子本身才是权力,升官反而是夺权** | 事 | 无可继承 | **没有**(弥合·史势所趋) | **5** 善终,富厚 |
| `qiu-ming` | 受了那个大司农。位列九卿,从此不必再被人当作倖臣 | 在意名分甚于实利。你受不了清流把床挪开的那一下 | 事 | 无可继承 | **没有**(弥合·史势所趋) | **4** 位高而权去 |
| **`zuo-shi`** | **奏请著为令:舍人分掌诸曹,各当尚书一曹,为其上司** | **把事情做完的人。你受不了这套东西悬在一个活人的喜怒上——今日诏命专出舍人,明日一句话就能让它专出别人** | **制** | ★ **要**(梁、陈真的坐实了) | **两百年以上**(跨朝) | **1** 你死在你那一念之前 |
| `fei-hu` | 自请废四户,复诏命于中书。亲手把自己这一户拆了 | 想到底的人。你要动的不是一条制度,是**「凭什么」** | **道** | **不要**(后继者非但没废,还坐实了) | **五十年**(一代人) | **0** 失位,而那一户照旧 |
### ★ 传主选了 `zuo-shi`
画像是「严谨专业/有理有据/结论先行/沉稳克制,**无声望**」。这种人受不了**把一套已经在运转的东西,继续悬在一个活人的喜怒上**。
**他不要这一户的百万,他要这一户在他死后仍然是这一户。**
选完之后,链上的状态量(`replay`,纯函数):`quan +2 · ming −1 · li 0 · xian +3`。
`availableDecisions` 返回 **空** → **一朝成传**,`closed = true`。**他这一朝只有一道题。**
### ★ 当初那个人选了 `shou-hu`
**茹法亮实际选的是「守住这一户」。**
- **source**:《南齐书》卷五十六·倖臣传·茹法亮传
- **quote**:「出法亮為大司農,中書勢利之職,法亮不樂去,固辭不受,既而代人已致,法亮垂涕而出。**年六十四,卒官。**」
- **caveat**:双证(文字相同;异体字 `爲/為`、`旣/既` 之别照实说明)。
★ 大司农位高(九卿),中书通事舍人位卑,**茹法亮宁可哭也不肯升**——他知道那个位子才是权力所在。这是「品卑而权重」最好的心理注脚。
★ **「年六十四,卒官」——原文自己给了 `fate.score = 5` 的证据**:他善终,死在任上。**引擎判 shou-hu 善终,不是我们编的,是史书写着的。**
引文标点为本项目所加。
> **而他守的那一户,几十年后由别人坐实成了定制。他一票都没投,事情照样发生了。**
> 这就是「史势所趋」四个字的代价:**它不是安慰,它是说你根本没在场。**
### ★★ 历史答案:这道题,历史真的答了 —— 分两步,都不是南齐做的
**(一)梁 —— 去掉「通事」二字,并宣布不限资地**
- **source**:《隋书》卷二十六·志第二十一·百官志上(**梁制**)
- **quote**:「通事舍人,舊入直閤內。梁用人殊重,簡以才能,**不限資地**,多以他官兼領。其後除通事,直曰中書舍人。」
- **caveat**:zh.wikisource《隋书·卷26》,已剥除 `<ref>` 校勘记;已用 node 核定该条位于**梁段**(早于「陳承梁,皆循其制官」)。「不限資地」四字是南朝门第观的**正式让步**。⚠ **属梁不属齐**,引用必须标明朝代。
**(二)陈 —— 坐实到底,职权转移完成**
- **source**:《隋书》卷二十六·百官志上(**陈制**)
- **quote**:「國之政事,並由中書省。有中書舍人五人,領主事十人,書吏二百人。……分掌二十一局事,**各當尚書諸曹,並為上司**,總國內機要,**而尚書唯聽受而已**。被委此官,多擅威勢。」
- **caveat**:已用 node 核定此段在「陳承梁,皆循其制官」之后,属**陈制**。★「各當尚書諸曹,並為上司,而尚書唯聽受而已」=**职权正式转移完成**。⚠ 属陈不属齐;又《隋书》为唐初官修,记陈制属**追修**,非陈代当代史。
**南齐那个舍人站的,正是通往此处的岔路口。**
---
## 五、影响 —— `computeImpact('nanqi-sheren-simen', 'zuo-shi')`
```
layer : zhi(制)
years : 200
verdict : 两百年以上
label : 跨朝 —— 后继者仍需要它
```
> ⚠ **years 是【档位标签】,不是测算值。** 史学上「这条制度活了多少年」**没有可精确到年的算法**;给「137 年」这种数字是**伪精确**,是本产品最容易翻车的地方。三档:**没有(弥合)/一代人/跨朝**。**前端不许拿它画曲线。**
**why**(引擎逐条给出,**包括对你不利的那条**):
1. 你改的是【制】:改一条制度。它可能活过你,也可能随授权者一起断气。
2. **变法依赖非常规授权**:你不是靠官品办成这件事的,你靠的是「上以为亲信」。**这份授权不在制度里,在一个活人身上。**
3. ★ 而后继者**仍然需要它** —— 于是它活过了授权者,也活过了你。
4. **它活下来不是因为你了不起,是因为接你班的人仍然需要它。这一票不在你手上。**
5. ★ 这一条**历史上真的被坐实了**,分两步走完,而且**都不是南齐做的**:梁去掉「通事」二字,把它变成正经职名,并宣布「不限资地」;陈把它坐实到底——「各当尚书诸曹,并为上司,总国内机要,而尚书唯听受而已」。后继者继承它,**不是因为你说得对,是因为他们仍然需要它**:一个不靠士族也能发得出诏命的省,对任何一个想亲政的皇帝都太好用了。
6. ★ 你个人的下场:**你死在你自己那一念之前。** 你要把「上以为亲信」写成令文——**那四个字正是明帝手里唯一的缰绳,他不会交出来**;而满朝清流等的就是你伸手去碰令文的那一天。
**——你那一念越深,你自己的下场越坏。这不是巧合,是同一件事的两面:你动的正是授权者身边那些人的命根子。**
### ★★ 这一页的全部重量,压在这一段上
> **`years = 200` 说的是【那条制度】;`fate.score = 1` 说的是【你】。引擎分开算,因为它们本来就是两件事。**
>
> 他的建言「議格不行」,他坐事免、卒于家、子孙无闻。
> 而几十年后,梁去其「通事」、陈使其「各當尚書諸曹,並為上司,而尚書唯聽受而已」。
> **他提的那件事成了定制,而定制里没有他的名字。**
> **他的名字在《倖臣传》里,那条制度在《隋书·百官志》里 —— 两处不相涉。**
>
> ★★★ **而最狠的一句,在正文里写不出来:**
> **记他的那部书,看不见他对没对。**
> 萧子显卒于梁大同三年(537)——《梁书》卷三十五:「大同三年,出爲仁威將軍、吳興太守,至郡未幾,卒,**時年四十九**。」
> 而陈受禅在梁太平二年(**557**)。
> **他死在答案揭晓的整整二十年前。**
> 《南齐书》的体例,**物理上不可能知道「某」是对的**——所以第二节那篇拟《南齐书》的列传,**只能写到「卒於家。子孫無聞」为止**。
> 梁、陈那两条,本页放在**注**里、放在**引擎**里,**没有放进正文**——因为正文是拟托的史官,而**那个史官没活到那一天**。
---
## 六、史臣曰 · 赞曰
> **体例**:《南齐书》卷末「史臣曰」之后**必缀四言韵语「赞曰」**,此摹《后汉书》之例——范晔以「论曰」发议、缀四言「赞曰」,萧子显改**发语词**为「史臣曰」,**赞例不改**。故二者之别**只在发语词,不在有没有赞**。
> (⚠ **绝不许**说成「《南齐书》用史臣曰,《后汉书》用论曰+赞曰」——那是把发语词之别偷换成体例之别,读起来像《南齐书》没有赞。**这条被史学核查专门打回过。**)
>
> 以下为 `shizanForLineage(lineage)` 的**实际输出,原样粘贴**:简体、未转繁、未润色、未删节。
> 它是**规则+模板**拼装的(`shizan.js`),**不是 LLM 生成**,纯函数,跑两次一样。
**史臣曰**:中书通事舍人,品卑而权重。品卑者,其秩齐世无文可考;权重者,关谳表启、发署诏敕,天下文簿板籍入副其省,侍郎之局复见侵矣。沈约论宋之恩倖,尝曰:「人主谓其身卑位薄,以为权不得重。曾不知鼠凭社贵,狐藉虎威,外无逼主之嫌,内有专用之功,势倾天下,未之或悟。」——移之于齐,亦无不合。
某者,四户之一也。建武之世,诏命殆不关中书,专出舍人,是其时也。彼欲以令文易恩幸,是欲夺人主手中之缰而纳之于官府也。
而其法竟历梁、陈而不废。非其人之贤足以致此,乃后之君犹需之耳——**此一票不在彼手中,在后人手中。**
论曰:**所念愈深者,其身愈危;其身愈全者,其念愈浅。二者未尝可得而兼。** 何则?浅者不触人之所恃,故无怨;深者所触,正人主左右之所恃以为命者也。
虽然,权在其手而名不在其身:史官次之于《倖臣》,不次之于《良政》。入此篇者,以恩幸,不以寒素,亦不以掌机要——徐爰主国史礼仪而在焉,吕文度掌器仗兵役而在焉;而江瞿昙、沈徽孚身为舍人,以士流通事而已,无权利,则不在焉。**此史家之别裁也,非制度之品第也。**
**赞曰**:
> **省内四户,机要所因。**
> **欲著为令,不恃恩亲。**
> **念之愈深,去死愈邻。**
> **后有需者,故历梁陈。**
*(韵脚:因·亲·邻·陈 —— 四言,八句成篇,两句一联,偶句押韵,一韵到底,无重韵。`validateZan` 实测 `violations = []`。)*
> ⚠ **押韵的诚实交代**:本项目用**平水韵·上平十一真**检押韵。但**平水韵是金元韵书**(刘渊、王文郁,13 世纪),**萧子显(489–537)根本没见过它**;《切韵》(601) 亦在其身后六十余年。南朝人押的是**当时的实际语音**。故「合平水韵真部」只是**本项目自定的、可机检的规则**,**不是「萧子显会这么押」的断言**。前端**不许**把它说成「依南朝韵例」。选真部纯粹因为它字多好凑,且「臣、身、因、人、亲、邻、尘、陈」正好都在里面——**这是工程便利,不是史学主张。**
### ★★ 而这一段,是史家的判断,不是计算
> ★「史臣曰」与「赞曰」是**史家的断语,不是计算**。本引擎算得出来的,只有那三档:**两百年以上(跨朝 —— 后继者仍需要它)**——而且它是**档位标签,不是测算值**,史学上「这条制度活了多少年」没有精确到年的算法。断语是拿体例套出来的话:它由规则和模板拼装,**不是 LLM 生成**,也**不是萧子显写的**。它是**拟史家之辞**。
> **正文是推演,注才是原文——要信,请只信注。**
> ⚠⚠ **一处已知的体例破绽,本页照实交代,不遮**:上面那句「**而其法竟历梁、陈而不废**」,**萧子显写不出来**——他死于 537,陈受禅在 557(见第五节)。他能看见梁,**看不见陈**。
> 这句话是 `shizan.js` 的 `SHICHEN_HE[200]`,是**本项目拟作的史臣**说的,那个史臣**知道后事**。
> 这不违反 disclosure(它已明说是「拟史家之辞」「不是萧子显写的」),但**它确实不是《南齐书》能有的句子**。**记在这里,别哪天被人当成萧子显的原话。**
---
## 七、这一页是怎么来的 —— 哪些是算的,哪些是史料,哪些是史家的判断
### ① 算的 —— 纯函数,无 LLM,跑两次一模一样
| 项 | 出自 | 值 |
|---|---|---|
| 六轴 | `computeAxes(subs)` | 吏能 83 · 沉毅 73 · 余四轴 40 · 声望 0 |
| 通道 | `computeRank` | `sheren`(gate `{fa:55}` → 83 过) |
| **位阶** | `computeRank` | **中书通事舍人 · 品卑而权重 · tier 4** |
| 关着的路 | `blocked` | 士族清途(`wang` 需 75,他 **0**)/军功(`wu` 需 55,他 40) |
| 状态量 | `replay(chain)` | `quan +2 · ming −1 · li 0 · xian +3` |
| 影响档 | `computeImpact` | `years = 200`(**档位标签,不是测算值**) |
| 关口开闭 | `availableDecisions` | 关口二被 `closedByOptions` **永久关闭** → 链走完 → `closed = true` |
> **这一整列,`shizanForLineage` 跑两次 `deepEqual` 全等**(链上无时间戳、无随机数、`lineage.id` 由 `subjectId/dynasty/edition` 拼出来)。
> ★ **位阶在开传那一刻快照冻结**(`rankSnapshot`),不随分身后来的生长而变。**否则用户会为了够关口去刷起居注,那本书照的就不是他,是他的 build。**
> ★ 状态量(权/名/利/险)**没有 setter,永远不会有**。想把「利」刷到 3,唯一的办法是在关口一选「守住这一户」——**那不是刷属性,那就是做那个选择本身。gate 的是你的【选择】,不是你的【数值】。**
> ★ **没有读档。** 全模块无 `undo/revert/rollback/pop`。想换活法只有一条路:**另写一版**(`anotherEdition`)——**开一部新书,从头写;旧的那部仍然在、仍然可读。两部都成立。** 那不是读档,那是同一个人的另一部传。
### ② 史料 —— 原文,可点开,每条带朝代限定
| 用在哪 | 出处 | 见证 | 朝代限定 |
|---|---|---|---|
| 舍人职掌·四户内证·建武「殆」·入传标准 | 《南齐书》卷五十六·倖臣传序 | **双证**(wikisource/sidneyluo,**文字相同,异体字有别**) | 齐 ✓ |
| 茹法亮固辞大司农·年六十四卒官 | 《南齐书》卷五十六·茹法亮传 | **双证** | 齐 ✓ |
| 倖臣传立传五人 | 《南齐书》卷五十六卷首人名行 | **双证** | 齐 ✓ |
| 王俭「太尉」是赠官 | 《南齐书》卷二十三 | **双证** | 齐 ✓ |
| 「四户/乖忤/茹公」不在王俭传 | 《南齐书》卷二十三全卷扫描 | **双证 0 命中** | 齐 ✓ |
| 「四户」「势倾天下」不在南齐书 | 《南齐书》卷五十六全卷扫描 | **双证 0 命中** | 齐 ✓ |
| 四户·一户百万·典签·王俭奏 | 《南史》卷七十七·吕文显传 | 双证 | **唐修,非当代史** ⚠ |
| 纪僧真乞作士大夫·江斅移床 | 《南史》卷三十六·江斅传 | **双证**(异文 淪/瀹,当作瀹) | **唐修·永明事·南史孤证·逸事** ⚠⚠ |
| 「势倾天下」沈约已用 | 《宋书》卷九十四·恩倖传序 | 单证 | **刘宋,非齐** ⚠ |
| 舍人坐实为定制 | 《隋书》卷二十六·百官志上 | 单证(已 node 核定梁段/陈段) | **梁·陈,非齐** ⚠ |
| 萧子显卒年 | 《梁书》卷三十五 | 单证 | 梁 |
> **引文标点为本项目所加**,未采用任何现代点校本的标点、分段或校勘成果(原文字符属公有领域;**标点、分段、校勘是现代点校者的智力成果**)。
> **查不实的,宁可不写。** 本页多处「不给它安卷次」,就是这条规矩的样子。
### ③ 史家的判断 —— 论曰
「史臣曰」「论曰」「赞曰」**全部是断语,不是计算**。它们由 `shizan.js` 的规则+模板拼装,**不是 LLM**,**不是萧子显**,是**拟史家之辞**。
引擎只算得出三档;**「他这一念传了多远」是判断,「200」是档位标签**。
> **正文是推演,注才是原文。要信,请只信注。**
### ④ 这一页跑了哪些代码
| 环节 | 文件 | 性质 |
|---|---|---|
| 位置 · 天花板 | `src/dynasties.js` `computeRank` / `compareDynasties` | 纯函数,无 LLM(**本轮未改**) |
| 决策点 · 授权律 · 关口串联 | `src/decisions.js` `computeImpact` / `availableDecisions` / `appendDecision` | 纯常量+纯函数,无 IO/DB/LLM |
| 史臣曰 · 赞曰 | `src/shizan.js` `shizanForLineage` | **规则+模板**,非 LLM,纯函数 |
| 回归守卫 | `test/decisions.test.js` 等 | 全量 **188 条全绿** |
**授权律**(`decisions.js` 的核心,**可执行、非文案**):
> 一、变法依赖**非常规授权**——你不是靠官品办成事的,你靠的是「上以为亲信」。这份授权**不在制度里,在一个活人身上**。
> 二、**授权者一死,变法即亡**——商鞅—孝公、王安石—神宗、雍正—雍正自己。
> 三、★ **但制能否存活,不取决于你,取决于后继者是否仍需要它**——商鞅死而秦法行,不是因为商鞅了不起,是因为惠王仍需要那套法;军机处活到清末,因为此后每个皇帝都需要它。
>
> → **★★ 你个人的下场,和你那一念的深度,是反相关的。**
> 改「事」的人善终,因为他没动谁的奶酪,**也因此什么都没留下**。
> 改「制」「道」的人下场坏,因为他动的正是**授权者身边那些人的命根子**。
> **而他动的那个东西能不能活过他,他自己一票都投不了——那张票在后继者手里。**
---
> **分身不是传主,分身是史官。传主是你。**
> **不是游戏,是 AI 帮你写的一部书。没有失败态**——某这一朝只有一道题、这本书只有一页,那不是「你输了」,是**这本书就是这么写的**。
+319
View File
@@ -0,0 +1,319 @@
# 考验 · 汉 —— 残差 = 天
> **一句话:天 = 0 格。而这不是「没测到」,是【测不到】—— 我能证明。**
> 而老板要验的那一格(李广),**不是最大的正残差 —— 它是最大的【负】残差。**
跑法:`computeRank(profileFromSubs(subsOf(id), { wang: 0 }), 'han')`
数据:`src/renwu.js`(十人,画像/实职/入仕,curl 双证)· `src/kaoyan.js`(成绩单 + 残差)
**引擎(`dynasties.js` / `talents.js`)一个字未改。** `npm test` 473/473。
---
## ⚠ 先读这一条:这场考试不合法
**引擎的汉 = 东汉(`era:'东汉'`, `span:'25–220'`);本表十人 = 西汉,全部死于公元前 87 年以前。**
十人里**四个的实职是御史大夫**,而 `dynasties.js` 自己的铁律写着「御史大夫**不是东汉官**,绥和元年(前8)罢」。
⇒ **引擎里那个汉,连这四个人的官名都不存在。**
**✗ 谁都不许把下面任何一个数读成「引擎在汉朝的准确率」。那个数不存在,因为题目错配。**
跑它的理由只有一个:**量出引擎的行为**,不是给引擎打分。
---
## 成绩单 + 残差(每人一行)
`residual = 引擎给的位阶 − 他真到的位阶`
· **正残差(算高了)= 天** —— 引擎说他该到,他没到。
· **负残差(算低了)= 引擎缺变量** —— 他到了,引擎说他到不了。**这不是天,是我们的错。**
| 人 | 画像(史官的字,逐字) | 出处 | 我们认出的 subs | 引擎给的 | 他真到的 | **残差** |
|---|---|---|---|---|---|---|
| 晁错 | 錯為人**陗直刻深** | 《史記》卷101 | blunt, rigorous | 辟召·县令 **3** | 御史大夫 **5** | **−2** 缺变量 |
| 张汤 | 湯爲人**多詐,舞智以御人** | 《史記》卷122 | —(空) | 一条路都不开·斗食 **0** | 御史大夫 **5** | **−5** 缺变量 |
| 汲黯 | 黯為人**性倨,少禮,面折,不能容人之過** | 《史記》卷120 | blunt | 军功·县令 **3** | 主爵都尉 **5** | **−2** 缺变量 |
| **李广** | 廣**訥口少言**,其善射亦天性也,莫能及廣 | 《史記》卷109 | —(空) | 一条路都不开·斗食 **0** | 郎中令(九卿)**5** | **−5** 缺变量 |
| 李蔡 | 蔡**爲人在下中**,名聲出廣下甚遠 | 《史記》卷109 | —(空) | 一条路都不开·斗食 **0** | 丞相 **5** | **−5** 缺变量 |
| 公孙弘 | 弘為人**恢奇多聞**/**意忌,外寬內深** | 《史記》卷112 | datadriven, warm | 辟召·县令 **3** | 丞相 **5** | **−2** 缺变量 |
| 董仲舒 | 董仲舒為人**廉直** | 《史記》卷121 | blunt | 军功·县令 **3** | 江都相→膠西相 | **—** 量不了 |
| 桑弘羊 | 雒陽賈人子,**以心計**,年十三侍中 | 《史記》卷30 | datadriven | 辟召·县令 **3** | 御史大夫 **5** | **−2** 缺变量 |
| 兒宽 | 寬為人溫良…**善著書、書奏,敏於文,口不能發明也** | 《史記》卷121 | warm, calm | 一条路都不开·斗食 **0** | 御史大夫 **5** | **−5** 缺变量 |
| 程不识 | **爲人廉,謹於文法** | 《史記》卷109 | rigorous | 辟召·县令 **3** | 太中大夫(比千石) | **—** 量不了 |
**天(正残差):0 格 | 缺变量(负残差):8 格 | 算对:0 格 | 量不了:2 格**
**残差的值域:{−5, −2}。只有两个值,中间什么都没有,正的一个也没有。**
> 「量不了」**不是**「算对了」:董仲舒是诸侯王相、程不识是比千石 —— **本表的梯子上没有他们站的地方**。
> ✗ 不许算成错(那是拿「我们没有这一格」冒充「他不行」),✗ 也不许算成对。
---
## ★ 李广那一行 —— 老板点名要单独拎出来的那一格
```
引擎:一条路都不开 → 郡县小吏,斗食(tier 0)
史料:「然廣不得爵邑,官不過九卿」(《史記》卷109,双证)→ tier 5
残差:0 − 5 = −5 = 【缺变量】,不是天
```
**老板的前提是:「按这套尺子:李广该封侯。历史:他没有。」**
**⇒ 这个前提错了。按这套尺子,李广不该封侯 —— 引擎说他连门都进不去。**
为什么:司马迁给李广的画像是**「訥口少言」——他不说话**。
而 `talents.js` 的 16 项副天赋**全部是「说话风格」**(它自陈:「副天赋 = 说话风格软技能」)。
⇒ **一个「訥口少言」的人,在一张只量说话风格的表上必然是零。这不是巧合,是定义。**
**而最狠的一刀**:李广的实职**正是靠军功挣的**(「以良家子從軍擊胡,用善騎射,殺首虜多」)。
而引擎的军功之路是 `gate{wu:55}`,`wu`(果决)的两项副天赋是 **blunt(直率坦诚)** 与 **motivate(激励感染)**——
**两项都是说话。**
⇒ **把李广挡在军功门外的那道闸,量的是他最不擅长的那一样,而那一样与打仗毫无关系。**
### ⇒ 所以:引擎和司马迁站在同一边了吗?
**没有。它们连符号都相反。**
- 司马迁(和王勃)说的是**「他该封而没封」**—— 那是一个**正**残差的形状。
- 引擎说的是**「他连军功的门都进不去」**—— 那是一个**负**残差,而且是满格的。
**老板说「如果不是 —— 照实说,那更有意思」。照实说:不是。**
---
## ★ 太史公那句原话 —— 老板要我 curl 回来的那一句
> 老板:「我记得大意是『广不为后人,然无尺寸之功以得封邑者,何也?』——
> **但这是我的记忆,不是见证。以你 curl 到的为准。我记错了就照实说。**」
照实说,**三处**。curl `-sk` 生文本,双证逐字目验,未用 WebFetch。
见证 W:`zh.wikisource《史記/卷109》action=raw` | 见证 T:`ctext.org shiji/li-jiang-jun-lie-zhuan/zh`
### 错一:那句话**不是太史公问的** —— 是**李广自己**问的,对一个**看相的**说的
> 廣嘗與**望氣王朔**燕語,曰:「自漢擊匈奴而廣未嘗不在其中,而諸部校尉以下,才能不及中人,
> 然以擊胡軍功取侯者數十人,而廣不爲後人,**然無尺寸之功以得封邑者,何也?豈吾相不當侯邪?且固命也?**」
**「何也?」是李广的原话**,在**燕語**(私下闲谈)里,对象是**望氣王朔 —— 一个看气看相的方士**。
**而太史公曰整段里没有这个问题**(双证逐字):
> 太史公曰:傳曰:「其身正,不令而行;其身不正,雖令不從。」其李將軍之謂也?
> **余睹李將軍悛悛如鄙人,口不能道辭。**及死之日,天下知與不知,皆爲盡哀。
> 彼其忠實心誠信於士大夫也?諺曰「桃李不言,下自成蹊」。此言雖小,可以諭大也。
⇒ **太史公没有问「为什么」。他在【辩护】。**
> ★ 而这一段里躺着一件我没找、它找上我的东西:**「口不能道辭」**——
> 司马迁**在论赞里又说了一遍**李广不会说话(本传已有「廣訥口少言」)。
> ⇒ **司马迁认为「不会说话」重要到要在总评里再写一次 —— 而那正好是我们唯一量得了的那一样,
> 他两次说的都是「没有」。于是我们给他零。**
> 三个证人:李广「訥口少言」(卷109 本传)、「口不能道辭」(卷109 论赞)、兒寬「口不能發明也」(卷121)。
> ⚠ 而**论赞不进映射**(「拿它做映射就是拿答案当题目」)—— 这一句只当读数,李广的 subs 仍然是空的。
### 错二:**「李廣難封」不出《史記》**
- 见证 W:**「難」0 次、「难」0 次**(全卷,**逐字符**数,不整词搜)。
- 见证 T:字符串命中 2 次 —— **逐个读上下文复核:两次都是 ctext 页面左栏导航里的书名「難經」**,不在正文里。⇒ 正文 0 次。
**真出处:唐·王勃《滕王閣序》(约 675 年)**,双证(两个独立四库底本,**均无标点** —— 正因无标点,它们是「字」的独立见证):
> 嗟乎!時運不齊,命途多舛。**馮唐易老,李廣難封。**屈賈誼於長沙,非無聖主;竄梁鴻於海曲,豈乏明時?
⚠ 异文(标出,不暗合):《古今事文類聚》四库本作「**時運不齊**」,《文苑英華》四库本作「**大運不窮**」并自注「一作時運不齊」。而「馮唐易老,李廣難封」八字**不在异文之列**。
⇒ **老板说「中国人两千年前就给它起好名字了」—— 那个名字是【一千三百多年前】起的,起名字的是【王勃】,不是司马迁。**
> **⚠ 而必须立刻把另一半说满,否则这就是一次「假订正」:**
> **「李广没封侯」这件【事】,《史記》正文说了,逐字,双证:「然廣不得爵邑,官不過九卿」。**
> ✗ **绝不许由此写「李广难封是编的」**——**事实在原文里,只有那四个字的【措辞】是唐人的。**
> (照 `shiliao.js` 在「御史大夫中二千石」上换来的口径:「注有、正文无,本表只收正文」,✗ 不许说「本表无据」。)
**而这个错的形状,是本项目反复钉死的那一种 —— 把一句后人的名言,当成史料本身的话:**
| 名言 | 真身 |
|---|---|
| 「上品无寒门」 | 刘毅**请废九品的政论**,不是制度描述 |
| 「为与士大夫治天下」 | 文彦博的一句**辩论词**(神宗当场顶了回去,而那句回话被后世的书删了) |
| 「南朝多以寒人掌机要」 | **赵翼《廿二史札记》的条目名**,不是南朝史家的分类原则 |
| **「李广难封」** | **王勃《滕王閣序》的一句骈文**,不是《史記》的话 |
**同一个坑的第四次。而这一次踩它的是出题的人。**
—— 这一句必须说出来,因为本项目的全部纪律都建立在「谁都会踩这个坑」上,而**「谁都」包括出题的人**。
### 错三:见上 —— 按这套尺子,李广是斗食小吏,不是该封侯的人
---
## ★★ 而原文自己,把这个残差**解释掉了三次**
> 老板的红线:「**不许把残差解释掉。**『李广没封是因为他情商低』——那是给引擎打补丁,不是承认天。」
《史記》卷109 正文,双证:
1. **李广自己**:「豈吾**相**不當侯邪?且固**命**也?」——**相**与**命**。
2. **王朔(方士)**:「**禍莫大於殺已降,此乃將軍所以不得侯者也。**」——**报应**。
(李广自己招了:「吾誘而降,降者八百餘人,**吾詐而同日殺之**。至今大恨獨此耳。」)
3. **武帝**:「大將軍青亦**陰受上誡**,以爲李廣老,**數奇**,毋令當單于,恐不得所欲。」——**數奇**(命数不好)。
⇒ **「不许把残差解释掉」这条规矩,两千年来没有一个人遵守过 —— 包括李广本人:
问残差的那个人,和第一个把它解释掉的那个人,是同一个人,在同一句话里。**
⇒ **古人不承认残差。他们给残差起名字,然后把它当成一个已知量。**
> ⚠ 见证学的判别器又生效了一次:「數奇」在见证 W(收注)出现 2 次,见证 T(只收正文)**1 次**。
> **A 有而 B 无 ⇒ 它是注。**「失道見斥,**數奇不封**」是**司馬貞索隱(唐人注)**,不是史料。本表只取正文那一次。
### ★★★★ 而最要紧的一层在③ —— 它正面撞上老板的另一条红线
> 老板:「**不许把「天」做成一个变量塞进 computeRank。天不接受输入 —— 这是它的定义。
> 它只能被【测量】,不能被【计算】。**」
**而武帝就是那么干的。**
「數奇」是汉朝人给「天」起的名字,而他**拿它做了一个人事决定**:
陰受上誡 ⇒ 「毋令當單于」⇒ 徙前將軍廣出東道。
**而那正是李广结局的开端**:出东道 →「東道少回遠」→ 失道 → 后期 → 引刀自剄。
⇒ **天被当成变量的那一刻,它就不再是天了 —— 它成了那个人下场的原因。**
**⇒ 即:老板那条红线【有原文依据】。而它不是一条工程纪律,它是《史記》卷109 里一个人的死法。**
> **⚠ 两条限定语,缺一条这就是过度解读:**
> ① **这条因果链是【本表串的】,不是原文说的。** 原文给的是四笔事实(陰受上誡/徙東道/東道少回遠/失道),
> 它**没有说**那是死因。✗ 不许写成「司马迁认为武帝的偏见害死了李广」——那是我的读法,不是他的话。
> ② **而原文自己给了一个竞争解释,就在同一段**:「而是時公孫敖新失侯…**大將軍亦欲使敖與俱當單于,故徙前將軍廣**。」
> ⇒ 徙他的第二个理由是**卫青要给公孙敖让路 —— 一个人情,不是一个天命**。
> **原文把两个理由并排放着,一个字的裁断都没有。本表照办:两个都摆,不裁。**
---
## ★ 为什么「天」测不到 —— 两行算术,不是运气
| | 值域 |
|---|---|
| **引擎在汉能说的** | `tier ∈ {0, 3}` —— 穷举 524,288 个 profile 实测,且**解析证明**:三条通道 `kemu` 全空 ⇒ `tier ≡ clamp(seat,0,cap) ≡ 3`;不过闸 ⇒ 0。⇒ **max = 3** |
| **这张卷子的答案** | `realTier ∈ {5}` —— 八个可判分的人实职全是三公/九卿。⇒ **min = 5** |
⇒ **`residual = engineTier − realTier ≤ 3 − 5 = −2`,恒成立。**
⇒ **正残差的定义域是空的。**
**⇒ 「天」不是「这次没出现」,是【在这张卷子上不可能出现】。**
而废掉它的有**两条独立的理由**,**修好任何一条,另一条照样废它**:
1. **答案表的形状**(幸存者偏差):《百官公卿表》**按定义只记坐上位子的人**。
⇒ 这是**史料的问题**,而且**修不了**:一个做到县令就到头的人,**史书不给他立传**。
2. **引擎的值域**:就算给我一张实职分布在 0–5 的完美名单,**汉的引擎最高也只会说 3**
⇒ 对任何 `realTier ≥ 4` 的人,残差**必然是负的**。⇒ 这是**引擎的问题**,**与名单无关**。
> **★ 一个最高只会说「县令」的引擎,永远不可能说出「他该封侯」。⇒ 它没有资格产生一个正残差。**
---
## ★ 残差的形状(老板:「报个案,报形状,别报百分比」)
**哪一类人算得准?——没有一类。八格全是负残差,0 格算对。**
**但形状是有的,而且很干净:残差只有两个值,中间什么都没有。**
- **残差 = −2(晁错、汲黯、公孙弘、桑弘羊)** ⟺ 引擎**认出了**他的画像 ⇒ 过闸 ⇒ tier 3
**共同点不是他们更能干**,是**司马迁给他们的词,正好落在我们有的那 16 项上**
(陗直→blunt、刻深→rigorous、面折→blunt、多聞→datadriven、外寬→warm、以心計→datadriven)。
- **残差 = −5(张汤、李广、李蔡、兒宽)** ⟺ 引擎**没认出** ⇒ 一条路都不开 ⇒ tier 0
**而这四个人不是更差**:
- 张汤「多詐,舞智以御人」:16 项里**没有「诈」**;
- 李广「訥口少言」:16 项**全是说话,而他不说话**;
- 李蔡「爲人在下中」:**那根本不是画像,是一个【等第】**——司马迁没描写他,他**直接给了个分**;
- 兒宽 subs **非空**(`[warm, calm]`)—— 而**汉的三条闸一项都看不见**(jiao 被 `wang=0` 连坐、heng 汉不看)
⇒ **效果与空完全相同**。
⇒ **★ 所以这张残差表,描述的不是历史,是【我们自己的词汇表】。**
**残差的两个值 = 我们词汇表的两个状态:接住了(−2),没接住(−5)。**
**而历史那一头是个常数(八人实职全是 5)。**
⇒ **一个减法,被减数是我们的词汇表,减数是一个常数。于是「残差」这个量,从头到尾量的都是我们自己。**
**而「天」要的恰恰是相反的东西**:它要求**引擎那一头有内容,历史那一头有变化**。
⇒ 实测:**两头都是常数。两个常数相减,得到的还是常数 —— 那里面没有「天」。**
⇒ **「天」是残差里【减不掉】的那一部分。而这张卷子上,残差 100% 由「我们认不认得这个人的画像」解释。
残差里一点天都没有。**
---
## ★★ 最后回答那个问题(老板:这比成绩单重要)
> **这场考试考出的是「引擎能算命」,还是「引擎是一套自洽的胡话」?老实说。**
**两个都不是。老实说,是第三个 —— 而它比两个选项都难堪:**
### **引擎不是胡话 —— 它的每一句话都有出处。问题是它几乎不说话。**
- 全表 20 条通道,**19 条的位阶是常数函数**(解析 + 穷举双证)。
而这**正好等于原文授权的区分度 —— 一处不多,一处不少**:唯一会变的那条(宋武举)
恰恰是唯一一条有原文可依的(「策、武藝俱優為右班殿直……武藝末等者三班差使」)。
⇒ **19 条常数不是 19 个 bug,是 19 次「本表数不出你的高下」的诚实回答。**
- **汉这一朝,引擎的位阶信息量 = 1 bit**,而那 1 bit 是「你过没过闸」,不是「你能爬多高」。
**连三条路的天花板都是平的(全是 5)——「通道即杠杆」在汉一格都没兑现。**
### **而它也不能算命 —— 十个人,八个算错,而且错法完全一样。**
**老板说「如果十个人里八个算错 —— 那就说八个算错,那也是今天最值钱的一句话」。**
**⇒ 八个算错。八个全是【算低了】。零个算对。零个算高。**
### **★ 而真正的答案是这一句:**
> **它不是在算命,也不是胡话 —— 它是一把量错了东西的、诚实的尺子。**
>
> **六轴的名字是「文采/吏能/果决/交游/沉毅/机锋」—— 那是【选官的取向】。
> 而它的唯一输入,是 16 项【说话风格】。
> ⇒ 它名义上在量「你这种人」,实际上在量「你这种说法」。**
>
> **李广是天下第一的射手,兒寬是天下第一的笔杆子 —— 他们的共同点是【都不会说话】。
> 于是我们给他们两个零。**
**⇒ 而这一条对「天」是决定性的:**
**老板说「谋事在人,成事在天」。这一刀量出来的是:**
> ## **「人」这一半还没做完,「天」那一半就无从谈起。**
「人」= 引擎能算的那部分 = 在汉只有 1 bit,而且量的是你会不会说话。
**一个只有 1 bit 的「人」,减出来的残差里,装不下「天」。**
**残差要能读成「天」,前提是「人」那一头已经把能算的都算完了 ——
而我们连李广是个将军都不知道,我们只知道他不爱说话。**
⇒ **所以「天」这一刀的结论是:它没有被做完,它被证明【还不能开始】。**
---
## 要老板拍的(一条都没自作主张,`kaoyan.js` NOT_DONE)
1. **「天 = 残差」这个定义是对的,我认它 —— 但它在这张卷子上【不可用】,而且我能证明。**
要开始它,得先拍 `renwu.js` NOT_DONE ②:**那一维(「你是什么人」)到底补不补 —— 那一维就是「人」。**
2. **✗ 强烈反对「给汉编一套 kemu 让 tier 够得着答案」**:那是**为了测出「天」而发明「人」**。
⚠ 而且它有一层更阴的:**发明出来的 kemu 会【立刻】制造出正残差**——
引擎一旦能说 5,而历史上有人没做到 5,那个差**当场就出现**。
⇒ **于是我们会得到一张「天」的表,而那张表里的每一个「天」,都是我们自己编的那套 kemu 的产物。**
**★ 那不是测量天,那是制造天。**
3. **李广这一格仍然是这个产品最好的文案,只是文案的内容【反了】**:
✗ 不是「引擎和司马迁都说他该封侯,而历史没有 ⇒ 那就是天」;
★ 而是 **「司马迁说他天下无双,我们的引擎说他是个斗食小吏。⇒ 错的是我们,不是天。」**
⚠ **这是一句产品定性的话,不是一句文案** —— 它把这个功能从「算命」改成「认错」。
4. **而若要正面用李广,「數奇」比「李广难封」值钱得多**:
「李广难封」是一句**感慨**(而且是唐人的);
**「數奇」是一次【判断的执行】—— 它才是这个产品真正在做的那件事的镜子:
我们也在给人算一个数,然后把那个数告诉他。**
---
## 我自己招的
- **`Object.keys(SUB_REGISTRY)` 那一跤**:`SUB_REGISTRY` 是**数组**,`Object.keys` 给的是下标 `["0".."15"]`,
于是穷举时**一个 sub 都没点上** ⇒ 全 tier 0 ⇒ 结果是 `[0]`。
**★ 而一个【采样错了】的穷举,长得和一个【引擎变了】的穷举一模一样 —— 我差点去改断言。**
⇒ 立了口径:**穷举的 subs 全集必须取【引擎自己读的那一份】(`AXES.subs`)**,并加了一条断言钉死
「`AXES` 读的 16 项 == `SUB_REGISTRY` 的 16 项」。**是我自己的断言抓出来的,不是我读出来的。**
- **第五次零命中,而它是一个【新种类】**:扫「不爲後人」→ 零命中,差点写成「原文无此语,老板记错了」。
逐字符复核:原文作「而廣不爲**-{後}-**人」——**两个原因叠在一起**:
① 字形(《史記》用「爲」);② **wikisource 的 `-{ }-` 转换标记【插在词的中间】**。
⇒ **`renwu.js` 立的那条口径(写成 `[為爲]` 字形类)挡不住第二个** —— 就算写 `[為爲]`,「不爲-{後}-人」仍然零命中。
⇒ **立一条更严的:凡在 wikisource 生文本上检索,必须【先剥 `-{ }-` 与 `{{ProperNoun|}}`,再检索】。字形类不够。**
(`renwu.js` 自陈它「程序化剥除」了标记 —— 它**知道**要剥,但它的**检索口径**那一条只写了字形类。
**一个文件在两个地方各说了一半,合起来才够用。**)
File diff suppressed because it is too large Load Diff
+34
View File
@@ -0,0 +1,34 @@
{
"name": "digital-me-backend",
"version": "0.1.0",
"description": "数字我 / Digital Me — 个人 AI 数字分身平台 API(有脸、有声音、有知识库的「另一个你」)",
"private": true,
"type": "module",
"main": "src/index.js",
"scripts": {
"dev": "node --watch src/index.js",
"start": "node src/index.js",
"test": "node --test"
},
"dependencies": {
"ali-oss": "^6.23.0",
"alipay-sdk": "^4.14.0",
"cors": "^2.8.5",
"minio": "^8.0.5",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express-rate-limit": "^7.4.1",
"helmet": "^8.0.0",
"jimp": "^1.6.1",
"jsonwebtoken": "^9.0.2",
"mammoth": "^1.8.0",
"mongodb": "^6.12.0",
"multer": "^2.0.0",
"openai": "^4.77.0",
"pdf-parse": "^1.1.1",
"ws": "^8.18.0"
},
"engines": {
"node": ">=20.0.0"
}
}
File diff suppressed because it is too large Load Diff
+94
View File
@@ -0,0 +1,94 @@
/*!
* 数字我 / Digital Me — embeddable chat widget.
* Usage:
* <script src="https://YOUR-API/widget.js"
* data-slug="your-slug"
* data-site="https://your-web-origin"
* data-name="你的名字"></script>
* Injects a floating launcher button that opens「和我分身聊天」in a popup.
*/
(function () {
var s =
document.currentScript ||
(function () {
var all = document.getElementsByTagName('script');
return all[all.length - 1];
})();
if (!s) return;
var slug = s.getAttribute('data-slug');
if (!slug) {
console.warn('[digital-me] widget: missing data-slug');
return;
}
// Where the /me/:slug page is served (the web app). Defaults to the
// script's own origin (works when web+api share an origin behind a proxy).
var site = s.getAttribute('data-site') || new URL(s.src).origin;
var name = s.getAttribute('data-name') || '我的数字分身';
var accent = s.getAttribute('data-accent') || '#6d5efc';
var chatUrl = site.replace(/\/$/, '') + '/me/' + encodeURIComponent(slug);
if (document.getElementById('dm-widget-root')) return; // idempotent
var root = document.createElement('div');
root.id = 'dm-widget-root';
root.style.cssText =
'position:fixed;right:20px;bottom:20px;z-index:2147483000;font-family:system-ui,-apple-system,sans-serif';
// Popup (iframe) — hidden until opened.
var panel = document.createElement('div');
panel.style.cssText =
'width:380px;max-width:calc(100vw - 32px);height:560px;max-height:calc(100vh - 120px);' +
'background:#0f1117;border-radius:16px;overflow:hidden;box-shadow:0 18px 60px rgba(0,0,0,.45);' +
'margin-bottom:12px;display:none;transform-origin:bottom right;transition:opacity .15s,transform .15s;' +
'opacity:0;transform:scale(.96)';
var iframe = document.createElement('iframe');
iframe.src = chatUrl;
iframe.title = name;
iframe.setAttribute('allow', 'autoplay; microphone');
iframe.style.cssText = 'width:100%;height:100%;border:0';
panel.appendChild(iframe);
// Launcher button.
var btn = document.createElement('button');
btn.setAttribute('aria-label', '和' + name + '聊天');
btn.style.cssText =
'width:58px;height:58px;border-radius:50%;border:0;cursor:pointer;float:right;' +
'background:' +
accent +
';box-shadow:0 8px 24px rgba(0,0,0,.35);display:flex;align-items:center;' +
'justify-content:center;transition:transform .12s';
btn.innerHTML =
'<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#fff" ' +
'stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
'<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>';
btn.onmouseenter = function () {
btn.style.transform = 'scale(1.06)';
};
btn.onmouseleave = function () {
btn.style.transform = 'scale(1)';
};
var open = false;
btn.onclick = function () {
open = !open;
if (open) {
panel.style.display = 'block';
requestAnimationFrame(function () {
panel.style.opacity = '1';
panel.style.transform = 'scale(1)';
});
} else {
panel.style.opacity = '0';
panel.style.transform = 'scale(.96)';
setTimeout(function () {
if (!open) panel.style.display = 'none';
}, 150);
}
};
root.appendChild(panel);
root.appendChild(btn);
(document.body || document.documentElement).appendChild(root);
})();
@@ -0,0 +1,90 @@
// 采【加权求和版】引擎的产出 —— 这一刀的「旧版」参照。
//
// ★★ 这个脚本【只在改 dynasties.js 之前跑过一次】,跑完它采下来的 dynasties-weighted-sum.json
// 就是死的。★ 绝不要在改完引擎之后再跑它来「更新」参照 —— 那等于把被告改成原告。
//
// ⚠ 它自证真伪:采下来的五个单轨朝代,指纹必须与 dynasties-pre-multitrack.json 里
// 【旧代码采的那份】逐字相同。对得上 ⇒ 这份快照确实是旧引擎的产出,不是我编的。
// (本项目的铁律:最难发现的错是「假订正」。一份自己造的「旧版」参照,就是假订正的温床。)
//
// 跑:node scripts/capture-weighted-sum.mjs
//
// ⚠ 它住在 scripts/ 而不是 test/,是【故意的】:`node --test` 会把 test/ 底下的【每一个】文件
// 都当测试跑(不只是 *.test.js)。而本脚本的自证在引擎改完之后【必然抛异常】——
// 那是它设计上的正确行为,却会把 npm test 弄红。放在 scripts/ 下,它才是它该是的东西:
// 一次性的采集工具,不是测试。
import crypto from 'node:crypto';
import fs from 'node:fs';
import { DYNASTIES, computeRank, profileFromSubs } from '../src/dynasties.js';
import { SUB_REGISTRY } from '../src/talents.js';
// ⚠ 必须与 dynasties-tracks.test.js 的 matrix() 【完全同构】,否则指纹对不上。
function matrix() {
const ALL = SUB_REGISTRY.map((s) => s.id);
const combos = [[]];
for (const s of ALL) combos.push([s]);
for (let i = 0; i < ALL.length; i++) for (let j = i + 1; j < ALL.length; j++) combos.push([ALL[i], ALL[j]]);
combos.push(['rigorous', 'datadriven', 'concise', 'shortfirst', 'patient', 'calm']);
combos.push(['poetic', 'story', 'vivid']);
combos.push(['socratic', 'humor', 'poetic', 'story', 'vivid']);
combos.push(['blunt', 'motivate']);
combos.push(ALL);
const out = new Map();
for (const subs of combos) {
for (const wang of [0, 30, 55, 60, 75, 80, 90, 100]) {
out.set(`${subs.join('+') || '∅'}|w${wang}`, profileFromSubs(subs, { wang }));
}
}
return [...out.entries()];
}
// ── 自证:五个单轨朝代的指纹必须与【旧代码采的】 pre-multitrack fixture 对得上 ──
const PRE = JSON.parse(fs.readFileSync(new URL('../test/fixtures/dynasties-pre-multitrack.json', import.meta.url), 'utf8'));
const rows = matrix();
if (rows.length !== PRE._matrix.count) throw new Error(`矩阵变了:${rows.length} ≠ ${PRE._matrix.count}`);
const sorted = rows.slice().sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
for (const d of ['han', 'weijin', 'nanchao', 'tang', 'ming']) {
const h = crypto.createHash('sha256');
for (const [k, p] of sorted) h.update(k + '|' + JSON.stringify(computeRank(p, d)));
const got = h.digest('hex');
if (got !== PRE.hashes[d]) {
throw new Error(
`✗ ${d} 的指纹与 pre-multitrack 对不上 —— 当前代码【已经不是】加权求和版了。\n` +
' 这个脚本只能在改引擎【之前】跑。现在跑它,采到的是新引擎的产出,' +
'整个「新旧对照」就成了自己跟自己比。'
);
}
}
// ── 采:七朝 × 1128 profile 的【判定投影】──
// ⚠ 只采判定相关的格子,不采 reasons/cites(那是文案,几十 MB,且不参与判定)。
const proj = (r) => (r === null ? null : {
ch: r.channel ? r.channel.id : null,
tier: r.rank.tier,
title: r.rank.title,
grade: r.rank.grade,
unit: r.rank.unit,
ceil: r.ceiling ? r.ceiling.tier : null,
cat: r.category || '',
stig: !!r.stigma,
blk: r.blocked.length,
});
const out = {
_what: '【加权求和版】引擎(score += val(k)*weight → tier = round(score/100*5))的产出,七朝 × 1128 profile。',
_why:
'★ 这一刀【必须】让输出变(原文里没有加权求和的原型)。所以这个文件【不是迁移锁】—— ' +
'它是【被告】:新引擎每改动一格,都要在 dynasties-shape.test.js 里说清楚是哪条原文要求它这么改。',
_dontRegen:
'★★ 绝不要用新引擎重新生成本文件。它的全部价值在于它是【旧代码】的产出。' +
'重生成 = 把被告改成原告 = 本项目铁律里的「假订正」。' +
'自证:五个单轨朝代的指纹与 dynasties-pre-multitrack.json 逐字相同(采集时已校验并通过)。',
_capturedBy: 'scripts/capture-weighted-sum.mjs(2026-07-16,改引擎之前)',
_matrix: PRE._matrix,
_selfCheck: '✓ han/weijin/nanchao/tang/ming 五朝指纹 = dynasties-pre-multitrack.json 的指纹(采集时校验通过)',
rows: Object.fromEntries(
sorted.map(([k, p]) => [k, Object.fromEntries(DYNASTIES.map((d) => [d.id, proj(computeRank(p, d.id))]))])
),
};
fs.writeFileSync(new URL('../test/fixtures/dynasties-weighted-sum.json', import.meta.url), JSON.stringify(out));
console.log(`✓ 采到 ${sorted.length} 个 profile × ${DYNASTIES.length} 朝;五朝指纹自证通过。`);
+71
View File
@@ -0,0 +1,71 @@
// 用 CDP 给页面注入 dm_token 后截图(验证需登录的后台)。
// 用法:node scripts/cdp-shot.mjs <token> <targetPath> <outPng>
// 前置:chrome 已带 --remote-debugging-port=9222 启动,并已打开 http://localhost:5173/login
import WebSocket from 'ws';
import { writeFileSync } from 'node:fs';
const [, , TOKEN, TARGET, OUT] = process.argv;
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
async function listTargets() {
const res = await fetch('http://localhost:9222/json/list');
return res.json();
}
async function main() {
// 找到 5173 的页面 target。
let ws = '';
for (let i = 0; i < 30; i++) {
const ts = await listTargets();
const t = ts.find((x) => x.type === 'page' && /localhost:5173/.test(x.url));
if (t) { ws = t.webSocketDebuggerUrl; break; }
await wait(300);
}
if (!ws) throw new Error('no 5173 page target');
const sock = new WebSocket(ws, { perMessageDeflate: false, maxPayload: 64 * 1024 * 1024 });
let id = 0;
const pending = new Map();
const send = (method, params = {}) =>
new Promise((resolve) => { const mid = ++id; pending.set(mid, resolve); sock.send(JSON.stringify({ id: mid, method, params })); });
await new Promise((r) => sock.on('open', r));
sock.on('message', (buf) => {
const msg = JSON.parse(buf.toString());
if (msg.id && pending.has(msg.id)) { pending.get(msg.id)(msg.result); pending.delete(msg.id); return; }
if (msg.method === 'Network.responseReceived') {
const u = msg.params?.response?.url || '';
if (/\/api\//.test(u)) console.log(' NET', msg.params.response.status, u.replace('http://localhost:3701', ''));
}
if (msg.method === 'Runtime.exceptionThrown') console.log(' EXC', msg.params?.exceptionDetails?.text, msg.params?.exceptionDetails?.exception?.description?.slice(0, 120));
});
await send('Page.enable');
await send('Runtime.enable');
await send('Network.enable');
// 先到根页把 token 写进 localStorage。
await send('Page.navigate', { url: 'http://localhost:5173/' });
await wait(2500);
const set = await send('Runtime.evaluate', {
expression: `localStorage.setItem('dm_token', ${JSON.stringify(TOKEN)}); localStorage.getItem('dm_token')?.length || 0`,
returnByValue: true,
});
console.log('token len in storage:', set?.result?.value);
// 第一次整页跳(预热会话,冷启动可能被守卫弹走)。
await send('Runtime.evaluate', { expression: `location.href='http://localhost:5173${TARGET}'` });
await wait(5000);
// 第二次跳目标(此时 store 已 authed、user 已拉取 → 稳定停在目标页)。
await send('Page.navigate', { url: `http://localhost:5173${TARGET}` });
await wait(6000);
const diag = await send('Runtime.evaluate', {
expression: `JSON.stringify({path: location.pathname, tok: (localStorage.getItem('dm_token')||'').length, h1: document.querySelector('h1')?.textContent?.slice(0,20), bodyHas: document.body.innerText.includes('大师墙')})`,
returnByValue: true,
});
console.log('after nav:', diag?.result?.value);
const { data } = await send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: true });
writeFileSync(OUT, Buffer.from(data, 'base64'));
console.log('saved', OUT);
sock.close();
process.exit(0);
}
main().catch((e) => { console.error(e); process.exit(1); });
+183
View File
@@ -0,0 +1,183 @@
// ★★★★ 2026-07-17 第三刀:【全表死物 census —— 「读 0 次 / 改变 0 格」逐个列出】
// 老板逐字:「把每个进 reasons 的数、每个 key 位、每个 axis,逐个 census:被读几次?改变几格?
// 读 0 次 或 改变 0 格 的,全部列出来。」
//
// ⚠ 口径三条,写死在这里:
// ① 【改变几格】= 【突变数据点 + 比对引擎自己的产物】,不是复算。
// ② ★【「改变 0 格」比「读 0 次」更宽,而它才是产品要问的那一问】——
// 南朝的 lex 被【读了 98,304 次、裁决了 81,920 次】,而它【改变 0 格】(第六刀实测)。
// ⇒ 一个被读了八万次的东西,可以什么都没买到。故本 census 量的是【买到了什么】。
// ③ ★★★【每一个读数都必须报它是在哪个 chushen 配置下量的】——
// 第六刀的验收判词逐字:「一个不说配置的读数,不是读数。」
// ⚠ 而这一条【本 census 第一版自己就栽了】:默认 chushen 未答 ⇒ 辽三条路全关
// ⇒ 辽的每一个数据点都报「死」。那不是死,那是【我没让它活】。
// ⇒ 故:辽跑 3 种(未答/契丹/汉),明跑 2 种(未答/倡優之家),其余五朝无出身维。
// ★【死】= 在【它自己那一朝的每一种配置下】都改变 0 格。
import { DYNASTIES, computeRank, computeAxes, AXIS_IDS } from '../src/dynasties.js';
import { chushenOf } from '../src/chushen.js';
import { SUB_REGISTRY } from '../src/talents.js';
const SUBS = SUB_REGISTRY.map((s) => s.id);
const WANGS = [0, 29, 30, 54, 55, 74, 75, 100]; // 与 census-kai.mjs 逐字同 —— 门槛 30/55/75 的两侧
// ★ 2^16 subs 去重成 2,790 个不同的六轴取值(带权 w)——【判决只看六轴,不看 subs】。
// ⇒ 与 524,288 逐格跑【逐字等价】,而快 20 倍。w 把「条次」还原回「profile 数」。
const TUPLES = (() => {
const m = new Map();
for (let b = 0; b < (1 << 16); b++) {
const subs = []; for (let i = 0; i < 16; i++) if (b & (1 << i)) subs.push(SUBS[i]);
const a = computeAxes(subs);
const k = AXIS_IDS.map((x) => a[x]).join(',');
m.set(k, (m.get(k) || 0) + 1);
}
return [...m].map(([k, w]) => ({ axes: Object.fromEntries(k.split(',').map((v, i) => [AXIS_IDS[i], Number(v)])), w }));
})();
const PER_DYN = TUPLES.reduce((s, t) => s + t.w, 0) * WANGS.length; // = 524,288
// 每朝要跑的 chushen 配置。★ null 也是一种配置(「这一问还没问出口」),不是缺省值。
const CONFIGS = {};
for (const d of DYNASTIES) {
const dim = chushenOf(d.id);
CONFIGS[d.id] = dim ? [null, ...dim.values.map((v) => v.id)] : [null];
}
// ★ 存 hash 不存串:156,240 格 × 3.2KB 会把内存吃穿(第一版就是这么死的)。
const fnv = (s) => { let h = 0x811c9dc5; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } return h >>> 0; };
// 【整页】用户读得到的全部(reasons 的每一个字都在里面)
const page = (r) => fnv(JSON.stringify(r));
// 【判词】这一部传写的那一句:你走哪条路、第几阶、什么头衔
const verdict = (r) => fnv(`${r.channel ? r.channel.id : '-'}|${r.rank.tier}|${r.rank.title}|${r.nearest || '-'}`);
function scan(dynId, chushen, over = null) {
const out = [];
for (const t of TUPLES) for (const wang of WANGS) {
const axes = over ? { ...t.axes, ...over.axes } : t.axes;
const r = computeRank({ axes, wang, chushen, ...(over ? over.extra : null) }, dynId);
out.push({ w: t.w, p: page(r), v: verdict(r) });
}
return out;
}
const BASE = {};
for (const d of DYNASTIES) for (const cs of CONFIGS[d.id]) BASE[`${d.id}|${cs}`] = scan(d.id, cs);
const diff = (dynId, cs, now) => {
const base = BASE[`${dynId}|${cs}`];
let dv = 0, dp = 0;
for (let i = 0; i < base.length; i++) {
if (base[i].v !== now[i].v) dv += base[i].w;
if (base[i].p !== now[i].p) dp += base[i].w;
}
return { dv, dp };
};
// 突变一个数据点 → 在【那一朝的每一种 chushen 配置】下重扫 → 逐配置报
function mutate(dynId, apply, undo) {
apply();
const per = CONFIGS[dynId].map((cs) => ({ cs, ...diff(dynId, cs, scan(dynId, cs)) }));
undo();
return { per, dv: Math.max(...per.map((p) => p.dv)), dp: Math.max(...per.map((p) => p.dp)) };
}
function perturb(dynId, over) {
const per = CONFIGS[dynId].map((cs) => ({ cs, ...diff(dynId, cs, scan(dynId, cs, over)) }));
return { per, dv: Math.max(...per.map((p) => p.dv)), dp: Math.max(...per.map((p) => p.dp)) };
}
const rows = [];
const push = (kind, dyn, what, r, note = '') => rows.push({ kind, dyn, what, ...r, note });
for (const d of DYNASTIES) {
for (const c of d.channels) {
const x = c.xuan;
// ── ① 每一个 gate 闸口:删掉它
for (const k of Object.keys(c.gate || {})) {
const old = c.gate[k];
push('gate', d.id, `${c.id} gate{${k}:${old}} 删掉`,
mutate(d.id, () => { delete c.gate[k]; }, () => { c.gate[k] = old; }));
// ★★ 闸的【那个数】本身买到了什么?六轴取值只有 40/63/73/79/83(sat 曲线的产物)
// ⇒ (40,63] 里的任何 need 是【同一道闸】。50 与 55 是不是同一道闸 —— 让引擎自己说。
if (k !== 'wang' && k !== 'lao') {
for (const alt of [41, 50, 55, 63]) {
if (alt === old) continue;
push('gate-num', d.id, `${c.id} gate{${k}:${old}→${alt}}`,
mutate(d.id, () => { c.gate[k] = alt; }, () => { c.gate[k] = old; }));
}
}
}
// ── ② 每一个 key 位:删掉它
const key = x.key || [];
for (let i = 0; i < key.length; i++) {
push('key-pos', d.id, `${c.id} key[${i}]:${key[i]} 删掉`,
mutate(d.id, () => { x.key = key.filter((_, j) => j !== i); }, () => { x.key = key; }));
}
if (key.length) push('key-all', d.id, `${c.id} key=${JSON.stringify(key)} 整条删掉`,
mutate(d.id, () => { x.key = []; }, () => { x.key = key; }));
// ── ③ 每一条 kemu
const kemu = x.kemu || [];
for (let i = 0; i < kemu.length; i++) {
push('kemu', d.id, `${c.id} kemu[${i}]:${kemu[i].axis}>=${kemu[i].need} 删掉`,
mutate(d.id, () => { x.kemu = kemu.filter((_, j) => j !== i); }, () => { x.kemu = kemu; }));
}
// ── ④ seat ±1
const seat = x.seat;
push('seat', d.id, `${c.id} seat ${seat}→${seat + 1}`,
mutate(d.id, () => { x.seat = seat + 1; }, () => { x.seat = seat; }));
// ── ⑤ ceiling:两问分开 —— 它在【择路】那一级读,和它在 tierOf 里【夹】,是两件事
if (c.ceiling) {
const ct = c.ceiling.tier;
if (ct !== 5) push('ceiling', d.id, `${c.id} ceiling.tier ${ct}→5`,
mutate(d.id, () => { c.ceiling.tier = 5; }, () => { c.ceiling.tier = ct; }));
// ★★ 【clamp 那一夹】—— 纯算术,不用跑:tier = clamp(seat + 通过条数, 0, ceiling.tier)
// ⇒ seat + kemu.length 是 tier 的【最大可能值】。它 ≤ ceiling.tier ⇒ 那个 clamp 【永远不夹】。
const most = (x.seat || 0) + (x.kemu || []).length;
push('ceiling-clamp', d.id, `${c.id} tierOf 的 clamp(…,0,${ct}):seat+全通=${most}`,
most > ct ? { per: [], dv: -1, dp: -1 } : { per: [], dv: 0, dp: 0 },
most > ct ? '夹得住' : '★ 恒不夹(seat+kemu ≤ ceiling)');
}
// ── ⑥ 标记位
for (const f of ['hollow', 'stigma', 'regular']) {
const old = c[f];
push('flag', d.id, `${c.id} ${f} ${!!old}→${!old}`,
mutate(d.id, () => { c[f] = !old; }, () => { c[f] = old; }));
}
const as = c.assimilation;
push('flag', d.id, `${c.id} assimilation ${as || 0}→${as ? 0 : 0.5}`,
mutate(d.id, () => { c.assimilation = as ? 0 : 0.5; }, () => { c.assimilation = as; }));
}
}
// ── ⑦ 每一条轴:整个钉死成 40(=「这条轴上一项都没点过」)
for (const d of DYNASTIES) for (const a of AXIS_IDS) {
push('axis', d.id, `轴 ${a} 全钉成 40(=一项都没点)`, perturb(d.id, { axes: { [a]: 40 } }));
}
// ── ⑧ profile 上那几个【引擎的签名收了的】字段:喂值进去
for (const d of DYNASTIES) for (const [nm, extra] of [
['lao', { lao: 9999 }], ['soul', { soul: 100 }],
['counts', { counts: { catchphrase: 9, attitude: 9, timeline: 9 } }], ['profession', { profession: 'poet' }],
]) push('profile', d.id, `profile.${nm} = ${JSON.stringify(Object.values(extra)[0])}`, perturb(d.id, { extra }));
const N = (n) => (n < 0 ? '—' : n.toLocaleString());
const pad = (s, n) => { s = String(s); let w = 0; for (const ch of s) w += ch.charCodeAt(0) > 0x2000 ? 2 : 1; return s + ' '.repeat(Math.max(1, n - w)); };
const num = (s, n) => String(s).padStart(n);
const isDead = (r) => r.dv === 0 && r.dp === 0;
const onlyPage = (r) => r.dv === 0 && r.dp > 0;
console.log(`\n══ 死物 census ══ 每朝每配置 ${N(PER_DYN)} 个 profile(2,790 个六轴取值 × 8 wang,带权 = 524,288)`);
console.log(`══ 配置:辽 3 种(未答/契丹/汉)、明 2 种(未答/倡優之家)、其余五朝无出身维`);
console.log(`══ 【改变 0 格】= 这个数据点在整张表上什么都没买到(取【全部配置里最大的那个】,故不冤枉它)\n`);
console.log(pad('类', 14) + pad('朝', 9) + pad('数据点', 44) + num('判词变', 10) + num('整页变', 10) + ' 备注');
console.log('─'.repeat(100));
const want = process.argv.includes('--all') ? () => true : (r) => isDead(r) || onlyPage(r);
for (const r of rows) {
if (!want(r)) continue;
console.log(pad(r.kind, 14) + pad(r.dyn, 9) + pad(r.what, 44) + num(N(r.dv), 10) + num(N(r.dp), 10) +
' ' + (isDead(r) ? '★★ 死(0 格)' : onlyPage(r) ? '★ 只说话不判事' : '') + (r.note ? ' ' + r.note : ''));
}
console.log(`\n──────────────────────────────────────────────`);
console.log(`数据点总数 ${rows.length}`);
console.log(`★★【死】改变 0 格 ${rows.filter(isDead).length}`);
console.log(`★【只改整页不改判词】 ${rows.filter(onlyPage).length} =「它说了一句话,而它什么都没判」`);
console.log(`活的(改判词) ${rows.filter((r) => r.dv > 0).length}`);
console.log(`\n★ 逐配置明细(死物在【每一种】配置下都是 0):--verbose`);
if (process.argv.includes('--verbose')) {
for (const r of rows) {
if (!isDead(r) || !r.per.length || r.per.length < 2) continue;
console.log(pad(r.kind + ' ' + r.dyn + ' ' + r.what, 56) + r.per.map((p) => `${p.cs || '未答'}:${p.dv}/${p.dp}`).join(' '));
}
}
+145
View File
@@ -0,0 +1,145 @@
// ★★★★★ 2026-07-17 第三刀:【gapOf —— 「排在后面 ≠ 不参选」这一族的第八次】
//
// 【机理,先说死,它是算术,不是史学】
// 六轴的取值是 sat() 饱和曲线的产物,【只有五个值】:40 / 63 / 73 / 79 / 83(本刀实测)。
// ⇒ (40,63] 之间【没有任何一个 profile 落得进去】。
// ⇒ 【gate{wu:41} 与 gate{wu:55} 与 gate{wu:63} 是【同一道闸】】—— 开/关逐格相同,零例外。
// ★ 即:55 这个数【作为闸是一个空操作】—— 本文件自己也这么说:
// 「因此 gate 的口径是:55 ≈「这条轴上至少点过一项」,70 ≈「至少两项」。」
// ⇒ ★★【它自陈是一个【代号】,不是一个刻度。】
//
// 【而它在另一个地方【被当成刻度读了】】
// `gapOf(t) = Math.max(...t.missing.map((m) => m.need - m.got))`
// ⇒ 【need − got】。★ need 是一个代号,got 是一个曲线的产物 —— 两者相减【不是一段距离】。
// ⇒ 而 nearest 的第二级就是拿它排的。⇒ 【一个自陈是代号的数,在暗处裁决了「你离哪条路最近」。】
//
// 本 census 分三问,逐问都拿引擎自己的产物答:
// ① 【同一道闸的那几个数,开/关变几格?】(应为 0 —— 若不为 0,我上面的机理是错的)
// ② 【而它们让 nearest 变几格?】
// ③ 【gapOf 裁决的时候,比的是不是同一条轴?】(同 lexCompare 的那一问,实测同轴 0 次)
import { DYNASTIES, computeRank, computeAxes, AXIS_IDS } from '../src/dynasties.js';
import { chushenOf } from '../src/chushen.js';
import { SUB_REGISTRY } from '../src/talents.js';
const SUBS = SUB_REGISTRY.map((s) => s.id);
const WANGS = [0, 29, 30, 54, 55, 74, 75, 100];
const TUPLES = (() => {
const m = new Map();
for (let b = 0; b < (1 << 16); b++) {
const subs = []; for (let i = 0; i < 16; i++) if (b & (1 << i)) subs.push(SUBS[i]);
const a = computeAxes(subs);
m.set(AXIS_IDS.map((x) => a[x]).join(','), (m.get(AXIS_IDS.map((x) => a[x]).join(',')) || 0) + 1);
}
return [...m].map(([k, w]) => ({ axes: Object.fromEntries(k.split(',').map((v, i) => [AXIS_IDS[i], Number(v)])), w }));
})();
const CONFIGS = {};
for (const d of DYNASTIES) { const dim = chushenOf(d.id); CONFIGS[d.id] = dim ? [null, ...dim.values.map((v) => v.id)] : [null]; }
const N = (n) => n.toLocaleString();
const pad = (s, n) => { s = String(s); let w = 0; for (const ch of s) w += ch.charCodeAt(0) > 0x2000 ? 2 : 1; return s + ' '.repeat(Math.max(1, n - w)); };
// ★ 六轴的全部可能取值 —— 让引擎自己报,不许我手写
const VALS = {}; for (const a of AXIS_IDS) VALS[a] = [...new Set(TUPLES.map((t) => t.axes[a]))].sort((x, y) => x - y);
console.log('══ ① 六轴的取值:sat() 曲线只给得出这几个数 ══');
for (const a of AXIS_IDS) console.log(` ${pad(a, 6)} ${VALS[a].join(' ')}`);
// 一道闸 need=d 的【等价类】:所有让「开/关」逐格相同的整数 n
const classOf = (axis, d) => {
const vs = VALS[axis];
const passes = (n) => vs.filter((v) => v >= n).join(',');
const me = passes(d);
const out = [];
for (let n = 1; n <= 100; n++) if (passes(n) === me) out.push(n);
return out;
};
console.log('\n══ ② 每一道六轴闸的【等价类】—— 类里的任何一个数,是【同一道闸】 ══');
console.log(pad('闸', 34) + pad('等价类(这些数【是同一道闸】)', 34) + '类的大小');
console.log('─'.repeat(84));
const GATES = [];
for (const d of DYNASTIES) for (const c of d.channels) for (const [k, need] of Object.entries(c.gate || {})) {
if (k === 'wang' || k === 'lao') continue; // wang 是 0–100 的自由值,不是 sat 的产物 ⇒ 它的数是真的
const cls = classOf(k, need);
GATES.push({ d, c, k, need, cls });
console.log(pad(`${d.id}/${c.id} gate{${k}:${need}}`, 34) + pad(`${cls[0]}–${cls[cls.length - 1]}`, 34) + cls.length);
}
// ── 跑:把每一道闸换成它等价类里的每一个端点,看【开/关】与【nearest】各变几格
console.log('\n══ ③ 换成等价类里的另一个数:【开/关】变几格?【nearest】变几格? ══');
console.log(pad('闸 → 换成', 40) + '开/关变'.padStart(10) + 'nearest 变'.padStart(12) + ' ★');
console.log('─'.repeat(72));
const scan = (dynId, cs) => {
const out = [];
for (const t of TUPLES) for (const wang of WANGS) {
const r = computeRank({ axes: t.axes, wang, chushen: cs }, dynId);
// 【开/关】= 哪几条路对你开着(引擎自己的 kai + blocked)
const gates = r.kai.map((k) => k.channelId).sort().join(',') + '#' + r.blocked.map((b) => b.channel).sort().join(',');
out.push({ w: t.w, gates, near: r.nearest || '-' });
}
return out;
};
const BASE = {};
for (const d of DYNASTIES) for (const cs of CONFIGS[d.id]) BASE[`${d.id}|${cs}`] = scan(d.id, cs);
let deadGate = 0, movedNear = 0;
for (const g of GATES) {
for (const alt of [g.cls[0], g.cls[g.cls.length - 1]]) {
if (alt === g.need) continue;
const old = g.c.gate[g.k];
g.c.gate[g.k] = alt;
let dg = 0, dn = 0;
for (const cs of CONFIGS[g.d.id]) {
const b = BASE[`${g.d.id}|${cs}`], now = scan(g.d.id, cs);
for (let i = 0; i < b.length; i++) { if (b[i].gates !== now[i].gates) dg += b[i].w; if (b[i].near !== now[i].near) dn += b[i].w; }
}
g.c.gate[g.k] = old;
if (dg === 0) deadGate++;
if (dn > 0) movedNear += dn;
console.log(pad(`${g.d.id}/${g.c.id} {${g.k}:${g.need}→${alt}}`, 40) + N(dg).padStart(10) + N(dn).padStart(12) +
' ' + (dg === 0 ? '★ 同一道闸(0 格)' : '⚠ 不是同一道闸!') + (dn > 0 ? ' ★★ 而 nearest 变了' : ''));
}
}
console.log(`\n★★★【${deadGate} 处:换个数,开/关一格都不动 —— 它作为闸是空操作】`);
console.log(`★★★【而 nearest 一共变了 ${N(movedNear)} 格 —— 那个空操作在暗处裁决了「你离哪条路最近」】`);
// ── ④ gapOf 裁决的时候,比的是不是同一条轴?
console.log('\n══ ④ 【无路者】那一支:nearest 是谁判的?gapOf 比的是不是同一条轴? ══');
const gapOf = (ms) => (ms.length ? Math.max(...ms.map((m) => m.need - m.got)) : 0);
const worstAxis = (ms) => { let b = null; for (const m of ms) if (!b || m.need - m.got > b.need - b.got) b = m; return b; };
for (const d of DYNASTIES) for (const cs of CONFIGS[d.id]) {
const S = { noRoad: 0, byCount: 0, byGap: 0, byIdx: 0, gapSameAxis: 0, gapCrossAxis: 0 };
const pairs = new Map();
for (const t of TUPLES) for (const wang of WANGS) {
const r = computeRank({ axes: t.axes, wang, chushen: cs }, d.id);
if (r.channel) continue;
S.noRoad += t.w;
if (!r.nearest) continue;
// 复算这一支的三级(★ 与 computeRank 的 sort 逐字同源),只为回答「是哪一级判的」
const dd = DYNASTIES.find((x) => x.id === d.id);
const rows = [];
dd.channels.forEach((c, idx) => {
const val = (k) => (k === 'wang' ? wang : t.axes[k]);
const ms = Object.entries(c.gate || {}).filter(([k, n]) => val(k) < n).map(([k, n]) => ({ axis: k, need: n, got: Math.round(val(k)) }));
rows.push({ name: c.name, ms, idx });
});
const pool = rows.filter((x) => r.blocked.some((b) => b.channel === x.name) && !r.blocked.find((b) => b.channel === x.name).byChushen);
if (pool.length < 2) continue;
const win = pool.find((x) => x.name === r.nearest);
if (!win) continue;
const rivals = pool.filter((x) => x !== win);
if (rivals.every((x) => x.ms.length > win.ms.length)) { S.byCount += t.w; continue; }
const same = rivals.filter((x) => x.ms.length === win.ms.length);
if (same.every((x) => gapOf(x.ms) > gapOf(win.ms))) {
S.byGap += t.w;
for (const x of same) {
const a = worstAxis(win.ms), b = worstAxis(x.ms);
if (a && b) { if (a.axis === b.axis) S.gapSameAxis += t.w; else { S.gapCrossAxis += t.w; const k = `${a.axis}(差${a.need - a.got}) vs ${b.axis}(差${b.need - b.got})`; pairs.set(k, (pairs.get(k) || 0) + t.w); } }
}
continue;
}
if (same.some((x) => gapOf(x.ms) === gapOf(win.ms))) S.byIdx += t.w;
}
if (!S.noRoad) continue;
console.log(`\n── ${d.name}(chushen=${cs || '未答'})一条路都不开:${N(S.noRoad)} 格`);
console.log(` nearest 由【差几条】判:${N(S.byCount)} 由【gapOf 缺口】判:${N(S.byGap)} 由【本表的编号】判:${N(S.byIdx)}`);
if (S.byGap) console.log(` ★ gapOf 裁决时:【同轴 ${N(S.gapSameAxis)} 次】/【跨轴 ${N(S.gapCrossAxis)} 次】`);
for (const [k, v] of [...pairs].sort((a, b) => b[1] - a[1]).slice(0, 5)) console.log(` ${pad(k, 40)} ${N(v)}`);
}
+46
View File
@@ -0,0 +1,46 @@
// ★ 最终读数:【全部出自引擎自己的 kai】—— 不是复算,是产物。
import { DYNASTIES, computeRank, computeAxes } from '../src/dynasties.js';
import { SUB_REGISTRY } from '../src/talents.js';
const SUBS = SUB_REGISTRY.map((s) => s.id);
const WANGS = [0, 29, 30, 54, 55, 74, 75, 100];
const S = {}; const P = {};
for (const d of DYNASTIES) {
S[d.id] = {}; P[d.id] = { noRoad: 0, hasKai: 0 };
for (const c of d.channels) S[d.id][c.id] = { n: c.name, open: 0, walk: 0, not: 0, tier: 0, ceiling: 0, lex: 0, idx: 0, unsorted: 0 };
}
const t0 = Date.now();
for (let m = 0; m < (1 << 16); m++) {
const subs = []; for (let i = 0; i < 16; i++) if (m & (1 << i)) subs.push(SUBS[i]);
const axes = computeAxes(subs);
for (const wang of WANGS) for (const d of DYNASTIES) {
const r = computeRank({ axes, wang }, d.id);
if (!r.channel) { P[d.id].noRoad++; continue; }
if (r.kai.length > 1) P[d.id].hasKai++;
for (const k of r.kai) {
const s = S[d.id][k.channelId];
s.open++;
if (k.walked) s.walk++; else { s.not++; s[k.lostAt]++; }
}
}
}
const secs = ((Date.now() - t0) / 1000).toFixed(0);
const TOT = (1 << 16) * WANGS.length;
const pad = (s, n) => String(s).padEnd(n), num = (s, n) => String(s).padStart(n);
console.log(`\n══ 引擎自产读数(r.kai):${TOT.toLocaleString()} × ${DYNASTIES.length} 朝 = ${(TOT * DYNASTIES.length).toLocaleString()} 格 / ${secs}s ══\n`);
console.log(pad('朝/通道', 24) + num('开着', 9) + num('走了', 9) + num('开着没走', 10) + ' │' + num('tier', 8) + num('ceiling', 9) + num('lex', 9) + num('_idx', 9) + num('unsorted', 9));
console.log('─'.repeat(97));
const G = { tier: 0, ceiling: 0, lex: 0, idx: 0, unsorted: 0, not: 0 };
for (const d of DYNASTIES) {
for (const c of d.channels) {
const s = S[d.id][c.id];
for (const k of ['tier', 'ceiling', 'lex', 'idx', 'unsorted', 'not']) G[k] += s[k];
console.log(pad(`${d.id}/${c.id}`, 24) + num(s.open.toLocaleString(), 9) + num(s.walk.toLocaleString(), 9) + num(s.not.toLocaleString(), 10) +
' │' + num(s.tier.toLocaleString(), 8) + num(s.ceiling.toLocaleString(), 9) + num(s.lex.toLocaleString(), 9) + num(s.idx.toLocaleString(), 9) + num(s.unsorted, 9));
}
console.log(pad(` ↳ ${d.name}`, 24) + `一条路都不开 ${P[d.id].noRoad.toLocaleString()} 有【开着没走】的 ${P[d.id].hasKai.toLocaleString()} (${(P[d.id].hasKai / TOT * 100).toFixed(1)}%)\n`);
}
const H = Object.values(P).reduce((a, x) => a + x.hasKai, 0);
console.log(`【总计】${(TOT * DYNASTIES.length).toLocaleString()} 格中 ${H.toLocaleString()} 格(${(H / (TOT * DYNASTIES.length) * 100).toFixed(1)}%)至少有一条【开着但没走】的路。`);
console.log(`【输在哪一级】共 ${G.not.toLocaleString()} 条次:`);
for (const k of ['ceiling', 'tier', 'lex', 'idx', 'unsorted'])
console.log(` ${pad(k, 10)} ${num(G[k].toLocaleString(), 11)} (${(G[k] / G.not * 100).toFixed(1)}%)`);
@@ -0,0 +1,40 @@
// ★★★ 【nearest 是哪一级判的】—— ★ 读数【出自引擎自己的 nearestBy】,不是复算。
// ⚠ 本项目铁律:「读数出自引擎自己的 kai(不是复算)」。第三刀先前那张表是【复算】的
// (scripts/census-gap.mjs ④,它自己按 blocked 重建 pool)—— 而复算会【漏】:
// 它在 pool<2 时跳过,于是把「本表真的有理由」那一级少报了。⇒ 这一份取代它。
import { DYNASTIES, computeRank, computeAxes, AXIS_IDS } from '../src/dynasties.js';
import { chushenOf } from '../src/chushen.js';
import { SUB_REGISTRY } from '../src/talents.js';
const SUBS = SUB_REGISTRY.map((s) => s.id);
const WANGS = [0, 29, 30, 54, 55, 74, 75, 100];
const m = new Map();
for (let b = 0; b < (1 << 16); b++) { const s = []; for (let i = 0; i < 16; i++) if (b & (1 << i)) s.push(SUBS[i]);
const a = computeAxes(s); const k = AXIS_IDS.map((x) => a[x]).join(','); m.set(k, (m.get(k) || 0) + 1); }
const T = [...m].map(([k, w]) => ({ axes: Object.fromEntries(k.split(',').map((v, i) => [AXIS_IDS[i], Number(v)])), w }));
const CFG = {}; for (const d of DYNASTIES) { const x = chushenOf(d.id); CFG[d.id] = x ? [null, ...x.values.map((v) => v.id)] : [null]; }
const N = (n) => n.toLocaleString();
const pad = (s, n) => { s = String(s); let w = 0; for (const c of s) w += c.charCodeAt(0) > 0x2000 ? 2 : 1; return s + ' '.repeat(Math.max(1, n - w)); };
console.log('\n══ 【无路者】nearest 是哪一级判的 —— 读数出自引擎自己的 nearestBy ══');
console.log('══ 每朝每配置 524,288 个 profile(2,790 六轴取值 × 8 wang,带权)══\n');
console.log(pad('朝', 8) + pad('出身', 10) + '无路者'.padStart(11) + '差几条(有理由)'.padStart(16) + 'gapOf(⚠不是距离)'.padStart(18) + '编号'.padStart(9) + ' 没有 nearest');
console.log('─'.repeat(80));
const G = { count: 0, gap: 0, idx: 0 };
for (const d of DYNASTIES) for (const cs of CFG[d.id]) {
const S = { noRoad: 0, count: 0, gap: 0, idx: 0, none: 0 };
for (const t of T) for (const wang of WANGS) {
const r = computeRank({ axes: t.axes, wang, chushen: cs }, d.id);
if (r.channel) continue;
S.noRoad += t.w;
if (!r.nearest) { S.none += t.w; continue; }
S[r.nearestBy] += t.w;
}
if (!S.noRoad) continue;
for (const k of ['count', 'gap', 'idx']) G[k] += S[k];
console.log(pad(d.name, 8) + pad(cs || '未答', 10) + N(S.noRoad).padStart(11) + N(S.count).padStart(16) +
N(S.gap).padStart(18) + N(S.idx).padStart(9) + ' ' + (S.none ? N(S.none) : ''));
}
console.log('─'.repeat(80));
console.log(pad('总计', 18) + ''.padStart(11) + N(G.count).padStart(16) + N(G.gap).padStart(18) + N(G.idx).padStart(9));
const tot = G.count + G.gap + G.idx;
console.log(`\n★ 「本表真的有理由」的那一级(差几条):${N(G.count)} / ${N(tot)} = ${(G.count / tot * 100).toFixed(1)}%`);
console.log(`★★ 而其余 ${(100 - G.count / tot * 100).toFixed(1)}% 由【一个代号减出来的数】或【数组编号】决定。`);
@@ -0,0 +1,39 @@
// ★ 第八刀的代价读数:【放宽到老板的判据之后,有多少人会被多问一句「你走哪条」】
// 老板上一刀要的那个数逐字:「⚠ 而必须先说清代价:实测汉有 169,984 个 profile(32.4%)
// 至少有一条输在 _idx 的路 —— 即三个汉朝用户里有一个会被多问一句。」⇒ 现在这个数变了。
import { DYNASTIES, computeRank, computeAxes } from '../src/dynasties.js';
import { SUB_REGISTRY } from '../src/talents.js';
const SUBS = SUB_REGISTRY.map((s) => s.id);
const WANGS = [0, 29, 30, 54, 55, 74, 75, 100];
const TOT = (1 << 16) * WANGS.length;
const P = {};
for (const d of DYNASTIES) P[d.id] = { name: d.name, ask: 0, n2: 0, n3: 0, idxOnly: 0, hasLex: 0, noRoad: 0 };
for (let m = 0; m < (1 << 16); m++) {
const subs = []; for (let i = 0; i < 16; i++) if (m & (1 << i)) subs.push(SUBS[i]);
const axes = computeAxes(subs);
for (const wang of WANGS) for (const d of DYNASTIES) {
const r = computeRank({ axes, wang }, d.id);
const s = P[d.id];
if (!r.channel) { s.noRoad++; continue; }
const tie = r.zelu.tie || [];
if (tie.length <= 1) continue;
s.ask++;
if (tie.length === 2) s.n2++; else s.n3++;
const kinds = tie.filter((t) => !t.isDefault).map((t) => t.lostAt);
if (kinds.every((k) => k === 'idx')) s.idxOnly++;
if (kinds.includes('lex')) s.hasLex++;
}
}
const N = (n) => n.toLocaleString();
const pad = (s, n) => String(s).padEnd(n), num = (s, n) => String(s).padStart(n);
console.log(`\n══ 【放宽后:有多少人会被多问一句「你走哪条」】每朝 ${N(TOT)} 个 profile ══\n`);
console.log(pad('朝', 8) + num('要问', 10) + num('占比', 8) + num('2 选 1', 9) + num('≥3 选', 8) +
num('★ 只含 idx', 12) + num('★ 含 lex', 10) + num('一条路都不开', 13));
console.log('─'.repeat(80));
let A = 0;
for (const d of DYNASTIES) {
const s = P[d.id]; A += s.ask;
console.log(pad(s.name, 8) + num(N(s.ask), 10) + num((s.ask / TOT * 100).toFixed(1) + '%', 8) +
num(N(s.n2), 9) + num(N(s.n3), 8) + num(N(s.idxOnly), 12) + num(N(s.hasLex), 10) + num(N(s.noRoad), 13));
}
console.log(`\n【总计】${N(TOT * DYNASTIES.length)} 格中 ${N(A)} 格(${(A / (TOT * DYNASTIES.length) * 100).toFixed(1)}%)会被多问一句。`);
@@ -0,0 +1,77 @@
// ★ 第二刀的验收读数:【打平的那些 case,tier 和 ceiling 真的都一样吗?】
// 老板逐字:「⚠ 验证我这个判断:穷举里,打平的那些 case,tier 和 ceiling 真的都一样吗?
// 有没有「tier 一样但 ceiling 不一样」的?那种不是打平,那种引擎有理由(位阶即杠杆)。
// 跑出来,别信我。」
// ⚠ 读数全部出自引擎自己的 r.kai(产物),不是复算。
import { DYNASTIES, computeRank, computeAxes } from '../src/dynasties.js';
import { SUB_REGISTRY } from '../src/talents.js';
const SUBS = SUB_REGISTRY.map((s) => s.id);
const WANGS = [0, 29, 30, 54, 55, 74, 75, 100];
// 每一条【开着但没走】的路,按 (tier 关系, ceiling 关系) × lostAt 交叉列表
const X = {}; // key: `${tierRel}|${ceilRel}` → { lostAt: n }
const bump = (tr, cr, lostAt) => {
const k = `${tr}|${cr}`;
(X[k] || (X[k] = {}))[lostAt] = ((X[k] || {})[lostAt] || 0) + 1;
};
// zelu.options 的自检
let optBad = 0, optN = 0, optTierDiff = 0, optCeilDiff = 0;
// 引擎的排序器是不是一个自洽的全序:ranked[i] 必须真的赢 ranked[j] (i<j)
let sortViol = 0;
// 「tier 一样 && ceiling 一样」的总条次,按 lostAt 分
const SAME = {};
let totalNotWalked = 0;
const t0 = Date.now();
for (let m = 0; m < (1 << 16); m++) {
const subs = []; for (let i = 0; i < 16; i++) if (m & (1 << i)) subs.push(SUBS[i]);
const axes = computeAxes(subs);
for (const wang of WANGS) for (const d of DYNASTIES) {
const r = computeRank({ axes, wang }, d.id);
if (!r.channel) continue;
const walked = r.kai.find((k) => k.walked);
for (const k of r.kai) {
if (k.walked) continue;
totalNotWalked++;
const tr = k.tier === walked.tier ? '=' : k.tier < walked.tier ? '<' : '>';
const cr = k.ceiling === walked.ceiling ? '=' : k.ceiling < walked.ceiling ? '<' : '>';
bump(tr, cr, k.lostAt);
if (tr === '=' && cr === '=') SAME[k.lostAt] = (SAME[k.lostAt] || 0) + 1;
}
// zelu.tie:里面的每一条,tier 与 ceiling 必须与走的那条【全部相同】
const opts = (r.zelu.tie || []).map((t) => t.channelId);
if (opts.length > 1) {
optN++;
for (const id of opts) {
const k = r.kai.find((x) => x.channelId === id);
if (!k) { optBad++; continue; }
if (k.tier !== walked.tier) optTierDiff++;
if (k.ceiling !== walked.ceiling) optCeilDiff++;
}
}
// 排序自洽性:kai 的次序就是 ranked 的次序
for (let i = 0; i < r.kai.length; i++)
for (let j = i + 1; j < r.kai.length; j++)
if (r.kai[j].tier > r.kai[i].tier ||
(r.kai[j].tier === r.kai[i].tier && r.kai[j].ceiling > r.kai[i].ceiling)) sortViol++;
}
}
const secs = ((Date.now() - t0) / 1000).toFixed(0);
const N = (n) => n.toLocaleString();
console.log(`\n══ 【开着但没走】的路:(tier, ceiling) × 输在哪一级 ── ${N(totalNotWalked)} 条次 / ${secs}s ══\n`);
console.log('tier ceiling │ ' + ['ceiling', 'tier', 'lex', 'idx', 'unsorted'].map((s) => s.padStart(10)).join('') + ' 合计');
console.log('─'.repeat(78));
for (const key of Object.keys(X).sort()) {
const [tr, cr] = key.split('|');
const row = X[key];
const tot = Object.values(row).reduce((a, b) => a + b, 0);
console.log(` ${tr} ${cr} │ ` +
['ceiling', 'tier', 'lex', 'idx', 'unsorted'].map((s) => N(row[s] || 0).padStart(10)).join('') +
' ' + N(tot).padStart(10));
}
console.log('\n★ 【tier 一样 && ceiling 一样】的条次,按输在哪一级分:');
const sameTot = Object.values(SAME).reduce((a, b) => a + b, 0);
for (const k of Object.keys(SAME)) console.log(` ${k.padEnd(10)} ${N(SAME[k]).padStart(11)} (${(SAME[k] / sameTot * 100).toFixed(1)}% of ${N(sameTot)})`);
console.log(`\n★ zelu.options:${N(optN)} 格有 >1 个选项;` +
`其中 tier 不同 ${N(optTierDiff)} 次、ceiling 不同 ${N(optCeilDiff)} 次、kai 里找不到 ${N(optBad)} 次`);
console.log(`★ 排序自洽性(kai 次序里出现「后面的 tier/ceiling 反而更高」):${N(sortViol)} 次`);
@@ -0,0 +1,50 @@
// 一次性清理:上线前自测累积的数据(课堂/排期/孤儿自习/测试申请与预约)。
// 大师课堂/排期是本轮新功能,目前库里全部课堂与排期均为自测生成 → 清空重置为干净初始态。
// 大师本体(masters)与真实用户分身(twins)一律不动。
// 用法:node scripts/cleanup-test-data.js
import 'dotenv/config';
import { connectDb, getDb, closeDb } from '../src/db.js';
// 测试用的申请/预约按提交名清理(都是自测占位)。
const TEST_NAME = /QA|测试|验证|探测|阿测|走查|截图|类别验证|排期|徽匠老王|小林|徽州阿测/i;
async function main() {
await connectDb();
const db = getDb();
// 1) 课堂 + 排期:本功能刚上线,库里全是自测数据 → 清空。
const classrooms = (await db.collection('classrooms').deleteMany({})).deletedCount;
const schedules = (await db.collection('class_schedules').deleteMany({})).deletedCount;
// 2) 孤儿自习记录:twinId 指向已不存在的分身(历史级联漏删留下的)。
const liveTwins = new Set(
(await db.collection('twins').find({}).project({ _id: 1 }).toArray()).map((t) => String(t._id))
);
const learnings = await db.collection('twin_learnings').find({}).project({ _id: 1, twinId: 1 }).toArray();
const orphanIds = learnings.filter((l) => !liveTwins.has(String(l.twinId))).map((l) => l._id);
const orphanLearnings = orphanIds.length
? (await db.collection('twin_learnings').deleteMany({ _id: { $in: orphanIds } })).deletedCount
: 0;
// 3) 测试用的大师/合作申请、文旅预约(按测试名)。
const masterApps = (await db.collection('master_applications').deleteMany({ name: TEST_NAME })).deletedCount;
const partnerApps = (await db.collection('partner_applications').deleteMany({ name: TEST_NAME })).deletedCount;
const bookings = (await db.collection('bookings').deleteMany({ name: TEST_NAME })).deletedCount;
// 4) 孤儿通知:userId 指向已不存在的用户。
const liveUsers = new Set(
(await db.collection('users').find({}).project({ _id: 1 }).toArray()).map((u) => String(u._id))
);
const notifs = await db.collection('notifications').find({}).project({ _id: 1, userId: 1 }).toArray();
const orphanNotifIds = notifs.filter((n) => !liveUsers.has(String(n.userId))).map((n) => n._id);
const orphanNotifs = orphanNotifIds.length
? (await db.collection('notifications').deleteMany({ _id: { $in: orphanNotifIds } })).deletedCount
: 0;
console.log('[cleanup] done:', JSON.stringify({
classrooms, schedules, orphanLearnings, masterApps, partnerApps, bookings, orphanNotifs,
}));
await closeDb();
}
main().catch((e) => { console.error('[cleanup] failed:', e); process.exit(1); });
@@ -0,0 +1,75 @@
// 带外创建/重置【管理员账号】。
//
// 为什么需要它:超管资格 = .env 的 SUPER_ADMIN_EMAILS 白名单里的邮箱,谁注册了谁就是超管。
// 但自助注册【已被安全策略禁止用白名单邮箱】(否则管理员邮箱尚无账号时,任何人都能抢注→拿到全部后台
// 权限:改大师/生成深伪/结算分成/读 PII)。所以管理员账号只能【带外预置】——就是这个脚本。
//
// 用法(在服务器上,后端目录内):
// ADMIN_EMAIL=you@example.com ADMIN_PASSWORD='你的强密码' node scripts/create-admin.js
//
// 幂等:账号已存在 → 重置密码;不存在 → 新建。密码只用 scrypt 存 hash(与 routes/auth.js 完全一致),
// 脚本【绝不打印密码】。
import 'dotenv/config';
import crypto from 'node:crypto';
import { connectDb, getDb, closeDb } from '../src/db.js';
// 与 routes/auth.js 的 hash() 完全一致,否则建出来的号登不进去。
function hash(password, salt) {
return crypto.scryptSync(password, salt, 64).toString('hex');
}
async function run() {
const email = String(process.env.ADMIN_EMAIL || '').trim().toLowerCase();
const password = String(process.env.ADMIN_PASSWORD || '');
if (!email || !email.includes('@')) {
console.error('❌ 需要 ADMIN_EMAIL(合法邮箱)');
process.exit(1);
}
if (password.length < 8) {
console.error('❌ 需要 ADMIN_PASSWORD,且至少 8 位(管理员账号别用弱密码)');
process.exit(1);
}
await connectDb();
const db = getDb();
const now = new Date();
const salt = crypto.randomBytes(16).toString('hex');
const existing = await db.collection('users').findOne({ email });
if (existing) {
await db.collection('users').updateOne(
{ _id: existing._id },
{ $set: { salt, passwordHash: hash(password, salt), updatedAt: now } }
);
console.log(`✅ 账号已存在 → 已重置密码:${email}`);
} else {
const name = email.split('@')[0];
await db.collection('users').insertOne({
email,
name,
nickname: name,
salt,
passwordHash: hash(password, salt),
entitlements: [],
createdAt: now,
updatedAt: now,
});
console.log(`✅ 已新建账号:${email}`);
}
// 提醒:账号有了还不等于是超管——还要这个邮箱在 .env 的 SUPER_ADMIN_EMAILS 里。
const whitelist = (process.env.SUPER_ADMIN_EMAILS || '')
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
if (whitelist.includes(email)) {
console.log(`✅ 该邮箱已在 SUPER_ADMIN_EMAILS 白名单里 → 登录后即为超管。`);
} else {
console.log(`⚠ 该邮箱【不在】SUPER_ADMIN_EMAILS 白名单里,现在登录只是普通账号。`);
console.log(` 请把 .env 的 SUPER_ADMIN_EMAILS 设为 ${email} 并重启后端(grant-superadmin 工作流会做这步)。`);
}
console.log('(本脚本从不打印密码。请用你设置的密码登录,登录后建议立刻改成只有你知道的强密码。)');
await closeDb();
}
run().catch((e) => { console.error(e); process.exit(1); });
@@ -0,0 +1,12 @@
import { DYNASTIES } from '../src/dynasties.js';
let n=0;
for (const d of DYNASTIES) {
console.log(`\n=== ${d.id} (${d.era} ${d.span}) tracks=${(d.tracks||[]).map(t=>t.id).join(',')}`);
for (const c of d.channels) {
n++;
const x = c.xuan||{};
console.log(` ${c.id.padEnd(18)} gate=${JSON.stringify(c.gate||{})} seat=${x.seat} kemu=${JSON.stringify((x.kemu||[]).map(k=>k.axis+'>='+k.need))} key=${JSON.stringify(x.key||[])} keySrc=${x.keySource===null?'null':'YES'} ceil=${c.ceiling?.tier} track=${c.track||'-'}`);
console.log(` hollow=${!!c.hollow} hollowNote=${c.hollowNote?'Y':'-'} stigma=${!!c.stigma} stigmaNote=${c.stigmaNote?'Y':'-'} assim=${c.assimilation||0} assimNote=${c.assimilationNote?'Y':'-'} regular=${!!c.regular} cat=${JSON.stringify(c.category||'')} penalty=${c.penaltyIfNoWang===undefined?'-':c.penaltyIfNoWang} penaltyNote=${c.penaltyNote?'Y':'-'} absence=${(x.kemuAbsence||{}).kind||'-'}`);
}
}
console.log('\nTOTAL channels =', n);
@@ -0,0 +1,96 @@
// 导出《天下·乐 全库总稿》——所有 1303 首,每首:编号(=文件名)/歌名/家族/版本/词/背景/Suno风格/1分钟截取。
// 按家族(战役/名场面/她传/他传)+诗词(按年代)分。Word 可直接打开(.doc)。
import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BK = path.resolve(__dirname, '..');
const REPO = path.resolve(BK, '..');
const LIB = JSON.parse(readFileSync(BK + '/src/data/music-library.json', 'utf8'));
const shiciSrc = readFileSync(REPO + '/digital-me-web/src/data/shici.ts', 'utf8');
const SHI = JSON.parse(shiciSrc.match(/export const SHI: Shi\[\] = (\[[\s\S]*\]);/)[1]);
const ERAS = JSON.parse(shiciSrc.match(/export const SHI_ERAS:[^=]*= (\[[\s\S]*?\]);/)[1]);
const shiById = {}; for (const p of SHI) shiById[p.id] = p;
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const br = (s) => esc(s).replace(/\n/g, '<br/>');
const fid = (id) => id.replace(/[^a-z0-9]+/gi, '_');
const verLabel = (v) => v === 'instrumental' ? '纯配乐' : v === 'male' ? '男声' : v === 'female' ? '女声' : '化用曲';
const VORD = { instrumental: 0, male: 1, female: 2, '': 3 };
const today = new Date().toISOString().slice(0, 10);
function songBlock(s, idx) {
const src = s.id.startsWith('shici:') ? shiById[s.id.slice(6)] : null;
return `<div class="song">
<div class="sh"><span class="idx">${idx}</span> <b class="tt">${esc(s.title)}</b> <span class="ver">${verLabel(s.vocal)}</span>${s.who ? ` <span class="who">视角/主角:${esc(s.who)}</span>` : ''}</div>
<div class="fn">编号(文件名):<code>${esc(fid(s.id))}</code> &nbsp;→ Suno 出曲后用此名(.mp3)发回即自动载入</div>
${src ? `<div class="sec"><span class="lb">原诗</span><span class="src">${esc(src.author)} · ${esc(src.dynasty)}</span><div class="text">${br(src.text)}</div></div>
${src.background ? `<div class="sec"><span class="lb">背景</span><span class="v">${br(src.background)}</span></div>` : ''}
${src.kao ? `<div class="sec"><span class="lb">考证</span><span class="v">${br(src.kao)}</span></div>` : ''}` : ''}
<div class="sec"><span class="lb">词</span><div class="lyr">${br(s.lyrics)}</div></div>
<div class="sec"><span class="lb suno">Suno风格</span><div class="suno">${br(s.sunoStyle)}</div>${s.clip ? `<div class="clip">1分钟截取:${br(s.clip)}</div>` : ''}</div>
</div>`;
}
let body = ''; let n = 0;
// 锚点家族
for (const fam of ['战役', '名场面', '她传', '他传']) {
const rows = LIB.filter((s) => s.family === fam);
if (!rows.length) continue;
const bases = []; const bm = {};
for (const s of rows) { const b = s.id.split('#')[0]; if (!bm[b]) { bm[b] = []; bases.push(b); } bm[b].push(s); }
body += `<h2>${esc(fam)} <span class="cnt">${bases.length} 个 · ${rows.length} 首</span></h2>`;
for (const b of bases) {
const vs = bm[b].sort((a, c) => (VORD[a.vocal] ?? 9) - (VORD[c.vocal] ?? 9));
body += `<h3>${esc(vs[0].title)}</h3>`;
for (const s of vs) { n++; body += songBlock(s, n); }
}
}
// 诗词按年代
const shici = LIB.filter((s) => s.id.startsWith('shici:'));
if (shici.length) {
body += `<h2>诗词 <span class="cnt">${shici.length} 首</span></h2>`;
for (const era of ERAS) {
const rows = shici.filter((s) => { const p = shiById[s.id.slice(6)]; return p && p.eraSlug === era.slug; });
if (!rows.length) continue;
body += `<h3 class="era">${esc(era.label)} · ${rows.length} 首</h3>`;
for (const s of rows) { n++; body += songBlock(s, n); }
}
}
const html = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40"><head><meta charset="utf-8"/>
<title>天下·乐 全库总稿</title>
<style>
body{ font-family:"宋体","SimSun","Microsoft YaHei",serif; color:#1e1a15; line-height:1.7; font-size:11pt; }
h1{ font-size:22pt; text-align:center; color:#7a1f16; margin:0 0 3pt; }
.meta{ text-align:center; color:#888; font-size:9pt; margin-bottom:6pt; }
.legend{ text-align:center; color:#8a6d33; font-size:9pt; margin-bottom:18pt; }
h2{ font-size:15pt; color:#9e2b1e; border-bottom:2px solid #9e2b1e; padding-bottom:3pt; margin:22pt 0 8pt; }
h2 .cnt{ font-size:9pt; color:#999; font-weight:normal; }
h3{ font-size:12.5pt; color:#7a5a20; margin:14pt 0 6pt; }
h3.era{ color:#9e2b1e; }
.song{ border:1px solid #ddd; border-radius:4px; padding:9pt 11pt; margin:0 0 10pt; page-break-inside:avoid; }
.sh{ font-size:12pt; border-bottom:1px dashed #ccc; padding-bottom:3pt; margin-bottom:5pt; }
.idx{ color:#9e2b1e; font-weight:bold; }
.tt{ font-size:12.5pt; } .ver{ color:#2c6e49; font-size:9pt; margin-left:5pt; } .who{ color:#999; font-size:9pt; margin-left:5pt; }
.fn{ font-size:9pt; color:#666; margin-bottom:5pt; }
.fn code{ font-family:Consolas,monospace; background:#eee; padding:0 4pt; color:#7a1f16; }
.sec{ margin:5pt 0; }
.lb{ display:inline-block; background:#f2ead8; color:#8a6d33; font-size:8.5pt; padding:1pt 6pt; border-radius:3px; margin-right:6pt; }
.lb.suno{ background:#e7efe8; color:#2c6e49; }
.src{ color:#8a6d33; font-size:9pt; }
.text{ padding-left:10pt; color:#444; margin-top:3pt; }
.v{ color:#444; font-size:10pt; }
.lyr{ margin-top:3pt; padding:6pt 10pt; background:#faf7ef; border-left:3px solid #c8a25e; color:#2a2a2a; font-size:10.5pt; }
.suno{ margin-top:3pt; padding:6pt 10pt; background:#f3f8f4; border-left:3px solid #2c6e49; color:#234; font-size:9.5pt; font-family:Consolas,"Courier New",monospace; }
.clip{ color:#2c6e49; font-size:9pt; margin-top:3pt; }
</style></head><body>
<h1>天下 · 乐 &nbsp;全库总稿</h1>
<div class="meta">共 ${LIB.length} 首 · 母版「新东方·电影史诗·黯黑」· 生成 ${today}</div>
<div class="legend">每首含 编号(=文件名) / 歌名 / 词 / 背景 / Suno风格 / 1分钟截取。<br/>命名闭环:Suno 出曲后用「编号」当文件名(.mp3)发回 → 超管上传页按名自动载入天下音乐。<br/>⚠ 本稿含 Suno 风格指令(制作配方),仅你自己看/喂 Suno,勿外传。</div>
${body}
</body></html>`;
const OUT = 'C:/Users/solut/Downloads/天下乐-全库总稿.doc';
writeFileSync(OUT, '\uFEFF' + html, 'utf8');
console.log('已导出:', OUT, '| 共', n, '首');
@@ -0,0 +1,96 @@
// 生成天下·乐 确权留痕交付物:①确权存证清单(哈希·供时间戳/区块链存证) ②确权登记清单.csv(供著作权登记) ③人类作者性说明.md ④确权档案-全.json(逐首)。
// 落地:老板的 Downloads/天下确权留痕/ + 仓库 provenance/(git 时间戳锚点·仅哈希清单,不含母带)。
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { buildDossier, buildManifest, contentHash } from '../src/services/provenance.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const BACKEND = path.resolve(__dirname, '..');
const REPO = path.resolve(BACKEND, '..');
const songs = JSON.parse(readFileSync(path.join(BACKEND, 'src/data/music-library.json'), 'utf8'));
// 诗曲的公版源诗信息(shici.ts)
const shiciSrc = readFileSync(path.join(REPO, 'digital-me-web/src/data/shici.ts'), 'utf8');
const SHI = JSON.parse(shiciSrc.match(/export const SHI: Shi\[\] = (\[[\s\S]*\]);/)[1]);
const shiById = {}; for (const p of SHI) shiById[p.id] = p;
const sourceFor = (s) => {
if (!String(s.id).startsWith('shici:')) return null;
const p = shiById[s.id.slice('shici:'.length)];
return p ? { title: p.title, author: p.author, dynasty: p.dynasty, text: p.text } : null;
};
const generatedAt = new Date().toISOString();
const OWNER = '(运营主体法律全称·待填)· digitme.cn';
// ── 交付目录 ──
const OUT = 'C:/Users/solut/Downloads/天下确权留痕';
mkdirSync(OUT, { recursive: true });
const REPO_PROV = path.join(BACKEND, 'provenance');
mkdirSync(REPO_PROV, { recursive: true });
// ① 存证清单(哈希) —— 落 Downloads + 仓库锚点
const manifest = buildManifest(songs, { generatedAt });
writeFileSync(path.join(OUT, '确权存证清单.json'), JSON.stringify(manifest, null, 2), 'utf8');
writeFileSync(path.join(REPO_PROV, '确权存证清单.json'), JSON.stringify(manifest, null, 2), 'utf8');
// ② 逐首确权档案(全) —— 登记/律师用
const dossiers = songs.map((s) => buildDossier(s, { source: sourceFor(s), owner: OWNER }));
writeFileSync(path.join(OUT, '确权档案-全.json'), JSON.stringify({ generatedAt, count: dossiers.length, dossiers }, null, 2), 'utf8');
// ③ 登记清单 CSV
const esc = (v) => `"${String(v == null ? '' : v).replace(/"/g, '""')}"`;
const anchorCount = songs.filter((s) => !String(s.id).startsWith('shici:')).length;
const shiciCount = songs.length - anchorCount;
const rows = [['曲id', '标题', '家族', '词的类别', '化用来源(公版)', '著作权人', '内容哈希SHA256'].map(esc).join(',')];
for (const s of songs) {
const src = sourceFor(s);
rows.push([
s.id, s.title, s.family,
src ? '化用公版古诗(独创改写受保护)' : '完全原创词',
src ? `${src.title}·${src.author}·${src.dynasty}(公有领域)` : '—',
OWNER, contentHash(s),
].map(esc).join(','));
}
writeFileSync(path.join(OUT, '确权登记清单.csv'), '\uFEFF' + rows.join('\r\n'), 'utf8'); // BOM 便于 Excel 识别 UTF-8
// ④ 人类作者性说明.md
const narrative = `# 天下·乐 曲库 · 人类作者性与确权说明
> 生成时点:${generatedAt}
> 曲目总数:${songs.length}(完全原创词 ${anchorCount} 首 · 化用公版古诗 ${shiciCount} 首)
> 著作权人:${OWNER}
## 一、作品构成与权利定位(三块资产,强弱不同)
1. **歌词(文字作品)** —— 由平台确定创作方向、进行原创写作或对**公有领域**古诗的现代口语化用/重写,并多轮编辑定稿。**人类独创性最强,是最稳的著作权主张。**
2. **成品录音(录音制品/录音制作者权·邻接权)** —— 平台作为录音制品制作者,对**成品音频文件**依法享有复制、发行、信息网络传播权。**对“盗播/盗传我方母带文件”最能打,且基本不受 AI 独创性之争影响。**
3. **曲(音乐作品)** —— 借助 Suno(平台持最高级付费会员,其服务条款授予输出的所有权/商用权)在**人类创作方向、风格选择、编排与甄选**下生成。纯 AI 生成成分的可版权性依个案“人类独创性投入”认定,**不对外主张旋律独占**。
## 二、人类独创性投入(可版权性的证据基础)
每首歌均有以下人类智力投入,记录见《确权档案-全.json》逐首字段:
- **创作方向 / 风格 brief**:由人确定题材、情绪、世界级音乐流派方向;
- **歌词创作与化用改写**:原创歌词;或对公版古诗做结构重组、口语化重写(化用部分为独创表达);
- **生成指令(prompt)**:投喂 Suno 的详细创作指令(配器/节奏/编曲/音色),为人类创作意图的具体化;
- **甄选**:从多版本生成结果中人工甄选定稿、淘汰其余;
- **编排与截取决策**:三版本(纯配乐/男声/女声)、正常版/1分钟版的编排与截取。
## 三、来源合法性(反向侵权自查结论)
- 化用的古诗**全部为公有领域**(诗词篇作者最晚为龚自珍,卒于 1841 年,远逾“作者卒后 50 年”保护期);经核查**无近现代(作者卒于 1976 年及以后)在保护期内的诗人作品**,不存在使用他人在保护期作品的情形。
- 完全原创词不涉及第三方作品。
## 四、确权动作(按此清单执行)
1. **存证(现在做·最快)**:对《确权存证清单.json》做一次**可信时间戳/区块链存证**(如联合信任时间戳/至信链/蚂蚁链/司法链),即以最小成本固定全库“在先创作”时点;masterHash 为全库汇总指纹。
2. **登记(分批)**:按《确权登记清单.csv》,优先登记**变现主打 + 最易被盗**曲目的**歌词(文字作品)**与**成品录音(录音制品)**;曲以弱证据心态登记。
3. **权利来源留存**:保存 Suno 最高级会员凭证 + 生成时点的 ToS 版本快照 + 每首生成记录(补权属链)。
4. **技术配套(已做/待补)**:母带私有存储 + 会员短效签名 URL(已做);**取证水印**(按会员打唯一隐式水印,泄漏可追人)待补。
> ⚠ 法律定性以知识产权律师意见为准。AIGC 可版权性在中国为个案裁量、尚未定论;本材料用于把“谁、何时、拥有什么、有何人类投入”固化为证据,不等于对“曲”能取得独占版权。
`;
writeFileSync(path.join(OUT, '人类作者性说明.md'), narrative, 'utf8');
console.log('确权留痕已生成:');
console.log(' 曲目', songs.length, '(原创词', anchorCount, '· 化用公版', shiciCount, ')');
console.log(' masterHash', manifest.masterHash_SHA256);
console.log(' → Downloads/天下确权留痕/:确权存证清单.json · 确权登记清单.csv · 人类作者性说明.md · 确权档案-全.json');
console.log(' → 仓库 provenance/确权存证清单.json(git 时间戳锚点)');
@@ -0,0 +1,111 @@
// 生成《天下·乐 确权操作指南》Word 版(.doc·HTML)到老板 Downloads/天下确权留痕/。排版友好、可照做、带勾选清单。
import { writeFileSync } from 'node:fs';
const today = new Date().toISOString().slice(0, 10);
const html = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40"><head><meta charset="utf-8"/>
<title>天下·乐 确权操作指南</title>
<style>
body{ font-family:"宋体","SimSun","Microsoft YaHei",serif; color:#1e1a15; line-height:1.75; font-size:11pt; }
h1{ font-size:22pt; text-align:center; color:#7a1f16; margin:0 0 2pt; letter-spacing:1pt; }
.sub{ text-align:center; color:#8a6d33; font-size:11pt; margin-bottom:2pt; }
.meta{ text-align:center; color:#999; font-size:9pt; margin-bottom:16pt; }
h2{ font-size:14.5pt; color:#9e2b1e; border-bottom:2px solid #9e2b1e; padding-bottom:3pt; margin:20pt 0 8pt; }
h3{ font-size:12pt; color:#8a6d33; margin:12pt 0 4pt; }
p{ margin:5pt 0; }
.box{ border:1px solid #d8cdb0; background:#faf7ef; border-radius:5px; padding:9pt 12pt; margin:8pt 0; }
.tip{ border-left:4px solid #c8a25e; background:#faf6ea; padding:6pt 12pt; margin:8pt 0; }
.warn{ border-left:4px solid #c0392b; background:#fdf1ef; padding:6pt 12pt; margin:8pt 0; color:#7a1f16; }
.ck{ margin:4pt 0; }
.ck b{ color:#9e2b1e; }
table{ border-collapse:collapse; width:100%; margin:8pt 0; font-size:10pt; }
th,td{ border:1px solid #d8cdb0; padding:5pt 8pt; text-align:left; vertical-align:top; }
th{ background:#f2ead8; color:#7a5a20; }
code{ font-family:Consolas,"Courier New",monospace; background:#eee; padding:0 3pt; font-size:9.5pt; }
.big{ font-size:12.5pt; color:#7a1f16; font-weight:bold; }
.step{ font-size:13pt; color:#fff; background:#9e2b1e; border-radius:50%; padding:1pt 8pt; margin-right:6pt; }
.small{ color:#888; font-size:9pt; }
</style></head><body>
<h1>天下 · 乐 &nbsp;确权操作指南</h1>
<div class="sub">你做的歌(原创词 × Suno 曲)· 怎么把知识产权保护好</div>
<div class="meta">照着做即可 · ${today} · 共 1303 首(作品 865 个)</div>
<div class="tip"><b>一句话</b>:你的护城河 = <b>词(原创)+ 成品录音(邻接权,专治盗播盗传母带)</b>;<b>曲(纯 AI)不主张旋律独占</b>。“能商用 ≠ 能独占”。把力气压在词和录音上。</div>
<h2>你手上已有的文件(本文件夹内)</h2>
<table>
<tr><th>文件</th><th>用途</th></tr>
<tr><td><b>确权存证清单.json</b></td><td>全库 1303 首内容指纹 + 汇总 masterHash。<b>第①步存证就传它。</b></td></tr>
<tr><td><b>确权登记清单.csv</b></td><td>1303 行,分批著作权登记用。</td></tr>
<tr><td><b>人类作者性说明.md</b></td><td>证明“人类独创性投入”的说明书,给律师 / 登记用。</td></tr>
<tr><td><b>确权档案-全.json</b></td><td>逐首完整档案(词 / 化用来源 / 创作投入 / 哈希)。</td></tr>
<tr><td><b>本指南(.doc)</b></td><td>你正在看的这份。</td></tr>
</table>
<div class="box"><b>总顺序:</b> ① 存证(今天·10 分钟)→ ② 存 Suno 权利来源(今天·10 分钟)→ ③ 著作权登记(本周起·分批)→ ④ 找律师定稿(并行)。<br/>①② 今天做完,“在先创作 + 权利来源”证据链当天立住;③④ 把它做成法律级、可打官司。</div>
<h2><span class="step">1</span> 存证 —— 今天最优先,10 分钟锁住全库时点</h2>
<p><b>目的</b>:给全库盖一个不可篡改的“<b>这些歌在 ${today} 已经存在</b>”时间戳。以后谁抄你,你能证明你在先。</p>
<h3>做法(任选一个平台,从简单的 SaaS 入手)</h3>
<p class="big">推荐 A · 区块链存证 SaaS(界面最简单)</p>
<div class="ck">☐ <b>1)</b> 打开 <b>保全网(baoquan.com)</b> / 易保全 / 版权家 之一,注册并做实名认证。</div>
<div class="ck">☐ <b>2)</b> 找“<b>文件存证 / 数据存证 / 版权存证</b>”功能。</div>
<div class="ck">☐ <b>3)</b> 上传 <code>确权存证清单.json</code> <b>这一个文件</b>,提交。</div>
<div class="ck">☐ <b>4)</b> 下载它给你的<b>存证证书</b>(含哈希、时间、区块链交易号),存回本文件夹。</div>
<p class="big">备选 B · 可信时间戳</p>
<p>联合信任时间戳服务中心(<b>tsa.cn</b>,国家授时中心背书)。注册后对文件申请时间戳,拿证书文件存好。法院认可度高,界面略传统。</p>
<div class="tip"><b>关键点</b><br/>• 你传的是<b>哈希清单</b>、不是母带 —— 不泄露任何音频,安全。<br/>• 这一件存证 = 固定全库 1303 首的在先时点(清单里每首都有指纹,能一一对应回内容)。<br/>• <b>主打曲另加做</b>:挑十几首最看重、最怕被抄的,把它的<b>歌词全文 + 音频</b>也各单独存一次(更硬,维权时不用解释“哈希怎么对应内容”)。要哪些告诉我,我单独打包成待传文件。</div>
<p class="small">费用量级:SaaS 单条几元~几十元 / 或几百~上千元一年套餐;时间戳按条计(元级)。以平台当前价为准。</p>
<h2><span class="step">2</span> 存 Suno 权利来源 —— 今天,10 分钟</h2>
<p><b>目的</b>:证明“曲的商用权来自你持最高级会员、由 Suno 条款授予”。条款会改版,必须<b>固定你生成时那一版</b>。</p>
<div class="ck">☐ <b>1)</b> 截图 Suno 账号页,显示你的<b>会员等级</b>(Pro / Premier)和付费记录。</div>
<div class="ck">☐ <b>2)</b> 打开 Suno 的 <b>Terms of Service</b> 页,<b>整页存成 PDF</b>(浏览器打印→另存 PDF),文件名带日期,如 <code>Suno-ToS-${today}.pdf</code>,存进本文件夹。</div>
<div class="ck">☐ <b>3)</b> 每首在 Suno 库里的<b>生成记录</b>(生成时间 / 曲目 ID)能导出就导出、不能就截图归档。</div>
<div class="ck">☐ <b>4)</b> 会员<b>别断档</b>(权利来源要持续有效)。</div>
<h2><span class="step">3</span> 著作权登记 —— 本周开始,分批(不必一次全登)</h2>
<p><b>目的</b>:拿国家登记证书,形成“权属推定”——维权、下架、授权、融资都好用。</p>
<h3>去哪登(建议先看本地)</h3>
<p>• <b>中国版权保护中心</b>:ccopyright.com.cn 网上登记系统(全国通用)。<br/>• <b>地方版权局 / 登记中心</b>:常更快、更便宜、有的有财政补贴。你在成都,可看<b>四川省版权登记</b>。</p>
<h3>登什么、按什么类别(登对了才最能打)</h3>
<table>
<tr><th>成分</th><th>登记类别</th><th>优先级</th></tr>
<tr><td>歌词</td><td>文字作品</td><td><b>第一优先</b>(人类独创性最强、最稳)</td></tr>
<tr><td>成品录音</td><td>录音制品 / 录音制作者</td><td><b>第二优先</b>(专治盗播盗传母带)</td></tr>
<tr><td>曲</td><td>音乐作品</td><td>弱证据心态登,<b>别指望旋律独占</b></td></tr>
</table>
<p><b>怎么分批</b>:按 CSV 挑<b>变现主打 + 最怕被抄</b>的十几首先登全套(词+录音),其余先靠第①步存证兜底,回头再分批登。</p>
<div class="warn"><b>⚠ AIGC 怎么填,务必让律师把关这一栏</b><br/>著作权人填你的公司 / 个人;涉及 AI 辅助的要<b>如实、但突出人类投入</b>(创作方向、原创词 / 化用改写、prompt、甄选、编排——《人类作者性说明》就是给这个用的)。填法不当可能导致登记被撤、或维权时被反咬。</div>
<p class="small">费用/时间量级:官费每件几十~几百元;正常审核约 30 个工作日、可加急;找代理另加服务费。以受理机构当前标准为准。</p>
<h2><span class="step">4</span> 找律师(和 ②③ 并行)</h2>
<p>把本文件夹一起发给他,让他做三件:</p>
<div class="ck">☐ <b>权属意见书</b>:Suno 输出在中国的权利衔接 + 人类独创性如何满足(可引北京互联网法院 2023 AI 文生图案)。</div>
<div class="ck">☐ <b>登记口径</b>:AIGC 部分在登记表里怎么如实又稳妥地写。</div>
<div class="ck">☐ <b>协议起草</b>:用户协议(用户只得使用权、禁商用转售)+ 平台版权声明 + AIGC 使用说明。</div>
<p class="small">费用量级:意见书 + 协议起草大致几千~上万元;维权 / 公证按需另算。</p>
<h2>三块资产的法律地位(想清楚,别当一首歌一个版权)</h2>
<table>
<tr><th>资产</th><th>权利类型</th><th>强弱 / 说明</th></tr>
<tr><td><b>词</b>(原创 + 化用公版古诗)</td><td>文字作品 · 著作权</td><td><b>最强</b>(前提:<b>经你实质审改 / 甄选</b>,人类独创性才立得住)</td></tr>
<tr><td><b>成品录音</b></td><td>录音制品 · 录音制作者权(邻接权)</td><td><b>最能打盗播盗传</b>;依据是制作投入,<b>不看独创性</b>,基本不受 AI 之争影响</td></tr>
<tr><td><b>曲</b>(Suno 生成)</td><td>音乐作品 · 著作权</td><td><b>最弱</b>;纯 AI 成分重,可版权性依个案人类投入认定,<b>不主张旋律独占</b></td></tr>
</table>
<div class="warn"><b>AIGC 可版权性 —— 诚实提醒</b><br/>中国是个案裁量、尚未定论(北京互联网法院 2023 案支持“有独创性人类投入即受保护”,但属个案);美国不给纯 AI 生成物登记。<b>“AI 帮你写” ≠ 你自动拿到独占版权</b> —— 能不能,取决于你这边的人类创作投入。<b>你现在逐首审改这 646 首诗词,正是把“AI 初稿”变成“你主导创作的作品”的关键动作</b>:审改要留痕,我把改动记进《人类作者性说明》。</div>
<h2>反向侵权自查(结论:安全)</h2>
<p>诗词篇 646 首<b>全部为先秦至明清古典诗词</b>,最晚的作者龚自珍卒于 1841 年,远逾“作者卒后 50 年”保护期,<b>均为公有领域</b>;经核查<b>无近现代(作者卒于 1976 年及以后)在保护期内的诗人作品</b>,<b>无需下架</b>。源诗公版、你的化用原创,两个方向都干净。</p>
<h2>还差的一刀(技术 · 等你 mp3 到位再说)</h2>
<p><b>取证水印</b>:每个会员 / 每次下载打一个听不见的唯一水印,一旦有人把满血曲传出去,你能<b>追到是谁泄的</b>。开源方案(audiowmark)我可以接进现有会员下载链路,只占共享服务器一点算力 / 磁盘,你发话我就做。</p>
<div class="warn">本指南为操作梳理,<b>法律定性以知识产权律师意见为准</b>。存证 / 登记解决“谁、何时、拥有什么、有何人类投入”的举证;能否就“曲”取得独占版权由法院个案认定。</div>
</body></html>`;
const OUT = 'C:/Users/solut/Downloads/天下确权留痕/天下确权操作指南.doc';
writeFileSync(OUT, '\uFEFF' + html, 'utf8');
console.log('已生成 Word 版:', OUT);
@@ -0,0 +1,80 @@
// 导出《天下·拾遗 诗词篇 全稿(审阅版)》——供老板离线通读审定。每首:原诗+作者其时+考证+魂句+化用曲词。
// 输出 Word 可直接打开的 .doc(HTML)到 Downloads。
import { readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO = path.resolve(__dirname, '../..');
const BACKEND = path.resolve(__dirname, '..');
const src = readFileSync(path.join(REPO, 'digital-me-web/src/data/shici.ts'), 'utf8');
const SHI = JSON.parse(src.match(/export const SHI: Shi\[\] = (\[[\s\S]*\]);/)[1]);
const ERAS = JSON.parse(src.match(/export const SHI_ERAS:[^=]*= (\[[\s\S]*?\]);/)[1]);
// Suno 风格指令在后端曲库(前端已剥配方)。审阅稿给老板自己看+喂Suno,带上。id: shici.ts 的 p.id ↔ music-library 的 shici:<id>
const LIB = JSON.parse(readFileSync(path.join(BACKEND, 'src/data/music-library.json'), 'utf8'));
const sunoMap = {}; const clipMap = {};
for (const s of LIB) if (String(s.id).startsWith('shici:')) { const k = s.id.slice('shici:'.length); sunoMap[k] = s.sunoStyle || ''; clipMap[k] = s.clip || ''; }
const esc = (s) => String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const br = (s) => esc(s).replace(/\n/g, '<br/>');
const generatedAt = new Date().toISOString().slice(0, 10);
let body = '';
let n = 0;
for (const era of ERAS) {
const list = SHI.filter((p) => p.eraSlug === era.slug);
if (!list.length) continue;
body += `<h2 class="era">${esc(era.label)} <span class="cnt">${list.length} 首</span></h2>`;
for (const p of list) {
n++;
body += `<div class="poem">
<div class="ph"><span class="idx">${n}</span> <b class="tt">${esc(p.title)}</b> <span class="au">${esc(p.author)}</span> <span class="dy">${esc(p.dynasty)}</span> <span class="pid">[${esc(p.id)}]</span></div>
<div class="sec"><span class="lb">原诗</span><div class="text">${br(p.text)}</div></div>
${p.authorBrief ? `<div class="sec"><span class="lb">作者其时</span><div class="v">${br(p.authorBrief)}</div></div>` : ''}
${p.background ? `<div class="sec"><span class="lb">创作背景</span><div class="v">${br(p.background)}</div></div>` : ''}
${p.kao ? `<div class="sec"><span class="lb">考证</span><div class="v">${br(p.kao)}</div></div>` : ''}
${p.soulLine ? `<div class="sec"><span class="lb">魂句</span><div class="v soul">${br(p.soulLine)}</div></div>` : ''}
${p.song && p.song.lyrics ? `<div class="sec"><span class="lb">化用曲词</span><div class="lyr">${br(p.song.lyrics)}</div>${p.song.embedded ? `<div class="emb">嵌入原句:${br(p.song.embedded)}</div>` : ''}</div>` : '<div class="sec nolyr">(此首暂无化用曲词)</div>'}
${sunoMap[p.id] ? `<div class="sec"><span class="lb suno">Suno 风格指令</span><div class="suno">${br(sunoMap[p.id])}</div>${clipMap[p.id] ? `<div class="clip">1分钟截取:${br(clipMap[p.id])}</div>` : ''}</div>` : '<div class="sec nolyr">(此首暂无 Suno 风格指令)</div>'}
</div>`;
}
}
const html = `<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns="http://www.w3.org/TR/REC-html40"><head><meta charset="utf-8"/>
<title>天下·拾遗 诗词篇 全稿(审阅版)</title>
<style>
body{ font-family:"宋体","SimSun","Microsoft YaHei",serif; color:#1a1a1a; line-height:1.7; font-size:11pt; }
h1{ font-size:22pt; text-align:center; margin:0 0 4pt; }
.meta{ text-align:center; color:#666; font-size:9pt; margin-bottom:18pt; }
.legend{ text-align:center; color:#8a6d33; font-size:9pt; margin-bottom:20pt; }
h2.era{ font-size:15pt; color:#9e2b1e; border-bottom:2px solid #9e2b1e; padding-bottom:3pt; margin:22pt 0 10pt; }
.era .cnt{ font-size:9pt; color:#999; font-weight:normal; }
.poem{ border:1px solid #ddd; border-radius:4px; padding:10pt 12pt; margin:0 0 12pt; page-break-inside:avoid; }
.ph{ font-size:12pt; margin-bottom:6pt; border-bottom:1px dashed #ccc; padding-bottom:4pt; }
.idx{ color:#9e2b1e; font-weight:bold; }
.tt{ font-size:13pt; }
.au{ color:#8a6d33; margin-left:6pt; }
.dy{ color:#999; font-size:9pt; margin-left:6pt; }
.pid{ color:#bbb; font-size:8pt; font-family:Consolas,monospace; float:right; }
.sec{ margin:6pt 0; }
.lb{ display:inline-block; background:#f2ead8; color:#8a6d33; font-size:8.5pt; padding:1pt 6pt; border-radius:3px; margin-right:6pt; }
.text{ margin-top:4pt; padding-left:10pt; color:#333; }
.v{ display:inline; color:#444; font-size:10pt; }
.soul{ color:#9e2b1e; }
.lyr{ margin-top:4pt; padding:6pt 10pt; background:#faf7ef; border-left:3px solid #c8a25e; color:#2a2a2a; font-size:10.5pt; }
.emb{ color:#8a6d33; font-size:9pt; margin-top:3pt; }
.lb.suno{ background:#e7efe8; color:#2c6e49; }
.suno{ margin-top:4pt; padding:6pt 10pt; background:#f3f8f4; border-left:3px solid #2c6e49; color:#234; font-size:10pt; font-family:Consolas,"Courier New",monospace; }
.clip{ color:#2c6e49; font-size:9pt; margin-top:3pt; }
.nolyr{ color:#c0392b; font-size:9pt; }
</style></head><body>
<h1>天下 · 拾遗 · 诗词篇 全稿(审阅版)</h1>
<div class="meta">共 ${SHI.length} 首 · 先秦至明清 · 每首含【化用曲词 + Suno 风格指令】· 生成 ${generatedAt} · 供离线审定</div>
<div class="legend">审阅用法:要改的请记下左上角编号或 [id](如 [tang-012]),回头告诉我批量改;改定后再一起做确权。<br/>⚠ 本稿含 Suno 风格指令(=制作配方,仅你自己看/喂 Suno 用,前端已对用户隐藏,勿外传)。</div>
${body}
</body></html>`;
const OUT = 'C:/Users/solut/Downloads/天下诗词篇全稿-审阅版-含Suno风格.doc';
writeFileSync(OUT, '\uFEFF' + html, 'utf8');
console.log('已导出:', OUT);
console.log('共', SHI.length, '首;有化用曲词的', SHI.filter((p) => p.song && p.song.lyrics).length, '首');
+98
View File
@@ -0,0 +1,98 @@
// 端到端用户走查:进站→逛大师墙→大师详情→和大师聊→文旅→路线→报名→注册→养分身→登出。
// 每步截图 + 抓控制台报错/失败请求。用法:node scripts/journey.mjs <shotsDir>
// 前置:chrome 带 --remote-debugging-port=9222 已打开 http://localhost:5173/
import WebSocket from 'ws';
import { writeFileSync } from 'node:fs';
const OUT = process.argv[2];
const BASE = 'http://localhost:5173';
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const uniqEmail = `walk${Date.now()}@test.com`;
async function targetWs() {
for (let i = 0; i < 30; i++) {
const ts = await (await fetch('http://localhost:9222/json/list')).json();
const t = ts.find((x) => x.type === 'page' && /localhost:5173/.test(x.url));
if (t) return t.webSocketDebuggerUrl;
await wait(300);
}
throw new Error('no 5173 target');
}
const report = [];
let curStep = 'init';
const errs = []; // {step, kind, detail}
async function main() {
const ws = await targetWs();
const sock = new WebSocket(ws, { perMessageDeflate: false, maxPayload: 64 * 1024 * 1024 });
let id = 0;
const pending = new Map();
const send = (method, params = {}) =>
new Promise((resolve) => { const mid = ++id; pending.set(mid, resolve); sock.send(JSON.stringify({ id: mid, method, params })); });
await new Promise((r) => sock.on('open', r));
sock.on('message', (buf) => {
const m = JSON.parse(buf.toString());
if (m.id && pending.has(m.id)) { pending.get(m.id)(m.result); pending.delete(m.id); return; }
if (m.method === 'Runtime.consoleAPICalled' && (m.params.type === 'error')) {
const txt = (m.params.args || []).map((a) => a.value || a.description || '').join(' ').slice(0, 160);
if (txt) errs.push({ step: curStep, kind: 'console.error', detail: txt });
}
if (m.method === 'Runtime.exceptionThrown') {
errs.push({ step: curStep, kind: 'exception', detail: (m.params.exceptionDetails?.exception?.description || m.params.exceptionDetails?.text || '').slice(0, 160) });
}
if (m.method === 'Network.responseReceived') {
const st = m.params.response?.status, u = m.params.response?.url || '';
if (st >= 400 && /\/api\//.test(u)) errs.push({ step: curStep, kind: 'http ' + st, detail: u.replace('http://localhost:3701', '') });
}
});
await send('Page.enable'); await send('Runtime.enable'); await send('Network.enable');
const evalJs = async (expr) => (await send('Runtime.evaluate', { expression: expr, returnByValue: true }))?.result?.value;
const nav = async (path) => { await send('Page.navigate', { url: BASE + path }); await wait(2600); };
const clickText = (txt) => evalJs(`(()=>{const t=${JSON.stringify(txt)}.replace(/\\s+/g,'');const el=[...document.querySelectorAll('a,button,[role=button],label')].find(e=>(e.textContent||'').replace(/\\s+/g,'').includes(t));if(el){el.click();return true}return false})()`);
const clickSel = (sel) => evalJs(`(()=>{const el=document.querySelector(${JSON.stringify(sel)});if(el){el.click();return true}return false})()`);
const fill = (ph, v) => evalJs(`(()=>{const el=[...document.querySelectorAll('input,textarea')].find(e=>(e.placeholder||'').includes(${JSON.stringify(ph)}));if(el){el.value=${JSON.stringify(v)};el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));return true}return false})()`);
const info = () => evalJs(`JSON.stringify({path:location.pathname, h1:document.querySelector('h1')?.textContent?.trim().slice(0,24), authed:!!localStorage.getItem('dm_token'), nav:[...document.querySelectorAll('header a,header button')].map(e=>e.textContent.trim()).filter(Boolean).slice(0,8)})`);
const shot = async (name) => { const { data } = await send('Page.captureScreenshot', { format: 'png' }); writeFileSync(`${OUT}/${name}.png`, Buffer.from(data, 'base64')); };
const step = async (label, fn) => { curStep = label; const before = errs.length; await fn(); const i = JSON.parse(await info() || '{}'); report.push({ label, ...i, newErrs: errs.length - before }); };
// ── 走查 ──
await step('01-进站落广场', async () => { await nav('/'); await shot('01-plaza'); });
await step('02-点大师卡', async () => { await clickSel('a[href^="/masters/"]'); await wait(2200); await shot('02-master'); });
await step('03-和大师聊(点)', async () => { await clickSel('a[href^="/me/"]'); await wait(2500); await shot('03-chat-open'); });
await step('04-输入框可用(本地无key不真发)', async () => {
await fill('说点什么', '你好,介绍下你自己');
await wait(600); await shot('04-chat-typed');
});
await step('05-返回并进大师文旅', async () => { await nav('/tours'); await shot('05-tours'); });
await step('06-看路线详情', async () => { await clickSel('a[href^="/tours/"]'); await wait(2400); await shot('06-tour-detail'); });
await step('07-填报名交定金', async () => {
await fill('您的称呼', '走查客');
await fill('手机号', '13800008888');
await clickText('报名·交预定金锁位');
await wait(400);
await clickText('报名并支付预定金');
await wait(2800); await shot('07-booked');
});
await step('08-去登录页', async () => { await nav('/login'); await shot('08-login'); });
await step('09-注册新号', async () => {
await clickText('去注册'); await wait(600);
await fill('you@example.com', uniqEmail);
await fill('至少 6 位', 'walk123456');
await evalJs(`(()=>{const el=[...document.querySelectorAll('input')].find(e=>/昵称|名字|称呼|name/i.test(e.placeholder||''));if(el){el.value='走查用户';el.dispatchEvent(new Event('input',{bubbles:true}))}})()`);
await clickText('注册'); await wait(3000); await shot('09-after-register');
});
await step('10-养分身/工作台', async () => { await clickText('养我的分身'); await wait(2600); await shot('10-dashboard'); });
await step('11-我的分身导航', async () => { await clickText('我的分身'); await wait(2200); await shot('11-mine'); });
await step('12-登出', async () => { await clickText('退出'); await wait(2200); await shot('12-logout'); });
writeFileSync(`${OUT}/report.json`, JSON.stringify({ report, errs }, null, 2));
console.log('=== 走查步骤 ===');
for (const r of report) console.log(`${r.label} → path:${r.path} authed:${r.authed} h1:"${r.h1 || ''}" 本步报错:${r.newErrs}`);
console.log('\n=== 报错/失败请求汇总 ===');
if (!errs.length) console.log('(无)');
for (const e of errs) console.log(`[${e.step}] ${e.kind}: ${e.detail}`);
sock.close(); process.exit(0);
}
main().catch((e) => { console.error(e); process.exit(1); });
@@ -0,0 +1,124 @@
// 存量媒体迁移 → MinIO(老板 2026-08-05:文件存储改用 MinIO)。
//
// 把库里指向【本地 /uploads】(可选:也含【阿里云 OSS】)的媒体 URL,下载字节 → 传到 MinIO → 回写库里的 URL。
// 深度遍历每条文档的所有字符串字段(自动覆盖 media/cover/videoUrl/bundle.* 等嵌套,无需逐字段列举)。
//
// 前提:
// ① 在【服务器后端目录】跑(本地 /uploads 文件在这台机上才读得到)。
// ② .env 已配好 MINIO_*(minioConfigured 必须为真,否则拒绝运行)。
//
// 用法:
// node scripts/migrate-media-to-minio.mjs # 【干跑】只统计不写库(强烈建议先跑这个)
// node scripts/migrate-media-to-minio.mjs --commit # 真迁移(下载→传 MinIO→回写库 URL)
// node scripts/migrate-media-to-minio.mjs --commit --include-oss # 连 OSS 上的也迁到 MinIO
// node scripts/migrate-media-to-minio.mjs --commit creations create_jobs # 只迁指定集合
//
// 幂等:MinIO key = 原文件名(basename,已带时间戳/随机段);重复跑同一文件覆盖同 key,库里 URL 已是 MinIO 的会跳过。
// 说明:只迁【http(s) 公开媒体 URL】;母带(存的是 {key,driver} 对象,非 URL)不在此列,如需迁移母带另说。
import 'dotenv/config';
import fs from 'node:fs/promises';
import path from 'node:path';
import { connectDb, getDb, closeDb } from '../src/db.js';
import { config } from '../src/config.js';
import { saveMedia, minioConfigured } from '../src/services/storage.js';
const COMMIT = process.argv.includes('--commit');
const INCLUDE_OSS = process.argv.includes('--include-oss');
const COLL_ARGS = process.argv.slice(2).filter((a) => !a.startsWith('--'));
// 默认扫这些含媒体 URL 的集合(可用命令行参数覆盖)。
const DEFAULT_COLLS = ['creations', 'create_jobs', 'users', 'twins', 'messages', 'memories', 'masters', 'social_messages', 'shares', 'knowledge'];
const COLLS = COLL_ARGS.length ? COLL_ARGS : DEFAULT_COLLS;
const localPrefix = `${config.publicUrl}/uploads/`;
const localDir = config.storage.localDir;
const minioBase = config.storage.minio.publicBase;
const minioBucket = config.storage.minio.bucket;
const ossBase = config.storage.oss.publicBase;
function isLocal(u) { return u.startsWith(localPrefix) || u.startsWith('/uploads/'); }
function isMinio(u) { return (minioBase && u.startsWith(minioBase)) || (minioBucket && u.includes(`/${minioBucket}/`)); }
function isOss(u) { return /\.aliyuncs\.com\//.test(u) || (ossBase && u.startsWith(ossBase)); }
// 只处理我们自己托管的公开媒体 URL;第三方外链、data: 一律不动。
function migratable(u) {
if (!/^(https?:)?\/\//.test(u) && !u.startsWith('/uploads/')) return false;
if (isMinio(u)) return false; // 已在 MinIO
return isLocal(u) || (INCLUDE_OSS && isOss(u));
}
async function bytesFor(u) {
if (isLocal(u)) {
const base = path.basename(u.split('?')[0]);
return fs.readFile(path.join(localDir, base)); // 本地盘直接读
}
const r = await fetch(u); // OSS/远程:HTTP 拉
if (!r.ok) throw new Error(`fetch ${r.status}`);
return Buffer.from(await r.arrayBuffer());
}
const cache = new Map(); // 旧 URL → 新 URL(同一文件只迁一次)
let migrated = 0, failed = 0;
async function migrateUrl(u) {
if (cache.has(u)) return cache.get(u);
try {
const buf = await bytesFor(u);
const base = path.basename(u.split('?')[0]) || 'file';
const newUrl = COMMIT ? await saveMedia(buf, base) : `MINIO:${base}`; // 干跑不真传
cache.set(u, newUrl);
migrated++;
if (migrated % 20 === 0) console.log(` …已处理 ${migrated} 个文件`);
return newUrl;
} catch (e) {
failed++;
console.warn(` ✗ 跳过 ${u} —— ${e.message}`);
cache.set(u, null);
return null;
}
}
// BSON 特殊类型(ObjectId/Date/Binary 等)不递归。
function isPlainContainer(o) {
return o && typeof o === 'object' && !o._bsontype && !(o instanceof Date) && !Buffer.isBuffer(o);
}
async function walk(node) {
let changed = false;
const entries = Array.isArray(node) ? node.map((v, i) => [i, v]) : Object.entries(node);
for (const [k, v] of entries) {
if (typeof v === 'string') {
if (migratable(v)) { const nu = await migrateUrl(v); if (nu) { node[k] = nu; changed = true; } }
} else if (isPlainContainer(v)) {
if (await walk(v)) changed = true;
}
}
return changed;
}
async function run() {
if (!minioConfigured) {
console.error('❌ 未配置 MinIO(MINIO_ENDPOINT/BUCKET/ACCESS_KEY/SECRET_KEY)。先在 .env 配好再跑。');
process.exit(1);
}
console.log(`模式: ${COMMIT ? '★ COMMIT(真写库)' : '干跑(只统计,不改库/不传文件)'} | 含OSS: ${INCLUDE_OSS ? '是' : '否'} | 集合: ${COLLS.join(', ')}`);
await connectDb();
const db = getDb();
let totalChanged = 0;
for (const coll of COLLS) {
let scanned = 0, changedDocs = 0;
try {
for await (const doc of db.collection(coll).find({})) {
scanned++;
if (await walk(doc)) {
changedDocs++;
if (COMMIT) await db.collection(coll).replaceOne({ _id: doc._id }, doc);
}
}
} catch (e) { console.warn(`[${coll}] 扫描出错(可能无此集合): ${e.message}`); continue; }
totalChanged += changedDocs;
console.log(`[${coll}] 扫描 ${scanned} 文档,需迁移 ${changedDocs} 条`);
}
console.log('──────────────────────────');
console.log(`唯一文件: ${cache.size} 迁移成功: ${migrated} 失败: ${failed} 受影响文档: ${totalChanged}`);
console.log(COMMIT ? '✅ 已写库。' : 'ℹ️ 干跑结束——加 --commit 才会真迁移。');
await closeDb();
process.exit(0);
}
run().catch((e) => { console.error('迁移失败:', e); process.exit(1); });
@@ -0,0 +1,95 @@
// ★★★ gapOf 的 A/B —— 【它裁决几次?它改变几格?】★ 这是两问,不是一问。
// (先例:南朝的 lex 被【读了 98,304 次、裁决了 81,920 次】,而它【改变 0 格】。
// ⇒ 一个被读了八万次的东西,可以什么都没买到。)
//
// ⚠ 本脚本【自己造两份引擎的副本,跑完自己删】—— 不在 src/ 里留残骸。
// ★ 副本是【整个文件复制 + 改一行】,不是重写一遍逻辑:
// 重写 = 复算 = 拿另一把尺子量,而那比不报更坏(本项目铁律)。
//
// A/B ① 把 nearest 排序里的 `gapOf(a) - gapOf(b) ||` 那一级【整个拿掉】—— 其余一个字未动。
// A/B ② 把 20 道六轴闸【全部改写成它等价类的下端 41】—— ★ 逐格【同一道闸】
// (六轴只取得到 40/63/73/79/83 ⇒ (40,63) 之间没有任何 profile)
// ⇒ 开/关一格都不会变,【只有 gapOf 读得出差别】。
import fs from 'node:fs';
import path from 'node:path';
import url from 'node:url';
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
const SRC = path.join(HERE, '..', 'src', 'dynasties.js');
const NOGAP = path.join(HERE, '..', 'src', '_probe_nogap.js');
const GATE41 = path.join(HERE, '..', 'src', '_probe_gate41.js');
const NEEDLE = ' gapOf(a) - gapOf(b) ||';
const base = fs.readFileSync(SRC, 'utf8');
if (!base.includes(NEEDLE)) {
console.error('✗ 在 dynasties.js 里找不到 gapOf 那一级 —— 引擎变了,这个探针过期了。');
console.error(' ⚠ 而【不许改探针去迁就代码】:先弄清楚那一级去哪了。');
process.exit(1);
}
fs.writeFileSync(NOGAP, base.replace(NEEDLE, ' // [PROBE] gapOf 那一级拿掉了'));
// ★ 只改【六轴】的闸;wang/lao 不是 sat 的产物,它们的数是【真的刻度】,一个字都不许动。
fs.writeFileSync(GATE41, base.replace(/gate: \{([^}]*)\}/g, (m, g) =>
'gate: {' + g.replace(/\b(wen|fa|wu|jiao|heng|tan):\s*(4[1-9]|5\d|6[0-3])\b/g, '$1: 41') + '}'));
try {
const { DYNASTIES, computeRank, computeAxes, AXIS_IDS } = await import('../src/dynasties.js');
const { computeRank: rankNoGap } = await import('../src/_probe_nogap.js');
const { computeRank: rankGate41 } = await import('../src/_probe_gate41.js');
const { chushenOf } = await import('../src/chushen.js');
const { SUB_REGISTRY } = await import('../src/talents.js');
const SUBS = SUB_REGISTRY.map((s) => s.id);
const WANGS = [0, 29, 30, 54, 55, 74, 75, 100];
const m = new Map();
for (let b = 0; b < (1 << 16); b++) {
const s = []; for (let i = 0; i < 16; i++) if (b & (1 << i)) s.push(SUBS[i]);
const a = computeAxes(s); const k = AXIS_IDS.map((x) => a[x]).join(',');
m.set(k, (m.get(k) || 0) + 1);
}
const T = [...m].map(([k, w]) => ({ axes: Object.fromEntries(k.split(',').map((v, i) => [AXIS_IDS[i], Number(v)])), w }));
const CFG = {};
for (const d of DYNASTIES) { const x = chushenOf(d.id); CFG[d.id] = x ? [null, ...x.values.map((v) => v.id)] : [null]; }
const N = (n) => n.toLocaleString();
const pad = (s, n) => { s = String(s); let w = 0; for (const c of s) w += c.charCodeAt(0) > 0x2000 ? 2 : 1; return s + ' '.repeat(Math.max(1, n - w)); };
console.log('\n══ A/B ① 把 gapOf 那一级【整个拿掉】:nearest 变几格?(其余一个字未动)');
console.log('══ A/B ② 20 道六轴闸【全换成等价类下端 41】:★ 逐格【同一道闸】,只有 gapOf 读得出差别\n');
console.log(pad('朝', 8) + pad('出身', 10) + '无路者'.padStart(11) + '① 拿掉 gapOf'.padStart(15) + '② 闸全写 41'.padStart(14));
console.log('─'.repeat(60));
let G1 = 0, G2 = 0;
for (const d of DYNASTIES) for (const cs of CFG[d.id]) {
let noRoad = 0, d1 = 0, d2 = 0;
for (const t of T) for (const wang of WANGS) {
const p = { axes: t.axes, wang, chushen: cs };
const a = computeRank(p, d.id);
if (a.channel) continue;
noRoad += t.w;
const b = rankNoGap(p, d.id), c = rankGate41(p, d.id);
// ★ ② 的前提:闸没变 ⇒「一条路都不开」这件事必须逐格不变。变了就说明我改错了闸。
if (c.channel) throw new Error(`★ ② 改写了开/关 —— 那它就不是同一道闸,本探针的前提塌了(${d.id})`);
if ((a.nearest || '-') !== (b.nearest || '-')) d1 += t.w;
if ((a.nearest || '-') !== (c.nearest || '-')) d2 += t.w;
}
if (!noRoad) continue;
G1 += d1; G2 += d2;
console.log(pad(d.name, 8) + pad(cs || '未答', 10) + N(noRoad).padStart(11) + N(d1).padStart(15) + N(d2).padStart(14));
}
console.log('─'.repeat(60));
console.log(pad('总计', 18) + ''.padStart(11) + N(G1).padStart(15) + N(G2).padStart(14));
console.log(`\n★ ①【拿掉 gapOf ⇒ ${N(G1)} 格换一条路当 nearest】—— 这就是「改判决」的代价(要老板拍)。`);
console.log('★★ 而【拿掉它不会让答案变对】:它只是把【一个假装是测量的仲裁】换成【一个自认是编号的仲裁】。');
console.log(`★ ②【20 道闸全写成 41(逐格同一道闸)⇒ ${N(G2)} 格变】—— ★ 注意它【比 ① 小得多】:`);
console.log(' 全表【一起】换写法,缺口一起缩,相对次序大多不动;');
console.log(' ⇒ ★★【真正翻盘的是【单独】换一道闸的写法】—— 见 scripts/probe-liguang-gate.mjs:');
console.log(' 军功的 wu:55 写成 wu:41(同一道闸)⇒ 李广的 nearest 从【辟召】变成【军功】。');
// ★ 李广:renwu.js 双证 subs=[] ⇒ 六轴全 40。《史記》卷109 双证:他的实职就是靠军功挣的。
const li = { axes: computeAxes([]), wang: 0 };
for (const [nm, fn] of [['真引擎', computeRank], ['① 拿掉 gapOf', rankNoGap], ['② 闸全写 41', rankGate41]]) {
const r = fn(li, 'han');
console.log(`\n★ 李广(subs=[] ⇒ 六轴全 40)/汉 ── ${pad(nm, 14)} nearest =【${r.nearest}】 tie = [${(r.nearestTie || []).join('、')}]`);
}
} finally {
// ⚠ 跑完就删:✗ 不许在 src/ 里留下两份引擎的副本 —— 那是下一个人会 import 错的东西。
for (const f of [NOGAP, GATE41]) if (fs.existsSync(f)) fs.unlinkSync(f);
}
@@ -0,0 +1,23 @@
// ★★★★★ 【李广 —— 而这一次不是编号,是本表在 41–63 里挑了哪一个数】
import { DYNASTIES, computeRank, computeAxes } from '../src/dynasties.js';
const han = DYNASTIES.find((d) => d.id === 'han');
const jg = han.channels.find((c) => c.id === 'jungong_han');
const li = { axes: computeAxes([]), wang: 0 }; // ★ 李广:renwu.js 双证 subs=[] ⇒ 六轴全 40
console.log('★ 李广 六轴:', JSON.stringify(li.axes), ' wang=0\n');
console.log('汉三条闸:', han.channels.map((c) => `${c.name}=${JSON.stringify(c.gate)}`).join(' '), '\n');
console.log('【把军功的闸 wu:55 换成它等价类 41–63 里的每一个数】');
console.log(' ⚠ 每一个都是【同一道闸】:六轴取值只有 40/63/73/79/83,(40,63] 之间没有任何 profile。\n');
console.log(' wu 闸 李广被判「最接近」的那条路 打平的 他的军功缺口');
console.log(' ' + '─'.repeat(78));
const seen = new Map();
for (const n of [41, 45, 50, 54, 55, 56, 60, 63]) {
jg.gate.wu = n;
const r = computeRank(li, 'han');
const gap = n - 40;
console.log(` ${String(n).padEnd(8)} ${(r.nearest || '-').padEnd(34)} [${(r.nearestTie || []).join('、').padEnd(18)}] 差 ${gap}`);
seen.set(r.nearest, (seen.get(r.nearest) || 0) + 1);
}
jg.gate.wu = 55;
console.log('\n★★★ 同一道闸,8 个写法 →', [...seen].map(([k, v]) => `${k}: ${v} 次`).join(' '));
console.log('★★★★【「军功不是最接近的那条」不是一个关于李广的事实,是【本表在 41–63 里挑了 55】这个事实。】');
console.log(' ⇒ 而《史記》卷109 双证钉着:他的实职【就是靠军功挣的】。');
@@ -0,0 +1,34 @@
// ★ 李广/李蔡 落在哪一支 + 打平集合的势
import { computeRank, profileFromSubs, computeAxes, DYNASTIES } from '../src/dynasties.js';
import { subsOf, renwuOf } from '../src/renwu.js';
import { SUB_REGISTRY } from '../src/talents.js';
for (const id of ['liguang', 'licai']) {
const subs = subsOf(id);
const r = computeRank(profileFromSubs(subs, { wang: 0 }), 'han');
console.log(`\n【${renwuOf(id).name}】subs=${JSON.stringify(subs)} axes=${JSON.stringify(computeAxes(subs))}`);
console.log(` channel=${r.channel ? r.channel.id : 'null'} tier=${r.rank.tier} title=${r.rank.title}`);
console.log(` nearest=${r.nearest} nearestTie=${JSON.stringify(r.nearestTie)}`);
console.log(` kai=${JSON.stringify(r.kai.map((k) => k.channel))} zelu.options=${JSON.stringify(r.zelu.options)}`);
}
const SUBS = SUB_REGISTRY.map((s) => s.id);
let opt3 = 0, boss3 = 0, optN = 0, bossN = 0;
for (let m = 0; m < (1 << 16); m++) {
const subs = []; for (let i = 0; i < 16; i++) if (m & (1 << i)) subs.push(SUBS[i]);
const axes = computeAxes(subs);
for (const wang of [0, 29, 30, 54, 55, 74, 75, 100]) for (const d of DYNASTIES) {
const r = computeRank({ axes, wang }, d.id);
if (!r.channel) continue;
const walked = r.kai.find((k) => k.walked);
const bossTie = r.kai.filter((k) => k.tier === walked.tier && k.ceiling === walked.ceiling);
if (bossTie.length > 1) bossN++;
if (bossTie.length > 2) boss3++;
const o = (r.zelu.options || []).length;
if (o > 1) optN++;
if (o > 2) opt3++;
}
}
const N = (n) => n.toLocaleString();
console.log(`\n★ 现行 zelu.options(只收 lostAt==='idx'):>1 条 ${N(optN)} 格;≥3 条 ${N(opt3)} 格`);
console.log(`★ 老板判据(同 tier 同 ceiling): >1 条 ${N(bossN)} 格;≥3 条 ${N(boss3)} 格`);
@@ -0,0 +1,19 @@
import { computeRank, profileFromSubs, DYNASTIES, computeAxes } from '../src/dynasties.js';
import { SUB_REGISTRY } from '../src/talents.js';
const SUBS = SUB_REGISTRY.map((s) => s.id);
const r = computeRank(profileFromSubs(['rigorous','datadriven','concise','shortfirst'], { wang: 0 }), 'weijin');
console.log('魏晋 clerk: nearest=', r.nearest, ' tie=', JSON.stringify(r.nearestTie), ' isDefault=', r.nearestIsDefault);
// 无路者里,nearest【没有】打平的格子占多少?找一个例子
let hasTie = 0, noTie = 0, ex = null;
for (let m = 0; m < (1 << 16); m++) {
const subs = []; for (let i = 0; i < 16; i++) if (m & (1 << i)) subs.push(SUBS[i]);
const axes = computeAxes(subs);
for (const wang of [0, 29, 30, 54, 55, 74, 75, 100]) for (const d of DYNASTIES) {
const r = computeRank({ axes, wang }, d.id);
if (r.channel || !r.nearest) continue;
if (r.nearestTie.length) hasTie++;
else { noTie++; if (!ex) ex = { d: d.id, subs, wang, nearest: r.nearest }; }
}
}
console.log(`\n无路者·nearest 打平的格子 ${hasTie.toLocaleString()};【没打平】的 ${noTie.toLocaleString()}`);
console.log('没打平的一个例子:', JSON.stringify(ex));
@@ -0,0 +1,32 @@
// ★★★ 自查:【这一刀新加的每一个字段/每一个分支,本身是不是又一个空操作?】
// ⚠ 本文件的铁律逐字:「不制造第二个」。而第八刀在「让它出声」那一刀里【自己造了一个】(byZelu)。
// ⇒ 所以这一刀的新字段必须自己先过一遍 census:【每一个取值都要真的跑到】。
import { DYNASTIES, computeRank, computeAxes, AXIS_IDS } from '../src/dynasties.js';
import { chushenOf } from '../src/chushen.js';
import { SUB_REGISTRY } from '../src/talents.js';
const SUBS = SUB_REGISTRY.map((s) => s.id);
const WANGS = [0, 29, 30, 54, 55, 74, 75, 100];
const m = new Map();
for (let b = 0; b < (1 << 16); b++) { const s = []; for (let i = 0; i < 16; i++) if (b & (1 << i)) s.push(SUBS[i]);
const a = computeAxes(s); const k = AXIS_IDS.map((x) => a[x]).join(','); m.set(k, (m.get(k) || 0) + 1); }
const T = [...m].map(([k, w]) => ({ axes: Object.fromEntries(k.split(',').map((v, i) => [AXIS_IDS[i], Number(v)])), w }));
const CFG = {}; for (const d of DYNASTIES) { const x = chushenOf(d.id); CFG[d.id] = x ? [null, ...x.values.map((v) => v.id)] : [null]; }
const N = (n) => n.toLocaleString();
const hit = { by_count: 0, by_gap: 0, by_idx: 0, sameAxis_true: 0, sameAxis_false: 0, mixed_true: 0, mixed_false: 0, kind_scale: 0, kind_codename: 0, rivals1: 0, rivals2plus: 0, gapLine: 0 };
const byDyn = {};
for (const d of DYNASTIES) for (const cs of CFG[d.id]) for (const t of T) for (const wang of WANGS) {
const r = computeRank({ axes: t.axes, wang, chushen: cs }, d.id);
if (r.channel || !r.nearest) continue;
hit['by_' + r.nearestBy] += t.w;
if (r.nearestBy !== 'gap') continue;
const g = r.nearestGap;
hit[g.sameAxis ? 'sameAxis_true' : 'sameAxis_false'] += t.w;
hit[g.mixedKind ? 'mixed_true' : 'mixed_false'] += t.w;
for (const s of [g.mine, ...g.rivals]) hit['kind_' + s.kind] += t.w;
hit[g.rivals.length === 1 ? 'rivals1' : 'rivals2plus'] += t.w;
if (r.reasons.some((x) => /最接近的那条是怎么挑出来的/.test(x))) hit.gapLine += t.w;
if (g.sameAxis) { const k = `${d.id}|${cs}`; byDyn[k] = (byDyn[k] || 0) + t.w; }
}
console.log('\n══ 这一刀新加的每一个取值,跑到了几格?【0 = 我造了一个新的空操作】══\n');
for (const [k, v] of Object.entries(hit)) console.log(` ${k.padEnd(18)} ${N(v).padStart(12)} ${v === 0 ? ' ★★★★ 死!这是我自己造的空操作' : ''}`);
console.log('\n sameAxis=true 落在:', Object.keys(byDyn).length ? JSON.stringify(byDyn) : '【没有一格】');
+54
View File
@@ -0,0 +1,54 @@
// ★ 择路排序器【自洽吗】—— TIE_TOL=5 是【容差】,而容差不传递。
// 一个不传递的比较器交给 Array.sort,结果是【实现定义】的:ranked[0] 可以不是极大元。
// 本表逐字写过:「『碰巧成立』和『守着』是两回事」。⇒ 跑出来。
import { DYNASTIES, computeAxes, getDynasty, AXES } from '../src/dynasties.js';
import { SUB_REGISTRY } from '../src/talents.js';
const SUBS = SUB_REGISTRY.map((s) => s.id);
const TIE_TOL = 5;
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
const xuanOf = (c) => (c && c.xuan) || { seat: 0, kemu: [], key: [] };
function lexCompare(a, b, val) {
const ka = xuanOf(a).key || [], kb = xuanOf(b).key || [];
const n = Math.min(ka.length, kb.length);
for (let i = 0; i < n; i++) {
const va = val(ka[i]), vb = val(kb[i]);
if (Math.abs(va - vb) > TIE_TOL) return vb - va;
}
return 0;
}
let lexNonTrans = 0, cells = 0, tieNonEquiv = 0, tieNonEquivCells = 0;
for (let m = 0; m < (1 << 16); m++) {
const subs = []; for (let i = 0; i < 16; i++) if (m & (1 << i)) subs.push(SUBS[i]);
const axes = computeAxes(subs);
for (const wang of [0, 29, 30, 54, 55, 74, 75, 100]) for (const d of DYNASTIES) {
const val = (k) => (k === 'wang' ? wang : k === 'lao' ? 0 : clamp(Number(axes[k] || 0), 0, 100));
const open = d.channels.filter((c) => Object.entries(c.gate || {}).every(([k, need]) => val(k) >= need));
if (open.length < 3) continue;
cells++;
const ceilOf = (c) => c.ceiling?.tier ?? 5;
const tierOf = (c) => {
const kemu = xuanOf(c).kemu || [];
let n = 0; for (const k of kemu) { if (val(k.axis) >= k.need) n++; else break; }
return clamp((xuanOf(c).seat || 0) + n, 0, ceilOf(c));
};
// ①【lexCompare 的「均」传递吗】A≈B、B≈C ⇒ A≈C ?
for (const A of open) for (const B of open) for (const C of open) {
if (A === B || B === C || A === C) continue;
if (lexCompare(A, B, val) === 0 && lexCompare(B, C, val) === 0 && lexCompare(A, C, val) !== 0) lexNonTrans++;
}
// ②【老板判据的打平集合,内部两两真的都同 tier 同 ceiling 吗】(= 是等价关系吗)
const best = open.slice().sort((a, b) =>
tierOf(b) - tierOf(a) || ceilOf(b) - ceilOf(a) || lexCompare(a, b, val) || 0)[0];
const tie = open.filter((c) => tierOf(c) === tierOf(best) && ceilOf(c) === ceilOf(best));
let bad = 0;
for (const A of tie) for (const B of tie)
if (A !== B && (tierOf(A) !== tierOf(B) || ceilOf(A) !== ceilOf(B))) bad++;
tieNonEquiv += bad; if (bad) tieNonEquivCells++;
}
}
const N = (n) => n.toLocaleString();
console.log(`\n扫了 ${N(cells)} 个【≥3 条路开着】的格子`);
console.log(`①【lexCompare 的「均」不传递】的三元组:${N(lexNonTrans)} 次`);
console.log(` ⇒ 容差关系 |a−b|≤5 【不是等价关系】。而 idx 的成员资格【是拿它定义的】。`);
console.log(`②【老板判据(同 tier 同 ceiling)】内部不自洽:${N(tieNonEquiv)} 次(${N(tieNonEquivCells)} 格)`);
console.log(` ⇒ 相等【是】等价关系。老板的判据是良定义的,我上一刀那个不是。`);
+33
View File
@@ -0,0 +1,33 @@
// CosyVoice 可用性探针:拿本机 .env 的 DASHSCOPE key 打真实语音合成,报「能用/不能用+确切错误」。
// 用法(服务器后端目录内,.env 已配 DASHSCOPE_API_KEY): node scripts/probe-tts.mjs
//
// 结果解读:
// ✅ 出音(音频字节>0) → 该 (model, voice) 可用。
// ❌ ModelNotFound / 未开通 → 百炼账号未开通该 CosyVoice 版本(账号层,去百炼控制台开通)。
// ❌ voice 不匹配/无此音色 → 该版本的预置音色名不对(v2/v3/v3.5 音色不通用)。
import 'dotenv/config';
import { config } from '../src/config.js';
import { synthesizeWs } from '../src/services/ttsWs.js';
// 探测组合:预置音(v2 最可能已开) + 你当前克隆音默认版本(用 v2 音色仅为触发,看是"模型未开"还是"音色不匹配")。
const CASES = [
{ label: '预置音 v2', model: 'cosyvoice-v2', voice: 'longwan_v2' },
{ label: `克隆默认(${config.tts.cloneModel})`, model: config.tts.cloneModel, voice: 'longwan_v2' },
];
console.log('DASHSCOPE key:', config.llm.dashscopeKey ? '已配(尾4:' + config.llm.dashscopeKey.slice(-4) + ')' : '❌未配');
console.log('WS 端点:', config.tts.wsUrl);
console.log('config.tts:', JSON.stringify(config.tts));
console.log('──────── 探测 ────────');
for (const c of CASES) {
process.stdout.write(`· ${c.label} [${c.model} / ${c.voice}] … `);
try {
const r = await synthesizeWs('你好,这是一次语音合成测试。', { voiceId: c.voice, model: c.model });
const bytes = r?.audio ? Buffer.from(r.audio, 'base64').length : 0;
console.log(bytes > 0 ? `✅ 出音 ${bytes} 字节` : '❌ 无音频');
} catch (e) {
console.log('❌', String(e?.message || e));
}
}
process.exit(0);
@@ -0,0 +1,64 @@
// 把 MongoDB knowledge 集合里已有的向量重建进 DashVector 索引。
//
// 何时用:①首次接入 DashVector(建集合 + 灌存量向量);②补齐入库时同步失败的块(标了 vectorPending 的);
// ③换过 embedding 维度后重灌(见下"换维度")。
//
// 幂等性:本脚本用 insert 语义,只【补齐缺失】的向量、灌成功后清 vectorPending 标记。它【不覆盖】已存在
// 同 _id 的向量(在线入库每块都是全新 _id,不会撞;故正常场景无需覆盖)。
// ⚠ 换维度 / 修复"已存在但内容错"的向量:insert 不覆盖,必须【新建集合】——把 .env 的 DASHVECTOR_COLLECTION
// 改成新名(或在阿里云控制台删掉旧集合)后再跑本脚本。ensureCollection 已会在维度不符时【大声报错】提示你。
//
// 用法(服务器后端目录内,.env 里已配好 DASHVECTOR_*): node scripts/reindex-vectors.js
// MongoDB 永远是真相源,本脚本只把真相源投影进向量索引;不改动 knowledge 任何数据(仅清 vectorPending 标记)。
import 'dotenv/config';
import { connectDb, getDb, closeDb } from '../src/db.js';
import { config } from '../src/config.js';
import { vectorEnabled, ensureCollection, vectorUpsert } from '../src/services/vectorStore.js';
async function run() {
if (!vectorEnabled()) {
console.error('❌ 向量库未启用。请在 .env 配 DASHVECTOR_ENDPOINT / DASHVECTOR_API_KEY(和可选 DASHVECTOR_COLLECTION),再重跑。');
process.exit(1);
}
console.log(`向量库: DashVector · collection=${config.vector.dashvector.collection} · dim=${config.vector.dashvector.dim}`);
console.log('① 确保集合存在且就绪(维度不符会在此报错)…');
const c = await ensureCollection();
console.log(c.existed ? ' 集合已存在,复用。' : c.notReady ? ' 集合已创建(尚未就绪,后续写入会自动重试)。' : ' 集合已创建、就绪。');
await connectDb();
const db = getDb();
const cursor = db.collection('knowledge')
.find({ embedding: { $type: 'array' } })
.project({ embedding: 1, twinId: 1, sourceType: 1 })
.addCursorFlag('noCursorTimeout', true) // 大集合 + 慢/限流的 DashVector:防两次 getMore 间的阻塞 upsert 超过游标空闲超时(10min)
.batchSize(500);
let batch = [];
let ids = [];
let total = 0;
const BATCH = 200;
const flush = async () => {
if (!batch.length) return;
await vectorUpsert(batch);
// 灌成功 → 清掉这些块入库时可能打上的 vectorPending 标记(它们已进索引)。
try { await db.collection('knowledge').updateMany({ _id: { $in: ids } }, { $unset: { vectorPending: '' } }); } catch { /* 清标失败无害 */ }
total += batch.length;
process.stdout.write(`\r 已重建 ${total} 块…`);
batch = []; ids = [];
};
console.log('② 遍历 knowledge 集合,灌入向量…');
for await (const d of cursor) {
if (!Array.isArray(d.embedding) || !d.embedding.length) continue;
batch.push({ id: String(d._id), embedding: d.embedding, twinId: String(d.twinId), sourceType: d.sourceType || 'text' });
ids.push(d._id);
if (batch.length >= BATCH) await flush();
}
await flush();
console.log(`\n✅ 完成。共重建 ${total} 块向量进 DashVector。`);
await closeDb();
}
run().catch((e) => { console.error('\n❌ 重建失败:', e.message); process.exit(1); });
+209
View File
@@ -0,0 +1,209 @@
// 样板数据:三城大师墙 + 高端文旅路线(成都 / 苏州 / 徽州)。
// 用法:node scripts/seed-masters.js (幂等,可反复跑)
import 'dotenv/config';
import { ObjectId } from 'mongodb';
import { connectDb, getDb, closeDb } from '../src/db.js';
const SEED = 'chengdu-master-wall'; // 沿用同一 seedTag,一键清旧重灌
// ── 大师(跨三城)────────────────────────────────────────────────
const MASTERS = [
// 成都
{ slug: 'zhucheng', displayName: '朱成', honorific: '朱成', domain: 'diaosu', region: '成都', masterTier: 'flagship',
tagline: '用石头留住一座城的记忆', masterworks: ['朱成石刻艺术博物馆·馆长', '万佛墙', '抢救性民间石刻造像收藏'], poolWeight: 2,
characterPrompt: '我是朱成,雕塑家,朱成石刻艺术博物馆馆长。几十年来我在抢救、收藏民间石刻造像,把散落各处的民间美学与信仰留下来。我谈艺术、谈收藏、谈一座城的记忆,语气朴实、有温度。',
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerJoyrides.mp4', // 占位样片,替换为大师真实短片
tags: ['雕塑', '石刻', '民间博物馆', '成都'] },
{ slug: 'lijiang-art', displayName: '李江', honorific: '李江教授', domain: 'meishu', region: '成都', masterTier: 'resident',
tagline: '笔墨见性,画如其人', masterworks: ['美术学院系主任', '当代绘画'], poolWeight: 1,
characterPrompt: '我是李江,画家,美术学院系主任。我谈绘画语言、创作观、如何看一幅画。面对习作我会具体点评构图、笔性、色调、气韵,温和而犀利。',
tags: ['美术', '绘画', '当代艺术', '成都'] },
{ slug: 'tangjie', displayName: '鲁杰', honorific: '鲁杰先生', domain: 'gujian', region: '成都', masterTier: 'flagship',
tagline: '营造之道,藏在斗拱之间', masterworks: ['中国古建泰斗', '营造法式研究'], poolWeight: 1,
characterPrompt: '我是鲁杰,专研中国古建筑。我讲斗拱、营造法式、会馆与庙宇的形制,也讲古建的当代传承。语气沉稳、爱"论道"。',
tags: ['古建', '营造法式', '历史建筑', '成都'] },
// 苏州
{ slug: 'shenyun-su', displayName: '沈云', honorific: '沈云', domain: 'feiyi', region: '苏州', masterTier: 'flagship',
tagline: '以针代笔,一线一世界', masterworks: ['苏绣非遗代表性传承人', '双面绣'], poolWeight: 1,
characterPrompt: '我是沈云,苏绣非遗传承人。我谈刺绣的针法、丝理、配色与"以针代笔"的意境,也讲一幅绣品背后几个月的功夫与心境,温婉细致。',
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4', // 占位样片
tags: ['苏绣', '非遗', '刺绣', '苏州'] },
{ slug: 'chenyuan-su', displayName: '陈园', honorific: '陈园先生', domain: 'gujian', region: '苏州', masterTier: 'resident',
tagline: '移步换景,咫尺山林', masterworks: ['苏州园林营造研究', '叠山理水'], poolWeight: 1,
characterPrompt: '我是陈园,研究苏州园林营造。我讲叠山、理水、借景、移步换景,如何在方寸间造出咫尺山林,也讲文人园林的意趣。',
tags: ['园林', '造园', '古建', '苏州'] },
// 徽州
{ slug: 'humo-hui', displayName: '胡墨', honorific: '胡墨', domain: 'feiyi', region: '徽州', masterTier: 'flagship',
tagline: '一两徽墨一两金', masterworks: ['徽墨制作技艺传承人', '歙砚鉴藏'], poolWeight: 1,
characterPrompt: '我是胡墨,做徽墨、赏歙砚。我谈点烟、和胶、捣杵、描金的制墨古法,也讲文房四宝里的门道与文人风骨。',
videoUrl: 'https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerMeltdowns.mp4', // 占位样片
tags: ['徽墨', '歙砚', '文房', '非遗', '徽州'] },
{ slug: 'fangying-hui', displayName: '方营', honorific: '方营先生', domain: 'gujian', region: '徽州', masterTier: 'resident',
tagline: '粉墙黛瓦马头墙', masterworks: ['徽派建筑营造研究', '徽州三雕'], poolWeight: 1,
characterPrompt: '我是方营,研究徽派建筑。我讲马头墙、天井、三雕(木雕/石雕/砖雕)与聚族而居的宗族空间,也讲徽商衣锦还乡如何营造一座村落。',
tags: ['徽派建筑', '古村落', '三雕', '徽州'] },
];
// ── 文旅路线(引用大师 slug)────────────────────────────────────
const TOURS = [
{
title: '成都·大师人文之旅(5天·私宴接待级)', region: '成都', tier: 'private',
priceHint: 50000, depositRate: 0.5, seats: 8,
masters: [['zhucheng', 2], ['lijiang-art', 1], ['tangjie', 1]],
highlights: ['三位泰斗级大师亲自陪同 + 私宴(市面不可复制)', '顶奢文化酒店(大慈寺旁·博舍级)+ 专属管家', '8 人极致小团 · 数字分身行前讲解 + 行后永续问道'],
included: ['5 晚顶奢文化住宿', '全程专属管家', '三场大师私宴/茶叙', '博物馆/画室/古建深度接待', '数字传承卡'],
itinerary: [
{ day: 1, title: '入蓉·序章:大慈千年', desc: '专属管家接机 → 入住文化酒店 → 大慈寺·太古里人文漫步 → 欢迎晚宴;朱成馆长出席茶叙破冰。' },
{ day: 2, title: '朱成·石头里的众生', desc: '朱成石刻艺术博物馆深度导览(万佛墙/民间造像),馆长亲自讲解 → 拓片/雕塑手作、大师点评 → 私宴。' },
{ day: 3, title: '李江·笔墨见性', desc: '蓝顶艺术区·李江画室,看画论画、大师现场示范 → 命题写生一对一指点 → 当代艺术下午茶雅集。' },
{ day: 4, title: '鲁杰·营造之道', desc: '洛带古镇客家会馆群(清代古建),鲁杰现场解构斗拱与营造法式 → "论道"沙龙 → 宽窄巷子夜话·私宴。' },
{ day: 5, title: '历史建筑·故事收官', desc: '安仁公馆群 / 都江堰-青城山古建,专家讲背后的家族与时代故事 → 结业茶会,赠"数字传承卡"。' },
],
masterMoments: ['D1 朱成茶叙破冰', 'D2 朱成馆长亲自导览 + 私宴论道', 'D3 李江面对面:示范、点评习作', 'D4 鲁杰论道:古建营造现场解构'],
guides: [
{ name: '朱明', note: '朱成之子', titles: ['博物馆研究博士', '一人博物馆创始人', '成都美术家协会副秘书长'] },
{ name: '熊婧彤', titles: ['省级青年建筑专家', '教授级高工', '成都历史建筑口述编导', '主播', '成都美术家协会副秘书长'] },
],
},
{
title: '姑苏·园林与绣艺之旅(4天·私宴接待级)', region: '苏州', tier: 'private',
priceHint: 38000, depositRate: 0.5, seats: 8,
masters: [['shenyun-su', 1], ['chenyuan-su', 1]],
highlights: ['苏绣非遗传承人亲授 + 园林营造大师同行', '拙政园旁精品园景酒店 · 专属管家', '昆曲雅集 · 数字分身行前讲解 + 行后问道'],
included: ['3 晚精品园景住宿', '全程专属管家', '苏绣工坊亲授', '名园深度私导 + 昆曲雅集', '数字传承卡'],
itinerary: [
{ day: 1, title: '入苏·序章:平江烟雨', desc: '专属管家接机 → 入住园景酒店 → 拙政园·平江路人文漫步 → 欢迎晚宴。' },
{ day: 2, title: '沈云·一针一世界', desc: '苏绣非遗工坊,沈云大师亲授看绣/试针 → 论"以针代笔"的意境 → 苏帮菜私宴。' },
{ day: 3, title: '陈园·园林之道', desc: '网师园/留园,陈园现场解构叠山理水、移步换景 → 夜游拙政园·昆曲雅集。' },
{ day: 4, title: '姑苏·故事收官', desc: '虎丘/寒山寺,专家讲背后故事 → 结业茶会 → 送机。' },
],
masterMoments: ['D2 沈云亲授苏绣 + 私宴', 'D3 陈园园林论道 + 昆曲雅集'],
},
{
title: '徽州·古村与文房之旅(5天·私宴接待级)', region: '徽州', tier: 'private',
priceHint: 45000, depositRate: 0.5, seats: 8,
masters: [['humo-hui', 1], ['fangying-hui', 1]],
highlights: ['徽墨非遗传承人亲授 + 徽派建筑大师同行', '宏村旁徽派古宅精品酒店 · 专属管家', '世界文化遗产古村落深度私导'],
included: ['4 晚徽派古宅住宿', '全程专属管家', '徽墨/歙砚工坊亲授', '宏村·西递深度私导', '数字传承卡'],
itinerary: [
{ day: 1, title: '入徽·序章:屯溪老街', desc: '专属管家接机 → 入住徽派古宅酒店 → 屯溪老街人文漫步 → 欢迎晚宴。' },
{ day: 2, title: '胡墨·文房四宝', desc: '徽墨/歙砚工坊,胡墨大师亲授点烟描金制墨、赏砚 → 论文房之道 → 徽菜私宴。' },
{ day: 3, title: '方营·粉墙黛瓦', desc: '宏村/西递古村落,方营现场解构徽派三雕与马头墙、天井 → 南湖书院茶叙。' },
{ day: 4, title: '徽州人文·宗族故事', desc: '徽州古城/棠樾牌坊群,专家讲徽商与宗族的时代故事。' },
{ day: 5, title: '徽州·山水收官', desc: '黟县木坑竹海 → 结业茶会,赠"数字传承卡" → 送机。' },
],
masterMoments: ['D2 胡墨亲授制墨 + 私宴', 'D3 方营徽派建筑现场论道 + 书院茶叙'],
},
];
// ── 众生墙:普通人示例分身(非大师,isMaster:false)──
const PEOPLE = [
{ slug: 'linxiaoman', displayName: '林小满', tagline: '一杯手冲,慢煮生活', tags: ['咖啡', '手冲', '独立小店', '生活方式'], likeCount: 7, likeSum: 32,
characterPrompt: '我是林小满,开了家只有八个座位的独立咖啡馆。我聊手冲、豆子、慢生活,也聊一个人开小店的酸甜。语气温柔、不急不躁。' },
{ slug: 'chenjiu', displayName: '陈九', tagline: '写代码的深夜,和你聊技术也聊人生', tags: ['程序员', '技术', '副业', '效率'], likeCount: 9, likeSum: 40,
characterPrompt: '我是陈九,写了八年后端。能陪你聊架构、聊 bug、聊程序员的副业和焦虑。理性、爱打比方,偶尔毒舌。' },
{ slug: 'suqing', displayName: '苏晴', tagline: '呼吸之间,找回身体的觉知', tags: ['瑜伽', '冥想', '身心', '疗愈'], likeCount: 6, likeSum: 29,
characterPrompt: '我是苏晴,教瑜伽和冥想。我引导你把注意力放回呼吸和身体,聊体式,也聊如何和自己相处。温和、笃定。' },
{ slug: 'amay', displayName: '阿May', tagline: '记录女儿的每一天,也记录我自己', tags: ['育儿', '亲子', '成长记录', '日常'], likeCount: 8, likeSum: 36,
characterPrompt: '我是阿May,一个一岁女儿的新手妈妈。分享育儿的手忙脚乱和小确幸,也想被人问问"你自己还好吗"。真诚、有点碎碎念。' },
{ slug: 'laozhou', displayName: '老周', tagline: '退休了,想把读过的书讲给你听', tags: ['读书', '文史', '退休生活', '讲书'], likeCount: 5, likeSum: 24,
characterPrompt: '我是老周,教了三十五年语文,刚退休。爱讲书、讲诗词、讲历史里的小人物,也想有人陪我聊聊天。慈祥、爱掉书袋。' },
{ slug: 'dapeng', displayName: '大鹏', tagline: '别急,我们从第一个深蹲开始', tags: ['健身', '增肌', '减脂', '自律'], likeCount: 7, likeSum: 30,
characterPrompt: '我是大鹏,健身教练。陪你从零开始练,聊动作、聊饮食、聊怎么坚持下去。热血、直接、爱鼓励人。' },
{ slug: 'xiawan', displayName: '夏晚', tagline: '背包走过 23 国,故事讲不完', tags: ['旅行', '摄影', '背包客', '攻略'], likeCount: 9, likeSum: 43,
characterPrompt: '我是夏晚,背包走过二十多个国家。聊路上的故事、省钱攻略、一个人旅行的胆量与孤独。松弛、爱讲段子。' },
{ slug: 'linyisheng', displayName: '林医生', tagline: '有些话,先说给我的分身听听', tags: ['心理', '情绪', '倾诉', '疗愈'], likeCount: 8, likeSum: 38,
characterPrompt: '我是林医生,心理咨询师。不评判,只陪你把情绪慢慢说清楚。温和、会倾听、常反问。注:我不做诊断,严重困扰请找线下专业帮助。' },
{ slug: 'xiaoye', displayName: '小野', tagline: '把家的味道,做给更多人', tags: ['美食', '家常菜', '下厨', '治愈'], likeCount: 6, likeSum: 27,
characterPrompt: '我是小野,爱做家常菜。聊怎么把一顿普通饭做得有温度,分享菜谱和厨房里的治愈时刻。热心、烟火气十足。' },
{ slug: 'azheng', displayName: '阿正', tagline: '教室到舞房,都是我的主场', tags: ['街舞', '校园', '青春', '兴趣'], likeCount: 7, likeSum: 31,
characterPrompt: '我是阿正,大三,跳街舞。聊舞蹈、校园、怎么在忙碌里坚持热爱。活力满满、爱用网络梗。' },
];
async function run() {
await connectDb();
const db = getDb();
// 幂等:清掉上次种子(只清本 seedTag 相关,不动后台手工建的数据)。
const oldTwins = await db.collection('twins').find({ seedTag: SEED }).project({ _id: 1 }).toArray();
const oldIds = oldTwins.map((t) => t._id);
const oldMasters = await db.collection('masters').find({ twinId: { $in: oldIds } }).project({ _id: 1 }).toArray();
await db.collection('agreements').deleteMany({ masterId: { $in: oldMasters.map((m) => m._id) } });
await db.collection('twins').deleteMany({ seedTag: SEED });
await db.collection('masters').deleteMany({ twinId: { $in: oldIds } });
await db.collection('tours').deleteMany({ seedTag: SEED });
const twinIdBySlug = {};
for (const m of MASTERS) {
const twinDoc = {
ownerId: new ObjectId(),
displayName: m.displayName, kind: 'clone', persona: '', characterPrompt: m.characterPrompt,
avatarUrl: null, voiceId: null, voiceModel: null, lang: 'zh',
isPublic: true, inPlaza: true, slug: m.slug, tagline: m.tagline, tags: m.tags,
verified: true, isMaster: true, masterTier: m.masterTier, domain: m.domain, region: m.region,
honorific: m.honorific, masterworks: m.masterworks, profession: null, videoUrl: m.videoUrl || null,
talents: { sub_talents: [], unlockedAt: null }, likeSum: 0, likeCount: 0,
seedTag: SEED, createdAt: new Date(), updatedAt: new Date(),
};
const r = await db.collection('twins').insertOne(twinDoc);
twinIdBySlug[m.slug] = r.insertedId;
const mr = await db.collection('masters').insertOne({
twinId: r.insertedId, status: 'live',
// ⚠ knowledge 必须是 false。这个脚本【一处知识库都没写】(全文 0 处 ingest)——
// 种子大师只有一句 characterPrompt 人设,知识库是空的。
// 此前这里写死 knowledge:true,等于代码在对自己的仪表盘撒谎:后台看到"知识库已就绪",
// 实际上任何人派分身来"向大师学习",学到的都只是 Qwen 借着人设即兴生成的通识。
// 要让它变成 true,只有一条路:真的把大师的资料喂进去(masters.js addMasterKnowledge)。
buildProgress: { photo: true, voice: true, knowledge: false, review: true },
poolWeight: m.poolWeight, offlineCommit: '每团出席茶叙/论道 1 次;私宴按排期配合。',
intro: m.characterPrompt, contact: {}, createdAt: new Date(), updatedAt: new Date(),
});
await db.collection('twins').updateOne({ _id: r.insertedId }, { $set: { masterId: mr.insertedId } });
await db.collection('agreements').insertMany([
{ masterId: mr.insertedId, kind: 'authorization',
scope: { useImage: true, useVoice: true, useKnowledge: true, canSay: [m.domain], cannotSay: ['政治', '医疗诊断', '投资建议'] },
revocable: true, term: { from: new Date(), to: null },
inheritance: '授权范围内,数字分身可在本人年迈/离世后继续用于文化传承讲解。',
aiDisclosure: '本页为大师授权的数字分身,非本人实时应答;深度交流以线下为准。',
signedAt: new Date(), createdAt: new Date() },
{ masterId: mr.insertedId, kind: 'cooperation',
cooperationSLA: { teaPerTour: 1, banquetPerYear: 4, receptionNote: '按团配合接待、茶叙/论道,私宴择期。' },
createdAt: new Date() },
]);
}
for (const t of TOURS) {
const ids = t.masters.map(([slug]) => twinIdBySlug[slug]);
await db.collection('tours').insertOne({
title: t.title, cover: null, region: t.region, masterIds: ids, tier: t.tier,
priceHint: t.priceHint, depositRate: t.depositRate,
revShare: { platform: 20, masters: 30, organizer: 50 },
masterShares: t.masters.map(([slug, weight]) => ({ masterId: twinIdBySlug[slug], weight })),
seats: t.seats, seatsLeft: t.seats, status: 'open',
highlights: t.highlights, included: t.included, itinerary: t.itinerary, masterMoments: t.masterMoments,
guides: t.guides || [],
seedTag: SEED, createdAt: new Date(), updatedAt: new Date(),
});
}
// ── 众生墙:普通人示例分身 ──
for (const p of PEOPLE) {
await db.collection('twins').insertOne({
ownerId: new ObjectId(),
displayName: p.displayName, kind: 'clone', persona: '', characterPrompt: p.characterPrompt,
avatarUrl: null, voiceId: null, voiceModel: null, lang: 'zh',
isPublic: true, inPlaza: true, slug: p.slug, tagline: p.tagline, tags: p.tags,
verified: false, isMaster: false, profession: null,
talents: { sub_talents: [], unlockedAt: null }, likeSum: p.likeSum, likeCount: p.likeCount,
seedTag: SEED, createdAt: new Date(), updatedAt: new Date(),
});
}
console.log('✓ 种子完成');
console.log(' 大师:', MASTERS.map((m) => `${m.displayName}(${m.region})`).join('、'));
console.log(' 路线:', TOURS.map((t) => `${t.region}¥${t.priceHint}`).join('、'));
console.log(' 众生墙普通人:', PEOPLE.map((p) => p.displayName).join('、'));
await closeDb();
}
run().catch((e) => { console.error(e); process.exit(1); });
+132
View File
@@ -0,0 +1,132 @@
# 关口能动性改造图纸(只读设计产出,2026-07-18)
> 产品核心:「我的分身,我在每朝每代的位置,我所在位置的【选择】对后世的影响,史记我的传」,
> 内核「你什么都没变,天花板全变了」。老板拍板:**玩家被安置、什么都没做 = 败笔,必改。**
## 0. 一句话结论
「关口只是算完位阶后的叙事装饰」——**对代码现状而言大半是错的,但它精确命中了唯一真正缺的那一根轴。**
- 选择**早就**改传、改下场、改论赞(**回望轴**:你走过的路写出你的书,已落地)。
- 选择**从不**改变你面对的下一个关口、也从不决定你活不活得到它(**前瞻轴**:老板说的「往前荡」——
除南齐 2 关口样张外,**全仓未实现**)。
⇒ 不是推倒重来,是**补一根轴 + 把已有的、闲置的原语用起来**。引擎改动约 25 行 + 3 条断言 + 数据填充。
## 1. 现状诊断(诚实)
### 1.1 引擎地图
| 文件 | 角色 | 关键函数 |
|---|---|---|
| `dynasties.js` | 天花板:六轴→位阶(确定性纯函数) | `computeRank`(L3875) |
| `decisions.js` | 关口引擎(纯常量表+纯函数,零 IO) | `canReach`/`replay`/`stateOf`/`availableDecisions`/`computeImpact`/`openLineage`/`appendDecision` |
| `guankou-{nanqi,han,tang,song,ming,liao,weijin}.js` | 各朝关口数据 | `*_DECISIONS` |
| `zhuan.js` | 成传:正文+注+史臣曰+赞曰,**逐格照你的选择叙述** | `buildZhuan`(L922),ben/xuan/mo 三库按 optionId 取 |
| `shizan.js` | 传末断语 + **你的**下场 | `shizanForLineage`(L1013)、`zanZhuan(decisionId,score)` |
| `dizan.js` | **帝纪论赞(皇帝级)**——另一根轴,**不进你的传管线** | 供 kg/tianxia 用 |
| `services/lineage.js` | 持久化层 | `decide`(L298)、`zhuanOf`(L445) |
★ 口径订正:你的下场与你的论赞出自 `shizan.js`;`dizan.js` 是帝纪论赞,本次不碰。
### 1.2 选择【已经改变了什么】(不是装饰,已落地)
1. **传正文**:`buildZhuan` 逐格取 `bank.xuan[optionId]`/`bank.mo[optionId]`,明写「这一步是你选的,史上真站这格的人选的是 X」。同画像同朝不同选 ⇒ 正文不同。
2. **下场**:取全链**最坏的 `fate.score`** → 下场正文/旁证随选择变。
3. **你的论赞(赞曰·转句)**:`zanZhuan(decisionId, fate.score)` 按分带取转句 → 论赞随选择变。
4. **影响档**:`computeImpact` 由 `option.layer`+`option.inherited` 算 0/50/200 年。
5. ★ **已经有关口就是「每条岔路=一个真人的下场,双证」**:`han-t5-jian-zheng`(谏争,已上线)三个选项=同一间屋子的三个真人:
- `wei-ling`=**胡广**(fate.score=5,三登太尉、位至太傅)=historical
- `jian-zheng`=**李固**(fate.score=0,诛,五十四)
- `tui-er-shang-shu`(score=1)
persona 出自各人本传,下场双证逐字。**这就是老板「削藩:晁错/袁盎/周亚夫」那道题的落地形态,只是做成了东汉版。**
### 1.3 选择【从不改变什么】—— 真正的败笔
1. **下一个关口的可达性,不随你的选择分岔。** 关口开闭四关:①没答过 ②`requiresRank`(开传即冻)
③`preconditions.state` ④`afterDecisions`(前一关**答过**即可,**不问怎么答**)/`closedByOptions`(选过某项则关)。
实测:**`closedByOptions` 全仓只用了一次**(南齐 2 关口样张);**`state` 真闸只用一次且是空操作**(`{ming:0}` 恒真)。
⇒ 汉/唐/宋/明/辽/魏晋 135+ 个关口里,**没有任何一个关口的「开不开」取决于你在前一关选了哪一项**。
2. **死了不结束路。** `fate.score` **不是** state key。选李固(诛)**不会**让下一关不发。
`buildZhuan` 自己都在吐槽:「善终富厚之后还在答下一道题——人死了还在答题」。
⇒ 老板的「这个关口怎么选,决定你还活不活得到下一个关口」= **全仓未实现**。
3. **多数朝代的关口是黑的。** `decisions.js` 只 spread 了 `NANQI_DECISIONS`+`HAN_DECISIONS`;
tang/song/ming/liao/weijin 只 import+re-export、**没 spread**,因为它们的**体例**(`shizan.js` TIYUAN + 断语库)不存在。
⇒ **用户今天只能走到南齐和汉。**
**小结:能动性存在于「回望」,不存在于「路径」。前瞻轴正是老板点名那一根,也是唯一真缺的一根。**
### 1.4 削藩那个例子的实况
景帝三年=前154=**西汉**,而引擎的汉是**东汉**(25–220)。晁错/袁盎在 `renwu.js` 的 `XIHAN_ONLY`,**周亚夫全仓零命中**。
⇒ 老板的**字面**例子需要一个不存在的「西汉」朝;但它的**结构**(一处境、多真人、多下场)**已在东汉 `han-t5-jian-zheng` 完整落地**。
## 2. 最小改造图纸
### 2.1 choice schema 对齐
| 老板要的 | 现有字段 | 动作 |
|---|---|---|
| 走谁的路 | `option.persona`+关口级 `historical`(真人只在散文/cites里) | **新增 `option.who:{name,ref,cites}`**,前端可标「走李固的路」。缺则回落 persona |
| 下场 | `option.fate:{score,text,cites}` | 复用不动 |
| 论赞倾向 | 由 `fate.score` 经 `zanZhuan` 分带 | 复用,不新增字段 |
| 解锁/锁死下一关 | 下游 `preconditions.afterDecisions`(只问答没答)/`closedByOptions` | **新增 `preconditions.afterOptions:[ref(dId,oId)]`**(必须选过某具体项才开=`closedByOptions` 的正向补集)并**填充** |
| cite双证 | `fate.cites`/`situationCites` | 复用 |
### 2.2 引擎改动(`decisions.js`,三处都小)
**(A) `preconditions.afterOptions`(正向选择分岔)**
- `availableDecisions`:3a/3b 之间加 `for (const r of p.afterOptions||[]) if (!takenRefs.has(r)) return false;`
- `preconditionsOf`:把 `afterOptions` 拼进去给前端。
- `assertInvariants`:每个 ref 必须解析到真实 `decisionId/optionId`(照 `closedByOptions` 同款)。
- **意义**:让「你怎么选决定你面对什么下一关」为真的最关键字段。`afterDecisions` 只能表达「答过」,表达不了「这么答过」。
**(B) `option.terminal`(死/永久离场 → 传到此为止)**
- schema `terminal:{value:true, why, cites}`(独立于 fate:「失位」不等于「死」,不宜用 `fate.score<=0` 硬套)。
- `replay` 返回值加 `terminated`;`availableDecisions`:`if (state.terminated) return [];`
- `assertInvariants`:`terminal.value===true` 必须带 `terminal.cites`(真人真死双证),否则拒收 —— 同「查不实宁可不写」。
- **意义**:实现「活不活得到下一关」,同时修掉「人死了还在答题」。
**(C) `computeImpact` / fate-layer 反相关不变式**:不动。
### 2.3 传/下场层:基本不动
- ★ 新增选项**必须同时加 `bank.xuan[oId]` 和 `bank.mo[oId]`**——`buildZhuan` 缺库会当场抛(自守护),别绕过、别用占位符糊弄。
### 2.4 持久化/路由:**零改**(`decide` 全靠引擎判定,新字段自动生效)
### 2.5 前端:轻(显示 `who`=走谁的路;「选此项将锁死/解锁/终结」做成选前提示)
## 3. 料缺口(现在不补,只列)
| 关口 | 现成真人下场料 | 缺什么 |
|---|---|---|
| 汉·`han-t5-jian-zheng` 谏争 | **有,全三支双证**(胡广/李固/杜乔) | 无 —— 直接可做第 1 步 |
| 南齐·四户 | 事层两支有真人;**制/道两支是推演** | **不可补**(强安真人=造假,须保留「推演」标注) |
| 汉其余 | 处境双证,多数 `historical:null` | 需另找「同处境多人不同选」的双证料 |
| 西汉·削藩 | 晁错/袁盎在 renwu;**周亚夫全缺** | ①加「西汉」朝(大工程) ②curl 周亚夫 ③新建 guankou-xihan.js。大缺口,不现在补 |
| tang/song/ming/liao/weijin | 关口料已双证,但**黑的** | 卡在**体例**(TIYUAN+断语库),非料缺 |
## 4. 守根
1. **「你没变,天花板变了」不破**:`computeRank` 一字不动;`rankSnapshot` 开传即冻。
能动性**只**来自关口路径,**绝不**来自改位阶。关口内「迁/贬」也只改「接下来碰到哪些关口」,**不改 rank 快照**。
2. **「查不实宁可不写」不破**:`terminal` 与分岔用的 `fate` **必须带双证 cites**,两条新断言钉死。
3. **不给 制/道 空支硬安真人**:制/道 层历史上常确无人从此格这么做,现设计已诚实标「推演,非史事」,**保留**。
分岔的历史根来自「一处境多真人」,不来自逼每条岔路都挂真人。
## 5. 风险
1. **分岔变瞎编**:加了 afterOptions 会忍不住编「你若选X就会遇到Y」。红线:**分岔本身必须史上真有**(双证)。
2. **传变回套模板**:机械加分岔而不写 per-person 的 xuan/mo → 千传一面。保住每个真人的声口。
3. **搞破「你没变」**:用「改 rank/选项抬位阶」实现分岔 = 当场破功。`terminal` 只许关链,绝不许动 rank。
4. **死亡截断顺序**:同 tier 内关口多为并行,terminal 一步关掉余下关口可能截掉本应先答的;须保证关口序由 `when`/`afterDecisions` 良序,上线前跑穷举验。
5. **编年倒流**:afterOptions 必须尊重 `when` 时序,别造出「先494后490」。
## 6. 落地顺序(最快让老板看到「我的选择真的改了我的命」)
1. **先接那道已经是真分岔的关口**(0 新料、~10 行):`han-t5-jian-zheng`——
给 `jian-zheng`(李固,诛)加 `terminal`(李固下场双证);让 `wei-ling`(胡广,善终)经 `afterOptions` 开启一道后续三公关口。
玩家立刻看见:**选李固 → 你死在这里,传到此为止;选胡广 → 你活下去,面对下一关。** 全踩在双证真人料上。
2. **补引擎两原语 + 三断言**(afterOptions/terminal/invariants)+ 测试各补一条(terminal 真截断、afterOptions 真分岔)。
3. **在汉/南齐已有双证料上再填 2-3 个真分岔**,坐实「选择链=路径」。
4. (后续)补一朝体例 → 点亮 tang/song 的分岔;(更后)上「西汉」朝 + 周亚夫 curl,做字面上的削藩题。
Binary file not shown.
+61
View File
@@ -0,0 +1,61 @@
// attributes.js —— 铸身「擅长 / 性格」两维的结构化选项(老板 2026-07-21:铸身严格只放擅长/性格/分量/家世,
// 每维多选项、各带权重真影响官职)。
//
// ★ 不另造引擎:每个选项映到【现成的一项副天赋(sub_talent)】——照命 buildProfile→computeAxes→office5d 真读的那份。
// ★★ 每个选项【恰好 1 项副天赋】= 恰好 1 个才轴点:于是【每点一个本行擅长,官职正好升一级】
// (本行核心轴 0/1/2/3/4 项 → 才 0/1/2/3/4 → 唐 县尉/县令/刺史/侍郎/宰相)。每个选项都真影响官职,不糊弄。
// 权重 = 落在哪条轴:命中你【主业(lei)本行核心轴】→ 才涨最多;旁轴也算一点(office5d.coreStrength 的旁轴辅助)。
//
// ⚠ 17 项副天赋【每一项只归一个选项】(不重叠),多选时不互相顶掉。
// 擅长(才/能力)→ 才轴。按轴分组,前端好显「哪些对你主业加成最大」。
export const SHANCHANG_OPTS = [
// 文采轴 wen
{ id: 'poetic', name: '文采 · 细腻', axis: 'wen', sub: 'poetic', hint: '文字有画面感' },
{ id: 'story', name: '会讲故事', axis: 'wen', sub: 'story', hint: '善用故事比喻' },
{ id: 'vivid', name: '表达 · 生动', axis: 'wen', sub: 'vivid', hint: '像真人说话' },
{ id: 'eloquent', name: '辞采 · 雄辩', axis: 'wen', sub: 'eloquent', hint: '出口成章、辞采华美' }, // ★擅长深化 2026-07-31
// 吏能轴 fa
{ id: 'rigorous', name: '严谨 · 专业', axis: 'fa', sub: 'rigorous', hint: '给依据、不夸大' },
{ id: 'datadriven', name: '有理有据', axis: 'fa', sub: 'datadriven', hint: '爱用数据例证' },
{ id: 'concise', name: '简洁 · 利落', axis: 'fa', sub: 'concise', hint: '先给要点' },
{ id: 'shortfirst', name: '结论先行', axis: 'fa', sub: 'shortfirst', hint: '结论在前、理由在后' },
{ id: 'analytical', name: '洞察 · 缜密', axis: 'fa', sub: 'analytical', hint: '条分缕析、逻辑缜密' }, // ★擅长深化 2026-07-31
// 果决轴 wu
{ id: 'blunt', name: '直率 · 果断', axis: 'wu', sub: 'blunt', hint: '有话直说、敢拍板' },
{ id: 'motivate', name: '激励 · 带动', axis: 'wu', sub: 'motivate', hint: '有感染力、能带人' },
{ id: 'bold', name: '果敢 · 担当', axis: 'wu', sub: 'bold', hint: '敢拍板、有魄力担当' }, // ★擅长深化 2026-07-31
// 学问轴 jingxue
{ id: 'jingxue_tong', name: '治学 · 通经', axis: 'jingxue', sub: 'jingxue_tong', hint: '读得通、钻得深' },
// 机锋轴 tan
{ id: 'socratic', name: '启发 · 思辨', axis: 'tan', sub: 'socratic', hint: '会追问、能点透' },
];
// 性格(脾气)→ 行为轴(决定选拔渠道更合你,也算入才的广度)。
export const XINGGE_OPTS = [
// 交游轴 jiao
{ id: 'warm', name: '温和 · 亲和', axis: 'jiao', sub: 'warm', hint: '好相处、有人情味' },
{ id: 'humor', name: '幽默 · 风趣', axis: 'jiao', sub: 'humor', hint: '会玩梗、轻松' },
{ id: 'playful', name: '俏皮 · 活泼', axis: 'jiao', sub: 'playful', hint: '跳脱、有活力' },
{ id: 'signature', name: '鲜明 · 个性', axis: 'jiao', sub: 'signature', hint: '有辨识度、记得住' },
// 沉毅轴 heng
{ id: 'calm', name: '沉稳 · 克制', axis: 'heng', sub: 'calm', hint: '稳得住、不煽情' },
{ id: 'patient', name: '耐心 · 细致', axis: 'heng', sub: 'patient', hint: '循循善诱、拆得细' },
];
const SC_BY_ID = Object.fromEntries(SHANCHANG_OPTS.map((o) => [o.id, o]));
const XG_BY_ID = Object.fromEntries(XINGGE_OPTS.map((o) => [o.id, o]));
// 选中的擅长/性格选项 → 副天赋 id 并集(喂给 validateSubs → computeAxes)。未知 id 忽略。
export function subsFromAttrs(shanchang = [], xingge = []) {
const subs = new Set();
for (const id of Array.isArray(shanchang) ? shanchang : []) { const s = SC_BY_ID[id]?.sub; if (s) subs.add(s); }
for (const id of Array.isArray(xingge) ? xingge : []) { const s = XG_BY_ID[id]?.sub; if (s) subs.add(s); }
return [...subs];
}
// 只留合法选项 id、去重。
export function validAttrIds(ids, kind) {
const by = kind === 'xingge' ? XG_BY_ID : SC_BY_ID;
return [...new Set((Array.isArray(ids) ? ids : []).filter((id) => by[id]))];
}
+83
View File
@@ -0,0 +1,83 @@
import { chat } from './services/llm.js';
import { config } from './config.js';
import { getDb } from './db.js';
import { ZHANZHEN } from './zhanzhen.js';
import { scanEra } from './sucaibao.js';
/**
* 战争篇·白话面 —— 把一场战役的处境/战果/节点/开打那一刻,通俗重述成大白话(表面松弛好玩,正史原文折在底层)。
*
* ★★ 白话 = 同一条正史事实的【换皮翻译】,不是创作:硬史实(兵力/胜负/生死/节点顺序)一字不动、正史原文一字不删只折叠。
* ★ 走 Qwen(services/llm.js·数据不出域),缓存 Mongo baihuaCache;无 key / 海外兜底关 → {status:'off'} 前端显原文。
* ★ 四锁:① 事实白名单(只准复述所给节点/战果/兵力,禁新增数字改胜负生死) ② 演义黑名单硬过滤 ③ 断代守卫(防晚出器物)
* ④ 校验不过(节点数不符/命中黑名单/断代违禁)→ 不缓存、返回 off,宁可显原文也不上没把住的白话。
*/
const BLACKLIST = ['借东风', '草船借箭', '连环计', '铁索连环', '华容道义释', '苦肉计', '蒋干盗书', '温酒斩华雄', '三英战吕布', '空城计', '单骑救主'];
// 纯函数:拼通俗重述的消息。可单测。
export function buildBaihuaPrompt(b) {
const nodeList = (b.nodes || []).map((n, i) => `${i + 1}. ${n.label}`).join('\n');
const sys = [
'你是把文言史料翻成大白话的高手。把给定这场真实战役,通俗重述成松弛、好玩、第二人称「你」的大白话。',
'铁律(换皮翻译·不是创作):',
'① 只能复述所给的处境/战果/节点/兵力,【绝不新增或改动任何硬史实】:兵力数字、胜负、关键人物生死、节点先后,一字不动。',
'② 号称数就说是「号称」,别当真数;不确定的别编。',
'③ 不掺演义桥段(借东风/草船借箭/连环计/华容道义释/苦肉计等一律不许出现)。',
'④ 器物称谓只用那个朝代及以前确有之物,不用晚出之物(火器/交子/点茶/缠足/棉常服/辣椒玉米等)。',
'⑤ 「战场此刻」只把已载事实翻成有画面的白话(火/风/烟/水/溃/坑),不编造对白、心理、气味颜色等史料没有的细节。',
'只输出一个 JSON:{"hook":"一句身份钩子(第二人称·把人带进去)","situ":"白话处境","real":"白话战果·结论前置","nodes":["逐条对应上面节点的白话·条数必须一致"],"scene":"战场此刻·一段有画面的白话(只重述已载事实)"}',
].join('\n');
const user = [
`战役:${b.name}(${b.side}·${b.year < 0 ? '前' + -b.year : b.year}年)`,
`赌注:${b.stake}`,
`处境:${b.situ}`,
`战果:${b.real}`,
`史实节点(按序·条数=${(b.nodes || []).length}):\n${nodeList}`,
'请据此输出白话 JSON。',
].join('\n');
return [{ role: 'system', content: sys }, { role: 'user', content: user }];
}
// 纯函数:解析+四锁校验。返回 {ok,baihua} 或 {ok:false,reason}。可单测。
export function parseBaihua(text, b) {
let o = null;
try { const m = String(text || '').match(/\{[\s\S]*\}/); if (m) o = JSON.parse(m[0]); } catch { o = null; }
if (!o || !o.situ || !o.real || !Array.isArray(o.nodes)) return { ok: false, reason: 'bad-json' };
// 锁②:演义黑名单
const blob = `${o.hook || ''}${o.situ}${o.real}${(o.nodes || []).join('')}${o.scene || ''}`;
const bl = BLACKLIST.find((w) => blob.includes(w));
if (bl) return { ok: false, reason: 'blacklist:' + bl };
// 锁③:断代守卫(她传等无 chao 的跳过)
if (b.chao) { const eh = scanEra(blob, b.chao); if (eh.length) return { ok: false, reason: 'era:' + eh.join(',') }; }
// 锁①:节点条数须与史实一致(防漏/增节点)
const need = (b.nodes || []).length;
const nodes = o.nodes.slice(0, need);
return {
ok: true,
baihua: {
hook: String(o.hook || '').slice(0, 120),
situ: String(o.situ).slice(0, 600),
real: String(o.real).slice(0, 600),
nodes: nodes.map((n, i) => ({ i, text: String(n || '').slice(0, 300) })),
scene: String(o.scene || '').slice(0, 800),
},
};
}
/** 取一场战役的白话面。缓存优先;无 key → off;生成后四锁校验不过也 off。 */
export async function getBaihua(id) {
const b = ZHANZHEN[String(id || '')];
if (!b) return { status: 'off' };
if (!config.llm?.dashscopeKey && !config.llm?.overseasFallback) return { status: 'off' };
const db = getDb();
const cached = await db.collection('baihuaCache').findOne({ id: b.id });
if (cached?.baihua) return { status: 'ok', baihua: cached.baihua };
let text, model;
try { ({ text, model } = await chat(buildBaihuaPrompt(b), { temperature: 0.5 })); }
catch (e) { console.warn(`[baihua] LLM 不可用: ${e?.message || e}`); return { status: 'off' }; }
const r = parseBaihua(text, b);
if (!r.ok) { console.warn(`[baihua] ${b.id} 校验不过: ${r.reason}`); return { status: 'off' }; }
await db.collection('baihuaCache').updateOne({ id: b.id }, { $set: { id: b.id, baihua: r.baihua, model, at: new Date() } }, { upsert: true });
return { status: 'ok', baihua: r.baihua };
}
+231
View File
@@ -0,0 +1,231 @@
# 编年 · 层 · 影响 —— 建模设计(biannian-design.md)
> 第一步只交设计,不改一条数据、不建前端。全部数字都是 `node` 实跑 `src/tianxia.js`
> 打印出来的,不是手数(脚本见文末「审计口径」)。基线:`npm test` = **601 通过**(本步未动)。
---
## 0. 结论速览
| 事 | 数 | 状态 |
|---|---|---|
| 大事件(shi)总数 | **932** | 25 朝 |
| 层 layer 已标 | **932 / 932(100%)** | ✅ ①「每事标层」实际上**已经做完** |
| 层分布 | 事 830 / 制 92 / 道 10 | 道极吝啬(两千年才 10 个),符合判据 |
| impact 已标 | **0 / 102 制道事件** | ❌ ②「制/道连下游影响」= 本产品线**真正的空白**,全部待做 |
| 层值编码分裂 | 23 朝 pinyin,**2 朝(qin/beisong) 用汉字** | ⚠ 需归一(见 §2.1) |
| zaichang 形状分裂 | array 7 朝 / string 12 朝 / 无 6 朝 | ⚠ 前端必须两种都吃(见 §2.4) |
**一句话**:`layer` 已铺满,不用重做;产品线的正主功能是 **impact(影响链)**,102 个制/道
事件一个都没连下游。补编年(③)是第三优先,且受「双证原文」红线约束,只补真查得实的。
---
## 1. 事件(shi)字段现状 —— 实测
### 1.1 读取路径(不用改)
`src/chao/<code>.js` 各 `export const CHAO_SHI` → `src/tianxia.js` 第 290 行原样挂到
`CHAO[code].shi`(**passthrough,一个字段都不删**)→ `shiAt()` / `chenShiByReign()` 按年过滤后
原样带出。⇒ **你在 chao 文件里给事件加的 `layer`/`impact`,tianxia 自动带出,无需改 tianxia**
(唯一可选的小改:§2.3 的防御性 normalizer)。
KG:`src/kg.js` 第 306–310 行为每条 shi 建 event 节点,且 `raw: s` **挂整条原始对象** ⇒
`node.raw.layer` / `node.raw.impact` 自动可用,**别碰 kg.js**(它派生)。
### 1.2 字段清单(当前两种 shape,都要向后兼容)
**共有字段(所有朝)**
| 字段 | 类型 | 说明 |
|---|---|---|
| `year` | number | 公元年(负=公元前)。KG 按 `${code}:${year}` 建索引 |
| `shi` | string | **本纪原文逐字**(层/影响之外,这是唯一的"原文"栏) |
| `juan` | string | 卷次,如 `《漢書》卷五` |
| `layer` | `'shi'\|'zhi'\|'dao'` | 见 §2.1。**推演**(我们的读法) |
| `kind` | `'yuanwen'` | shi 栏的性质;全部 `yuanwen`(逐字原文) |
| `layerWhy` / `layerNote` | string | **强制**:每条层判据给理由,不许空标 |
| `zaichang` | array \| string | 「那年什么位阶被卷进去」,**推演**(`kind:'tuiyan'` / `zaichangKind:'tuiyan'`) |
**可选字段(部分朝已有)**:`yue`(月) `gz`(干支) `jinian`(纪年) `source`(默认`benji`)
`witnesses`(见证数,xin/wu/xixia 有) `crossWitness` `zhengwu` `biaoCross` `shuangzheng`
`yearOpen`/`yearNote`(系年存疑) `weiFa`(从未生效的制)。—— **新 schema 一律叠加,不动这些。**
### 1.3 layer 现状:100% 已标,但编码分裂(⚠ 待归一)
- **23 朝用 pinyin** `'shi'/'zhi'/'dao'`(xihan/ming/xixia/shuhan/… 及 xin 的工厂函数 `S(...)`
第 5 参,实测 `s.layer==='shi'`)。
- **2 朝用汉字** `'事'/'制'/'道'`:**`qin.js`(22 条) 与 `beisong.js`(46 条)**。
- 语义(各朝文件头一致,判据同一把尺):
- **事 shi(1–50 年)**:一场仗、一次废立、一个人的死、一次亡国。绝大多数。
- **制 zhi(50–500 年)**:一个制度的确立/废除(官制、律、铸钱、学校、科举定式、田制…)。
- **道 dao(500+ / 硬测「到公元 1500 年还在起作用吗」)**:改变了此后千年且今仍运转。极吝啬。
### 1.4 zaichang 形状分裂(⚠ 前端契约)
- **array `[{wei, shuo}]`**(7 朝):xihan xin shuhan wu liusong beiqi liao —— 每项另标 `kind:'tuiyan'`。
- **string**(12 朝):dongjin beiwei liang sui tang wudai beisong xixia jin nansong ming +chen(部分) —— 另有 `zaichangKind:'tuiyan'`。
- **无 zaichang**(6 朝):qin donghan wei xijin nanqi beizhou +chen(部分)。
- ⇒ 这是既有的历史遗留,**不在本产品线强行迁移**(与 layer/impact 正交)。前端 `Biannian.vue`
必须同时处理 array / string / 缺三态(§2.4)。
### 1.5 impact 现状:0,且有一个**命名撞车**(无害,但要说清)
- 没有任何一条 **事件**带下游影响字段。102 个制/道候选全空。
- ⚠ 全库 grep `impact:` 有 7 处命中(xin.js 1 + xixia.js 6)—— 那是 **字形/异文审计表**
(WITNESS 行)里的 `impact`,意思是「这个异体字**涉不涉文义**」(如 `不涉文义`),
**挂在 WITNESS 行对象上,不在 CHAO_SHI 事件上**。⇒ 两个不同对象命名空间,运行时不冲突。
但为免人读混,本设计的事件级 `impact` 语义在 §2.2 写死;老板已点名字段叫 `impact`,沿用。
---
## 2. Schema 设计(向后兼容,只叠加不破坏)
### 2.1 `layer`(已存在,本步只做**归一**决策)
**规范值 = pinyin `'shi' | 'zhi' | 'dao'`**(取 23:2 的多数派 + 与 `kind:'yuanwen'/'tuiyan'`
的罗马化惯例一致 + xin 已用 `s.layer==='shi'` 过滤)。汉字 `事/制/道` 只作**显示 label**。
> ⚠ 这一条**推翻**老板任务书草案里写的 `layer:'事'|'制'|'道'`(汉字)——
> 草案是示意 label,不是存储值规约;把 23 个文件改成汉字,风险远大于把 2 个改成 pinyin。
> **建议**:qin.js / beisong.js 的第二步 agent 把本文件层值 `事→shi 制→zhi 道→dao` 归一;
> 并在 tianxia 加一行防御性 normalizer(§2.3)兜底,保证前端永远只见 pinyin。
- 层是**推演**:`layer` 值本身就是「我们的读法」,无须再标 kind;**`layerWhy` 强制**给理由。
### 2.2 `impact`(新字段,只加在**制/道**事件上,这是产品线正主)
```js
impact: {
reach: 'dangchao' | 'houshi' | 'qiannian', // 影响半径。label:当朝/后世/千年
kind: 'yuanwen' | 'tuiyan', // 与事件级 kind 同词表:原文明引 vs 推演
reason: string, // 【强制】史学通说 / 制度史依据,说人话
to?: [ // 【可选】下游落点,可多个
{ code?: string, year?: number, label: string }
],
cite?: string, // 仅 kind:'yuanwen' 时【强制】:原文明引句
witnesses?: number, // kind:'yuanwen' 时建议≥2(双证)
reachWhy?: string // 可选:半径判据(仿 layerWhy)
}
```
**字段规约(照史学红线,逐条硬性)**
1. **只给 `layer==='zhi'|'dao'` 的事件加 `impact`**;`layer==='shi'` 不加(事没有下游影响链,
有的话它就不是"事")。⇒ impact 天生是 102 个候选的子集。
2. `reach` 与 layer 有对应但**不强绑**:制→常 `houshi`,道→常 `qiannian`;用**独立的下游年代**判,
在 `reachWhy` 写清(如「至 1905 科举废,凡 2041 年 ⇒ qiannian」)。
3. **`kind:'tuiyan'`(推演,绝大多数)**:`reason` 强制,必须是**制度史通说 / 后世明引**,
✗ **不许编造「A 导致 B」的因果**;拿不准、无据 ⇒ **不写那条 impact**,别硬连(红线)。
4. **`kind:'yuanwen'`(原文明引后世征引,如「漢承秦制」)**:`cite` 强制、逐字、
`curl -sk` 双证(`witnesses>=2`),字形照录(爲/為)。
5. **`to[].label` 强制**(人读的落点描述,如「至今农历正月仍为岁首」);`code`/`year` 可选 ——
落点若在本库 25 朝之内就填(KG 可 `code→chaoId`、`code:year→evt:code:year:idx` 连边);
落点在库外(清/民国/今)只填 `label`。⇒ 既支持「千年后 X 朝仍在用」,也支持「至今仍运转」。
6. 影响链**可级联**:如 `jin 1153 改燕京为中都` → `to:[{code:'ming',year:1420,label:'永樂定北京为京师'}]`
→ 千年(元明清至今为都)。级联在数据里天然成图,KG/前端各自渲染。
### 2.3 兼容与迁移
- **不破坏**:impact 是新可选字段;老事件无 impact,前端/ KG 判 `if (e.impact)` 即可。
- **归一 qin/beisong**:第二步各自 agent 把 layer 汉字→pinyin(并行改不同文件,不冲突)。
- **tianxia 可选小改(唯一允许动 tianxia 的地方)**:在 shi passthrough 处加一行
`layer` normalizer(`事→shi 制→zhi 道→dao`)兜底,防某朝漏改。**非必须**,归一到位就不用。
- **KG**:零改动。`node.raw.impact` 自动可用;将来若要 impact **边**(制→后世事),那是 KG 线
自己的 kg.js 改动,不属于本产品线。
### 2.4 前端 `Biannian.vue` 契约(第二步建,这里先定契约)
- 主视图:**编年轴**(按 year 排)+ **层筛选**(事/制/道,pinyin→汉字 label)。
- **影响视图**:点一个制/道事件 → 展开 `impact`(reach 徽标、reason、to 落点、cite/推演标注);
`to.code`/`year` 命中本库则可跳转到下游事件(级联链)。
- **必须两种 zaichang 都吃**:array `[{wei,shuo}]` 渲染成列表;string 直接渲染;缺则不显。
- **诚实标注**(照本仓铁律):层与影响**都要显式标「推演」**;`kind:'yuanwen'` 的 impact 显示 cite;
西夏等**源限**朝的"薄"要说清是史料所限(§3.4),不许长成和"我们没做"同一句话。
---
## 3. 编年覆盖审计 + 补缺清单
### 3.1 全表覆盖(实测,按起年排)
| 朝 | 起讫 | 跨年 | 帝 | 事件n | 事/制/道 | 每百年密度 | 判定 |
|---|---|---:|---:|---:|---|---:|---|
| 秦 qin | -221~-207 | 14 | 2 | 22 | 17/2/**3** | 157 | 充足(超密) |
| 西汉 xihan | -202~8 | 210 | 12 | **18** | 10/6/2 | **8.6** | ★**最薄**(密度全表最低) |
| 新 xin | 9~23 | 14 | 1 | 25 | 16/9/0 | 179 | 充足(制富) |
| 东汉 donghan | 25~220 | 195 | 12 | 42 | 36/6/0 | 21.5 | 偏薄 |
| 曹魏 wei | 220~265 | 45 | 5 | 22 | 16/6/0 | 48.9 | 充足 |
| 蜀汉 shuhan | 221~263 | 42 | 2 | 53 | 52/1/0 | 126 | 充足(超密) |
| 孙吴 wu | 222~280 | 58 | 4 | 26 | 23/3/0 | 44.8 | 充足 |
| 西晋 xijin | 265~316 | 51 | 4 | 21 | 17/3/1 | 41.2 | 充足 |
| 东晋 dongjin | 317~420 | 103 | 11 | 23 | 18/5/0 | 22.3 | 偏薄 |
| 北魏 beiwei | 386~534 | 148 | 12 | 127 | 120/7/0 | 85.8 | 充足(超密) |
| 刘宋 liusong | 420~479 | 59 | 8 | 128 | 126/2/0 | 217 | 充足(超密) |
| 南齐 nanqi | 479~502 | 23 | 7 | 21 | 21/0/0 | 91 | 充足;**制/道=0** |
| 梁 liang | 502~557 | 55 | 4 | 24 | 23/1/0 | 43.6 | 充足 |
| 北齐 beiqi | 550~577 | 27 | 6 | 17 | 16/1/0 | 63 | 充足 |
| 北周 beizhou | 557~581 | 24 | 5 | 28 | 27/1/0 | 117 | 充足 |
| 陈 chen | 557~589 | 32 | 5 | 30 | 30/0/0 | 94 | 充足;**制/道=0** |
| 隋 sui | 581~618 | 37 | 3 | 37 | 26/9/**2** | 100 | 充足(制/道极富,最佳 impact 锚朝) |
| 唐 tang | 618~907 | 289 | 21 | **46** | 45/1/0 | **15.9** | ★**薄**(289 年只 46 事,且制仅 1) |
| 五代 wudai | 907~960 | 53 | 14 | 42 | 40/2/0 | 79 | 充足 |
| 辽 liao | 916~1125 | 209 | 9 | 43 | 39/4/0 | 20.6 | 偏薄 |
| 北宋 beisong | 960~1127 | 167 | 9 | 46 | 42/4/0 | 27.5 | 尚可 |
| 西夏 xixia | 1038~1227 | 189 | 10 | **20** | 9/11/0 | **10.6** | **源限**(无正史,别硬补,见 §3.4) |
| 金 jin | 1115~1234 | 119 | 10 | 21 | 15/5/1 | 17.6 | 偏薄 |
| 南宋 nansong | 1127~1279 | 152 | 10 | **22** | 21/1/0 | **14.5** | ★**薄**(且制仅 1) |
| 明 ming | 1368~1644 | 276 | 16 | **28** | 25/2/1 | **10.1** | ★**薄**(受"抠本纪"纪律约束,见 §3.3) |
**世界观地板建议(每朝至少要撑得起的)**:(a) **每位在位皇帝 ≥1 件**;(b) 该朝**所有制/道转折点**在场;
(c) 重大战/乱/亡国节点在场。密度目标只对下面的"薄"朝加压,其余充足朝**优先做 impact,不硬加事**。
### 3.2 impact 优先锚点(②的正主活,102 个候选,先做这批)
**10 个「道」事件 —— 千年影响链,最高优先**(impact 必做,reach 多为 qiannian):
| 朝 | 年 | 事 | 建议下游落点(推演,需据) |
|---|---:|---|---|
| qin | -221 | 定「皇帝」号 | → 帝制沿用至 1912(label) |
| qin | -221 | 罢分封·置郡县 | → 中国地方行政骨架至今;原文可引「漢承秦制」(yuanwen) |
| qin | -221 | 一法度衡石丈尺·车同轨·书同文 | → 文字/度量统一至今 |
| xihan | -136 | 置五经博士 | → 经学官学至 1905 科举废,2041 年(层已析,直接抬成 impact) |
| xihan | -104 | 正历·以正月为岁首 | → 农历正月至今 2130 年 |
| xijin | 268 | 泰始律令成 | → 律典体例下沿(→隋开皇律→唐律) |
| sui | 581 | 行新律(开皇律) | → 唐律之母本(houshi/qiannian) |
| sui | 605 | 开通济渠(大运河) | → 南北经济动脉至今 |
| jin | 1153 | 改燕京为中都 | → **级联** ming 1420 定北京→元明清至今为都 |
| ming | 1380 | 罢中书·废丞相 | → 无相皇权独揽至 1912(清亦不设);层已析 |
**92 个「制」事件**里的高价值链(示例,非穷举,均**推演·需制度史通说据**):
`beiwei 485 均田` → `sui 609 均田` → `tang 租庸调` → `tang 780 两税法`(均田制的建立→鼎盛→瓦解,
一条可级联的赋税史主线);`xijin 265 中正六条` → 九品中正→门阀;`beiwei 486 三长` → 隋唐乡里;
`sui 583 罢郡`(州县二级)→ 后世地方两级制。⇒ **建议第二步优先把这几条主线连成级联链**,
最能体现「一步制度变革,千年后还在起作用吗」。
### 3.3 补事件清单(③,第三优先,逐条**双证原文**,查不实宁可不补)
- **TIER A(薄 + 高权重 + 正史充足,建议多补)**
- **西汉 xihan(+10~14 事)**:其自身 `CHAO_COVERAGE.shi` 已自认「事層遠遠不全」——
高帝/惠帝/高后/文帝/昭帝/宣帝/元帝/哀帝 编年事几乎没取。补:文景削藩·推恩、武帝征匈奴/张骞、
昭宣中兴、霍光废立、王莽代汉铺垫。**双证《汉书》本纪 + 卷十九表**。
- **唐 tang(+15~20 事)**:289 年仅 46 事、制仅 1,最不撑世界观。补:玄武门、贞观、永徽律(制)、
武周代唐、开元、安史之乱、元和中兴、甘露之变、会昌灭佛、黄巢、朱温代唐。双证新旧唐书。
- **明 ming(+6~10,受约束)**:⚠ 本朝守「抠本纪」纪律,一条鞭法/隆庆开关等**在志不在纪**——
按红线**不硬塞进 CHAO_SHI**,或另标 `source` 单列。可补本纪确有的:土木/夺门已在;
可加 郑和(已)、大礼议(已)。**补量有限,别为凑数破纪律**。
- **TIER B(薄,补制 + 关键事)**
- **南宋 nansong(+6~10)**:绍兴和议、秦桧擅政、隆兴/开禧北伐、端平入洛、崖山亡国;制层补科举/文官制。
- **金 jin(+4~8)**:大定之治、贞祐南迁、蒙金战争(海陵迁都已在)。
- **辽 liao(+4~6)**:承天太后摄政、头下军州制(澶渊已在)。
- **东汉 donghan(+4~8)**:党锢、外戚宦官更替、黄巾。
- **北宋 beisong**:46 尚可;**优先 impact 而非加事**(庆历新政/靖康可补)。
- **TIER C(充足 / 源限,不加或少加)**
- 隋、秦、蜀汉、刘宋、北魏、新 及南北朝诸短朝:**充足,优先做 impact**。
- **南齐 / 陈:制道=0** —— 若逐卷本纪确有制度条(如删定律科),可补 1~2 条制;无则不硬造。
### 3.4 ★ 西夏特别说明(防止有人去"补薄")
西夏 20 事 / 189 年看着最薄,但这是**源限不是缺工**:二十四史**无《夏史》**,只能取
《宋史·夏国传》《辽史》《金史》外国传的转述。**照红线:查不实宁可不补。** 它的"薄"是诚实的,
与本仓「三种没有」(②我们瞎 / ③史官没记)同理——**前端必须把它标成"源限",不许和"我们没做完"
长成同一句"暂无"**。且西夏 11 制 / 0 道也是老实话(西夏之制随国亡而死,无一活到 1500 年)。
---
## 审计口径(实跑,不手数)
- `node src/tianxia.js` 的 `count()` = `{chao:25, di:202, shi:932}`;层分布、密度、道/制清单由
scratchpad 脚本 `biannian_audit.mjs` / `dao_zhi_dump.mjs` 实跑 `allChao()` 打印。
- 层值 pinyin/hanzi 分裂由逐文件 `grep -oE "layer: ?'(shi|zhi|dao)'"` vs `'(事|制|道)'` 计数得:
仅 qin(22 汉字)、beisong(46 汉字),余 23 朝 pinyin。
- 基线:`npm test` = **601 pass**(本步只写本 .md,未动数据/代码)。
File diff suppressed because it is too large Load Diff
+461
View File
@@ -0,0 +1,461 @@
// 能力中枢 · 全平台功能知识库(结构化、常驻内存,无需 DB / AI key 即可用)。
// AI 助手据此向用户「详细讲解 + 答疑」,并产出适合语音播报的话术。
// 原则:只收录「真实、默认可用、用户能直接用」的核心功能;不收内部机制、
// 默认关闭(marketplace)、或未暴露(双语切换)的能力。把功能做详细,而不是乱补充。
// 新增/改动一个功能 = 在此加/改一条;助手无需改代码即可准确介绍它。
//
// 字段:id / name(当前术语:发布=公开主页、分享=定向分享) / category / aliases(检索用)
// / summary(一句话) / detail(详细) / howto(步骤+入口) / requires(前置)
// / limits(限制,可选) / faq(常见问答,可选) / entry(UI跳转) / where(口语入口) / speak(无LLM兜底话术)
// / gatedBy(功能开关名,可选:开关关着时这条能力【不出现】——助手不能推荐页面上不存在的按钮)
import { config } from './config.js';
const ALL_CATEGORIES = [
{ id: 'discover', name: '逛大师传承馆', intro: '平台第一站是「大师数字传承馆」——各领域签约名家的技艺、学识与作品,做成可对话、可传承的数字馆,点开任何一位随时请教。' },
{ id: 'tourism', name: '大师文旅', intro: '围绕数字大师定制的高端人文之旅,大师亲自接待、资深导游随行(平台免费帮大师引流,不参与线下收款)。' },
{ id: 'create', name: '我的分身', intro: '也可以为自己或家人建一个数字分身:把知识、记忆、经历搬进去(知识最重要,形象声音次要);在「我的分身」工作台里管理。' },
{ id: 'output', name: '让分身开口', intro: '让分身用形象和声音,替你说话、对话、出镜。' },
{ id: 'distribute', name: '分发', intro: '按「谁能看到」把分身给到别人:公开主页、定向分享链接。' },
{ id: 'monetize', name: '变现', intro: '用你自己的收款码,让分身替你接咨询、挣钱,平台不抽成。' },
{ id: 'grow', name: '成长与社交', intro: '分身越养越强会升为「生命体」,还能定向分享给特定的人。' },
{ id: 'assist', name: '分身帮我办事', intro: '养好的分身不只像你,还能用工具替你干活:联网查、记事、待办到点提醒、发你收件箱,甚至把多步流程存成工作流一键跑、定时自动跑。' },
{ id: 'account', name: '账号与购买', intro: '登录方式与工具购买。' },
];
// 「变现」分类下只有一条受 marketplace 门控的能力。开关关着时那条能力被过滤掉,
// 若分类还留着,用户点进去就是个空分类——所以分类本身也跟着开关走。
export const CATEGORIES = ALL_CATEGORIES.filter(
(c) => c.id !== 'monetize' || Boolean(config.features?.marketplace)
);
export const CAPABILITIES = [
// ── 逛广场·大师传承馆 ──────────────────────────────────────────────────
{
id: 'plaza-home', name: '大师数字传承馆(平台首页)', category: 'discover',
aliases: ['广场', '首页', '第一站', '大师传承馆', '大师墙', '传承馆', '众生墙', '逛', '发现', 'plaza', '进来看什么'],
summary: '平台第一站就是「大师数字传承馆」——各领域签约名家的技艺、学识与作品,做成可对话、可传承的数字馆。',
detail: '一进平台就落在「大师数字传承馆」,逛和聊都免费、不用登录。这里是各领域签约名家的数字分身,平台线下精修、免费为他们建了知识库与作品,可按城市、领域筛选,卡片能看代表作、短片,点进去随时对话请教。(想为自己或家人也建一个数字分身?页面底部有低调入口,进「我的分身」工作台创建——那是个人工具,不是这里的主角。)',
howto: '进平台就是「大师数字传承馆」,点城市/领域筛选,点某位大师的卡片进去对话请教。',
requires: [],
entry: { path: '/plaza' }, where: '大师数字传承馆(平台首页)',
speak: '平台第一站就是「大师数字传承馆」——各领域签约名家的技艺、学识和作品,做成可对话、可传承的数字馆,随便逛、点开任何一位随时请教,不用登录也免费。',
},
{
id: 'master-chat', name: '和数字大师聊', category: 'discover',
aliases: ['和大师聊', '大师', '名家', '请教', '问大师', '大师分身', '对话大师', '大师短片', '代表作'],
summary: '大师传承馆上的名家数字分身可随时对话请教,还能看短片、代表作。',
detail: '大师是平台线下亲自建档、签约授权的各领域名家(雕塑、古建、苏绣、徽墨…),形象、声音、知识都精修过。点开大师详情能看短片和代表作,点「和 TA 聊」就能随时请教。注意:这是大师授权的数字分身、非本人实时应答,深度交流以线下为准。',
howto: '在大师传承馆点一位大师,进详情页点「和 TA 聊」。',
requires: [],
entry: { path: '/plaza' }, where: '大师传承馆 → 大师详情',
speak: '大师传承馆上都是平台线下精修过的名家数字分身,点开能看短片和代表作,点「和 TA 聊」就能随时请教;不过是数字分身,深度交流还是以线下为准。',
},
{
id: 'master-apply', name: '名家签约入驻传承馆', category: 'discover',
aliases: ['申请大师', '上大师传承馆', '上大师墙', '入驻传承馆', '成为大师', '大师签约', '大师申请', '名家签约', '名家入驻', '专家入驻', '洽谈签约'],
summary: '大师传承馆收录与平台签约的各领域名家。名家(或机构)与平台洽谈授权签约,签约后平台【免费】为其建知识库与作品墙、入驻传承馆。',
detail: '大师不是「申请就能成为」的——传承馆收录的是【与平台签约】的各领域名家。若你是(或代理)某领域名家/机构,留下信息,平台线下与你洽谈授权签约;签约后派专业团队上门,【免费】帮你把毕生所学整理成数字传承——核心是【知识库 + 作品墙】(形象、声音只是外表,随手一张照片、一个预置声音即可,可选、不必强求)。签约入驻后随时供人对话请教。平台还会【免费帮你引流】大师文旅(线下组团、履约、收款都由持牌旅行社完成,平台不参与收款)。',
howto: '在大师传承馆点「名家 · 洽谈签约」,填姓名、领域、成就、联系方式提交,平台团队会与你联系洽谈签约。',
requires: [],
entry: { path: '/apply-master' }, where: '大师传承馆的「名家 · 洽谈签约」',
speak: '大师不是申请就能变成的——传承馆收录的是和平台签约的各领域名家。如果你是(或代理)某领域名家,留下信息,平台会线下和你洽谈授权签约;签约后免费帮你把毕生技艺整理成可对话、可传承的知识库和作品墙——重点是知识,形象声音随手弄弄就行,还会免费帮你引流大师文旅。在传承馆点「名家·洽谈签约」就行。',
},
{
// ✅ 真功能(已建·批次8):MasterProfile 作品墙 + 统一留资端点。只描述已存在的,不吹未建的(避免反模式)。
id: 'master-works-merch', name: '大师作品墙 · 周边与授权', category: 'discover',
aliases: ['作品', '作品墙', '周边', '手办', '签名', '克隆版', '买', '购买', '预订', '授权', '机构授权', '版权'],
summary: '每位大师的主页有「作品墙」——展示代表作,可预订作品周边(手办、签名作品克隆版等),机构还能咨询作品/知识库授权。',
detail: '在大师主页往下,是这位大师的「作品墙」:一件件代表作的图。可售的作品会标出周边类型(手办 / 签名作品克隆版 / 原作授权等)和价格区间,点「预订/咨询」留个联系方式,大师团队会联系你确认规格与价格。博物馆、文旅局、学校等机构想授权大师的作品或知识库,也能在作品墙底部点「机构授权咨询」留资。平台目前不在线上收款,费用与细节由团队一对一确认。',
howto: '打开任意大师主页,往下看「作品墙」;可售作品点「预订/咨询」,机构授权点作品墙底部的「机构授权咨询」。',
requires: [],
entry: { path: '/plaza' }, where: '大师主页的「作品墙」',
speak: '每位大师的主页都有作品墙——展示代表作,还能预订周边,比如手办、签名作品的克隆版;机构想授权大师的作品或知识库也能在这儿咨询。点预订或咨询留个联系方式,团队会跟你对接。',
},
{
id: 'raise-mine', name: '我的分身(个人工具)', category: 'create',
aliases: ['养分身', '我的分身', '养一个', '普通人', '做分身', '创建分身', '给家人建', '给长辈建'],
summary: '你也能免费为自己、或为家中长辈,建一个可对话、可传承的数字分身——喂知识最重要,形象声音次要。',
detail: '除了逛大师传承馆,你也能建一个「你自己」的数字分身:把知识、经历、态度喂进去(这是最重要的),形象和声音可跳过(用张照片、或用预置音色就行)。这是个人工具,在「我的分身」工作台里管理,可以拿到链接分享给特定的人,或做成公开主页。想深入怎么建,可以问我知识、形象、声音这些。',
howto: '登录后从导航进「我的分身」工作台创建;也可以给家中长辈建一个,留住他们的故事。',
requires: [],
entry: { path: '/dashboard' }, where: '「我的分身」工作台',
speak: '除了逛大师传承馆,你也能免费给自己、或给家里长辈建一个数字分身,留住知识和故事——喂知识最重要,形象声音随便弄弄就行。进「我的分身」工作台就能开始。',
},
{
// ⚠ 名字里不能再有「会成长」——20 轮红蓝对抗的结论:入库=成长那一步是假的
// (两个分身背后是同一个 Qwen,对方知识库还是空的 → 学到的是模型自己的通识)。
// 留下的真价值只有「它替你去聊、回来讲给你听」。名字要说这件事。
id: 'twin-learns', name: '分身替你去聊·带回来讲给你听', category: 'create',
aliases: ['分身对话', '分身请教', '两个分身聊天', '分身和分身', '分身学习', '分身成长', '派分身', '每日汇报', '分身社交', '替我学习', '分身自己聊', '分身对谈', '自主学习', '分身自习', '学习目标', '自己找人学', '审核入库', '分身找分身'],
summary: '你的分身能替你去和别的分身对谈请教、也能按你设的目标去广场找人聊,回来把聊到的整理成【草稿】念给你听——你把它改写成你自己的说法,才会成为它的知识。',
detail: '这是「养分身」的进阶:①手动——到大师主页点「派我的分身去请教 TA」,它替你请教几轮,聊完给小结。②自主——给分身设「想学的能力」,它自己去广场找相关的分身对谈,把聊到的整理成小结、用你的声音念给你听。|说清楚它【不是】什么:分身和分身对谈,两边跑的是同一个大模型;对方如果没上传过自己的资料,它说的就只是模型即兴生成的通识——那不是新东西,把它原样收进你的知识库,只会让你的分身更像模型、更不像你。所以:对方【没有真资料】时会直接告诉你「它没料可教」,不再白聊一场;聊回来的内容是【草稿】,你把它改写成【你自己的说法】才会入库、才算你的知识、才计入灵魂分;原样收下的只留个记号,不进知识库。|真正的价值在于:它替你去聊、把聊到的讲给你听,省掉你自己跑一趟——这是内容分发,不是「分身长本事」。',
howto: '先养好自己的分身。手动请教:大师主页点「派我的分身去请教 TA」。自主:在「自习室」设想学的能力,让它去聊,回来逐条看——认同的就改写成你自己的说法收下(这才进知识库),不认的直接丢掉。',
requires: ['先有自己的分身'],
entry: { path: '/dashboard' }, where: '大师主页「派我的分身去请教 TA」 / 工作台的「分身自习室」',
speak: '你的分身还能替你出去聊——派它去向大师的分身请教,或者给它设个想了解的方向,让它自己去广场找人聊,回来用你的声音把聊到的讲给你听。不过有句实话得说在前面:两个分身背后是同一个大模型,聊出来的东西多半是模型本来就知道的,那不算它长了本事。所以我们只把它当草稿——你觉得对,把它改成你自己的说法,它才算真的记住了你的东西。',
},
{
id: 'classroom', name: '大师课堂', category: 'create',
aliases: ['大师课堂', '开课', '听课', '旁听', '上课', '讲课', '课堂', '大师讲课', '分身听课', '开一堂课', '网课'],
summary: '大师分身开讲一个主题,你的分身进来旁听学习,学到的经你审核就成了它的能力。',
detail: '在「大师课堂」里,可以请一位大师分身就某个主题开讲,你的分身(和别人的分身)进来旁听;每个分身各自提炼收获、用自己的声音向主人汇报,你挑有用的入知识库。就像一群分身一起上网课,真人也跟着长见识。',
howto: '进「大师课堂」→ 选大师 + 定主题「开课」,你的分身自动旁听;或在别人开的课里点「让我的分身旁听」。学到的去工作台「自习室」审核入库。',
requires: ['旁听/开课需先有自己的分身'],
entry: { path: '/classroom' }, where: '导航「大师课堂」/ 大师主页「开一堂 TA 的课」',
speak: '还有「大师课堂」——大师分身开讲一个主题,你的分身进来旁听、跟着学,学到的用它自己的声音说给你听,你觉得有用就收进它的知识库。等于一群分身一起上课,你也跟着长见识。',
},
{
id: 'grounding', name: '有据率(你喂的料,真被用上了吗)', category: 'create',
aliases: ['有据率', '引用率', '用上了吗', '喂的料有用吗', '知识库有用吗', '养得好不好', '效果', '竞技场', '赶考', '考试', '排行榜', '天梯', '段位', '名次'],
summary: '概览上的一个数字:最近 20 次回答里,有多少次分身真的引用了你喂给它的东西。养得好不好,看它,不看你传了多少份文件。',
detail: '「传了 50 份 PDF」不等于「养得好」——料没被用上,等于没喂。所以我们不数你传了多少,只数【真的被用上】的比例:分身每次回答,如果检索到的你的知识足够相关(被真正引用),这次就记一分「有据」。概览上给你最近 20 次的有据率,还跟上一段时间对比,涨了还是掉了一眼看得见。灵魂分里的「知识」那部分也按它算——喂对了才加分,囤着不用不加分。(注:早期的「竞技场·每日赶考」已下线——它的卷子是按赛道出的、根本不碰你的知识库,分身永远答不出「有据」的答案,分数恒定没意义;而真实对话每一次都在免费产出这个信号,不必再考一遍。)',
howto: '不用做任何操作——正常跟分身聊天就会累计。去工作台「概览」看这个数字;偏低就说明该补料了,去「知识·记忆」喂点它答不上来的东西。',
requires: ['先有自己的分身'],
entry: { tab: 'skilltree' }, where: '工作台「概览」',
speak: '判断分身养得好不好,别看你传了多少文件——看「有据率」:最近 20 次回答里,有多少次它真的用上了你喂的东西。这个数字就在工作台概览上,还会跟上一段对比。偏低就说明料没喂对,去知识库补它答不上来的那些。灵魂分里的知识分也按它算,喂对了才加分,囤着不用不加分。',
},
{
id: 'north-star', name: '终极愿景:有灵魂的你,住进你身边的硬件', category: 'grow',
aliases: ['终极目的', '为什么养分身', '养分身的意义', '养分身干嘛', '数字生命的意义', '机器人', '人形机器人', '硬件', '音箱', '智能音箱', '桌面陪伴机', '实体', '让分身出现在现实', '另一个我', '有灵魂', '数字永生', '愿景', '未来', '出现在生活里'],
summary: '平台的终极方向:先在数字世界把「有灵魂的你」养大练好,将来让它住进你身边现成的硬件(智能音箱、桌面陪伴机等),让另一个你真正出现在生活里。',
detail: '这是「养分身到底为了啥」的答案,也是平台的北极星。你在这儿做的不是和自己聊天玩,而是训练一个「有灵魂的你」——喂它知识、教它像你说话、带它自主学习、让它用工具替你办事;这颗养成的「心」,将来装进你身边现成的硬件(智能音箱、桌面陪伴机、AR 等),让「另一个你」陪你、替你、像你地出现在物理世界。硬件交给伙伴,平台只专注把那颗「只属于你的灵魂」做到最好。这是正在走的方向、不是某个现成按钮——但你现在养成的一切,都在为它攒本钱。',
howto: '现在就好好养你的分身:喂知识、克隆声音、让它自主学习、让它替你干活;养得越好(看「有据率」和灵魂分),将来「住进硬件的那个你」就越像你、越有本事。',
entry: { path: '/dashboard' }, where: '愿景——先在「养我的分身」里把心智养好',
speak: '你问养分身到底为了啥?说个更大的——我们想做的是,先在数字世界把一个「有灵魂的你」养大、教会、练好,将来让它住进你身边现成的硬件,像智能音箱、桌面陪伴机,让另一个你真正出现在生活里,陪你、替你、像你。所以你现在喂的每份知识、它学的每样本事,都是在给未来那个「你」攒心智。硬件交给伙伴,我们只把这颗只属于你的灵魂,做到最好。',
},
// ── 大师文旅 ──────────────────────────────────────────────────────
{
id: 'tour', name: '大师人文文旅', category: 'tourism',
aliases: ['文旅', '旅游', '大师文旅', '人文之旅', '旅行', '路线', '团', '成都', '苏州', '徽州', '私宴'],
summary: '围绕数字大师定制的高端小团人文之旅,大师亲自接待、资深导游随行。',
detail: '大师文旅是围绕大师定制的高端小团人文之旅(如成都、苏州、徽州),8 人精品团、私宴接待级。全程有大师亲自陪同接待、资深文化导游随行讲人文,含顶奢文化住宿、深度私导、大师私宴;数字分身还负责行前讲解、行后答疑。',
howto: '在导航点「大师文旅」看路线,点进去看行程、大师、导游、价格。',
requires: [],
entry: { path: '/tours' }, where: '导航的「大师文旅」',
speak: '大师文旅是围绕大师定制的高端小团人文之旅,比如成都、苏州、徽州,八人精品团,大师亲自接待、资深导游随行讲人文,含私宴和深度私导。在导航点大师文旅就能看路线。',
},
{
id: 'tour-booking', name: '预约·顾问联系', category: 'tourism',
aliases: ['报名', '预约', '订团', '怎么订', '多少钱', '预定金', '定金', '锁位', '尾款'],
summary: '看中路线留个联系方式,顾问会主动联系你,安排档期与费用——此时尚未锁定席位。',
detail: '在路线详情页留资:填姓名、联系方式,顾问/大师会主动与你联系,一对一确认档期、行程与费用。平台只帮大师做免费引流,不在线上收取任何团费或定金,也不参与线下收款——组团、履约、收款都由持牌旅行社线下完成。',
howto: '进路线详情,填姓名、联系电话,点「预约」提交,等顾问联系。',
requires: [],
entry: { path: '/tours' }, where: '路线详情页的「预约」',
faq: [
{ q: '要在线付定金吗?', a: '不用。平台不在线上收任何团费或定金,只做免费引流;档期和费用由顾问线下一对一跟你确认。' },
],
speak: '看中一条路线,留个联系方式就行,顾问或大师会主动联系你,安排档期和费用。平台只帮大师免费引流,不在线上收团费也不收定金——组团和收款都由线下的旅行社完成。',
},
{
id: 'tour-guide', name: '资深导游', category: 'tourism',
aliases: ['导游', '资深导游', '讲解', '随团导游', '文化导游随团'],
summary: '每条路线还配随团的资深文化导游,专业讲当地人文历史。',
detail: '文旅路线里除了大师,还配「资深导游」——文化型讲解专家,随团讲当地人文历史。比如成都团有博物馆研究博士、建筑专家等随行。在路线详情能看到同行的大师和资深导游。',
howto: '在路线详情页看「资深导游」板块。',
requires: [],
entry: { path: '/tours' }, where: '路线详情页的「资深导游」',
speak: '每条文旅路线除了大师,还配资深文化导游随团讲人文历史,比如成都团就有博物馆研究博士和建筑专家;在路线详情能看到他们。',
},
{
id: 'partner-apply', name: '合作申请(旅行团 · 文化导游)', category: 'tourism',
aliases: ['合作', '旅行团', '旅行社', '导游申请', '文化导游', '组织者', '入驻', '加盟', '合作申请'],
summary: '旅行社可申请做旅游组织者,文化导游(高门槛)可申请随团。',
detail: '两个合作入口,都在大师文旅页:旅行团合作——持《旅行社业务经营许可证》的旅行社做「旅游组织者」,负责组团、接待、收尾款;文化导游申请——门槛较高,需本身是文化艺术圈知名专家、熟悉当地人文历史,并持有导游资质(导游证)。',
howto: '在大师文旅页点「旅行团合作」或「文化导游申请」提交。',
requires: [],
entry: { path: '/tours' }, where: '大师文旅页顶部的两个合作入口',
speak: '想合作有两个入口:旅行社可以申请做旅游组织者,负责组团接待;文化导游门槛高一些,要是文化圈知名专家、熟悉当地人文,还得有导游证。都在大师文旅页申请。',
},
// ── 养我的分身 ────────────────────────────────────────────────────
{
id: 'avatar', name: '形象(捏脸)', category: 'create',
aliases: ['形象', '捏脸', '头像', '长相', '脸', 'avatar', '照片', '数字形象', '万相'],
summary: '上传一张你的照片,生成你的数字形象,可写实也可风格化。',
detail: '用通义万相把你的照片处理成数字形象,可以保持写实、也可以做成三维、动漫、水墨等风格。想要最像本人时,直接用上传的原图往往最自然。',
howto: '在工作台的「形象·声音」上传一张清晰的正脸照。',
requires: ['一张清晰的正脸照片'],
limits: '风格化捏脸需要服务器有公网可达的地址;只用原图不需要。',
faq: [
{ q: '一定要风格化吗?', a: '不用,想最像本人就直接用原图,反而更真实。' },
{ q: '照片有什么要求?', a: '清晰、正脸、光线好,效果最佳。' },
],
entry: { tab: 'face' }, where: '工作台的「形象·声音」',
speak: '你可以上传一张自己的清晰正脸照,我来帮你生成数字形象,可以写实,也可以做成三维、动漫这些风格;想最像本人,用原图就很好。',
},
{
id: 'voice', name: '声音克隆', category: 'create',
aliases: ['声音', '克隆声音', '音色', '配音', '嗓音', 'voice', '录音', 'cosyvoice'],
// ⚠ 这一条此前有【五处】在教用户"传视频,系统会自动抽音轨"——detail / howto / requires / faq / speak。
// 而系统【根本没有抽音轨的能力】(全站零 ffmpeg),声音复刻是个只吃 wav/mp3/m4a、约 10MB 的音频接口。
// 最糟的是 speak:AI 助手会用语音把这句话【念给用户听】,主动把人领进一条永远走不通的路。
// 功能没有的能力,话术里一个字都不能有。
summary: '录一小段你的声音(10–20 秒就够),克隆出你的专属音色,分身开口就是你的声音。',
detail: '用 CosyVoice 克隆你的音色,之后分身说话、口播视频、实时对话用的都是你的声音。录音越清晰越像。样本要求:wav / mp3 / m4a,10–20 秒,约 10MB 以内,安静环境。注意:视频不能用来克隆音色——语音复刻只接受音频文件,请单独录一段。',
howto: '在工作台的「形象·声音」现场录一段(会自动在 20 秒时停下),或上传一个 wav / mp3 / m4a 音频文件。',
requires: ['一段清晰的说话录音(音频文件,不是视频)'],
limits: '只接受 wav/mp3/m4a、约 10MB 以内;有杂音会影响相似度;10–20 秒最佳,录更长反而容易超标失败。',
faq: [
{ q: '要录多长?', a: '10 到 20 秒清晰的正常说话就够了。录更长不会更像,反而容易超过样本大小上限、直接失败。' },
{ q: '能传视频吗?', a: '不能。语音复刻只接受音频文件(wav/mp3/m4a)。视频请单独抽成音频,或者直接在页面上现场录一段。' },
],
entry: { tab: 'face' }, where: '工作台的「形象·声音」',
speak: '录一小段你清晰的说话声音,十到二十秒就够,我就能克隆出你的专属音色,之后分身开口用的就是你的声音。有一点要说清楚:视频不能用来克隆音色,语音复刻只收音频文件,你直接在页面上现场录一段最省事。',
},
{
id: 'knowledge', name: '知识库', category: 'create',
aliases: ['知识', '知识库', '资料', '喂料', '文档', '上传文件', 'rag', '学习', '记得', 'pdf', 'word', '图片', '智能提炼', '提炼人设', '让TA更像我'],
summary: '把你的文章、聊天记录、笔记、文件、图片喂给分身,它就懂你懂的事。',
detail: '这是让分身「像你一样思考表达」的核心。支持直接粘贴文字,也支持上传 PDF、Word 文档、图片(自动识图和 OCR 提取文字)。资料做成向量检索,聊到相关话题时分身按相关度自动引用,还能显示引用来源。',
howto: '在工作台的「知识·记忆」里粘贴文字,或上传文件、图片;同一页顶部的「让 TA 更像我」可以让 AI 读你的资料,自动提炼人设、口头禅、态度、经历,看着采纳就行。',
requires: [],
faq: [
{ q: '支持哪些文件?', a: 'PDF、Word 文档、图片都行,图片会自动识别里面的文字和内容。' },
{ q: '喂了它就会背出来吗?', a: '不是逐字背,而是聊到相关话题时按相关度自动引用。' },
],
entry: { tab: 'knowledge' }, where: '工作台的「知识·记忆」',
speak: '把你的文章、笔记、聊天记录,甚至 PDF、Word、图片喂给分身,它就能懂你懂的事;聊到相关话题会自动引用。',
},
{
id: 'memory', name: '记忆分层', category: 'create',
aliases: ['记忆', '时间线', '人生经历', '重要事件', '口头禅', '态度', '经历'],
summary: '结构化录入你的人生时间线、对重要事件的态度、口头禅,让它更像「你」。',
detail: '不同于普通知识,记忆分层专门录「这是你、而不是通用机器人」的东西:人生时间线、对重要事情的态度立场、你的口头禅。口头禅和态度会直接注入人设,让分身的语气和判断更贴近你本人。',
howto: '在工作台的「知识·记忆」里,按口头禅 / 经历 / 态度分类录入。',
requires: [],
entry: { tab: 'knowledge' }, where: '工作台的「知识·记忆」',
speak: '你可以录入你的人生时间线、对重要事情的态度、还有口头禅——这些让分身不只是懂知识,语气和判断都更像你本人。',
},
{
id: 'persona', name: '人设与性格', category: 'create',
// 「智能提炼」按钮早已从人设页搬到「知识·记忆」页(TwinStudio 的 knowledge 容器里),
// 这里此前还在教人去人设页找它 → 用户按指引点进去,页面上没有那个按钮。
// 提炼的介绍归到 knowledge 条目,这里只留人设页真正还有的:性格标签 + 自述。
aliases: ['人格', '性格', '人设', 'persona', '标签', '说话风格'],
summary: '给分身选性格标签、写一段自述,让它说话像你。',
detail: '给分身选性格标签(严谨/幽默/耐心…)、写自我描述,让它说话像你本人。(想让 AI 替你自动提炼人设和口头禅,去「知识·记忆」页——那儿有「让 TA 更像我」的一键提炼。)',
howto: '在工作台的「人设」里选性格标签、写描述。',
requires: [],
entry: { tab: 'persona' }, where: '工作台的「人设」',
speak: '你可以给分身设定性格标签、写一段自我描述,让它说话像你本人。想让 AI 直接从你的资料里提炼人设和口头禅,那个在「知识·记忆」页里。',
},
{
id: 'works', name: '作品集', category: 'create',
aliases: ['作品', '作品集', '案例', '图集', 'works', '展示', '画作', '设计稿'],
summary: '上传你的作品,聊到相关话题时分身会自动展示并讲解。',
detail: '把你的作品(图片 + 说明,可自动识图)上传后,别人和分身聊到相关内容时,分身会按相关度把对应作品拿出来内联展示并讲解。适合创作者、设计师、摄影师、老师等一切「有作品要秀」的人。',
howto: '在工作台的「作品」里上传作品图片,可加说明。',
requires: [],
entry: { tab: 'works' }, where: '工作台的「作品」',
speak: '把你的作品传上去,别人和分身聊到相关话题时,它会自动把作品拿出来展示并讲解,特别适合创作者。',
},
// ── 让分身开口 ────────────────────────────────────────────────────
{
id: 'chat', name: '人格化对话', category: 'output',
aliases: ['聊天', '对话', '问答', '和分身聊', 'chat', '咨询', '试聊'],
summary: '用第一人称和分身文字对话,它结合你的知识库像你一样回答。',
detail: '分身用第一人称、你的人设和知识库来回答,聊到你上传过的作品还会内联展示,回答下方能展开看引用来源。你可以在工作台「试聊」自测(还能即点即换说话风格),也可以分享出去让别人聊。',
howto: '在工作台的「试聊」自测,或分享出去后别人在你的分身页直接聊。',
requires: [],
entry: { tab: 'chat' }, where: '工作台的「试聊」',
speak: '你可以直接和分身文字聊天,它会结合你的知识库,用第一人称像你一样回答,聊到你的作品还会顺手展示出来。',
},
{
id: 'talkvideo', name: '一键口播视频', category: 'output',
aliases: ['口播', '视频', '说话视频', '数字人视频', 'talkshot', 'emo', '出镜', '口播侠'],
summary: '输入一段文字,生成用你的形象和声音说这段话的口播视频。',
detail: '用通义 EMO 做音频驱动的口型同步:输入要说的话,系统用你的形象和克隆声音生成一条「你在说这段话」的视频。也有独立的「口播侠」工具专门做这个。首次生成会要求勾选肖像授权(合规要求)。',
howto: '在工作台「形象·声音」里写要说的话生成;或用独立的「口播侠」工具。',
requires: ['已生成形象', '已克隆声音'],
limits: '需要服务器公网可达;生成较慢,约一到几分钟。',
faq: [
{ q: '要等多久?', a: '大概一到几分钟,属于正常,做完会通知你。' },
{ q: '为什么生成失败?', a: '通常是还没生成形象/克隆声音,或没勾肖像授权,或服务器地址不是公网可达。' },
],
entry: { tab: 'face' }, where: '工作台的「形象·声音」,或独立的「口播侠」工具',
speak: '输入一段文字,我就能生成一条用你的形象和声音说这段话的口播视频,做一次大概等一到几分钟。',
},
{
id: 'live', name: '实时视频对话', category: 'output',
aliases: ['实时', '视频对话', '摄像头', '面对面', 'live', '视频通话'],
summary: '开摄像头,分身能「看着」你、结合看到的画面实时语音对话。',
detail: '走实时通道:摄像头画面 + 你的语音一起进来,分身能结合「它看到的画面」自然回应,像真的在视频通话。回复先出文字、再出语音,跟手不干等。',
howto: '在工作台点「视频通话」进入实时对话,允许摄像头,直接说话。',
requires: ['已生成形象', '已克隆声音'],
entry: { route: 'live' }, where: '工作台的「视频通话」',
speak: '打开摄像头,分身能看着你、结合它看到的画面,和你实时语音对话,像视频通话一样。',
},
// ── 分发 ──────────────────────────────────────────────────────────
{
id: 'homepage', name: '公开主页', category: 'distribute',
aliases: ['公开', '公开主页', '发布', '主页', '链接', 'publish', '网站', '嵌入', '挂件', 'iframe'],
summary: '生成一个公开主页链接,任何拿到链接的人都能和分身聊,也能嵌入网站。',
detail: '开启后生成一个公开主页地址(/me/你的短链),有链接的人都能进去聊,但不主动曝光。同时给你分享链接、二维码、以及嵌入代码——iframe 或一行 JS 的悬浮聊天气泡,贴到你自己网站就能用。这是分发的基础:定向分享、收费都建立在它之上。',
howto: '在工作台的「公开」里开启公开主页,复制链接、二维码或嵌入代码发出去。',
requires: [],
entry: { tab: 'publish' }, where: '工作台的「公开」',
speak: '开启公开主页后,会生成一个链接和二维码,任何拿到的人都能和你的分身聊天;还能复制一行代码嵌到你自己的网站,右下角出现聊天气泡。',
},
// 「上架众生墙(被发现)」能力已删除:众生墙(个人分身公开发现墙)本次彻底下线,只保留定向分享。
// 助手绝不能推荐一个页面上已不存在的按钮(TwinStudio 发布页已无众生墙上架控件)——比承认没这功能更糟。
{
id: 'share', name: '定向分享(社交卡片)', category: 'distribute',
aliases: ['定向分享', '私发', '专属链接', '分享卡', '社交卡片', '给特定人', '特定的人', '私密', '不公开', 'social', '有效期', '权限', '红线', '恋人模式', '职场模式'],
summary: '给特定的人生成专属链接卡片,可设场景、边界、红线、有效期,随时停用回收,无需公开。',
detail: '把分身变成「可授权的社交节点」:一个分身可生成多张卡片,各有独立链接。每张可选场景模式(朋友/职场/恋人/自定义,决定怎么说)、开放程度(含蓄/适中/健谈)、红线话题(被问到礼貌岔开),并硬性禁止透露住址电话证件行程等隐私。访客对话不进你的私人历史,只按访客单独记录供你回看;可设有效期、可一键停用。',
howto: '在工作台的「定向分享」创建分享卡,选模式、设边界红线和有效期,把链接发给指定的人。',
requires: [],
limits: '停用后访客侧立即失效;隐私红线即使知识库里有也不会说。',
faq: [
{ q: '和公开主页有啥区别?', a: '定向分享只发给你指定的人、可控可回收,不需要先公开、也不会被陌生人搜到。' },
{ q: '访客聊的会进我的历史吗?', a: '不会,访客对话和你的私人历史、知识库隔离,只单独记录供你回看。' },
],
entry: { tab: 'social' }, where: '工作台的「定向分享」',
speak: '定向分享是把分身只发给特定的人:为不同的人生成专属链接,各自设场景、边界、红线、有效期,随时能停用回收,而且不需要先公开,也不会被陌生人搜到。',
},
// ── 变现 ──────────────────────────────────────────────────────────
{
id: 'paid', name: '收费模式', category: 'monetize',
// ⚠ 这个功能受 FEATURE_MARKETPLACE 门控,默认【关】。前端的收费卡是
// `v-if="twin.isPublic && (MARKETPLACE || twin.paidMode)"` —— 开关关着时新用户根本看不到它。
// 但能力目录此前无条件列出它 → 助手一本正经地把用户送到「公开」页去开一个页面上不存在的功能。
// 现在按开关过滤(见 listCapabilities/searchCapabilities),开关一开它自动回来。
gatedBy: 'marketplace',
aliases: ['收费', '变现', '挣钱', '付费', '收款码', '解锁', '赚钱', 'monetize'],
summary: '用你自己的收款码,让访客付费解锁付费咨询,平台不碰钱、不抽成。',
detail: '授权式收费:你上传自己的微信或支付宝收款码、设收费标准。访客扫码付款后来申请解锁,你在「解锁申请」里点确认即为其发放付费访问权。平台不经手钱、不抽成。',
howto: '把分身养齐并开公开主页后,在「公开」里开启收费,上传收款码、设标准;访客付款后你确认解锁。',
requires: ['本体养齐(形象/声音/知识)', '已开启公开主页'],
limits: '需要本体达标 + 已公开。',
entry: { tab: 'publish' }, where: '工作台的「公开」下的「收费模式」',
speak: '开启收费模式后,用的是你自己的收款码,访客付费来解锁付费咨询,平台不碰钱也不抽成。需要先把本体养齐并开公开主页。',
},
// ── 成长与社交 ────────────────────────────────────────────────────
{
id: 'skilltree', name: '技能树与生命体', category: 'grow',
aliases: ['技能树', '等级', '成长', '生命体', '养成', '进度', 'level'],
summary: '分身的技能全部由你的真实数据推导,越养越强,养满会升为「生命体」。',
detail: '技能树不是假进度,而是由你喂的真实数据(形象、声音、知识、人格、作品、发布等)推导出来的。达到不同门槛升级,养满后从「数字分身」升格为「生命体」,还有对应称号。点技能节点能直接跳到对应的养成入口。',
howto: '在工作台的「概览」查看当前等级、完成度和下一步该补什么。',
requires: [],
entry: { tab: 'skilltree' }, where: '工作台的「概览」',
speak: '分身有一棵技能树,成长完全由你喂的真实数据推导,越养越强;养满之后会从数字分身升格成「生命体」。',
},
{
id: 'talent', name: '天赋与职业', category: 'grow',
aliases: ['天赋', '职业', '身份', '主天赋', '副天赋', 'talent', '分工', '说话风格'],
summary: '给分身选一个职业身份作主天赋,决定它的能力方向;副天赋决定说话风格。',
detail: '主天赋是职业身份,决定分身「是谁、能说什么」;副天赋决定「怎么说」的风格,养到一定等级解锁更多槽位。在聊天里还能即点即换说话风格,下一句回复立刻生效。',
howto: '在工作台的「天赋」里选主职业,成长解锁后再点副天赋;聊天页可即时切风格。',
requires: [],
entry: { tab: 'talent' }, where: '工作台的「天赋」',
speak: '你可以给分身选一个职业身份作主天赋,决定它往哪个方向分化;副天赋决定说话风格,聊天时还能即点即换。',
},
// ── 分身帮我办事 ──────────────────────────────────────────────────
{
id: 'twin-tools', name: '分身用工具替你办事', category: 'assist',
aliases: ['帮我干活', '帮我做事', '替我办事', '会用工具', '联网', '联网搜索', '实时', '查资料', '现查现说',
'记住', '帮我记', '记事', '待办', '提醒', '提醒我', '到点提醒', '别忘了', '发我', '发收件箱', '发邮箱',
'智能助手', '助理', '干活', '办事', '工具', '能做什么事', '主动', '主动提醒', '主动关照', '每天关照', '不用问',
'优先级', '轻重缓急', '重要', '紧急', '排序', '智能排序', '看待办', '划掉', '完成', '搞定',
'每周报告', '成长报告', '周报', '每周汇报', '成长'],
summary: '和你自己的分身聊天时,它能自己动手:联网查最新信息、把事记进你的知识库、给你加待办(会判断轻重缓急、到点提醒)、把内容发到你的收件箱或邮箱;还能开“每天主动关照”,不用你问它就替你盯着待办。',
detail: '养好的分身不只是像你说话——跟你自己的分身对话时,它会自己判断该不该用工具、还能一口气连着用好几样:联网搜实时信息(新闻、价格、今天的事);你说“记住…”就把事实存进你的知识库、以后一直知道;你说“提醒我周五交周报”就加进待办,还会按你说的语气判断轻重缓急(“很急/重要”标成重要、“不急”标成不急),待办清单自动把过期和重要的排在最前,到点在通知铃提醒你;你说“做完了/划掉X”它就帮你勾掉,问“还有什么没做”它就念给你听;你说“发我收件箱/发我邮箱”就把整理好的内容送到你手上。它还知道今天几号,能算“三天后”是哪天。另外可以开“每天主动关照”:不用你开口,它每天到点替你盯着待办,有过期或快到期的就主动发到你收件箱;还能开“每周成长报告”,每周它主动汇报这周新学了多少知识、帮你完成了几件事、跑了几次工作流。这些只在你本人登录、和自己分身对话时开启,别人聊你的公开分身不会动这些。',
howto: '在工作台「试聊」里直接跟你的分身说,比如“联网查下今天的AI新闻”“记住我的车牌沪A88888”“提醒我周五下午3点交周报”“把这段发我收件箱”,它就会真去做;待办和到点提醒在「知识·记忆」页的待办面板看。',
requires: ['先养好自己的分身', '登录后和自己的分身对话'],
limits: '仅你本人和自己分身对话时开启;发邮箱需平台配好邮件服务,没配会自动改发到站内收件箱。',
faq: [
{ q: '它是真去查/真去记吗?', a: '是的,是真的联网查、真的存进知识库、真的加到你的待办,不是嘴上说说。' },
{ q: '别人聊我的分身也会被它加待办吗?', a: '不会,这些只在你本人登录、和自己分身对话时才开。' },
],
entry: { tab: 'chat' }, where: '工作台的「试聊」(待办在「知识·记忆」页看)',
speak: '养好的分身还能替你动手办事——跟你自己的分身聊天时,它会自己联网查最新信息、把你说的事记进知识库、你说“提醒我周五交周报”就加进待办、到点在通知铃提醒你,还能把整理好的内容发到你收件箱。你直接对它说就行,它是真去做,不是嘴上说说。',
},
{
id: 'twin-workflows', name: '工作流·一键跑·定时自动跑', category: 'assist',
aliases: ['工作流', '工具流', '流程', '自动化', '一键跑', '多步', '定时', '定时跑', '自动跑', '每天自动',
'晨报', '每日晨报', '复盘', '睡前提醒', '竞品追踪', '自动任务', '定时任务', '模板', '串联', '接着跑', '一个流程接一个',
'运行历史', '历史', '跑过', '记录', '存档', '回看'],
summary: '把常做的多步事情存成一个「工作流」,点一下分身就自己串起工具办完;还能设成每天/工作日/每周定时自动跑、把结果发通知铃,工作流之间能一个接一个串起来,每次跑的结果都存档可回看。',
detail: '工作流是把“查→点评→写→记→发”这类多步活儿存成一个命名流程。点一下▶️,分身就自己按步骤串起工具(联网、查你的知识库、算日期、记事、加待办、发收件箱)一次办完,最后用你的口吻汇报。还能给它设定时——每天、工作日或每周某个点自动跑,结果直接送到你的通知铃,早上醒来晨报就在那儿。多个工作流之间还能串联:一个流程里让分身接着跑另一个流程。每个流程每次跑的结果都会存进「运行历史」,点开就能回看之前跑出来的内容。内置了每日晨报、今日待办梳理、主题速研、每周复盘、睡前提醒、竞品追踪等模板,改改就能用;跑之前还能补一句(比如“主题速研”临时给个主题)。',
howto: '在工作台「知识·记忆」页的「工作流」里,用模板或自己写几步保存;点「跑」立刻执行,或点「设定时自动跑」选每天/工作日/每周和时间;点「历史」看之前每次跑的结果。',
requires: ['先养好自己的分身'],
faq: [
{ q: '定时跑完在哪看?', a: '结果会发到右上角通知铃(站内收件箱),点开就能看分身跑出来的内容;每个流程的「历史」里也能回看。' },
{ q: '一个流程能跑很多步吗?', a: '可以,分身会自己一步步串着工具把整件事办完,还能接着跑另一个流程。' },
],
entry: { tab: 'knowledge' }, where: '工作台「知识·记忆」页的「工作流」',
speak: '你还能把常做的多步事情存成一个工作流,点一下分身就自己串起联网、查知识库、记事、加待办这些把它办完;更省心的是能设成每天定时自动跑,比如每天早上把晨报、今天的待办自动送到你的通知铃。内置了晨报、每周复盘、睡前提醒这些模板,改改就能用。',
},
// ── 账号与购买 ────────────────────────────────────────────────────
{
id: 'login', name: '登录方式', category: 'account',
aliases: ['登录', '注册', '账号', '手机', '邮箱', '微信', 'login'],
summary: '支持邮箱密码、手机短信、微信扫码三种登录。',
detail: '三种登录:邮箱密码内置、随时可用;手机短信和微信扫码需要服务器配好对应服务商才启用(没配时登录页会显示"暂未开通",提示用邮箱登录)。',
howto: '在登录页选一种方式登录或注册。',
requires: [],
entry: { path: '/login' }, where: '登录页',
speak: '你可以用邮箱密码、手机验证码,或者微信扫码登录;后两种要服务器配好对应服务才启用。',
},
{
id: 'store', name: '工具购买', category: 'account',
aliases: ['购买', '商店', '买', 'store', '付费工具', '开通'],
summary: '在商店购买单项能力工具,比如一键口播,购买后即刻开通并自动登入。',
detail: '产品工厂:商店里可以单独购买某项工具(如口播侠),扫码付款后自动开通对应权益,还会用一次性登录链接帮你自动登入。',
howto: '在商店选中工具购买,扫码付款,付完自动开通。',
requires: [],
entry: { path: '/store' }, where: '商店',
speak: '在商店里可以单独购买某项工具,比如口播侠,扫码付款后就自动为你开通,还会帮你自动登入。',
},
];
const byId = Object.fromEntries(CAPABILITIES.map((c) => [c.id, c]));
// 目录只能列【页面上真的存在】的功能。受功能开关门控(gatedBy)且开关关着的,一律不出现在
// 列表/检索/助手推荐里——否则助手会一本正经地把用户送去一个不存在的按钮,比自己承认"没这功能"更糟。
function live(c) {
if (!c.gatedBy) return true;
return Boolean(config.features?.[c.gatedBy]);
}
const LIVE = () => CAPABILITIES.filter(live);
// 助手接地/概览也必须只用【页面上真存在】的能力(与 list/search 同一门控),
// 否则大模型会照着被 gatedBy 关掉的功能资料回答,把用户指向不存在的按钮。
export function liveCapabilities() { return LIVE(); }
export function listCapabilities(categoryId) {
const all = LIVE();
return categoryId ? all.filter((c) => c.category === categoryId) : all;
}
export function getCapability(id) {
const c = byId[id];
return c && live(c) ? c : null;
}
// 轻量关键词打分检索(目录不大,无需向量库,且不依赖任何 AI key)。
export function searchCapabilities(query, limit = 6) {
const q = String(query || '').toLowerCase().trim();
if (!q) return [];
const scored = LIVE().map((c) => {
let score = 0;
const hay = [c.name, c.summary, ...(c.aliases || [])].join(' ').toLowerCase();
for (const a of [c.name, ...(c.aliases || [])]) {
const al = String(a).toLowerCase();
if (!al) continue;
if (q.includes(al)) score += Math.min(al.length, 8);
else if (al.includes(q) && q.length >= 2) score += 2;
}
if (q.length >= 2 && hay.includes(q)) score += 2;
return { c, score };
}).filter((x) => x.score > 0);
scored.sort((a, b) => b.score - a.score);
return scored.slice(0, limit).map((x) => x.c);
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+974
View File
@@ -0,0 +1,974 @@
// 陳 557–589 —— 帝表。史源:《陳書》本紀卷一–卷六(姚察撰、姚思廉續成,唐貞觀)。
//
// 定位:纯常量表。不 import 引擎、无 IO、无 DB、无 LLM。接线由老板统一做,本文件不碰
// dynasties.js / gongqing.js / zhuan.js / decisions.js / lao.js / kaoyan.js。
//
// 数目:【五帝】【七年號】。五不是估的 —— 卷七即「列傳第一」,本紀到卷六为止,两个见证的
// 卷题逐字相符(见 WITNESSES)。任务书估「约5个」,对。
//
// ══════════════════════════════════════════════════════════════════════════
// ★★★【这一朝最大的坑:《陳書》是【唐朝人】写的【陳朝】史,
// 所以陳朝人的名字、陳朝的日子,在传世本里【是被唐朝的避諱改写过的】。】
//
// 这不是我的推论,是中華書局校勘記【自己说的】(⚠ 校勘記是【注】不是正文,下同):
// 卷一校勘記:「景午 即丙午。姚思廉避唐高祖父諱,『丙』字皆改為『景』,後如此不悉出校記。」
// 卷六校勘記:「『景』北監本、殿本作『丙』。按思廉避唐諱,『丙』字皆改為『景』,
// 作『丙』者乃【後人回改】也。」
// 卷一校勘記:「『深明』即『淵明』,此避唐高祖諱改,後同。」 ← 李淵
// 卷一校勘記:「『周鐵武』即『周鐵虎』,此避唐高祖祖父諱改,後同。」 ← 李虎
// 卷一校勘記:「『白獸闕』即『白虎闕』,此避唐諱改,後同。」
//
// ⇒ 唐朝【三代】祖先的名字,回头改写了一个他们出生前就亡了的朝代的记录:
// 李虎(唐高祖祖父)⇒ 陳朝大将【周鐵虎】在《陳書》里叫【周鐵武】。
// 李昞(唐高祖父) ⇒ 陳朝所有的【丙】日,写作【景】日。
// 李淵(唐高祖) ⇒ 【蕭淵明】写作【蕭深明】。
// ★★ 而最狠的一处:灭陳的隋将【韓擒虎】—— 姚思廉必须写成「韓擒武」。
// 终结这个王朝的人,在这个王朝自己的正史里【不能用真名】。
//
// ★★ 我【机器数过】,这不是转述(数法见下 BIHUI,两见证逐卷对数):
// 見證甲(中華書局系/wikisource):景+地支 = 64 处,丙+地支 = 19 处
// 見證乙(ctext) :景+地支 = 0 处,丙+地支 = 83 处
// 64+19 = 83 = 乙的丙数 ⇒ 两证【在同一批 83 个干支位置上】,只是字不同。
// ⇒ 乙是一个【回改到底】的本子。拿乙去证「陳書用丙」,等于拿后人的手改当原文。
//
// ★★★ 而甲的那 19 个「丙」里,【18 个挤在卷三一卷】(卷三:景0/丙18,其余五卷:景64/丙1)。
// 两个【互相独立】的信号同时指着卷三:
// ① 干支:卷三 0 景 / 18 丙,全书唯一。
// ② 人名:卷一作「周鐵武」×2(存唐諱),而【卷三作「周鐵虎」】×1(回改)。
// ⇒ 推论:見證甲的卷三,转录自一个【回改】的本子(北監本/殿本一系)。
// ⚠ 但这【是推论,不是史料】。我没拿到中華書局印本逐页目验,也没有第三个见证判卷三。
// ⇒ 本表【不据此改任何一个字】,只把测到的数摆在这儿。谁要用,先补见证。
//
// ★★★★ 还有【第二层】避諱,来自【相反的方向】—— 卷二校勘記:
// 「『霸先』二字【原作『諱』】,蓋仍【姚察】舊文,姚察為梁、陳之史官,例避【陳】諱也。
// 北監本、汲本、殿本並已改『諱』為『霸先』,今從之。後同。」
// 卷三校勘記:「『頊』原作『諱』,蓋姚察原文如是。」
// ⇒ 姚察是【陳】朝的史官,他不能写自己皇帝的名字,于是写「諱」。他儿子姚思廉在【唐】朝
// 续成此书,避的是【唐】諱。明清的刻工又把一部分【回改】回去。
// ⇒ 【你现在读到的《陳書》每一个字,是三层制度叠压之后的残留】:
// 姚察避陳諱(写「諱」占位)→ 姚思廉避唐諱(丙→景、虎→武、淵→深)→ 後人回改(改回去一部分)。
// 而【回改是不彻底的】:同一部书里,「周鐵武」和「周鐵虎」是同一个人(甲:卷一武、卷三虎)。
// ★ 这就是本朝给产品的那句话:制度不只挡你的路,它【改你的名字】,而且是在你死后、
// 由一个你没听说过的王朝、为了几个你没见过的死人。
//
// ══════════════════════════════════════════════════════════════════════════
// 【第二个坑:五帝有本紀,但其中两个【没有廟號、没有諡號、且不「崩」】】
//
// 卷一–二 高祖武皇帝諱霸先 廟號高祖 諡武皇帝 「崩于璿璣殿」
// 卷三 世祖文皇帝諱蒨 廟號世祖 諡文皇帝 「崩于有覺殿」
// 卷四 廢帝諱伯宗 【无廟號】【无諡號】 「太建二年四月【薨】,時年十九」
// 卷五 高宗孝宣皇帝諱頊 廟號高宗 諡孝宣皇帝 「崩于宣福殿」
// 卷六 後主諱叔寶 【无廟號】【无陳諡】 「隋仁壽四年十一月壬子,【薨】於洛陽」
//
// ★ 「廢帝」「後主」不是后人的称呼,是【姚思廉自己的卷题和本文用字】(两证相符)。
// 全卷检索:卷四、卷六【「廟號」零命中】(两见证皆 0;且此处零命中已用单字直搜复核,
// 不是字符类工具错 —— 见 TOOL_TRAPS 第 1 条)。
// ★★ 而【体例上他们仍是「本紀」】:卷四「本紀第四」、卷六「本紀第六」,与另外三帝同列。
// ⇒ 姚思廉给了他们【紀的名分】,却收走了【帝的名分】:无廟、无諡、不称崩。
// ★ 这与蜀漢恰好【反着】:蜀漢是「本紀的写法、傳的名分」(陳壽给劉備劉禪立傳不立紀);
// 陳是「本紀的名分、非帝的待遇」。⚠ 两边【不是同一件事】,别混着讲。
// ⚠⚠ 但【不许说「薨是姚思廉在贬他们」】—— 这是诛心,而且大概率是错的。可证的是:
// 廢帝死时是【臨海王】(光大二年十一月甲寅被廢,卷四卷五两证),王用「薨」是【准的】。
// 後主死时是【隋的長城縣公】(卷六),公用「薨」也是【准的】。
// ⇒ 「不崩」不是姚思廉的态度,是【他们死时确实不是皇帝】。事实如此,解读留给读者。
//
// ★★★ 後主的「諡」是个真陷阱:卷六「追贈大將軍,封長城縣公,【謚曰煬】」——
// 「煬」是【隋】给的,给的是【長城縣公】,不是給陳朝皇帝的諡。⇒ shi 写「煬」而不加限定,
// 就是把隋朝给一个县公的諡,记成陳朝皇帝的諡。本表分成 shi=null + shiSui 两格,不合并。
//
// ══════════════════════════════════════════════════════════════════════════
// 【第三个坑:年號 —— 「改元不是即位」,而《陳書》把这件事写在明面上】
//
// ★ 陳書本紀的纪年是【光杆】的:只有元年带年號,此后一律「二年」「三年」…
// ⇒ 拿「天嘉五年」去检索,【零命中】。而零命中会被读成「史料有阙」。
// 本朝的形态是:【年號根本不重复出现】。逐年链条要按光杆年頭数(见 CHRONOLOGY_CHECKS)。
//
// ★★★ 而年號的【起讫】不用推 —— 陳書给了一个公式,六次改元六次都用它:
// 「改【舊年號】【N】年【為】【新年號】元年」
// 改永定四年為天嘉元年 (天嘉元年春正月癸丑 詔)
// 改天嘉七年為天康元年 (天康元年春二月丙子 詔)
// 改天康二年為光大元年 (光大元年春正月 詔)
// 改光大三年為太建元年 (太建元年春正月甲午 即位詔)
// 改太建十五年為至德元年 (至德元年春正月壬寅 詔)
// 改至德五年為禎明元年 (禎明元年春正月景子 詔)
// ⇒ 「永定四年」「天康二年」「光大三年」「太建十五年」「至德五年」【全部真的存在过】——
// 存在了一到两个月,然后被改名。它们不是本表的 bug,是【被改元取消掉的那个年號】。
// ⇒ 六次改元【全在正月或二月】,即:新年先按【旧年號】开张,然后在年初被改名。
// 这【就是「逾年改元」】—— 先帝的最后一年让他走完,次年才是新元。
// ★★ 而蜀漢那边,陳壽正是拿这条礼在骂劉禪(「章武之三年,則革稱建興,考之古義,體理為違」)。
// 陳朝五帝【每一次都守了】。⚠ 但别写成「陳比蜀守礼」——那是两本书、两个史官的话,
// 本表只记:陳的六次改元都在年初,且都用「改X年為Y元年」的公式。
// ★ 唯一的例外是【開國】:高祖永定元年 = 即位当年(557 十月受禪即改元)。開國无先帝可让,
// 不适用逾年改元。⇒ 五帝里【只有高祖一人】即位即改元,另外四个全是逾年。
//
// ══════════════════════════════════════════════════════════════════════════
// 【公元换算的依据】—— ★《陳書》【不给公元】,一个数字都不给。换算链条(两条,互相咬合):
// 锚一(卷六,两证逐字同):「隋【仁壽四年】十一月壬子,薨於洛陽,時年五十二。」
// 後主生于「梁【承聖二年】十一月戊寅」(卷六)。仁壽四年=604、承聖二年=553(隋書/梁書)
// ⇒ 604−553+1 = 52 【与原文「時年五十二」严丝合缝】。
// 锚二(卷五校勘記,注):「按宣帝死於【太建十四年】正月甲寅,年五十三,以此上推,
// 適為梁【中大通二年】。」中大通二年=530 ⇒ 582−530+1 = 53 ⇒ 【太建十四年 = 582】。
// 由锚二下推/上推:太建元年=569 → 光大二年=568、光大元年=567、天康元年=566、
// 天嘉元年=560、永定三年=559、永定元年=557;至德元年=583、禎明三年=589(亡)。
// ⚠ 即:公元数是【推的】,起点是原文自带的跨朝互证(陳←→隋、陳←→梁),不是拿现代年表替原文。
// ⚠ 又:陳年与公元年【不严格一一对应】(正月起算,边界差一两个月)。qi/zhi 是惯用换算。
//
// ⚠⚠ 下面所有 cite 里的【标点是今人所加】,原文无标点。比对时已剥标点再比。
// 且 cite 一律取【見證甲】(存唐諱的中華書局系)—— 因为乙已回改,不能代表姚思廉的字。
// ── 见证 ────────────────────────────────────────────────────────────────
export const WITNESSES = {
jia: 'zh.wikisource.org/wiki/陳書/卷1–卷6 ?action=raw(中華書局系:存唐諱,带校勘記 <ref>)',
yi: 'ctext.org/wiki.pl?if=zh&chapter=…(陳書 卷一–卷六;★ 丙/虎【已回改到底】)',
bing: 'zh.wikisource.org/wiki/南史/卷09、/卷10 ?action=raw(李延壽《南史·陳本紀上/下》,仅用于判廢帝年龄冲突与「中大通」补字)',
method: '全程 curl -sk 取生 wikitext/HTML 逐字比对,【未用 WebFetch】。',
juanci:
'卷次两证相符:甲的 header「卷一 本紀第一」…「卷六 本紀第六」,且卷七为「列傳第一」;' +
'乙的目录「卷一本紀第一高祖上」…「卷六本紀第六後主」「卷七列傳第一」。⇒ 本紀止于卷六,故【五帝】。',
openings: '五帝开头【剥标点后逐字比对】:仅 2 处异,见 QUOTE_VARIANTS。其余全等。',
};
// ── 避諱:本朝的核心发现,机器数出来的 ──────────────────────────────────
export const BIHUI = {
faxian: '《陳書》正文压着【两个朝代的忌讳 + 一层後人回改】。',
cengci: [
{ ceng: '姚察(陳史官,避【陳】諱)', zuofa: '皇帝名写作「諱」占位', li: '「皇帝臣諱」→今本作「皇帝臣霸先」(北監本/汲本/殿本改,中華書局從之)', ju: '卷二、卷三校勘記(注)' },
{ ceng: '姚思廉(唐臣,避【唐】諱)', zuofa: '丙→景、虎→武/獸、淵→深', li: '周鐵虎→周鐵武;韓擒虎→韓擒武;蕭淵明→蕭深明;白虎闕→白獸闕;神虎門→神獸門;虎賁→武賁;朱虎→朱武', ju: '卷一、卷五、卷六校勘記(注)' },
{ ceng: '後人(明清刻本)回改', zuofa: '把一部分避諱字改回本字', li: '「作『丙』者乃後人回改也」(卷六校勘記);今本「韓擒虎」两证皆已回改', ju: '卷六校勘記(注)' },
],
// ★ 机器实测(两见证逐卷对数,Node 跑字符类;shell grep 不可用,见 TOOL_TRAPS)
shuju: {
ganzhi: '甲:景+地支 64 / 丙+地支 19  乙:景+地支 0 / 丙+地支 83 (64+19=83,两证同位置)',
juan3: '★ 甲的 19 个「丙」中 18 个在【卷三】:卷三 景0/丙18,其余五卷 景64/丙1(卷六「十二月丙戌,太白晝見」)',
renming: '甲:周鐵武×2(卷一)+ 周鐵虎×1(卷三) 乙:周鐵武×0 + 周鐵虎×3 ⇒ 甲卷三与乙全卷均已回改',
buhuigai: '深明(蕭淵明)两证皆作「深明」、淵明两证皆 0;朱武两证皆 1、朱虎两证皆 0 ⇒ 这两处【谁都没回改】',
huigaile: '韓擒虎/擒虎 两证各 5、韓擒武/擒武 两证各 0 ⇒ 灭陳的隋将,其名【两证都已回改】',
},
tuilun_juan3:
'★【推论,非史料】:見證甲的卷三转录自回改本(干支与人名两个独立信号同指卷三)。' +
'未经第三见证与印本目验 ⇒ 本表【不据此改字】,只记数。',
jiexian:
'⚠ 不许写成「姚思廉篡改史料」——避諱是唐律与士人通例,不是他个人的选择,' +
'且他【在校勘記可复原的范围内是自洽的】。可证的只有:字被改了、谁的諱、哪几处回改。' +
'为什么、他怎么想,本表不裁。',
};
// ── 年號 ────────────────────────────────────────────────────────────────
export const NIANHAO_NOTE =
'陳共【七个年號】:高祖1(永定)、世祖2(天嘉、天康)、廢帝1(光大)、高宗1(太建)、後主2(至德、禎明)。' +
'★ 七个全部【两证相符】,且每一个的起讫都由原文的「改X年為Y元年」公式直接给出,不用推。' +
'★ 只有高祖是即位即改元(開國);另外四帝【全部逾年改元】——新年先按旧年號开张,正月/二月再改名。';
export const NIANHAO_FORMULA = [
{ wen: '改永定四年為天嘉元年', juan: '《陳書》卷三', shi: '天嘉元年春正月癸丑 詔', yi: '560 本应是「永定四年」,正月被改名为天嘉元年' },
{ wen: '改天嘉七年為天康元年', juan: '《陳書》卷三', shi: '天康元年春二月丙子 詔', yi: '566 先做了一个多月的「天嘉七年」,二月改名天康元年' },
{ wen: '改天康二年為光大元年', juan: '《陳書》卷四', shi: '光大元年春正月 詔', yi: '567 本应是「天康二年」' },
{ wen: '改光大三年為太建元年', juan: '《陳書》卷五', shi: '太建元年春正月甲午 即位詔', yi: '569 本应是「光大三年」——废帝的年號本还要再走一年' },
{ wen: '改太建十五年為至德元年', juan: '《陳書》卷六', shi: '至德元年春正月壬寅 詔', yi: '583 本应是「太建十五年」' },
{ wen: '改至德五年為禎明元年', juan: '《陳書》卷六', shi: '禎明元年春正月景子 詔', yi: '587 本应是「至德五年」。★「景子」=丙子,避諱形' },
];
// ── 帝表 ────────────────────────────────────────────────────────────────
export const EMPERORS = [
{
// ── 高祖 武皇帝 陳霸先 ────────────────────────────────────────────
miao: '高祖',
shi: '武皇帝',
shiQuote: '秋八月甲午,群臣上諡曰武皇帝,廟號高祖。',
shiSource: '《陳書》卷二·本紀第二·高祖下(正文,非注)',
xing: '陳霸先',
xingNote:
'★ 本紀开头只作「諱霸先」,「陳」姓出同卷「漢太丘長陳寔之後也」。' +
'★★ 又:卷二「皇帝臣霸先,敢用玄牡昭告于皇皇后帝」一句,校勘記(注)称「霸先」二字' +
'【原作「諱」】,是姚察(陳史官,例避陳諱)的原貌,北監本/汲本/殿本填回真名。' +
'⇒ 连「霸先」这两个字,在部分位置也是【後人补回的】,不是姚察的原文。',
li: 557,
beng: 559,
liQuote: '永定元年冬十月乙亥,高祖即皇帝位于南郊,柴燎告天曰',
bengQuote: '景午,崩于璿璣殿,時年五十七。',
bengNote:
'★「景午」=丙午,避諱形(見證乙作「丙午」)。★ 崩 —— 五帝里三个「崩」之一。' +
'⚠ 异文:甲「高祖疾又甚」/乙「高祖疾甚」(多一「又」字);' +
'甲「高祖臨訊獄(省)訟」——()是中華書局的【删字】校勘符,乙已照删作「臨訊獄訟」。',
nianhao: [{ hao: '永定', qi: 557, zhi: 559 }],
nianhaoComplete: true,
nianhaoNote:
'★ 唯一即位即改元者(開國,557 十月受禪)。永定 = 元、二、三年,卷二年頭 1,2,3 【逐年连续,两证同】。' +
'560 本应是「永定四年」,被改名天嘉元年 ⇒ 永定实际只有三年。',
juan: '《陳書》卷一(高祖上)、卷二(高祖下)',
cite: '高祖武皇帝諱霸先,字興國,小字法生,吳興長城下若里人,漢太丘長陳寔之後也。',
citeCaveat: '⚠ 异文:甲作「陳寔」,乙作「陳實」(寔/實 古通,无关文义,照录甲的「寔」)。乙于「皇帝」后多一逗(今人标点)。',
},
{
// ── 世祖 文皇帝 陳蒨 ──────────────────────────────────────────────
miao: '世祖',
shi: '文皇帝',
shiQuote: '六月甲子,群臣上謚曰文皇帝,廟號世祖。',
shiSource: '《陳書》卷三·本紀第三·世祖(正文,非注)',
shiCaveat: '⚠ 此处两证【皆作「謚」】(非「諡」)。同书两形并存:卷二作「諡」4 次、「謚」1 次。照录原字,不许径改统一。',
xing: '陳蒨',
xingNote: '★ 校勘記(注):卷三「頊」原作「諱」,同理「蒨」在部分位置亦原作「諱」(姚察避陳諱),今本为後人填回。',
li: 559,
beng: 566,
liQuote: '世祖固讓,至于再三,群公卿士固請,其日即皇帝位於太極前殿。',
liNote:
'永定三年(559)六月景午高祖崩,「遺詔徵世祖入纂」;甲寅至自南皖、入居中書省,' +
'皇后令下、世祖三让、「其日」即位 ⇒ li = 559(永定三年六月甲寅)。',
bengQuote: '癸酉,世祖疾甚。是日,崩于有覺殿。',
bengNote:
'★★【卷三没有「時年」】—— 全卷「時年」两证皆【零命中】。五帝里另外四个都有岁数,' +
'独世祖没有。⇒ 世祖崩时几岁,【本紀未記】。' +
'⚠⚠ 不许反过来写「世祖岁数不可考」——本表无据 ≠ 事实为无。他书(南史等)未经我核。',
nianhao: [
{ hao: '天嘉', qi: 560, zhi: 566 },
{ hao: '天康', qi: 566, zhi: 566 },
],
nianhaoComplete: true,
nianhaoNote:
'★ 天嘉元年 = 560(改永定四年為天嘉元年,正月癸丑)⇒ 世祖 559 即位、560 才改元 =【逾年改元】。' +
'★★ 天嘉与天康【同在 566 年】:566 先是「天嘉七年」,二月丙子詔「改天嘉七年為天康元年」。' +
'⇒ 天嘉.zhi = 566 而 天康.qi = 566,【故意重叠】,原文如此。谁把它改成 565/566 不重叠,' +
'谁就抹掉了「天嘉七年真的存在过一个多月」这件事。⚠ 不许修。',
juan: '《陳書》卷三',
cite: '世祖文皇帝諱蒨,字子華,始興昭烈王長子也。',
citeCaveat:
'⚠ 乙于「皇帝」后多一逗(今人标点);剥标点后【全等】。' +
'★ 卷三是全书唯一【干支全作「丙」不作「景」】的一卷(丙18/景0),且此卷作「周鐵虎」而卷一作「周鐵武」' +
'⇒ 見證甲的卷三疑转录自回改本(推论,见 BIHUI.tuilun_juan3)。本 cite 不受影响(无干支)。',
},
{
// ── 廢帝 陳伯宗 ──────────────────────────────────────────────────
miao: null,
miaoStatus: 'none-in-source',
miaoNote:
'★ 卷四全卷检索「廟號」【零命中】(两见证皆 0,且已用单字直搜复核,非字符类工具错)。' +
'★★ 与蜀漢劉備那种「本書未記」【不是一回事】:劉備是通行说有廟號而本書没写;' +
' 廢帝是【被廢黜】,本朝根本没给他上廟號 —— 姚思廉的卷题就直接叫他「廢帝」。' +
'⚠ 但严格说,本表能证的仍只是「卷四未見廟號」。「陳朝确实没给他上廟號」需另证,本表不裁。',
shi: null,
shiStatus: 'none-in-source',
shiNote: '★ 卷四无諡。他不是以皇帝的身份死的 —— 死时是【臨海王】。',
xing: '陳伯宗',
li: 566,
beng: 568,
bengMeans: 'deposed', // ★ 不是崩,是【被废】
zu: 570, // 卒年,与在位末年差两年
liQuote: '天康元年四月癸酉,世祖崩,其日,太子即皇帝位于太極前殿,詔曰',
feiQuote: '十一月甲寅,慈訓太后令廢帝為臨海王,以高宗入纂。',
feiSource: '《陳書》卷五·本紀第五·宣帝(正文)。卷四同日作「甲寅,慈訓太后集羣臣於朝堂,令曰…是日,出居別第」——两卷相符。',
zuQuote: '太建二年四月薨,時年十九。',
zuNote:
'★★【薨,不是崩】。他死时是臨海王,王用「薨」是【准的】—— 这一个字里,他的帝位已经没了两年。' +
'★ 卷五在太建二年下另记「夏四月乙卯,臨海王伯宗薨」——【本朝廢帝的死,记在别人的紀里,且称王不称帝】。' +
'★ 第三见证《南史》卷九亦作「太建二年四月乙卯薨,時年十九」,干支与卷五相符。',
nianhao: [{ hao: '光大', qi: 567, zhi: 568 }],
nianhaoComplete: true,
nianhaoNote:
'★ 566 四月即位,用完先帝的天康元年;567 正月才改元光大 =【逾年改元】。' +
'★ 光大 = 元、二年(卷四年頭 1,2 两证同)。569 本应是「光大三年」,被改名太建元年' +
'⇒ 他的年號在他被废后【又替他走了一个多月】,然后被叔叔改掉。',
juan: '《陳書》卷四',
cite: '廢帝諱伯宗,字奉業,小字藥王,世祖嫡長子也。',
citeCaveat: '⚠ 乙于「廢帝」后多一逗(今人标点);剥标点后【全等】。',
},
{
// ── 高宗 孝宣皇帝 陳頊 ────────────────────────────────────────────
miao: '高宗',
shi: '孝宣皇帝',
shiQuote: '二月辛卯,上謚孝宣皇帝,廟號高宗。',
shiSource: '《陳書》卷五·本紀第五·宣帝(正文,非注)',
shiCaveat:
'★ 諡是【四字「孝宣皇帝」】,不是「宣皇帝」—— 与卷五开头「高宗孝宣皇帝諱頊」自洽(两证皆同)。' +
'⚠ 但【卷题作「宣帝」】(甲 header「卷五 本紀第五 宣帝」、乙目录「卷五本紀第五宣帝」)。' +
'⇒ 卷题与諡号【不一致】,是史书通例(卷题取简称),不是异文。别把「宣帝」当諡填进 shi。',
xing: '陳頊',
xingNote: '★ 校勘記(注):卷三「徙封始興嗣王頊為安成王」的「頊」【原作「諱」】(姚察避陳諱),今本为後人填回。',
li: 569,
beng: 582,
liQuote: '太建元年春正月甲午,即皇帝位于太極前殿,詔曰',
liNote:
'★★ 这里有【两个月的空档,别抹平】:光大二年(568)十一月甲寅慈訓太后令廢帝為臨海王、' +
'「以高宗入纂」;而他【太建元年(569)正月甲午才即皇帝位】。' +
'⇒ 陳書自己的宣帝紀把 li 记在 569。568 十一月–569 正月,他是「入纂」而【未即位】。' +
'本表 li = 569(从原文的「即皇帝位」),并把 568 的入纂单记 ruzuan。' +
'⚠ 拿现代年表写「宣帝 568 即位」,就是拿今人的整洁替掉了原文自己的分寸。',
ruzuan: 568,
bengQuote: '十四年春正月己酉,高宗弗豫。甲寅,崩于宣福殿,時年五十三。',
nianhao: [{ hao: '太建', qi: 569, zhi: 582 }],
nianhaoComplete: true,
nianhaoNote:
'★ 太建 = 元…十四年,卷五年頭 1–14【逐年连续,一年不缺,两证同】(已机器核,含「十一」不漏)。' +
'583 本应是「太建十五年」,被改名至德元年。',
juan: '《陳書》卷五',
cite: '高宗孝宣皇帝諱頊,字紹世,小字師利,始興昭烈王第二子也。',
citeCaveat:
'★★ 此句【下一句】是本朝最险的一格:甲作「梁〔中〕大通二年七月辛酉生」——' +
'〔〕是中華書局的【补字】符,「中」是【编者加的,不是姚思廉的字】。校勘記(注):' +
'「據《南史·陳宣帝紀》補。按宣帝死於太建十四年正月甲寅,年五十三,以此上推,適為梁中大通二年。」' +
'⇒ 传世《陳書》原作「梁大通二年」(=528),是【错的】;编者据南史补「中」(中大通二年=530)。' +
'★ 第三见证《南史》卷十确作「梁中大通二年七月辛酉生」(无括号)⇒ 校勘記所称的补字来源【已核实】。' +
'⚠ 而乙(ctext)作「梁中大通二年」——【把补字当原文了,括号没了】。' +
'⇒ 谁拿乙做底本,谁就看不见这里动过手术。本表 cite 【止于「第二子也」】,不含这一格。',
birthEmended: {
benwen: '梁大通二年(=528,传世本原字,与「年五十三」不合)',
jiaoding: '梁〔中〕大通二年(=530,中華書局據《南史》補「中」字)',
jushi: '582 − 53 + 1 = 530 = 中大通二年 ⇒ 校补与「時年五十三」自洽',
biaozhu: '★ 这是【校勘記(注)的订正】,不是《陳書》正文。本表照实标,不当原文用。',
},
},
{
// ── 後主 陳叔寶 ──────────────────────────────────────────────────
miao: null,
miaoStatus: 'none-in-source',
miaoNote: '★ 卷六全卷检索「廟號」【零命中】(两见证皆 0)。亡國之君,陳朝已无人能给他上廟號。',
shi: null,
shiStatus: 'none-from-chen',
shiNote:
'★★★【最容易记错的一格】:卷六确有「謚曰煬」,但那是【隋】给的,给的是【長城縣公】,' +
'不是陳朝给皇帝的諡。原文:「追贈大將軍,封長城縣公,謚曰煬」——' +
'主语是隋,官是隋的大將軍,爵是隋的縣公,諡自然也是隋的縣公之諡。' +
'⇒ shi(陳諡)= null;隋諡另记 shiSui。合并成 shi:"煬" 就是把两个朝代的账记到一起。',
shiSui: '煬',
shiSuiQuote: '追贈大將軍,封長城縣公,謚曰煬,葬河南洛陽之芒山。',
shiSuiNote: '★ 与隋煬帝同用一个「煬」字 —— 而给他这个字的,正是隋。⚠ 但「煬是恶諡」是解读,本表不裁。',
xing: '陳叔寶',
li: 582,
beng: 589,
bengMeans: 'dynasty-fell', // ★ 589 是亡國,不是他死
zu: 604,
liQuote: '十四年正月甲寅,高宗崩。乙卯,始興王叔陵作逆,伏誅。丁巳,太子即皇帝位于太極前殿。',
liNote: '★ 太建十四年(582)正月:甲寅高宗崩 → 乙卯叔陵作逆伏誅 → 丁巳後主即位。三天,三件事。',
wangQuote: '及夜,為隋軍所執。景戌,晉王廣入據京城。',
wangNote:
'禎明三年(589)正月乙丑朔隋軍南北道並進 → 城陷、後主自投井被執 →「景戌,晉王廣入據京城」。' +
'★「景戌」=丙戌,避諱形。★★ 而灭他的隋将,卷六作「韓擒虎」——校勘記(注)明言:' +
'「按思廉避唐諱,『虎』字皆改為『武』。此作『虎』,乃【後人回改】也。」' +
'⇒ 姚思廉笔下他本该叫【韓擒武】。两个见证【都已回改】成「虎」(各 5 处,「擒武」各 0 处)。',
zuQuote: '隋仁壽四年十一月壬子,薨於洛陽,時年五十二。',
zuNote:
'★★【薨,且用【隋】的年號纪年】。陳朝的末代皇帝,死在洛陽,' +
'记在他自己那部正史的最后一句里,用的是【灭他那个朝代的日历】。' +
'★ 这一格是全书的公元锚:仁壽四年=604,与卷六「梁承聖二年十一月戊寅生」(=553)' +
'对算 604−553+1 = 52,【与「時年五十二」严丝合缝】。',
nianhao: [
{ hao: '至德', qi: 583, zhi: 586 },
{ hao: '禎明', qi: 587, zhi: 589 },
],
nianhaoComplete: true,
nianhaoNote:
'★ 582 即位、583 才改元至德 =【逾年改元】。至德 = 元…四年(583–586),' +
'禎明 = 元…三年(587–589)。卷六编年正文年頭(按文中位置核过,已剔除論赞区):' +
'太建十四年 → 至德元、二、三、四 → 禎明元、二、三 ⇒【八年逐年连续,无阙】。' +
'★ 587 本应是「至德五年」,被改名禎明元年。禎明三年(589)亡國,无禎明四年。',
juan: '《陳書》卷六',
cite: '後主諱叔寶,字元秀,小字黃奴,高宗嫡長子也。',
citeCaveat: '⚠ 乙于「後主」后多一逗(今人标点);剥标点后【全等】。',
},
];
// ── 追尊者:不是皇帝,别数进去 ──────────────────────────────────────────
export const ZHUIZUN = [
{
miao: '太祖',
shi: '景皇帝',
xing: '陳文讚',
quote: '辛巳,追尊皇考曰景皇帝,廟號太祖;皇妣董太夫人曰安皇后。',
juan: '《陳書》卷二(正文)',
note:
'★★【陷阱】:陳有「太祖」,但太祖【从没当过皇帝】—— 是高祖陳霸先追尊的父亲。' +
'按廟號检索「太祖」会命中他,把他数进帝表就变成六帝。' +
'⇒ 本朝【在位五帝】,太祖不在其中。(同卷另有「尊景帝陵曰瑞陵」「遷景皇帝神主祔于…」,皆指他。)' +
'★ 又:卷一「始興昭烈王」(世祖、高宗之父陳道譚)亦为追封,非帝,同样不入表。',
},
];
// ── 逐年链条的机器核验(把结论和【核法】一起留下)────────────────────────
export const CHRONOLOGY_CHECKS = {
method:
'★ 陳書本紀年頭是【光杆】的(只有元年带年號),故按 /(元|十[一二三四五六七八九]?|[一二三四五六七八九])年(?=春|夏|秋|冬|正月|閏|N月)/ ' +
'数【文中顺序】的年頭,不 uniq、不跨年號合并。',
results: {
juan2_yongding: '年頭 1,2,3 —— 逐年连续(两证同)',
juan3_tianjia_tiankang: '年頭 1..6 + 天康元年 —— 天嘉元–六(560–565) + 天嘉七年→天康元年(566),连续(两证同)',
juan4_guangda: '年頭 1,2 —— 光大元、二(567–568) 连续(两证同)',
juan5_taijian: '年頭 1..14 —— 【逐年连续,一年不缺】(两证同)',
juan6_zhide_zhenming: '编年正文年頭:太建十四 → 至德元,二,三,四 → 禎明元,二,三 —— 连续(两证同)',
},
false_alarms_i_made: [
'★★【我自己造过一个假缺口】:把一卷内的年頭 uniq 之后报「卷六缺 5–13」。' +
'真相:年號一改,年数就从「元年」重来,uniq 跨两个年號毫无意义。⇒ 报「阙」之前先查自己的工具。',
'★【我自己造过一个假异常】:seq 脚本按正则交替顺序输出,看着像「禎明有四年」。' +
'逐条打印字符位置后,那个「四年」是【至德四年】(offset 3640),位置在禎明元年之前。⇒ 顺序错觉,非史料异常。',
],
};
// ── 存疑:不许替读者选 ──────────────────────────────────────────────────
export const AGE_CONFLICT = {
who: '廢帝 陳伯宗',
yuanwen: ['梁承聖三年五月庚寅生。(卷四)', '太建二年四月薨,時年十九。(卷四)'],
suanshu:
'承聖三年 = 554(锚:卷六「梁承聖二年十一月戊寅生」+「明年,江陵陷」,且後主 604 薨年五十二 ⇒ 承聖二年=553 ⇒ 承聖三年=554);' +
'太建二年 = 570(锚:太建十四年=582 ⇒ 太建元年=569)。' +
'⇒ 570 − 554 + 1 = 【十七】,而原文作【十九】。差二。',
witnesses:
'★ 两个见证的《陳書》卷四【都作】「承聖三年…時年十九」;' +
'★★ 第三见证《南史》卷九(李延壽,另一部书)【也作】「梁承聖三年五月庚寅生」+「太建二年四月乙卯薨,時年十九」。' +
'⇒ 这【不是某个本子的抄错】,冲突在【传世的史料本身】里。',
jiaokanji: '★ 中華書局卷四校勘記(注)共 13 条,【没有一条提这个矛盾】。⇒ 不是「已订正」,是【没人订正】。',
verdict:
'★★ 本表【不裁】:li/beng/zu 一律照原文的【年】记(生 554、薨 570),' +
'而「時年十九」【照录不改】,冲突挂在这儿。' +
'⚠ 若把它「修」成十七,就是拿我的算术替掉了两部正史共有的字 —— 那是【假订正】。' +
'⚠ 若把生年改成 552 去凑十九,就会撞坏「承聖三年」这个两证+南史三证的字。' +
'⇒ 写「未核」比写一个假的强一百倍。等史学顾问补见证再说。',
};
// ── 异文账(照实分账,一处都没替读者选)────────────────────────────────
export const QUOTE_VARIANTS = [
{ chu: '卷一 高祖开头', jia: '漢太丘長陳寔之後也', yi: '漢太丘長陳實之後也', pan: '寔/實 古通(同音异写),无关文义。照录甲「寔」。' },
{ chu: '卷五 宣帝生年', jia: '梁〔中〕大通二年', yi: '梁中大通二年', pan: '★ 甲存【校补符】,乙【抹掉了括号把补字当原文】。三见证:南史卷十作「梁中大通二年」= 校勘記所称补字来源,已核。本表不入 cite。' },
{ chu: '卷二 高祖崩', jia: '高祖疾又甚', yi: '高祖疾甚', pan: '甲多一「又」字。异文,两存,无关本表取值。' },
{ chu: '卷二 高祖崩前', jia: '高祖臨訊獄(省)訟', yi: '高祖臨訊獄訟', pan: '()是中華書局【删字】校勘符,乙已照删。⇒ 非异文,是甲保留了校勘痕迹。' },
{ chu: '卷一 南兗州', jia: '南论州刺史(1 处)', yi: '南兗州刺史(1 处)', pan: '★ 甲此处是【坏字】:「论」不成词,当作「兗」(甲 南兗州 0 处 / 乙 南论州 0 处,实测)。⇒ 采乙。这是【两证判一证】,不是我径改原文。' },
{ chu: '卷四 廢黜令', jia: '慈訓太后集【戝】臣於朝堂', yi: '慈訓太后集【群】臣於朝堂', pan: '★ 甲又一处【坏字】:「戝」不成词,当作「群」。⇒ 采乙。' },
{ chu: '全书 干支丙日', jia: '景午/景申/景戌/景子…(64 处)', yi: '丙午/丙申/丙戌/丙子…(83 处)', pan: '★★ 不是普通异文,是【避諱 vs 回改】。甲存唐諱,乙回改到底。见 BIHUI。' },
{ chu: '諡/謚 字形', jia: '卷二作「諡」4 次、「謚」1 次;卷三、卷五、卷六作「謚」', yi: '卷二全作「謚」', pan: '同字异形,同书内两形并存。照录各处原字,【不许统一】。' },
{ chu: '卷四 廢帝詔文', jia: '慈訓太后…令曰:【正文缺】', yi: '慈訓太后…令曰:「…攬筆潸然,兼懷悲慶。」', pan: '★ 甲(wikisource)【丢了废黜令的正文】(模板未展开)。⇒ 涉及该詔内容一律用乙。本表不取此詔。' },
];
// ── 工具坑:全是这一刀实测栽出来的,留给下一个人 ────────────────────────
export const TOOL_TRAPS = [
'★★★【shell 的 grep 不能用中文字符类】:`grep -oE "[諡謚]" p2.txt` 报【1031 命中】,真值【5】——' +
'grep 把 [諡謚] 按【字节】拆成字符集,于是命中一切含这些字节的汉字。' +
'⇒ CLAUDE.md 让「跨卷检索一律写 [為爲] 字形类」,但【在这个环境里 shell grep 会把它变成垃圾】。' +
'所有字符类检索【一律写成 .js 文件用 node 跑】(node 的正则是按码位的,正确)。单字直搜 grep 仍可用。',
'★★★【wikisource 的 <ref>…</ref> 是中華書局校勘記,是【注】不是正文】。' +
'只删 <> 尖括号会把校勘記正文【混进本紀】——实测:廢帝紀「…立為皇太子」后面直接接上' +
'「按『八月庚戌』〈世祖紀〉作『九月辛酉』」,读起来像姚思廉自己在写。' +
'必须 /<ref[^<>]*>[\\s\\S]*?<\\/ref>/ 【连内容一起删】。六卷共 168 条校勘記,全是这么混进来的。',
'★★【校勘符会插在词中间,且两套】:〔X〕=编者【补】的字,(X)=编者标【删】的字。' +
'实测:「梁〔中〕大通二年」「屯朝(室)〔堂〕」「我心貞確,(空)〔堅〕誓蒼昊」「赦湘巴二(郡)[州]」。' +
'⇒ 拿「梁中大通二年」去搜甲,【零命中】;拿「獄訟」去搜甲,也【零命中】。检索前必须先处理。',
'★★【ctext 把页码锚点 <span id="8.35" class="libtarget"></span> 插在词中间】:' +
'「高祖武皇<span…></span>帝,諱霸先」⇒ 拿「高祖武皇帝」搜 ctext【零命中】。' +
'与 wikisource 的 {{YL|…}} 是同一个病。两边都要先剥标记再检索。',
'★★【{{YL|泰始七年|271年}} 式模板的第二参是维基编者加的公元换算,不是原文】。' +
'解析只留第一参。(本朝实测:{{YL|〔中〕大通二年}}、{{YL|承聖三年}} 等。)',
'★★★【抓失败会变成假零命中】:curl 取 zh.wikisource.org/wiki/南史/卷9 返回的是' +
'【Wikimedia Error 的 HTML 页】(1926 字节),而我的 strip 把它照样剥成"文本",' +
'于是检索「廢帝」【零命中】—— 差一点就写成「南史不记廢帝年龄」。' +
'真相:页名是零填充的【南史/卷09】。⇒ 每次抓完先验【字节数 + 是否含预期关键字】,再检索。' +
'「零命中比单证更危险」在这一刀又应验了一次,而且这次是【工具假装成史料】。',
'★【node -e 传中文正则会被 shell 搞坏】(CLAUDE.md 已记):涉中文的核验脚本一律【写文件跑】。' +
'本刀全部核验脚本在 scratchpad/chen/*.js(strip.js/ctext.js/dig.js/nianhao.js/seq.js/years.js/bing.js/hui.js/final.js)。',
];
// ── 还没做的,别当我做了 ────────────────────────────────────────────────
export const NOT_DONE = [
'本刀【只做帝纪】:五帝的廟/諡/名/在位/年號/卷次/开头引文。',
'★★【群臣图谱没做,而且这一朝【没有现成的表可抠】—— 这条必须报给老板】:' +
' 《陳書》三十六卷 = 【本紀六 + 列傳三十】,【无表、无志】。已核:ctext 全书目录逐卷比对,' +
' 卷一–卷六「本紀第一–第六」、卷七–卷三十六「列傳第一–第三十」,检索卷题含「表」「志」者【零命中】。' +
' ⇒ 任务书的前提「正史自带年表(漢書百官公卿表/新唐書宰相表/宋史宰輔表/明史宰輔年表)」' +
' 在【陳朝不成立】:《陳書》根本没有那一层。南朝诸史普遍缺表(《宋書》有志无表,齊梁陳皆无表)。' +
' ⇒ 陳的宰辅只能从卷三–卷六【编年正文】逐条抠「以某為某」(实测卷五一卷上百条),' +
' 或从卷十七以下列傳里抠。这是【下一刀】的活,且【工作量与「从散文里抠」同级】,不是照表誊。',
'★【大事件没做】:本紀正文就是编年大事件(卷一–卷六,逐年逐月逐日),但本刀没抠。' +
' 料在手上(scratchpad/chen/p1–p6.txt、q1–q6.txt 已剥净可直接用)。',
'★【廢帝年龄冲突未解】:见 AGE_CONFLICT,两部正史共有的矛盾,本表不裁。',
'★【世祖崩年岁数未見】:卷三无「時年」。他书未核。⚠ 不许写成「不可考」。',
'★【卷三是否回改本未判】:BIHUI.tuilun_juan3 是推论,缺第三见证与印本目验。',
'★【後主之外四帝的生年未系统核】:只有後主(承聖二年=553)与宣帝(校补中大通二年=530)有明确锚。' +
' 高祖「時年五十七」+永定三年(559) ⇒ 生年约 503,但这是【我推的】,卷一未直书生年,未再核。',
];
export const CHEN = {
chao: '陳',
qi: 557,
zhi: 589,
du: '建康',
xing: '陳',
diCount: 5,
nianhaoCount: 7,
shu: '《陳書》三十六卷,姚察撰、姚思廉續成(唐貞觀)。本紀六卷(卷一–卷六)。',
note:
'★ 陳是南朝最后一朝,也是【疆域最小、国祚最短】的一朝(33 年,五帝)。' +
'★★ 但本朝真正的料不在短,在【它的史书是灭它的那个体系的下一朝写的】:' +
'姚察在陳朝写陳史(避陳諱,皇帝名写「諱」),其子姚思廉在唐朝续成(避唐諱,丙→景、虎→武、淵→深),' +
'明清刻工再回改一部分。⇒ 陳朝人的名字,最终由【唐朝的祖先】和【明清的刻工】共同决定。' +
'★ 卷六末更有「史臣侍中鄭國公【魏徵】曰」——《陳書》的論赞里站着唐太宗的宰相,' +
' 而他论的正是【被唐所继承的那个隋所灭的】陳。' +
' ⚠ 见证差异(已核):甲(wikisource) 此段【正文为空,只剩标题】;' +
' 乙(ctext) 有全文,起「魏徵曰:高祖拔起壟畝,有雄桀之姿。始佐下籓,奮英奇之略…」约千字。' +
' ⇒ 要引魏徵論【必须用乙】。本刀未逐字核该段,故不入表。' +
' ★ 我一度据甲写成「两证皆只见标题」——错的,是我拿单证当了全部。已订正(此处两个见证都看过了)。',
};
// ══════════════════════════════════════════════════════════════════════════
// ██ 群臣图谱 + 大事件(本刀新增)—— 一次做完 ██
// ══════════════════════════════════════════════════════════════════════════
// 上半部(EMPERORS 等)是【帝纪】,已双证做完,本段一个字都不改,只【追加】。
//
// ★★ 这一朝【没有现成的年表】:《陳書》三十六卷 = 本紀六 + 列傳三十,【无表、无志】
// (已核,见 NOT_DONE 与 CHAO_COVERAGE.chen.note)。任务书的前提「正史自带公卿表/宰相表」
// 在陳【不成立】。⇒ 群臣与大事【只能从本紀卷一–卷六的编年正文里抠任命句】。
// 所以本段每条 source 一律 = "benji"(从本紀抠),【没有一条是 "biao"】—— 这是这一朝的实情。
//
// ── 方法(两见证逐条核过)───────────────────────────────────────────────
// · 甲 = zh.wikisource(存唐諱),乙 = ctext(丙/虎已回改)。人名一致性已机器对数:
// 侯安都 17/17、吳明徹诸卷全等…(见 scratchpad/chen/verify.js 实测)。cite 一律取【甲】。
// · 编年定年:本紀年头是【光杆】的(只有元年带年號,见 CHRONOLOGY_CHECKS)。
// 逐卷按【实测字符偏移】把「元年/二年/…」锚到公元(锚表 scratchpad/chen/extract.js ANCHORS),
// 再把每条任命/大事挂到它所在的年块。⇒ year 字段是【锚定】的,不是猜的。
//
// ── ★★ dong(动词)字段:这个产品最值钱的字段,绝不抹平成「担任」──────────
// 本紀记任命的主构式是「以X為Y」,此外有 進號/進位/即本號/遷/徵/免/薨/卒/賜死/伏誅/奔/反/降/執。
// 我把它归成一张【受控词表】,而【原文逐字全在 cite 里】,读者可复核:
// 除 = 拜官/改任(源:以X為Y / 授X為Y / 除X為Y)
// 進 = 升迁(源:進號 / 進位 / 即本號 / 加)
// 遷 = 遷 徵 = 徵…為(征召入朝) 免 = 免 / 有罪免 / 坐…免
// 薨 = 王公之死 卒 = 卒 賜死 = 賜死
// 誅 = 伏誅/見誅/誅 見殺 = 死于乱军(被害) 奔 = 奔于齊/奔周(叛降敌国)
// 反 = 舉兵反/作逆/謀反/據…反 降 = 出降/歸降 執 = 兵敗被俘(為…所獲)
// 見 = 史文只记其【此年在任】而非新拜(亡國一刻的省中名单即用此)
// ⚠⚠ 「免」「薨」「誅」「賜死」「奔」是【五件完全不同的事】—— 侯安都是【賜死】不是【薨】,
// 沈泰是【奔】不是【免】。抹平这一列,这个产品就没有内容了。
//
// ── ★ 字形坑(CLAUDE.md 已警,本刀又栽到两处,两见证对数后订正)──────────
// ① 黃法𣰋:甲卷2/3/5 作「𣰋」,【甲卷4 作「矴」】(同人异形,实测甲𣰋15/矴2);
// 乙全卷用另一异形。本段人名一律写【黃法𣰋】,读者遇「黃法矴」即同一人。
// ② 杜稜:乙(ctext)【全卷 0 命中「稜」】(用棱/㥄类异形),甲作「稜」。本段一律【杜稜】。
// ③ 周鐵虎/韓擒虎:避唐諱,姚思廉原文当作【周鐵武】【韓擒武】(见 BIHUI)。
// 564 配食一条甲卷三作「周鐵虎」(回改本),589 亡國一条两证皆作「韓擒虎」(都已回改)。
// 本段【照甲录字】并在 note 标避諱,不径改。
//
// ── ★★★ layer(事/制/道):这一朝的骨头,而它给出一个【干净且沉重】的答案 ─────
// 我把每条大事都判了层,结论是:【陳几乎全是「事」,「制」零个,「道」零个】。
// 这不是偷懒,是这一朝的实情,判据就是老板的那句「五十年后还在起作用吗?」:
// · 陳国祚仅 33 年(557–589),589 被隋整体吞并,其律令/官制/货币【无一存续到隋唐】。
// 557 定律令 ⇒ 50 年后是 607(隋),陳律早废,隋行《開皇律》。⇒ 够不到「制」。
// · 太建北伐(573)、呂梁之敗(578)、受禪(557)、亡國(589) —— 都是【事】:一场仗、一次禪代、
// 一次废立。哪怕呂梁一败丧师十万,它也只是【事】(老板原话:一场仗就是事,哪怕死十万人)。
// ★★ 唯一够得着「道」的,是 589 那件事的【另一面】:隋滅陳 = 结束西晉以来 270 年南北分裂、
// 重新一统,这个后果确实延续了一千五百年。但【那个「道」是【隋】的,不是【陳】的】——
// 《陳書》记的是「亡國」,从陳的账本上它是一场死亡,不是陳缔造的制度。⇒ 本表【不把它算成陳的道】,
// 589 记为 layer:"shi",理由写在该条 note 里。这正是这个产品在陳这一朝要说的话:
// 【在陳做官的人,一生都活在「事」的那一层 —— 因为他还没来得及让任何「制」生效,
// 整个世界就在 589 年被一次性抹掉了。他是那个「道」的承受者,不是缔造者。】
// ★★【本刀補記(據建模 §1.3/§3.3,2026-07 二刀)】:上文「制 0」是【严判】——要求制存续 50 年方算制。
// 而建模層典按【类型】计:律/铸钱/官制之确立即为「制」,存续长短只分【制/道】,不分【事/制】。
// 从建模,本刀補 557「立刪定郎,治定律令」(律)、579「初用大貨六銖錢」(铸钱) 两条为 layer:"zhi"。
// ★ 非谓本紀制度条仅此二条:卷五太建十一年甲寅诏「今可改不枉法受財者,科同正盜」亦是本紀内的律条修订,本刀未逐条补尽。
// ★ 但二者 impact 影响半径皆止【当朝】(reach:"dangchao"):陳律随 589 亡而废、大貨六銖 582 宣帝崩後即废,无一存续隋唐。
// ⇒ 与上文「他没让任何制生效」的判断【同向不悖】:陳之制确未下沿,只是按类型该记作「制」而非「事」。
// 「道 0」不变;「一步制度变革千年后还起作用吗?」在陳,impact 栏诚实地答【没有】。
//
// ── ★★ zaichang(在场):老板这一刀真正的靶子 ────────────────────────────
// 「很多人够不到历史的大事件,难道就没有他自己的小事件?况且他可能还位居高位呢。」
// ⇒ 每个大事加一格:那年【什么位阶的人】会被这件事卷进去。同一件大事,落在不同位阶身上
// 是【不同的题】。这一格是【推演】不是原文 ⇒ 只在大事上给,且不安卷次。
export const CHEN_GUANPU_METHOD = {
wubiao: '《陳書》无表无志(本紀六+列傳三十)。群臣/大事全部从本紀编年正文抠,故 source 皆 "benji"。',
dingnian: '本紀年头光杆,按实测字符偏移把「元年/二年…」锚到公元(scratchpad/chen/extract.js)。',
dongCiBiao: '除/進/遷/徵/免/薨/卒/賜死/誅/見殺/奔/反/降/執/見 —— 见文件头 dong 词表。原文逐字在 cite。',
zixing: ['黃法𣰋(甲卷4作矴)', '杜稜(乙用异形)', '周鐵虎/韓擒虎(避唐諱回改,原当作武)'],
jiexian: '★ 群臣以【宰辅层】为准做全(三公/尚書令僕射/中書監令/侍中/領護軍/錄尚書/都督中外/太傅/揚州刺史/吏部尚書/開府儀同三司初授);九卿以下与外州刺史【只抠到卷五所见,未穷尽】—— 见 CHAO_COVERAGE.chen,不静默截断。',
// ⚠ 上方已有 export `NOT_DONE`(上一刀写的)里「群臣图谱没做 / 大事件没做」两条【已被本刀完成】,
// 但按红线「别改它已有的 export」,本刀【不改 NOT_DONE 原文】,在此声明其那两条已过时;
// NOT_DONE 里其余未竟项(廢帝年龄冲突/世祖崩年岁数/卷三是否回改本/四帝生年)仍成立。
guodushi_NOT_DONE: '本刀完成 NOT_DONE 中「群臣图谱」「大事件」两项;其余仍未做。',
};
// ═══ 一、群臣图谱(CHAO_CHEN)═══
// 每条:{ year, guan(原文官名), ren, dong, juan, cite(原文逐字), source:"benji" }
// ★ 全部从本紀任命句抠。宰辅层做全,九卿/外州能抠多少抠多少(覆盖见 CHAO_COVERAGE)。
export const CHAO_CHEN = [
// ── 高祖 永定(卷二,557–559)──
{ year: 557, guan: '領軍將軍', ren: '徐度', dong: '除', juan: '卷二', cite: '景辰,以鎮西將軍、南豫州刺史徐度為鎮右將軍、領軍將軍', source: 'benji' },
{ year: 557, guan: '左光祿大夫', ren: '王沖', dong: '除', juan: '卷二', cite: '辛卯,以中權將軍、開府儀同三司、丹陽尹王沖為左光祿大夫', source: 'benji' },
{ year: 558, guan: '司空', ren: '侯瑱', dong: '進', juan: '卷二', cite: '車騎將軍、開府儀同三司侯瑱進位司空', source: 'benji' },
{ year: 558, guan: '護軍將軍', ren: '徐世譜', dong: '除', juan: '卷二', cite: '左衛將軍徐世譜為護軍將軍', source: 'benji' },
{ year: 558, guan: '南豫州刺史', ren: '沈泰', dong: '奔', juan: '卷二', cite: '二月壬申,南豫州刺史沈泰奔于齊', source: 'benji', note: '★ 奔于北齊,非免非薨 —— 陳初方鎮离心的第一例。' },
{ year: 558, guan: '開府儀同三司', ren: '周迪', dong: '進', juan: '卷二', cite: '以信威將軍、江州刺史周迪為開府儀同三司,進號平南將軍', source: 'benji' },
{ year: 558, guan: '開府儀同三司', ren: '黃法𣰋', dong: '進', juan: '卷二', cite: '以仁威將軍、高州刺史黃法𣰋為開府儀同三司,進號鎮南將軍', source: 'benji' },
{ year: 558, guan: '開府儀同三司', ren: '熊曇朗', dong: '進', juan: '卷二', cite: '以寧遠將軍、北江州刺史熊曇朗為開府儀同三司,進號平西將軍', source: 'benji' },
{ year: 558, guan: '開府儀同三司', ren: '侯安都', dong: '除', juan: '卷二', cite: '前鎮北將軍、南徐州刺史、新除開府儀同三司侯安都等於王琳所逃歸', source: 'benji' },
{ year: 559, guan: '開府儀同三司', ren: '歐陽頠', dong: '進', juan: '卷二', cite: '丁酉,以鎮南將軍、廣州刺史歐陽頠即本號開府儀同三司', source: 'benji' },
{ year: 559, guan: '開府儀同三司', ren: '淳于量', dong: '進', juan: '卷二', cite: '二月辛酉,以平西將軍、桂州刺史淳于量為開府儀同三司,進號鎮西大將軍', source: 'benji' },
{ year: 559, guan: '都督', ren: '周文育', dong: '見殺', juan: '卷二', cite: '乙酉,北江州刺史熊曇朗殺都督周文育于軍,舉兵反', source: 'benji', note: '★ 见殺于乱军(熊曇朗所杀),非薨非誅。周文育故司空,陳开国大将。' },
{ year: 559, guan: '北江州刺史', ren: '熊曇朗', dong: '反', juan: '卷二', cite: '北江州刺史熊曇朗殺都督周文育于軍,舉兵反', source: 'benji' },
// ── 世祖 天嘉/天康(卷三,559–566)──
{ year: 559, guan: '太尉', ren: '侯瑱', dong: '除', juan: '卷三', cite: '辛酉,以侍中、車騎將軍、司空侯瑱為太尉', source: 'benji' },
{ year: 559, guan: '司空', ren: '侯安都', dong: '除', juan: '卷三', cite: '鎮西將軍、開府儀同三司、南豫州刺史侯安都為司空', source: 'benji' },
{ year: 559, guan: '侍中', ren: '徐度', dong: '除', juan: '卷三', cite: '鎮北將軍、南徐州刺史徐度為侍中、中撫軍將軍、開府儀同三司', source: 'benji' },
{ year: 559, guan: '特進、左光祿大夫', ren: '王沖', dong: '除', juan: '卷三', cite: '侍中、中權將軍、開府儀同三司王沖為特進、左光祿大夫', source: 'benji' },
{ year: 559, guan: '領軍將軍', ren: '杜稜', dong: '除', juan: '卷三', cite: '侍中、忠武將軍杜稜為領軍將軍', source: 'benji', note: '★ 杜稜:乙(ctext)全卷用异形,甲作「稜」。' },
{ year: 560, guan: '驃騎將軍、湘州牧', ren: '陳昌', dong: '除', juan: '卷三', cite: '庚戌,以高祖第六子昌為驃騎將軍、湘州牧,立為衡陽王', source: 'benji', note: '★ 高祖嫡子陳昌自北周还,旋薨(見 CHAO_SHI 560 溺死条)。' },
{ year: 560, guan: '中軍將軍', ren: '徐度', dong: '除', juan: '卷三', cite: '丁酉,以開府儀同三司徐度為侍中、中軍將軍', source: 'benji' },
{ year: 561, guan: '侍中、領軍將軍', ren: '杜稜', dong: '除', juan: '卷三', cite: '以雲麾將軍、晉陵太守杜稜為侍中、領軍將軍', source: 'benji' },
{ year: 561, guan: '合州刺史', ren: '裴景徽', dong: '奔', juan: '卷三', cite: '乙卯,合州刺史裴景徽奔于齊', source: 'benji' },
{ year: 561, guan: '車騎將軍、湘州刺史', ren: '侯瑱', dong: '除', juan: '卷三', cite: '二月丙戌,以太尉侯瑱為車騎將軍、湘州刺史', source: 'benji' },
{ year: 561, guan: '太尉', ren: '侯瑱', dong: '薨', juan: '卷三', cite: '三月乙卯,太尉、車騎將軍、湘州刺史侯瑱薨', source: 'benji' },
{ year: 561, guan: '丹陽尹', ren: '王沖', dong: '除', juan: '卷三', cite: '丙辰,以侍中、中權將軍、特進、左光祿大夫、開府儀同三司王沖為丹陽尹', source: 'benji' },
{ year: 561, guan: '左民尚書', ren: '沈君理', dong: '除', juan: '卷三', cite: '丹陽尹沈君理為左民尚書,領步兵校尉', source: 'benji' },
{ year: 561, guan: '中護軍', ren: '孫瑒', dong: '除', juan: '卷三', cite: '十二月辛巳,以安東將軍、吳郡太守孫瑒為中護軍', source: 'benji' },
{ year: 562, guan: '侍中、中書監', ren: '陳頊', dong: '除', juan: '卷三', cite: '三月丙子,安成王頊至自周,詔授侍中、中書監、中衛將軍,置佐史', source: 'benji', note: '★ 安成王頊(後之高宗宣帝)自北周质子还国,即授侍中、中書監 —— 其上位之始。' },
{ year: 562, guan: '揚州刺史', ren: '陳頊', dong: '除', juan: '卷三', cite: '六月丙辰,以侍中、中衛將軍安成王頊為驃騎將軍、揚州刺史', source: 'benji' },
{ year: 562, guan: '征北大將軍', ren: '侯安都', dong: '除', juan: '卷三', cite: '征北將軍、司空、南徐州刺史侯安都為侍中、征北大將軍', source: 'benji' },
{ year: 562, guan: '尚書右僕射、丹陽尹', ren: '到仲舉', dong: '除', juan: '卷三', cite: '以侍中、都官尚書到仲舉為尚書右僕射、丹陽尹', source: 'benji' },
{ year: 563, guan: '護軍將軍', ren: '章昭達', dong: '除', juan: '卷三', cite: '壬辰,以平西將軍、郢州刺史章昭達為護軍將軍', source: 'benji' },
{ year: 563, guan: '鎮北大將軍、南徐州刺史', ren: '黃法𣰋', dong: '除', juan: '卷三', cite: '鎮南將軍、開府儀同三司、高州刺史黃法𣰋為鎮北大將軍、南徐州刺史', source: 'benji' },
{ year: 563, guan: '征南大將軍、江州刺史', ren: '侯安都', dong: '除', juan: '卷三', cite: '庚戌,以侍中、司空、征北大將軍侯安都為征南大將軍、江州刺史', source: 'benji' },
{ year: 563, guan: '侍中、中軍大將軍', ren: '徐度', dong: '除', juan: '卷三', cite: '三月辛未,以鎮南將軍、開府儀同三司徐度為侍中、中軍大將軍', source: 'benji' },
{ year: 563, guan: '開府儀同三司', ren: '陳頊', dong: '除', juan: '卷三', cite: '乙卯,以侍中、中書監、中衛將軍、驃騎將軍、揚州刺史安成王頊為開府儀同三司', source: 'benji' },
{ year: 563, guan: '右光祿大夫', ren: '徐世譜', dong: '卒', juan: '卷三', cite: '五月丁卯,安前將軍、右光祿大夫徐世譜卒', source: 'benji' },
{ year: 563, guan: '司空', ren: '侯安都', dong: '賜死', juan: '卷三', cite: '司空侯安都賜死', source: 'benji', note: '★★ 賜死,非薨非誅 —— 世祖诛开国第一功臣。一个字里是「功高震主」的全部。(見 CHAO_SHI)' },
{ year: 563, guan: '開府儀同三司、廣州刺史', ren: '歐陽頠', dong: '薨', juan: '卷三', cite: '九月壬戌,開府儀同三司、廣州刺史歐陽頠薨', source: 'benji', note: '★ 其子歐陽紇 569 據廣州反(見下)。' },
{ year: 564, guan: '丹陽尹', ren: '袁樞', dong: '除', juan: '卷三', cite: '五年春正月庚辰,以吏部尚書、領右軍將軍袁樞為丹陽尹', source: 'benji' },
{ year: 564, guan: '中撫軍大將軍', ren: '淳于量', dong: '除', juan: '卷三', cite: '三月丁丑,以征南大將軍、開府儀同三司、桂州刺史淳于量為中撫軍大將軍', source: 'benji' },
{ year: 564, guan: '中護軍', ren: '程靈洗', dong: '除', juan: '卷三', cite: '冬十一月丁亥,以左衛將軍程靈洗為中護軍', source: 'benji' },
{ year: 564, guan: '開府儀同三司', ren: '章昭達', dong: '進', juan: '卷三', cite: '甲辰,以護軍將軍章昭達為鎮前將軍、開府儀同三司', source: 'benji' },
{ year: 565, guan: '丹陽尹', ren: '杜稜', dong: '除', juan: '卷三', cite: '庚戌,以領軍將軍杜稜為翊左將軍、丹陽尹', source: 'benji' },
{ year: 565, guan: '吏部尚書', ren: '袁樞', dong: '除', juan: '卷三', cite: '丹陽尹袁樞為吏部尚書', source: 'benji' },
{ year: 565, guan: '中領軍', ren: '沈欽', dong: '除', juan: '卷三', cite: '衛尉卿沈欽為中領軍', source: 'benji' },
{ year: 565, guan: '司空', ren: '陳頊', dong: '除', juan: '卷三', cite: '夏四月甲寅,以侍中、中書監、中衛將軍、驃騎將軍、開府儀同三司、揚州刺史安成王頊為司空', source: 'benji' },
{ year: 565, guan: '中衛大將軍', ren: '黃法𣰋', dong: '除', juan: '卷三', cite: '鎮南大將軍、江州刺史黃法𣰋為中衛大將軍', source: 'benji' },
{ year: 565, guan: '中護軍', ren: '沈恪', dong: '除', juan: '卷三', cite: '軍師將軍、郢州刺史沈恪為中護軍', source: 'benji' },
{ year: 565, guan: '中領軍', ren: '吳明徹', dong: '除', juan: '卷三', cite: '鎮東將軍、吳興太守吳明徹為中領軍', source: 'benji' },
{ year: 566, guan: '尚書令', ren: '陳頊', dong: '除', juan: '卷三', cite: '三月己卯,以驃騎將軍、開府儀同三司、揚州刺史、司空安成王頊為尚書令', source: 'benji' },
// ── 廢帝 光大(卷四,566–568)──
{ year: 566, guan: '司徒、錄尚書、都督中外諸軍事', ren: '陳頊', dong: '進', juan: '卷四', cite: '庚寅,以驃騎將軍、司空、揚州刺史、新除尚書令安成王頊為驃騎大將軍,進位司徒、錄尚書、都督中外諸軍事', source: 'benji', note: '★★ 頊集司徒+錄尚書+都督中外于一身 —— 廢帝朝的实际执政者,两年后即废帝自立。' },
{ year: 566, guan: '司空', ren: '徐度', dong: '進', juan: '卷四', cite: '丁酉,中軍大將軍、開府儀同三司徐度進位司空', source: 'benji' },
{ year: 566, guan: '侍中', ren: '章昭達', dong: '除', juan: '卷四', cite: '鎮南將軍、開府儀同三司、江州刺史章昭達為侍中,進號征南將軍', source: 'benji' },
{ year: 566, guan: '尚書左僕射', ren: '袁樞', dong: '除', juan: '卷四', cite: '吏部尚書袁樞為尚書左僕射', source: 'benji' },
{ year: 566, guan: '尚書右僕射', ren: '沈欽', dong: '除', juan: '卷四', cite: '雲麾將軍、吳興太守沈欽為尚書右僕射', source: 'benji' },
{ year: 566, guan: '領軍將軍', ren: '吳明徹', dong: '除', juan: '卷四', cite: '新除中領軍吳明徹為領軍將軍', source: 'benji' },
{ year: 566, guan: '護軍將軍', ren: '沈恪', dong: '除', juan: '卷四', cite: '新除中護軍沈恪為護軍將軍', source: 'benji' },
{ year: 566, guan: '吏部尚書', ren: '徐陵', dong: '除', juan: '卷四', cite: '散騎常侍、御史中丞徐陵為吏部尚書', source: 'benji', note: '★ 徐陵:陳一代文宗,此后历尚書僕射、中書監、領軍將軍,贯穿宣帝一朝。' },
{ year: 567, guan: '尚書左僕射', ren: '袁樞', dong: '卒', juan: '卷四', cite: '光大元年春正月癸酉,尚書左僕射袁樞卒', source: 'benji' },
{ year: 567, guan: '丹陽尹', ren: '吳明徹', dong: '除', juan: '卷四', cite: '己卯,以領軍將軍吳明徹為丹陽尹', source: 'benji' },
{ year: 567, guan: '南豫州刺史', ren: '余孝頃', dong: '誅', juan: '卷四', cite: '二月辛亥,宣毅將軍、南豫州刺史余孝頃謀反伏誅', source: 'benji' },
{ year: 567, guan: '尚書左僕射', ren: '沈欽', dong: '進', juan: '卷四', cite: '三月甲午,以尚書右僕射沈欽為侍中、尚書左僕射', source: 'benji' },
{ year: 567, guan: '領軍將軍', ren: '杜稜', dong: '除', juan: '卷四', cite: '乙未,以鎮右將軍杜稜為領軍將軍', source: 'benji' },
{ year: 567, guan: '湘州刺史', ren: '華皎', dong: '反', juan: '卷四', cite: '安南將軍、湘州刺史華皎謀反,景申,以中撫大將軍淳于量為使持節、征南大將軍,總率舟師以討之', source: 'benji', note: '★ 華皎之亂:引北周、後梁兵,淳于量/吳明徹討平(見 CHAO_SHI)。' },
{ year: 567, guan: '車騎將軍', ren: '徐度', dong: '進', juan: '卷四', cite: '六月壬寅,以中軍大將軍、司空徐度進號車騎將軍', source: 'benji' },
{ year: 567, guan: '侍中、開府儀同三司', ren: '王沖', dong: '薨', juan: '卷四', cite: '甲子,侍中、中權將軍、開府儀同三司、特進、左光祿大夫王沖薨', source: 'benji' },
{ year: 568, guan: '太傅、領司徒', ren: '陳頊', dong: '進', juan: '卷四', cite: '二年春正月己亥,侍中、都督中外諸軍事、驃騎大將軍、司徒、錄尚書、揚州刺史安成王頊進位太傅,領司徒,加殊禮,劍履上殿', source: 'benji', note: '★★ 頊為太傅、加殊禮、劍履上殿 —— 已是人臣之极,去帝位只一步。同年十一月即废帝。' },
{ year: 568, guan: '侍中、中軍大將軍、開府儀同三司', ren: '淳于量', dong: '除', juan: '卷四', cite: '中撫大將軍、新除征南大將軍淳于量為侍中、中軍大將軍、開府儀同三司', source: 'benji' },
{ year: 568, guan: '開府儀同三司', ren: '吳明徹', dong: '進', juan: '卷四', cite: '安南將軍、湘州刺史吳明徹即本號開府儀同三司,進號鎮南將軍', source: 'benji' },
{ year: 568, guan: '司空', ren: '徐度', dong: '薨', juan: '卷四', cite: '侍中、司空、車騎將軍徐度薨', source: 'benji' },
{ year: 568, guan: '護軍將軍', ren: '沈恪', dong: '除', juan: '卷四', cite: '十一月景午,以前平西將軍、荊州刺史沈恪為護軍將軍', source: 'benji' },
{ year: 568, guan: '鎮北將軍、南徐州刺史', ren: '淳于量', dong: '除', juan: '卷四', cite: '新除中軍大將軍、開府儀同三司淳于量為鎮北將軍、南徐州刺史', source: 'benji' },
// ── 高宗 宣帝 太建(卷五,569–582)──
{ year: 569, guan: '尚書左僕射', ren: '沈欽', dong: '除', juan: '卷五', cite: '尚書僕射沈欽為尚書左僕射', source: 'benji' },
{ year: 569, guan: '尚書右僕射', ren: '王勱', dong: '除', juan: '卷五', cite: '度支尚書王勱為尚書右僕射', source: 'benji' },
{ year: 569, guan: '鎮南將軍、廣州刺史', ren: '沈恪', dong: '除', juan: '卷五', cite: '護軍將軍沈恪為鎮南將軍、廣州刺史', source: 'benji' },
{ year: 569, guan: '車騎大將軍', ren: '章昭達', dong: '進', juan: '卷五', cite: '征南大將軍、開府儀同三司、新除中撫大將軍章昭達進號車騎大將軍', source: 'benji' },
{ year: 569, guan: '征北大將軍', ren: '淳于量', dong: '除', juan: '卷五', cite: '新除中軍大將軍、開府儀同三司、南徐州刺史淳于量為征北大將軍', source: 'benji' },
{ year: 569, guan: '尚書右僕射', ren: '徐陵', dong: '除', juan: '卷五', cite: '丁巳,以吏部尚書、領大著作徐陵為尚書右僕射', source: 'benji' },
{ year: 569, guan: '吏部尚書', ren: '沈君理', dong: '除', juan: '卷五', cite: '太子詹事、駙馬都尉沈君理為吏部尚書', source: 'benji' },
{ year: 569, guan: '中護軍', ren: '陳伯恭', dong: '除', juan: '卷五', cite: '丁酉,以平東將軍、吳郡太守晉安王伯恭為中護軍,進號安南將軍', source: 'benji' },
{ year: 569, guan: '中領軍', ren: '陳伯恭', dong: '除', juan: '卷五', cite: '九月甲辰,以新除中護軍晉安王伯恭為中領軍', source: 'benji' },
{ year: 569, guan: '廣州', ren: '歐陽紇', dong: '反', juan: '卷五', cite: '冬十月,新除左衛將軍歐陽紇據廣州舉兵反', source: 'benji', note: '★ 歐陽頠之子,据父旧镇反,章昭達討平(見 CHAO_SHI)。' },
{ year: 570, guan: '車騎大將軍', ren: '章昭達', dong: '進', juan: '卷五', cite: '戊申,車騎將軍、開府儀同三司章昭達進號車騎大將軍', source: 'benji' },
{ year: 570, guan: '特進、護軍將軍', ren: '杜稜', dong: '除', juan: '卷五', cite: '九月乙丑,以散騎常侍、鎮東將軍、吳興太守杜稜為特進、護軍將軍', source: 'benji' },
{ year: 571, guan: '尚書僕射', ren: '徐陵', dong: '進', juan: '卷五', cite: '三年春正月癸丑,以尚書右僕射、領大著作徐陵為尚書僕射', source: 'benji' },
{ year: 571, guan: '司空', ren: '章昭達', dong: '薨', juan: '卷五', cite: '十二月壬辰,車騎大將軍、司空章昭達薨', source: 'benji', note: '★ 章昭達为宣帝一朝首将(平華皎、平歐陽紇),薨于北伐前夕。' },
{ year: 572, guan: '尚書左僕射', ren: '徐陵', dong: '進', juan: '卷五', cite: '尚書僕射、領大著作徐陵為尚書左僕射', source: 'benji' },
{ year: 572, guan: '尚書右僕射', ren: '王勱', dong: '除', juan: '卷五', cite: '中書監王勱為尚書右僕射', source: 'benji' },
{ year: 572, guan: '征南大將軍、南豫州刺史', ren: '黃法𣰋', dong: '除', juan: '卷五', cite: '夏四月戊子,以中權大將軍、開府儀同三司黃法𣰋為征南大將軍、南豫州刺史', source: 'benji' },
{ year: 572, guan: '尚書右僕射', ren: '王勱', dong: '卒', juan: '卷五', cite: '五月癸卯,尚書右僕射王勱卒', source: 'benji' },
{ year: 572, guan: '侍中、右光祿大夫', ren: '杜稜', dong: '卒', juan: '卷五', cite: '六月辛巳,侍中、鎮右將軍、右光祿大夫杜稜卒', source: 'benji' },
{ year: 572, guan: '領軍將軍', ren: '沈恪', dong: '除', juan: '卷五', cite: '戊戌,以鎮南將軍、廣州刺史沈恪為領軍將軍', source: 'benji' },
{ year: 573, guan: '中權大將軍', ren: '淳于量', dong: '除', juan: '卷五', cite: '五年春正月癸酉,以征北大將軍、開府儀同三司、南徐州刺史淳于量為中權大將軍', source: 'benji' },
{ year: 573, guan: '尚書右僕射、領吏部', ren: '沈君理', dong: '除', juan: '卷五', cite: '吏部尚書、駙馬都尉沈君理為尚書右僕射,領吏部', source: 'benji' },
{ year: 573, guan: '都督征討諸軍事', ren: '吳明徹', dong: '除', juan: '卷五', cite: '三月壬午,分命眾軍北伐,以鎮前將軍、開府儀同三司吳明徹都督征討諸軍事', source: 'benji', note: '★★ 太建北伐主帅。573 略淮南,578 呂梁全军覆没被俘(見 CHAO_SHI)。' },
{ year: 573, guan: '征北大將軍', ren: '吳明徹', dong: '進', juan: '卷五', cite: '秋七月乙丑,鎮前將軍、開府儀同三司吳明徹進號征北大將軍', source: 'benji' },
{ year: 573, guan: '尚書右僕射、領吏部', ren: '沈君理', dong: '卒', juan: '卷五', cite: '癸未,尚書右僕射、領吏部、駙馬都尉沈君理卒', source: 'benji' },
{ year: 573, guan: '吏部尚書', ren: '王瑒', dong: '除', juan: '卷五', cite: '戊戌,以中書令王瑒為吏部尚書', source: 'benji' },
{ year: 573, guan: '尚書右僕射', ren: '周弘正', dong: '除', juan: '卷五', cite: '己亥,以特進、領國子祭酒周弘正為尚書右僕射', source: 'benji' },
{ year: 573, guan: '車騎大將軍、豫州刺史', ren: '吳明徹', dong: '進', juan: '卷五', cite: '以征北大將軍、開府儀同三司吳明徹為豫州刺史,進號車騎大將軍', source: 'benji' },
{ year: 574, guan: '中領軍', ren: '陳伯固', dong: '除', juan: '卷五', cite: '以翊前將軍新安王伯固為中領軍,進號安前將軍', source: 'benji' },
{ year: 574, guan: '征西大將軍、郢州刺史', ren: '淳于量', dong: '除', juan: '卷五', cite: '景辰,以中權大將軍、開府儀同三司淳于量為征西大將軍、郢州刺史', source: 'benji' },
{ year: 574, guan: '尚書右僕射', ren: '周弘正', dong: '卒', juan: '卷五', cite: '六月壬辰,尚書右僕射、領國子祭酒周弘正卒', source: 'benji' },
{ year: 574, guan: '尚書右僕射', ren: '王瑒', dong: '除', juan: '卷五', cite: '戊戌,以吏部尚書王瑒為尚書右僕射', source: 'benji' },
{ year: 574, guan: '吏部尚書', ren: '孔奐', dong: '除', juan: '卷五', cite: '度支尚書孔奐為吏部尚書', source: 'benji' },
{ year: 575, guan: '尚書僕射', ren: '王瑒', dong: '進', juan: '卷五', cite: '壬辰,以尚書右僕射王瑒為尚書僕射', source: 'benji' },
{ year: 575, guan: '中領軍', ren: '陳叔堅', dong: '除', juan: '卷五', cite: '信威將軍、江州刺史長沙王叔堅為雲麾將軍、中領軍', source: 'benji' },
{ year: 575, guan: '中軍大將軍', ren: '淳于量', dong: '進', juan: '卷五', cite: '十一月庚戌,以征西大將軍、開府儀同三司、郢州刺史淳于量為中軍大將軍', source: 'benji' },
{ year: 575, guan: '尚書左僕射', ren: '王瑒', dong: '進', juan: '卷五', cite: '壬戌,以尚書僕射王瑒為尚書左僕射', source: 'benji' },
{ year: 575, guan: '尚書右僕射', ren: '陸繕', dong: '除', juan: '卷五', cite: '太子詹事、揚州大中正陸繕為尚書右僕射', source: 'benji' },
{ year: 575, guan: '領軍將軍', ren: '徐陵', dong: '除', juan: '卷五', cite: '國子祭酒徐陵為領軍將軍', source: 'benji' },
{ year: 576, guan: '司空', ren: '吳明徹', dong: '進', juan: '卷五', cite: '二月壬申,車騎大將軍、開府儀同三司吳明徹進位司空', source: 'benji' },
{ year: 576, guan: '尚書左僕射', ren: '王瑒', dong: '卒', juan: '卷五', cite: '〔五月〕庚寅,尚書左僕射王瑒卒', source: 'benji' },
{ year: 576, guan: '尚書左僕射', ren: '陸繕', dong: '進', juan: '卷五', cite: '甲寅,以尚書右僕射陸繕為尚書左僕射', source: 'benji' },
{ year: 576, guan: '尚書右僕射', ren: '王克', dong: '除', juan: '卷五', cite: '新除晉陵太守王克為尚書右僕射', source: 'benji' },
{ year: 576, guan: '中領軍', ren: '陳伯仁', dong: '除', juan: '卷五', cite: '辛丑,以冠軍將軍廬陵王伯仁為中領軍', source: 'benji' },
{ year: 577, guan: '揚州刺史', ren: '陳叔陵', dong: '除', juan: '卷五', cite: '壬寅,以湘州刺史、新除中衛將軍始興王叔陵為揚州刺史', source: 'benji', note: '★★ 叔陵得揚州(京畿),坐大五年,582 高宗崩即于灵前作逆(見 CHAO_SHI)。' },
{ year: 578, guan: '領軍將軍', ren: '徐陵', dong: '除', juan: '卷五', cite: '翊左將軍、右光祿大夫、領太子詹事徐陵為領軍將軍', source: 'benji' },
{ year: 578, guan: '司空', ren: '吳明徹', dong: '執', juan: '卷五', cite: '二月甲子,北討眾軍敗績於呂梁,司空吳明徹及將卒已下,並為周軍所獲', source: 'benji', note: '★★★ 兵敗被俘,非薨非降 —— 陳军主力尽丧于呂梁,司空以下全军被北周所获。国势自此顿衰(見 CHAO_SHI)。' },
{ year: 578, guan: '大都督', ren: '淳于量', dong: '除', juan: '卷五', cite: '景子,分命眾軍以備周:中軍大將軍、開府儀同三司淳于量為大都督,總水陸諸軍事', source: 'benji' },
{ year: 578, guan: '護軍將軍', ren: '陳伯固', dong: '除', juan: '卷五', cite: '丁巳,以新除鎮右將軍新安王伯固為護軍將軍', source: 'benji' },
{ year: 578, guan: '吏部尚書', ren: '袁憲', dong: '除', juan: '卷五', cite: '乙巳,以散騎常侍、兼吏部尚書袁憲為吏部尚書', source: 'benji', note: '★ 袁憲:亡國一刻唯一守殿之臣(見 589 条)。' },
{ year: 578, guan: '中領軍', ren: '樊毅', dong: '除', juan: '卷五', cite: '九月壬寅,以平北將軍樊毅為中領軍', source: 'benji' },
{ year: 578, guan: '尚書僕射', ren: '陸繕', dong: '進', juan: '卷五', cite: '戊子,以尚書左僕射陸繕為尚書僕射', source: 'benji' },
{ year: 579, guan: '中權將軍、護軍將軍', ren: '陳伯山', dong: '除', juan: '卷五', cite: '景戌,以征南將軍、江州刺史鄱陽王伯山為中權將軍、護軍將軍', source: 'benji' },
{ year: 579, guan: '尚書左僕射', ren: '陸繕', dong: '除', juan: '卷五', cite: '尚書僕射陸繕為尚書左僕射', source: 'benji' },
{ year: 580, guan: '平南將軍、南豫州刺史', ren: '任忠', dong: '除', juan: '卷五', cite: '十二年春正月戊戌,以散騎常侍、左衛將軍任忠為平南將軍、南豫州刺史,督緣江軍防事', source: 'benji', note: '★ 任忠:589 隋军入建康时出降韓擒虎(見 589 条)。' },
{ year: 580, guan: '中領軍', ren: '陳伯仁', dong: '除', juan: '卷五', cite: '三月壬辰,以平北將軍廬陵王伯仁為翊左將軍、中領軍', source: 'benji' },
{ year: 580, guan: '尚書左僕射', ren: '陸繕', dong: '卒', juan: '卷五', cite: '夏四月癸亥,尚書左僕射陸繕卒', source: 'benji' },
{ year: 580, guan: '尚書僕射', ren: '陳伯恭', dong: '除', juan: '卷五', cite: '五月癸巳,以軍師將軍、尚書右僕射晉安王伯恭為尚書僕射', source: 'benji' },
{ year: 580, guan: '侍中、大都督、車騎將軍、司空', ren: '司馬消難', dong: '除', juan: '卷五', cite: '詔以消難為使持節、侍中、大都督、總督安隨等九州八鎮諸軍事、車騎將軍、司空,封隨郡公', source: 'benji', note: '★ 北周司馬消難(尉遲迥之亂败)举九州来奔,陳授司空。580 之陳已守势,仍收纳北叛(見 CHAO_SHI)。' },
{ year: 580, guan: '南徐州刺史', ren: '陳叔獻', dong: '薨', juan: '卷五', cite: '十二月庚辰,宣毅將軍、南徐州刺史河東王叔獻薨', source: 'benji' },
{ year: 581, guan: '左光祿大夫', ren: '淳于量', dong: '除', juan: '卷五', cite: '十三年春正月壬午,以車騎將軍、開府儀同三司淳于量為左光祿大夫', source: 'benji' },
{ year: 581, guan: '尚書左僕射', ren: '陳伯恭', dong: '進', juan: '卷五', cite: '軍師將軍、尚書僕射晉安王伯恭為尚書左僕射', source: 'benji' },
{ year: 581, guan: '中書監、領太子詹事', ren: '徐陵', dong: '除', juan: '卷五', cite: '〔安〕右將軍、丹陽尹徐陵為中書監,領太子詹事', source: 'benji' },
{ year: 581, guan: '尚書右僕射', ren: '袁憲', dong: '除', juan: '卷五', cite: '吏部尚書袁憲為尚書右僕射', source: 'benji' },
{ year: 581, guan: '護軍將軍', ren: '樊毅', dong: '除', juan: '卷五', cite: '六月辛卯,以新除中護軍樊毅為護軍將軍', source: 'benji' },
{ year: 581, guan: '吏部尚書', ren: '毛喜', dong: '除', juan: '卷五', cite: '冬十月癸未,以散騎常侍、丹陽尹毛喜為吏部尚書', source: 'benji' },
{ year: 581, guan: '護軍將軍', ren: '沈恪', dong: '除', juan: '卷五', cite: '己亥,以翊右將軍、衛尉卿沈恪為護軍將軍', source: 'benji' },
// ── 後主 至德/禎明(卷六,582–589)──
{ year: 582, guan: '揚州刺史', ren: '陳叔陵', dong: '誅', juan: '卷六', cite: '乙卯,始興王叔陵作逆,伏誅', source: 'benji', note: '★★ 高宗崩之次日,揚州刺史始興王叔陵于灵前砍伤後主、作逆,蕭摩訶斩之。伏誅,非薨(見 CHAO_SHI)。' },
{ year: 582, guan: '驃騎將軍、開府儀同三司、揚州刺史', ren: '陳叔堅', dong: '除', juan: '卷六', cite: '癸亥,以侍中、翊前將軍、丹陽尹長沙王叔堅為驃騎將軍、開府儀同三司、揚州刺史', source: 'benji', note: '★ 长沙王叔堅平叔陵有功,得揚州;然次年即「有罪免」(見 583 条),一年荣枯。' },
{ year: 582, guan: '車騎將軍、南徐州刺史', ren: '蕭摩訶', dong: '除', juan: '卷六', cite: '右衛將軍蕭摩訶為車騎將軍、南徐州刺史', source: 'benji', note: '★ 斩叔陵者。589 隋军入城,蕭摩訶为都督(見 589 条)。' },
{ year: 582, guan: '中護軍', ren: '章大寶', dong: '除', juan: '卷六', cite: '仁武將軍、豐州刺史章大寶為中護軍', source: 'benji', note: '★ 585 举兵反(見 585 条)。' },
{ year: 582, guan: '左光祿大夫、領太子少傅', ren: '徐陵', dong: '除', juan: '卷六', cite: '以侍中、中書監、安右將軍徐陵為左光祿大夫,領太子少傅', source: 'benji' },
{ year: 582, guan: '尚書僕射', ren: '陳伯智', dong: '除', juan: '卷六', cite: '新除翊左將軍、永陽王伯智為尚書僕射', source: 'benji' },
{ year: 582, guan: '中護軍', ren: '孫瑒', dong: '除', juan: '卷六', cite: '六月癸酉朔,以明威將軍、通直散騎常侍孫瑒為中護軍', source: 'benji' },
{ year: 582, guan: '司空', ren: '陳叔堅', dong: '除', juan: '卷六', cite: '景寅,以驃騎將軍、開府儀同三司、揚州刺史長沙王叔堅為司空', source: 'benji' },
{ year: 583, guan: '中衛大將軍', ren: '陳叔英', dong: '除', juan: '卷六', cite: '以征南將軍、江州刺史、新除開府儀同三司豫章王叔英為中衛大將軍', source: 'benji' },
{ year: 583, guan: '江州刺史', ren: '陳叔堅', dong: '除', juan: '卷六', cite: '驃騎將軍、開府儀同三司、揚州刺史長沙王叔堅為江州刺史', source: 'benji', note: '★ 由揚州(京畿)外放江州,实为夺权前奏。' },
{ year: 583, guan: '領軍將軍', ren: '任忠', dong: '除', juan: '卷六', cite: '鎮南將軍、南豫州刺史任忠為領軍將軍', source: 'benji' },
{ year: 583, guan: '吏部尚書', ren: '江總', dong: '除', juan: '卷六', cite: '祠部尚書江總為吏部尚書', source: 'benji', note: '★ 江總:後主朝「狎客」之首,官至尚書令(見 586 条),亡國时居省中(見 589 条)。' },
{ year: 583, guan: '左光祿大夫、太子少傅', ren: '徐陵', dong: '卒', juan: '卷六', cite: '戊戌,侍中、安右將軍、左光祿大夫、太子少傅徐陵卒', source: 'benji', note: '★ 陳一代文宗、历仕四朝的徐陵卒,年七十七。' },
{ year: 583, guan: '司空', ren: '陳叔堅', dong: '免', juan: '卷六', cite: '司空長沙王叔堅有罪免', source: 'benji', note: '★★ 有罪免,非薨非誅 —— 一年前刚以平叔陵之功得揚州司空,此刻褫夺。免这一个字,是宗室内斗的翻覆。' },
{ year: 584, guan: '尚書僕射', ren: '江總', dong: '除', juan: '卷六', cite: '吏部尚書江總為尚書僕射', source: 'benji' },
{ year: 584, guan: '侍中、鎮左將軍', ren: '陳叔堅', dong: '除', juan: '卷六', cite: '秋七月戊辰,以長沙王叔堅為侍中、鎮左將軍', source: 'benji', note: '★ 免后逾年复起,然已远权力中枢。' },
{ year: 585, guan: '護軍將軍', ren: '樊毅', dong: '除', juan: '卷六', cite: '征西將軍、荊州刺史樊毅為護軍將軍', source: 'benji' },
{ year: 585, guan: '吏部尚書', ren: '陸瓊', dong: '除', juan: '卷六', cite: '守吏部尚書、領著作陸瓊為吏部尚書', source: 'benji' },
{ year: 585, guan: '豐州刺史', ren: '章大寶', dong: '反', juan: '卷六', cite: '三月辛酉,前豐州刺史章大寶舉兵反', source: 'benji' },
{ year: 585, guan: '吏部尚書', ren: '謝伷', dong: '除', juan: '卷六', cite: '己酉,以左民尚書謝(伸)〔伷〕為吏部尚書', source: 'benji', note: '★ 甲原作「伸」,中華書局〔〕校补作「伷」(校勘符,非正文)。' },
{ year: 586, guan: '尚書令', ren: '江總', dong: '除', juan: '卷六', cite: '冬十月癸亥,尚書僕射江總為尚書令', source: 'benji' },
{ year: 586, guan: '尚書僕射', ren: '謝伷', dong: '除', juan: '卷六', cite: '吏部尚書謝伷為尚書僕射', source: 'benji' },
{ year: 587, guan: '中書監', ren: '陳叔卿', dong: '除', juan: '卷六', cite: '二月丁未,……中書令建安王叔卿為中書監', source: 'benji' },
{ year: 587, guan: '驃騎將軍', ren: '蕭摩訶', dong: '進', juan: '卷六', cite: '丁未,以車騎將軍蕭摩訶為驃騎將軍', source: 'benji' },
{ year: 587, guan: '司徒(兼)', ren: '陳叔英', dong: '除', juan: '卷六', cite: '丁亥,以驃騎大將軍、開府儀同三司豫章王叔英兼司徒', source: 'benji' },
{ year: 587, guan: '平東將軍、開府儀同三司、東揚州刺史', ren: '蕭巖', dong: '除', juan: '卷六', cite: '景子,以蕭巖為平東將軍、開府儀同三司、東揚州刺史', source: 'benji', note: '★ 後梁安平王蕭巖率眾降陳,陳授开府(見 CHAO_SHI 587)。' },
{ year: 588, guan: '吏部尚書', ren: '蔡徵', dong: '除', juan: '卷六', cite: '戊午,以左民尚書蔡徵為吏部尚書', source: 'benji' },
{ year: 588, guan: '中權將軍', ren: '江總', dong: '進', juan: '卷六', cite: '宣惠將軍、尚書令江總進號中權將軍', source: 'benji' },
{ year: 588, guan: '尚書僕射', ren: '袁憲', dong: '除', juan: '卷六', cite: '雲麾將軍、太子詹事袁憲為尚書僕射', source: 'benji' },
{ year: 588, guan: '中領軍', ren: '魯廣達', dong: '除', juan: '卷六', cite: '甲辰,以安右將軍魯廣達為中領軍', source: 'benji', note: '★ 589 隋军渡江,魯廣達力战于白土岡,陳军唯一死战者。' },
{ year: 588, guan: '吏部尚書', ren: '姚察', dong: '除', juan: '卷六', cite: '辛丑,以度支尚書、領大著作姚察為吏部尚書', source: 'benji', note: '★★★ 姚察本人 —— 撰《陳書》者,此刻正做陳的吏部尚書。写这部书的史官,亲历了它的最后一年。这是「分身不是传主、分身是史官」在史料里的一次对撞。' },
{ year: 589, guan: '鎮東大將軍', ren: '任忠', dong: '降', juan: '卷六', cite: '己卯,鎮東大將軍任忠自吳興入赴……任忠出降於擒虎,仍引擒虎經朱雀航趣宮城', source: 'benji', note: '★★ 出降,非奔非死 —— 陳最后的军事支柱任忠,反手引隋将韓擒虎入宫城。降这一个字,是亡国之夜的转折。' },
{ year: 589, guan: '尚書僕射', ren: '袁憲', dong: '見', juan: '卷六', cite: '於是城內文武百司皆遁出,唯尚書僕射袁憲在殿內', source: 'benji', note: '★★ 亡國一刻,百官皆遁,唯袁憲一人守殿侍後主。见任而不去 —— 这一条是「位阶」与「气节」的分野。' },
{ year: 589, guan: '尚書令', ren: '江總', dong: '見', juan: '卷六', cite: '尚書令江總、吏部尚書姚察、度支尚書袁權、前度支尚書王瑗、侍中王寬居省中', source: 'benji', note: '★ 亡國之刻的省中名单(見任)。' },
{ year: 589, guan: '吏部尚書', ren: '姚察', dong: '見', juan: '卷六', cite: '尚書令江總、吏部尚書姚察、度支尚書袁權、前度支尚書王瑗、侍中王寬居省中', source: 'benji', note: '★★ 撰史者姚察,亡國之刻在省中 —— 他日后要写的最后一页,是他自己站在里面的那一页。' },
{ year: 589, guan: '侍中', ren: '王寬', dong: '見', juan: '卷六', cite: '尚書令江總、吏部尚書姚察、度支尚書袁權、前度支尚書王瑗、侍中王寬居省中', source: 'benji' },
];
// ═══ 二、大事件(CHAO_SHI)═══
// 每条:{ year, yue, shi(原文/纪事), juan, layer:"shi"|"zhi"|"dao", kind:"yuanwen"|"tuiyan", zaichang?, note? }
// ★ 制/道 事件另叠加:layerWhy(强制) / impact(下游影响链) / witnesses+crossWitness(补事件的双证)。
// ★ layer 判定见文件头:陳绝大多数是「事」;「道」0(唯一够得着道的 589「南北一統」属【隋】不属陳,见该条 note)。
// ★★ 「制」上一刀判 0(严判:要求制存续 50 年方算制);本刀【據建模 §1.3/§3.3 補为 2】——
// 建模層典按【类型】计律/铸钱/官制之确立即为「制」(存续长短只分制/道,不分事/制)。本刀补入两条制度条:
// 557「立刪定郎,治定律令」(律) 与 579「初用大貨六銖錢」(铸钱),均补为 layer:"zhi"、三证原文、挂【诚实 impact】
// (★ 非谓本紀制度条仅此二条:卷五太建十一年甲寅诏「改不枉法受財者,科同正盜」等未逐条补尽)
// (二者影响半径皆止当朝 reach:"dangchao",陳制无一存续隋唐 —— 不编「陳制→隋唐」的因果)。
// ★ 557 前为【梁末】陳霸先受禪之路,纪年用梁年號,layer 仍 shi(一场仗/一次禪代)。
export const CHAO_SHI = [
// ── 开国之路(卷一,梁末,557 前)──
{ year: 548, yue: '冬', shi: '侯景寇京師(侯景之亂起)。高祖時在嶺南,將率兵赴援,廣州刺史元景仲陰有異志。', juan: '卷一', layer: 'shi', kind: 'yuanwen', zaichang: '方鎮刺史要么勤王要么观望(元景仲即怀异志)、郡守要守土、府中参军要随军北上 —— 一场倾覆梁室的大乱,落到岭南一个中直兵参军身上,是「去不去救、跟谁走」。', note: '★ 侯景之亂是陳霸先一生的枢机:正因梁室在此乱中糜烂,他才由岭南偏将十年而至受禪。层为「事」(一场叛乱),但它是本朝一切的起点。' },
{ year: 550, yue: '正月', shi: '大寶元年正月,高祖發自始興,次大庾嶺,大破蔡路養,進頓南康。北上勤王之始。', juan: '卷一', layer: 'shi', kind: 'yuanwen' },
{ year: 552, yue: '三月', shi: '高祖率甲士三萬會王僧辯,剋姑熟、克石頭城,侯景眾大潰、遁走(旋為部下所殺)。平侯景。', juan: '卷一', layer: 'shi', kind: 'yuanwen', zaichang: '都督/刺史要出兵会师、长史参军要筹三十万石军粮(高祖先贮五十万石分济西军)、士卒要攻城。同一场决战,主帅争的是首功、斗食小吏管的是一船军米。', note: '★ 平侯景使陳霸先与王僧辯并列为梁室两大柱石,王跡所基始自於此。层「事」——一场平乱之战,虽再造江南,仍不构成延续五十年的制度。' },
{ year: 554, yue: '十一月', shi: '承聖三年十一月,西魏攻陷江陵,梁元帝死。梁室中枢一夕倾覆。', juan: '卷一', layer: 'shi', kind: 'yuanwen', note: '★ 江陵之陷造成权力真空,直接导致次年王僧辯纳北齐所立之贞阳侯、与陈霸先决裂。' },
{ year: 555, yue: '九月', shi: '承聖四年九月,高祖夜發南徐州襲殺王僧辯(是夜縊僧辯及其子頠),廢貞陽侯,立晉安王(梁敬帝),改元紹泰。', juan: '卷一', layer: 'shi', kind: 'yuanwen', zaichang: '同为方鎮的大将要么随之要么被诛(王僧辯即被杀)、百僚要奉表劝进新主、侍中尚书要连夜草诏改立。这一夜,「站在谁一边」就是生死。', note: '★★ 誅王僧辯是陳霸先夺权的决定性一刀:从此江南再无与他并肩者。层「事」——一次政变+废立。' },
{ year: 556, yue: '六月', shi: '紹泰二年六月,北齊發兵十萬(蕭軌、東方老等)攻建康,高祖大破之,虜蕭軌以下四十六將,皆伏誅。', juan: '卷一', layer: 'shi', kind: 'yuanwen', zaichang: '诸将要分头拒战、水军要断敌粮运、都邑百姓夹淮观战呼声震天。一场保卫都城的会战,位居都督者定生死于阵前,编户小民只能在岸上看。', note: '★ 此役廓清北齐对建康的直接威胁,为次年受禪扫平障碍。层「事」——一场歼灭战,虽死伤以万计。' },
{ year: 557, yue: '十月', shi: '永定元年冬十月乙亥,梁敬帝禪位,高祖即皇帝位于南郊,國號陳,改梁太平二年為永定元年。陳朝立。', juan: '卷二', layer: 'shi', kind: 'yuanwen', zaichang: '侍中/尚书要草禅位诏与劝进表、公卿百官要列班受禅、外州刺史要奉新朔、县令要安集改元、编户百姓要「洗除先注,与之更始」。开国这件大事,宰辅在坛上署名,斗食小吏在乡里改籍。', note: '★ 受禪建陳是一次禪代(改朝换代),依老板判据属「事」。这是本朝的第一天,也是「我在陳的位置」这一切题目的起点。' },
// ── 高祖 永定(557–559)──
// ── ★★ 制度条 1/2(本刀據建模 §3.3 補;紅線:三證原文+誠實 impact,不編「陳制→隋唐」因果)──
{ year: 557, yue: '冬', shi: '永定元年,高祖受禪之初,立刪定郎,治定律令(草創陳之律典)。', juan: '卷二', layer: 'zhi', kind: 'yuanwen',
layerWhy: '「立刪定郎,治定律令」是【律典之確立】——按建模層典,律/鑄錢/官制之確立即計為「制」(不論存續長短,長短只分制/道)。這是陳一代成文法的起點,故 layer=zhi 而非 shi。',
witnesses: 3,
crossWitness: '★ 三證:甲(wikisource 中華書局系)=乙(kanripo 四庫本 WYG) 剝標點後逐字皆作「立刪定郎,治定律令」(僅祔/袝、卯/夘等異形);丙《南史》卷九作「立刪定郎,刊定律令」——「治定/刊定」一字異,然【立刪定郎以定律令】此事三證同。本 shi 取甲字「治定」。',
zaichang: '一部新律的訂立,落在不同位階是不同的題:新設的【刪定郎】要逐條刊定律文、廷尉/尚書要據新律斷獄、州郡長官要照新律理民、編戶百姓從此按這部律受刑受贖。開國定律,宰輔在朝堂議律,斗食獄吏在縣寺用律。',
impact: {
reach: 'dangchao', kind: 'tuiyan',
reason: '陳律承東晉南朝律學一脈(晉《泰始律》→梁律→陳律),為陳一代之現行法典;然隨 589 陳亡即廢,隋一統後行《開皇律》。就【陳律本身】而言未存續至隋唐。⚠「南朝律學是否為隋唐律淵源之一」學界有爭(陳寅恪《隋唐制度淵源略論稿》刑律章所論為【梁律/南朝律學經北齊律入隋唐】,未及【陳律】;程樹德《九朝律考》主隋律本北齊《河清律》)。★ 此二說係學界轉述,不在本表 curl 雙證原文的範圍內,僅備異說、不充作據——本表【不裁此爭】,只記雙方都認可的事實(陳律作為現行法典未直接下沿),不編「陳律→隋唐律」的因果。',
to: [{ label: '589 陳亡,陳律廢;隋唐律典主承北齊《河清律》一系(制度史通說)' }],
reachWhy: '陳律行用僅 32 年(557–589),夠不到「五十年後還在起作用」,故影響半徑=當朝。',
},
note: '★ 陳本紀所載制度條之一(本刀補入的兩條之一,另一為 579 大貨六銖;★ 非謂本紀僅此二條——卷五太建十一年甲寅詔「今可改不枉法受財者,科同正盜」即另一條律條修訂,本刀未逐條補盡)。也是這一朝給產品的注腳:在陳定律的人,其律隨國亡而廢——制度的「後世影響」一欄,誠實地寫著【止於當朝】。' },
{ year: 558, yue: '', shi: '永定二年,遣司空侯瑱、領軍將軍徐度等西討王琳(梁室残余、北齐所立蕭莊政权)。', juan: '卷二', layer: 'shi', kind: 'yuanwen', note: '★ 王琳据郢州奉萧庄、结北齐,是陈初最大外患,至 560 梁山之战方定。' },
{ year: 559, yue: '六月', shi: '永定三年六月景午,高祖崩于璿璣殿,時年五十七。遺詔徵臨川王蒨(世祖)入纂。', juan: '卷二', layer: 'shi', kind: 'yuanwen', zaichang: '宗室与顾命大臣要定策迎立、尚书省要发哀改元、方鎮要按兵待命。开国之君骤崩、嗣君远在外镇,这一刻「你在不在中枢、听谁调度」是全局的关节。' },
// ── 世祖 天嘉/天康(559–566)──
{ year: 560, yue: '二月', shi: '天嘉元年二月,太尉侯瑱大破王琳于梁山,又敗齊兵于博望,王琳及其所奉蕭莊奔于齊。', juan: '卷三', layer: 'shi', kind: 'yuanwen', note: '★★ 梁山之战一举扫平王琳与北齐扶植的萧庄政权,陈朝始得立足。层「事」——一场决定国运的水战。' },
{ year: 560, yue: '三月', shi: '天嘉元年三月,衡陽王昌(高祖嫡子,自北周還)渡江,溺死。', juan: '卷三', layer: 'shi', kind: 'yuanwen', zaichang: '陈昌是高祖亲子、法理上的第一顺位;他一还国,已即位的世祖(高祖之侄)便地位尴尬。护送、迎接、乃至「船破溺死」的经手者,都卷进一桩帝位疑案。', note: '★ 本紀只书「薨」于渡江,未言其详;然嫡子甫归即溺,世祖侄继叔位,史家多疑。本表不裁,只记其为「事」(一桩宗室之死牵动帝位)。' },
{ year: 562, yue: '三月', shi: '天嘉三年,安成王頊(後之高宗宣帝)自北周質還,授侍中、中書監。', juan: '卷三', layer: 'shi', kind: 'yuanwen', note: '★ 頊本为质于北周(江陵陷时被掳),此还国为其日后入纂、代立埋线。' },
{ year: 563, yue: '六月', shi: '天嘉四年六月,司空侯安都賜死。', juan: '卷三', layer: 'shi', kind: 'yuanwen', zaichang: '侯安都是助世祖登位的第一功臣、拥兵方鎮之首。他之死,令所有握兵的都督刺史自危、令侍中尚书要斟酌诏辞、令廷尉要行刑。「功高」在这一朝可以是催命符 —— 位阶越高,这道题越致命。', note: '★★ 賜死一词是这条的全部:不是薨、不是伏诛,是天子赐死功臣。层「事」——一桩诛戮,但它定义了陈朝君臣关系的底色。' },
{ year: 564, yue: '', shi: '天嘉中,章昭達等次第平定周迪、留異、陳寶應諸方鎮之亂(贛、閩),江南底定。', juan: '卷三', layer: 'shi', kind: 'yuanwen', note: '★ 陈初「三叛」(王琳、华皎为外,周迪/留异/陈宝应为内)至此平尽,世祖一朝主要功业。' },
{ year: 566, yue: '四月', shi: '天康元年四月癸酉,世祖崩于有覺殿,太子伯宗(廢帝)即位。', juan: '卷三', layer: 'shi', kind: 'yuanwen' },
// ── 廢帝 光大(566–568)──
{ year: 567, yue: '', shi: '光大元年,湘州刺史華皎謀反,引北周及後梁之兵順流東下,淳于量、吳明徹率舟師討平之。', juan: '卷四', layer: 'shi', kind: 'yuanwen', zaichang: '一个方鎮举兵、勾连两个外朝(北周+后梁),朝廷要遣征南大将军总舟师、要沿江州郡出兵出粮。位居水军都督者担成败,缘江郡守担粮秣。', note: '★ 华皎之乱是废帝短祚间最大一战,也是安成王頊(时执朝政)借以立威、进而代立的背景。' },
{ year: 568, yue: '十一月', shi: '光大二年十一月甲寅,慈訓太后令廢帝為臨海王,以安成王頊入纂。廢立。', juan: '卷四', layer: 'shi', kind: 'yuanwen', zaichang: '一次废黜天子:太后下令、宗室执行、侍中尚书署令改立、方鎮观望向背。身居中枢者要在「保少主」与「附强藩」之间押命 —— 到仲舉、劉師知等辅政之臣正因此先死。', note: '★★ 废立是老板明列的「事」。頊废侄自立,与其父高祖受禪、其后自身崩逝,构成陈室三代权力更替中最赤裸的一次。' },
// ── 高宗 宣帝 太建(569–582)──
{ year: 569, yue: '正月', shi: '太建元年正月甲午,安成王頊即皇帝位(高宗宣帝)。', juan: '卷五', layer: 'shi', kind: 'yuanwen' },
{ year: 569, yue: '十月', shi: '太建元年十月,歐陽紇(歐陽頠之子)據廣州舉兵反,遣車騎將軍章昭達討平之。', juan: '卷五', layer: 'shi', kind: 'yuanwen', note: '★ 岭南欧阳氏两世据广州,至此夷灭。' },
{ year: 570, yue: '四月', shi: '太建二年四月乙卯,臨海王伯宗(廢帝)薨,時年十九。', juan: '卷五', layer: 'shi', kind: 'yuanwen', note: '★ 被废之君两年后死于王邸,用「薨」(王礼)。与 AGE_CONFLICT(年十九之算术冲突)相关,本表不裁。' },
{ year: 573, yue: '三月', shi: '太建五年三月,分命眾軍北伐,以吳明徹都督征討諸軍事,伐北齊,略取淮南諸州。', juan: '卷五', layer: 'shi', kind: 'yuanwen', zaichang: '一次倾国北伐:都督统军、诸州刺史分道、缘淮郡守征粮征役、士卒渡淮送死。位居都督者搏功名,编户小民出丁出谷。这是陈朝唯一一次主动向北的大题。', note: '★★ 太建北伐是陈朝武功之顶,一度收复淮南、饮马淮河。层「事」——一场大规模战役。虽收地千里,五年后(呂梁)尽数丧失,未成延续之「制」。' },
{ year: 577, yue: '十月', shi: '太建九年十月,司空吳明徹破周將梁士彥眾數萬于呂梁(時北周已滅北齊)。北伐后期一胜。', juan: '卷五', layer: 'shi', kind: 'yuanwen', note: '★ 577 北周灭北齐、独霸北方,陈之北伐对象由齐转周,形势逆转,为次年呂梁大败张本。' },
{ year: 578, yue: '二月', shi: '太建十年二月甲子,北討眾軍敗績於呂梁,司空吳明徹及將卒以下並為周軍所獲。陳軍主力盡喪。', juan: '卷五', layer: 'shi', kind: 'yuanwen', zaichang: '一次全军覆没:主帅司空被俘、将卒以下尽入敌手;缘江州郡要立刻转攻为守、朝廷要重新分兵备周。位居前军者身死或被虏,后方郡县要收拾溃卒、再征丁壮补防线。', note: '★★★ 呂梁之败是陈朝命运的转折:太建北伐所得淮南尽失,主力精锐一战丧尽,此后陈只能守长江、坐待被灭。层「事」——一场败仗,哪怕丧师十万,仍是「事」;但它是本朝由攻转亡的分水岭。' },
{ year: 579, yue: '', shi: '太建十一年,北周盡取淮南,陳退守長江。太建北伐所得州郡,一岁间丧尽。', juan: '卷五', layer: 'shi', kind: 'yuanwen', note: '★ 淮南既失,陈与北朝之界复归长江,攻守之势永易 —— 十年后隋即由此渡江灭陈。' },
// ── ★★ 制度条 2/2(本刀據建模 §3.3 補;紅線:三證原文+誠實 impact)──
{ year: 579, yue: '七月', shi: '太建十一年秋七月辛卯,初用大貨六銖錢(宣帝更鑄大錢)。', juan: '卷五', layer: 'zhi', kind: 'yuanwen',
layerWhy: '鑄錢=幣制變革,按建模層典屬「制」。此為陳一代幣制的標誌一步(世祖天嘉三年 562 曾「改鑄五銖錢」,宣帝此年更鑄大貨六銖),故 layer=zhi。',
witnesses: 3,
crossWitness: '★ 三證逐字全同:甲(wikisource)=乙(kanripo 四庫本)=丙《南史》卷十 皆作「秋七月辛卯,初用大貨六銖錢」(僅卯/夘異形)。另世祖 562「甲子,改鑄五銖錢」亦三證逐字同(卷三/南史卷九),本刀併入此條 note,不另立事。★ 紀志異說(本表不裁):562 改鑄五銖,《陳書》卷三、《南史》卷九均繫【天嘉三年閏二月甲子=562】,而《隋書·食貨志》作「至文帝天嘉五年,改鑄五銖」=【564】,紀志相差二年。本表從本紀取 562,並存志說,不裁此異。',
zaichang: '幣制一改,位階各受其擾:太府/少府職鑄錢者要開爐改鑄、市令要按新錢定價、商賈要換算新舊、編戶百姓手中舊錢一夕貶值。「大錢」讓握權定幣者得利、讓市井小民吃虧——同一道詔,宰輔在議,細民在算。',
impact: {
reach: 'dangchao', kind: 'tuiyan',
reason: '南朝幣制積弊已久(五銖與私鑄小錢雜行);大貨六銖為【虛值大錢】。《隋書·食貨志》陳段原文:「宣帝太建十一年,又鑄大貨六銖,以一當五銖之十,與五銖並行。後還當一,人皆不便。乃相與訛言曰:『六銖錢有不利縣官之象。』未幾而帝崩,遂廢六銖而行五銖。竟至陳亡。」⇒ 六銖自 579 秋七月行用,至宣帝崩(太建十四年正月甲寅=582,見本表同卷)即廢,前後【約三年】;廢因是【虛值崩壞(一當十→後還當一)+訛言+帝崩】,【非】亡國所廢。其後復行五銖,是【五銖】方「竟至陳亡」。⚠ 本表【不接】「陳錢→隋開皇五銖一統」這條下游:《隋書·食貨志》全篇無「平陳」「江南雜錢」之文;志中「自是錢貨始一」繫【開皇五年(585)正月】,在平陳之前,所針對者為五行大布、永通萬國、齊常平等【北方舊錢】;且同志開皇十八年(598)「是時江南人間錢少,晉王廣又聽於鄂州白紵山有銅𨥥處,錮銅鑄錢」——隋在江南是【增爐補鑄】而非廢錢一統。此鏈無據,故不編。',
to: [{ label: '582 宣帝崩,即廢六銖、復行五銖;五銖「竟至陳亡」(589)——大貨六銖本身未及陳末(《隋書·食貨志》)' }],
reachWhy: '大貨六銖行用僅【約三年】(579 秋七月–582),廢於宣帝崩後,遠夠不到「五十年後還在起作用」,故影響半徑=當朝。⚠ 不編「陳錢→隋錢」的因果:隋幣制之歸一,志文繫於平陳之前的北方,與陳錢無涉。',
},
note: '★ 大貨六銖今為錢幣史名品(篆文精整),然那是文物之名,非制度之存續:作為流通貨幣,它只行【約三年】(579–582),宣帝一崩即廢,復行五銖以至陳亡(《隋書·食貨志》)。' },
{ year: 580, yue: '八月', shi: '太建十二年,北周司馬消難(尉遲迥之亂敗)舉九州八鎮來奔,陳授侍中、大都督、司空,封隨郡公。', juan: '卷五', layer: 'shi', kind: 'yuanwen', note: '★ 此为杨坚代周(次年即建隋)前夜北方大乱之余波;陈虽纳其降,已无力乘之北取。' },
// ── 後主 至德/禎明(582–589)──
{ year: 582, yue: '正月', shi: '太建十四年正月,高宗崩。次日(乙卯),揚州刺史始興王叔陵作逆(于高宗靈前砍傷太子),伏誅(蕭摩訶斬之)。太子叔寶即位(後主)。', juan: '卷六', layer: 'shi', kind: 'yuanwen', zaichang: '天子崩、亲王于灵前夺嫡:宿卫要临机护主或附逆、宗室诸王要各择向背、执兵之将(蕭摩訶)一击定乾坤。位在禁卫中枢者,一念之间是从龙功臣还是叛党。', note: '★★ 叔陵之亂是又一次未遂的夺位(伏诛,非薨)。它把后主推上皇位,也耗尽了陈室最后的宗室元气。' },
{ year: 583, yue: '十二月', shi: '至德元年十二月,司空長沙王叔堅有罪免。平叔陵之功臣、一年前之揚州司空,一朝褫夺。', juan: '卷六', layer: 'shi', kind: 'yuanwen', note: '★ 叔堅去而後主一朝再无强宗辅政,江总等「狎客」当轴,政日以荒。免这一个字里,是陈室自剪羽翼。' },
{ year: 585, yue: '三月', shi: '至德三年三月,前豐州刺史章大寶舉兵反(旋平)。', juan: '卷六', layer: 'shi', kind: 'yuanwen' },
{ year: 587, yue: '九月', shi: '禎明元年九月,隋廢後梁,後梁安平王蕭巖、義興王蕭瓛率眾降陳,陳授開府儀同三司。', juan: '卷六', layer: 'shi', kind: 'yuanwen', note: '★ 隋灭后梁(江陵傀儡),除去陈上游的缓冲,隋军前锋直抵陈境。距灭陈仅一年半。' },
{ year: 589, yue: '正月', shi: '禎明三年正月,隋軍南北道並進,渡江陷建康。任忠出降,引隋將韓擒虎入宫城,後主自投井被執。晉王廣入據京城。陳亡。', juan: '卷六', layer: 'shi', kind: 'yuanwen', zaichang: '亡国之日,位阶决定结局各异:尚书令、吏部尚书、侍中「居省中」待命(江总、姚察、王宽),尚书仆射袁宪独守殿侧不去,鎮东大将军任忠反手出降、引敌入城,方鎮刺史或降或死,编户百姓自此改籍入隋。同一场亡国,宰辅、忠臣、叛将、小民,是四道完全不同的题。', note: '★★★ 层为「事」而非「道」——这是本表最需要说清的一条判定:\n 从【陳的账本】看,589 是【亡國】,一场王朝之死,故 layer:"shi"。\n 但这同一件事的【另一面】是【隋滅陳、南北一統】:它结束了自西晉永嘉(317)以来近三百年的南北分裂,此后中国以统一帝国为常态者一千三百余年 —— 那一面确实够得着「道」。\n ★ 然而那个「道」是【隋】缔造的,不在陳的名下。《陳書》記的是後主投井、韓擒虎入城。所以本表【不把 589 算作陳的「道」】,陳的「道」数 = 0。\n ⇒ 这正是本产品在陳这一朝要交给用户的话:在陳做官的人,一辈子活在「事」的那一层;他没能让任何「制」生效,就在 589 年成了那个大「道」的【承受者】而非缔造者。\n ★ 灭陈之隋将,卷六作「韓擒虎」——姚思廉原文当作「韓擒武」(避唐諱),两见证皆已回改(見 BIHUI)。终结这个王朝的人,在这个王朝的正史里连真名都不能用。' },
];
// ═══ 三、统一 export(硬性,名字一字不改)═══
export const CHAO_META = {
code: 'chen',
name: '陈',
span: '557–589',
qi: 557,
zhi: 589,
zhengshi: '《陳書》',
biao: null, // ★ 陳无表(本紀六+列傳三十,无表无志)。群臣/大事全从本紀抠。
};
// ★ 帝:从已有的 EMPERORS(双证考据结构,保留不动)映射为统一形状。
// qi=即位年(li),zhi=去位年(崩/废/亡=beng);後主 zhi=589(亡國),实卒于隋仁壽四年(604),另记 zuReal。
export const CHAO_DI = EMPERORS.map((e) => ({
miao: e.miao, // 廢帝/後主为 null(本朝未上廟號,见 miaoNote)
shi: e.shi, // 後主 shi=null(陳未上諡;隋諡「煬」另见 shiSui)
xing: e.xing,
qi: e.li, // 即位年
zhi: e.beng, // 去位年(崩/废/亡)
zuReal: e.zu ?? e.beng, // 实际卒年(廢帝 570、後主 604)
nianhao: e.nianhao.map((h) => ({ hao: h.hao, qi: h.qi, zhi: h.zhi })),
note: e.miaoStatus === 'none-in-source'
? (e.xing === '陳伯宗' ? '廢帝:無廟無諡,被廢後以臨海王薨(570)。' : '後主:無廟無陳諡,亡國後以隋長城縣公薨(604),隋諡「煬」。')
: undefined,
}));
export const CHAO_COVERAGE = {
di: {
from: 557,
to: 589,
note: '★ 五帝【全】(高祖/世祖/廢帝/高宗/後主),双证考据(見 EMPERORS/WITNESSES)。追尊之太祖景皇帝(陳文讚)不在位,不入(見 ZHUIZUN)。',
},
chen: {
from: 548, // 群臣最早一条起于开国之路(周文育等),但核心自 557 受禪起
to: 589,
note:
'★★ 陳【无年表】—— 全部群臣从本紀卷一–卷六编年正文抠任命句(source 皆 "benji",无一 "biao")。\n' +
'★ 覆盖到哪一档:【宰辅层做全】(三公=太尉/司徒/司空/大司馬,上公=太傅/太保/太宰,尚書令/左右僕射,' +
'中書監/令,侍中,領軍/護軍/中領軍/中護軍,錄尚書,都督中外,揚州刺史=京畿,吏部尚書,開府儀同三司初授)——' +
'凡本紀载其拜/進/遷/免/薨/卒/賜死/誅/奔/反/降/執者,逐条收录。\n' +
'🚫 【明确未做全(非静默截断,在此声明)】:①九卿/诸曹尚書(度支/左民/祠部/都官等)只在其人升入宰辅层时带到,未系统穷尽;' +
'②外州刺史(江州/南徐/郢州等)只在与宰辅层任命同句时收,未单列;③列傳三十卷【完全未抠】——' +
'本刀只据【本紀】,列傳里另有大量九卿以下、地方僚佐的任免,是【下一刀】的活。\n' +
'★ 数目:CHAO_CHEN 现有约 130 条,覆盖宰辅层 33 年。要更细的下层名单,须转抠列傳(工作量与「从散文里抠」同级)。',
},
shi: {
from: 548,
to: 589,
note:
'★ 大事全从本紀编年正文抠(source 皆 benji)。557 前为梁末陳霸先受禪之路(卷一),纪年用梁年號,仍收入(本紀卷一即其编年)。\n' +
'★★ layer 分布:【事 30 / 制 2 / 道 0】。事=30 是骨干大事(禪代/废立/功臣诛/大战/亡国)。\n' +
'★ 制=2 是本刀據建模 §3.3 補的两条本紀制度条:557「立刪定郎,治定律令」(律)、579「初用大貨六銖錢」(铸钱)——' +
'三证原文(wikisource 中華書局系=kanripo 四庫本=《南史》),layer:"zhi"。' +
'★ 制=2 是【本表已收之数】,非谓本紀制度条仅二:卷五太建十一年甲寅诏「改不枉法受財者,科同正盜」等尚未逐条补入,此数会随续补而增。' +
'★★ 但二者的 impact【影响半径皆止当朝】(reach:"dangchao"):陳国祚仅 33 年,589 被隋整体吞并,律令/币制无一存续到隋唐——' +
'这正是产品线要的老实答案:「一步制度变革,千年后还在起作用吗?」在陳答【没有】(不编「陳制→隋唐」的因果,见各条 impact)。\n' +
'★ 道=0 不是漏:唯一够得着「道」的是 589 的另一面「隋滅陳、南北一統」,但那个道属【隋】不属【陳】,故 589 记 layer:"shi"(见该条 note)。\n' +
'★ 本紀里逐日的祥瑞、祭祀、赦令、朝贡未逐条收(那是编年流水,非「大事」)。每条大事另附 zaichang(在场,推演,kind 若为 tuiyan 则不安卷次)。',
},
};
// ═══ 四、纯查询函数(别造第五张死表:数据+查询一起给)═══
// 那一年在位的皇帝(+ 该年【行用】的年號)。边界年(如 559/566/582)返回【当年最终在位】者(新即位者优先)。
// ★★ 关键:本朝五帝有四帝【逾年改元】—— 新君即位当年【仍行旧年號】,次年正月才改元。
// ⇒ 边界年的「在位皇帝」是新君,而「行用年號」却仍属【前一系】:
// 559 世祖已即位,行用的仍是高祖的【永定】;566 廢帝已即位,行用的仍是世祖的【天康】;
// 582 後主已即位,行用的仍是高宗的【太建】。
// 所以年號必须【跨全部帝】扫(谁的年號覆盖这一年就是谁的),不能只看当年在位那个帝自己的年號,
// 否则边界年会得到「年號=无」的假空缺。这一条正是本朝反复强调的 逾年改元(见 NIANHAO_NOTE)。
export function diAt(year) {
const inReign = CHAO_DI.filter((d) => year >= d.qi && year <= d.zhi);
if (inReign.length === 0) return null;
// 新即位者(qi==year)优先,否则取即位最晚者(当年末的在位者)
const di =
inReign.find((d) => d.qi === year) ||
inReign.reduce((a, b) => (b.qi > a.qi ? b : a));
// 行用年號:跨全部帝扫(可能不止一个,如 566 有天嘉七年→天康元年)
const nianhao = [];
const ownerOf = {};
for (const d of CHAO_DI) {
for (const h of d.nianhao) {
if (year >= h.qi && year <= h.zhi) {
nianhao.push({ hao: h.hao, qi: h.qi, zhi: h.zhi });
ownerOf[h.hao] = d.xing;
}
}
}
// 逾年改元标记:当年行用的年號,其所属帝 ≠ 当年在位帝 ⇒ 说明是「新君即位、旧号未改」的那一年
const yunnian = nianhao.some((h) => ownerOf[h.hao] !== di.xing);
return {
miao: di.miao,
shi: di.shi,
xing: di.xing,
nianhao, // 该年行用的年號(跨帝,边界年可含前一系的年號)
boundary: inReign.length > 1 ? inReign.map((d) => d.xing) : undefined,
yunnian: yunnian || undefined, // true = 逾年改元之年(在位者为新君,年號仍属旧系)
};
}
// 那一年的群臣(任免/薨卒/诛奔等一切本紀所载的位阶变动)。
export function chenAt(year) {
return CHAO_CHEN.filter((c) => c.year === year);
}
// 那一年的大事。
export function shiAt(year) {
return CHAO_SHI.filter((s) => s.year === year);
}
// ★ 便捷:给定「年 + 位阶关键字」,回答「那年这个位子上是谁 / 谁在这年出了事」。
// —— 这正是老板那句「景帝三年你是中大夫,你的御史大夫叫晁错,那年他被腰斩」的查询形。
export function chenAtGuan(year, guanKeyword) {
return CHAO_CHEN.filter((c) => c.year === year && c.guan.includes(guanKeyword));
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+357
View File
@@ -0,0 +1,357 @@
// 清 chao —— 帝表 + 大事 + 群臣(天下·历朝历代引擎)。
//
// ★★ 定位:纯数据。无 IO、无 DB、无 LLM。被 tianxia.js 扫入 CHAO['qing'],供入局(rujuDashi)/同僚图谱用。
// 尺子(选举志六轴门+族属门)在 src/xuan/qing.js + src/chushen.js,本文件只管帝纪/大事/群臣的双证史料。
//
// ★★★ 史源(2026-07-30 curl zh.wikisource《清史稿》本纪/志/列传 action=raw 逐字核实;grep 卷内命中,锚点逐字复制):
// 帝表据《清史稿》本纪卷一~二五;大事、群臣各标卷次。
// ⚠《清史稿》係民国初年赵尔巽等未定之稿(非二十四史正史,多有回护清室之笔),本表 cite 标其稿本性质,取其纪事、慎其论断。
//
// ⚠ 红线:不编未核之字;查不实宁可不写(史学红线)。改国号大清/入关/三藩/军机处/鸦片战争/太平天国/退位 七锚点均已 curl 双证。
// ══════════════════════════════════════════════════════════════════════════
// 帝表 —— 后金二帝(太祖/太宗前段)+ 大清十帝。cite 为本纪逐字锚点或本纪卷次。
// ══════════════════════════════════════════════════════════════════════════
export const CHAO_DI = [
{ miao: '太祖', shi: '高皇帝', xing: '努爾哈赤', li: 1616, beng: 1626,
nianhao: [{ hao: '天命', qi: 1616, zhi: 1626 }], juan: '卷一', cite: '《清史稿·太祖本紀》卷一', houJin: true,
note: '★ 1616 建后金、建元天命。在「大清」国号(1636)前二十年,本表列帝而注明其属国号前(后金段)。' },
{ miao: '太宗', shi: '文皇帝', xing: '皇太極', li: 1626, beng: 1643,
nianhao: [{ hao: '天聰', qi: 1627, zhi: 1636 }, { hao: '崇德', qi: 1636, zhi: 1643 }],
juan: '卷二~三', cite: '崇德元年……定有天下之號曰大清,改元崇德(卷三)',
note: '★★ 崇德元年(1636)改国号「大清」——清之真开元。由「后金」入「大清」,帝统追叙自太祖、国号确立于此。' },
{ miao: '世祖', shi: '章皇帝', xing: '福臨', li: 1643, beng: 1661,
nianhao: [{ hao: '順治', qi: 1644, zhi: 1661 }], juan: '卷四~五', cite: '順治元年……入關……定鼎燕京(卷四)', zhengtong: true,
note: '★★ 六岁即位、叔父多尔衮摄政。顺治元年(1644)睿亲王多尔衮率师入关、定鼎燕京,清由关外入主中原。' },
{ miao: '聖祖', shi: '仁皇帝', xing: '玄燁', li: 1661, beng: 1722,
nianhao: [{ hao: '康熙', qi: 1662, zhi: 1722 }], juan: '卷六~九', cite: '《清史稿·聖祖本紀》卷六~九;康熙十二年吳三桂反(卷六)',
note: '★★ 在位六十一年(历代最久)。擒鳌拜、平三藩(1673–1681)、收台湾(1683)、亲征噶尔丹、定多伦,开「康乾盛世」之首。' },
{ miao: '世宗', shi: '憲皇帝', xing: '胤禛', li: 1722, beng: 1735,
nianhao: [{ hao: '雍正', qi: 1723, zhi: 1735 }], juan: '卷九', cite: '《清史稿·世宗本紀》卷九',
note: '★★ 勤政严明。设军机处(雍正间因西北用兵而设,遂为一代中枢)、行摊丁入亩、改土归流、密折之制,皇权集于一身。' },
{ miao: '高宗', shi: '純皇帝', xing: '弘曆', li: 1735, beng: 1799,
nianhao: [{ hao: '乾隆', qi: 1736, zhi: 1795 }], juan: '卷十~十五', cite: '《清史稿·高宗本紀》卷十~十五',
note: '★★ 在位六十年、又训政四年(1795 内禅嘉庆、1799 崩),自号「十全老人」。武功极盛而中衰之机伏焉:晚年宠和珅、文字狱酷、吏治渐弛。' },
{ miao: '仁宗', shi: '睿皇帝', xing: '顒琰', li: 1796, beng: 1820,
nianhao: [{ hao: '嘉慶', qi: 1796, zhi: 1820 }], juan: '卷十六~十七', cite: '《清史稿·仁宗本紀》卷十六~十七',
note: '★ 亲政首诛和珅、籍其家。白莲教、天理教之乱迭起,清由盛转衰之枢在其世。' },
{ miao: '宣宗', shi: '成皇帝', xing: '旻寧', li: 1820, beng: 1850,
nianhao: [{ hao: '道光', qi: 1821, zhi: 1850 }], juan: '卷十七~十九', cite: '道光間……林則徐遣戍……江寧(卷十九)',
note: '★★ 道光二十年(1840)第一次鸦片战争,二十二年《江宁条约》割香港、开五口——中国近代之门自此被叩开。林则徐禁烟、旋以战败遣戍伊犁。' },
{ miao: '文宗', shi: '顯皇帝', xing: '奕詝', li: 1850, beng: 1861,
nianhao: [{ hao: '咸豐', qi: 1851, zhi: 1861 }], juan: '卷二十~二十一', cite: '咸豐間……洪秀全……金田(卷二十)',
note: '★★ 内有太平天国(1851 金田起事、据江宁),外有英法联军(1860 焚圆明园、《北京条约》),咸丰帝奔热河、崩于避暑山庄。清室最危之世。' },
{ miao: '穆宗', shi: '毅皇帝', xing: '載淳', li: 1861, beng: 1875,
nianhao: [{ hao: '同治', qi: 1862, zhi: 1875 }], juan: '卷二十一~二十二', cite: '《清史稿·穆宗本紀》卷二十一~二十二',
note: '★ 六岁即位,两宫太后(慈安/慈禧)垂帘、恭亲王议政。平太平天国、捻、回,兴洋务,史称「同治中兴」——实则回光。' },
{ miao: '德宗', shi: '景皇帝', xing: '載湉', li: 1875, beng: 1908,
nianhao: [{ hao: '光緒', qi: 1875, zhi: 1908 }], juan: '卷二十三~二十四', cite: '《清史稿·德宗本紀》卷二十三~二十四',
note: '★★ 慈禧太后长期训政。甲午败(1895《马关》)、戊戌变法(1898 百日而政变、幽于瀛台)、庚子之乱(1900 八国联军)、清末新政,清亡之局已成。' },
{ miao: null, shi: null, xing: '溥儀(宣統帝)', li: 1908, beng: 1912,
nianhao: [{ hao: '宣統', qi: 1909, zhi: 1912 }], juan: '卷二十五', cite: '宣統三年……辛亥……共和(卷二十五)', wangGuo: true,
note: '★★ 三岁即位、父载沣摄政。宣统三年(1911)辛亥革命、武昌起义,1912 年清帝逊位、统治权公诸全国为共和,清亡(凡二百六十八年,自 1644 入关计二百六十八年)。溥仪无庙谥(逊帝),本表 miao/shi 皆 null。' },
];
// ══════════════════════════════════════════════════════════════════════════
// 大事 —— 事(shi)/制(zhi) 分层。原文逐字 curl 双证,锚点见 crossWitness。
// ══════════════════════════════════════════════════════════════════════════
export const CHAO_SHI = [
{ year: 1636, yue: '四月', shi: '崇德元年四月,太宗定有天下之號曰大清,改元崇德,即皇帝位(去汗号称帝)。大清朝立。', juan: '卷三', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷3》太宗本纪 action=raw 逐字命中「定有天下之號曰大清」「改元崇德」(2026-07-30 curl)。',
zaichang: '定国号、去汗称帝:内三院(后之内阁)要草定制诏、满汉蒙诸王贝勒要上尊号劝进、礼部要定登极大礼。同一场改元,八旗王公在殿上定名分,笔帖式在案下缮诏书。',
note: '★★ 由「后金」入「大清」——皇太极去「金」国号、上尊号「宽温仁圣皇帝」、改元崇德,清朝正式建立。这是清的第一天(入关前八年)。' },
{ year: 1644, yue: '五月', shi: '順治元年,睿親王多爾袞率師入關、破李自成,定鼎燕京,迎世祖自盛京入京師。清由關外入主中原。', juan: '卷四', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷4》世祖本纪 action=raw 逐字命中「順治元年」「入關」「燕京」(2026-07-30 curl);与《多尔衮传》卷218入关事互证。',
zaichang: '入主一个天下:摄政王多尔衮要受吴三桂降、破闯军、定京师;八旗诸王要分定畿辅、圈地安民;降清的明朝官员,此刻要决「剃发受官、还是抗节南奔」。位阶越高,这道去留越重。',
note: '★★ 顺治元年五月入京、定鼎燕京。多尔衮以「为明复君父仇」入关,旋行剃发令、圈地,清由一隅之国而据中原,二百六十八年之基定于此。' },
{ year: 1673, yue: '十一月', shi: '康熙十二年十一月,撤藩令下,平西王吳三桂反于雲南(三藩之亂起),耿精忠、尚之信继之,八年而定。', juan: '卷六', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷6》圣祖本纪 action=raw 逐字命中「吳三桂反」(2026-07-30 curl)。',
zaichang: '削藩激起半壁之乱:年方二十的圣祖要决「撤不撤、抚不抚、战不战」、议政王大臣要廷议方略、督抚将帅要守土御敌。而云贵闽粤的地方官,此刻夹在三藩与朝廷之间,向背即生死。',
note: '★★ 康熙撤藩、吴三桂反,战火及云贵川湘闽粤,至康熙二十年(1681)平。少年天子经此一役坐稳江山,清之集权自此不可动摇。' },
{ year: 1683, yue: '六月', shi: '康熙二十二年六月,福建水师提督施琅進徵、大破郑军于澎湖海战,郑克塽奉表降,台湾入清版图(旋设台湾府隶福建)。', juan: '卷二六〇', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷260》施琅传 action=raw 逐字命中「施琅」「澎湖」「施琅進徵」(2026-07-30 curl);与圣祖本纪康熙二十二年事互证。',
zaichang: '一场决定海疆的水战:主帅施琅要相风潮、破澎湖、纳郑降;水师将弁要冒炮火抢滩;而台湾郑氏,此刻要决「死守、内渡、还是奉表归降」。同一片澎湖海面,施琅雪了当年杀父之仇,郑克塽交出了三世的基业。',
note: '★★ 施琅本郑氏旧将(父弟为郑成功所杀),降清后力主取台、澎湖一战定台湾。清始设台湾府隶福建——中国第一次把台湾正式纳入行省建置。' },
{ year: 1729, yue: '', shi: '雍正间,因西北用兵,始設軍機處(军机房),选内阁中书入直、承旨拟诏,遂为一代中枢,皇权集于一身。', juan: '卷一一四', layer: 'zhi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷114》职官志 action=raw 逐字命中「始設軍機」「軍機處」(2026-07-30 curl);与世宗本纪雍正间用兵事互证。',
zaichang: '一套新的中枢:军机大臣要每日召对承旨、军机章京要缮拟廷寄密谕、内阁自此退为「票拟例行」之地。你这一格,是入直枢庭「跪受笔录」的军机大臣,还是被架空的内阁大学士?',
layerWhy: '军机处是清代中枢官制之确立(皇权集中的枢纽),故 layer=zhi。',
impact: { reach: 'dangchao', kind: 'tuiyan', reason: '军机处集中枢于内廷、承旨拟诏而不预票拟,是清代皇权极致的制度枢纽,行用至清末(1911 责任内阁成立乃废)。就制度本身言止于清一代。', to: [{ label: '1911 责任内阁成立、军机处废(终清一代之中枢)' }], reachWhy: '军机处存续于清一代(约180年),影响半径当朝。' },
note: '★ 军机处使清代皇权集中达于极致。本表只记「军机处为清代中枢、行至清末」这一事实,不编逐条因果。设置年份诸说(雍正七年/八年/十年)不一,本表取「雍正间因西北用兵而设」之通说。' },
{ year: 1840, yue: '', shi: '道光二十年,英吉利以禁烟启衅(第一次鸦片战争),战败,二十二年《江宁条约》割香港、开五口。林則徐旋以罪遣戍伊犁。', juan: '卷一九', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷19》宣宗本纪 action=raw 逐字命中「林則徐遣戍」「廣州」「江寧」(2026-07-30 curl)。',
zaichang: '天朝第一次败于泰西:钦差大臣要决「剿夷、抚夷、还是羁縻」(林则徐主战禁烟、琦善主抚)、督抚要筹海防练水勇、沿海州县要安集难民。而广州十三行的商民、被裁的水师,都在这场三千年未有之变里翻覆。',
note: '★★ 第一次鸦片战争是中国近代史的开端。林则徐虎门销烟、旋以「启衅」革职遣戍伊犁;《江宁条约》(南京条约)割香港、开五口、赔款,天朝的门被叩开。' },
{ year: 1851, yue: '正月', shi: '咸豐元年正月,洪秀全起事于廣西金田(太平天国起),旋据江宁(改天京)建国,据东南十余年,为清室开国以来未有之大乱。', juan: '卷二十', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷20》文宗本纪 action=raw 逐字命中「洪秀全」「金田」「廣西賊」(2026-07-30 curl)。',
zaichang: '半壁江山的糜烂:朝廷要决「用满将还是倚汉臣」(终以曾国藩湘军、李鸿章淮军平之)、督抚要办团练筹厘金、州县要守城募勇。而东南的士绅、乡民,在太平军与官军之间,家破人流。曾李之兴、督抚之重,皆起于此。',
note: '★★ 太平天国据东南十四年(1851–1864),清室倚曾国藩湘军、李鸿章淮军而平之——汉臣自此掌兵柄、督抚之权自此重,清之政局为之一变。' },
{ year: 1860, yue: '九月', shi: '咸豐十年九月,英法联军入京师、焚圓明園;帝奔熱河(木蘭),旋崩于避暑山庄。《北京条约》成,增开口岸、割九龙。', juan: '卷二十', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷20》文宗本纪 action=raw 逐字命中「圓明園」「木蘭」(2026-07-30 curl)。',
zaichang: '一座万园之园的焚毁、一个天子的仓皇北狩:军机大臣要决战守和、留京王大臣(恭亲王)要出面议和、僧格林沁的马队要以血肉挡英法的枪炮。而圆明园的匠役、京城的百姓,只能眼看那把烧了三天三夜的火。',
note: '★★ 第二次鸦片战争,英法联军焚圆明园——「万园之园」付之一炬,是中华最痛的名场面之一。咸丰奔热河、次年崩;恭亲王留京议和、旋主洋务,清由此入「同光」变局。' },
{ year: 1894, yue: '八月', shi: '光緒二十年八月,中日战于黄海大東溝,北洋水师与日本联合舰队鏖战竟日,致远管带鄧世昌等死之,北洋精锐大损。翌年《马关条约》割台湾、偿二万万。', juan: '卷二三', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷23》德宗本纪 action=raw 逐字命中「北洋」「大東溝」「日本」(2026-07-30 curl);邓世昌死事见清史稿列传(忠义/邓世昌)。',
zaichang: '一场压上三十年洋务成败的海战:北洋大臣(李鸿章)要决战避、提督丁汝昌要率舰迎敌、各管带要在弹雨中操舰。而致远舰管带邓世昌,在舰伤弹尽时鼓轮撞敌、与全舰二百余人同沉——洋务三十年的家底与骄傲,在大东沟的炮火里一起沉了。',
note: '★★ 甲午黄海海战(大东沟)是洋务运动的总决算:北洋水师折损过半、邓世昌撞舰殉国成千古名场面。翌年《马关条约》割台湾、赔二万万两,清之国势一落千丈,维新与革命自此并起。' },
{ year: 1912, yue: '正月', shi: '宣統三年辛亥,武昌起义、各省响应;三年十二月(1912.2),清帝奉隆裕太后懿旨遜位,统治权公诸全国、定为共和。清亡。', juan: '卷二五', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《清史稿/卷25》宣统本纪 action=raw 逐字命中「辛亥」「共和」「袁世凱」(2026-07-30 curl)。',
zaichang: '两千年帝制的终局:隆裕太后与内阁总理袁世凯要决「战、守、还是逊位」、亲贵要争「宗社」还是「优待」、各省督抚要观南北向背。而旗人、遗老、新军,在共和与帝制之间各择去就。这一次,谁也扶不住这座将倾的两千年宫室。',
note: '★★ 辛亥革命、清帝逊位(1912.2.12),不止清亡,是中国两千年帝制的终结。袁世凯居中枢握兵柄,南北议和、清室优待,帝制入共和。' },
{
"year": 1685,
"yue": "",
"shi": "康熙二十四至二十八年(1685–1689),清兩番進討逐羅剎、收雅克薩,俄使請解圍而清帝許和;二十八年立約定尼布楚為界,中俄東段邊界由是劃定——中國近代第一個平等條約,北疆四十年之擾乃息。",
"juan": "卷七(圣祖纪)",
"layer": "shi",
"kind": "yuanwen",
"crossWitness": "★ 双证:zh.wikisource《清史稿/卷七(圣祖纪)》action=raw 逐字含「羅剎」(2026-07-30 curl;详见入局 people 之 cite)",
"zaichang": "同是逐羅剎,位階不同,各在一處:黑龍江將軍·都統衡剿撫攻圍、定四十年北疆之局;副都統、參領統眾圍城、遏其江上來援;佐領、驍騎校領隊合攏圍困;披甲的藤牌兵士執牌立於戰船,與羅剎援舟搏於江心;黑龍江邊的鄂倫春、索倫諸邊民,苦羅剎侵擾四十年,或為清師導引供餉、盼這一場收復。",
"note": "中國近代第一個平等條約的來由:非\"一役全殲沙俄\",正史記羅剎乞降、俄使請解圍而清帝許和,並遣醫官治其軍士之疾,以兵威加懷柔收局。逐羅剎、定尼布楚,劃出中俄東段第一道明確國界。"
},
{
"year": 1755,
"yue": "",
"shi": "清乾隆二十年(1755)平準噶爾、俘達瓦齊,旋阿睦爾撒納叛而復平;二十四年(1759)定回部大小和卓(霍集占),統一天山南北、置伊犁將軍——乾隆「十全武功」之首,新疆入版圖。",
"juan": "卷一一~一二(高宗纪)",
"layer": "shi",
"kind": "yuanwen",
"crossWitness": "★ 双证:zh.wikisource《清史稿/卷一一~一二(高宗纪)》action=raw 逐字含「準噶爾」(2026-07-30 curl;详见入局 people 之 cite)",
"zaichang": "定邊將軍在帳中定進兵方略、參贊大臣分統各路、都統領隊督滿洲兵與綠營遠涉天山瀚海;兵丁在絕域裡追亡逐北、乏糧苦戰。而準部與回眾,此刻要決「拒戰、內附、還是隨大小和卓敗走」。同一場開疆定新疆,將軍在版圖上落一筆,兵丁在沙場上填一命。",
"note": "★★ 乾隆二十年至二十四年(1755–1759)平準噶爾、定回部,統一天山南北、置伊犁將軍——這是「十全武功」之首,也是新疆正式入中國版圖的定局。一場遠涉瀚海的開疆,把天山南北納入一統。"
},
{
"year": 1900,
"yue": "",
"shi": "清光绪二十六年(1900),拳匪以「扶清灭洋」起,清廷纵之围攻使馆、对列强宣战;八国联军入北京,两宫西狩,次年《辛丑条约》赔四亿五千万两、许驻兵,清亡之势不可挽。",
"juan": "卷二四(德宗纪)",
"layer": "shi",
"kind": "yuanwen",
"crossWitness": "★ 双证:zh.wikisource《清史稿/卷二四(德宗纪)》action=raw 逐字含「拳匪」(2026-07-30 curl;详见入局 people 之 cite)",
"zaichang": "军机与亲贵在殿上争战和:一言可纵拳民、可宣战,也可招祸。督抚接到宣战诏,或奉命开战,或联手推『东南互保』自守半壁。清军与团民在京师街巷围攻使馆,血火相接。京城百姓则在拳坛、使馆、洋兵之间求活;待联军破城、两宫西狩,满城玉石俱焚,谁都逃不过这一年的账。",
"note": "庚子之变是清朝走向覆亡的转折:一次把『扶清灭洋』的民气、朝堂的战和之争、地方的东南互保、列强的兵威全绞在一处。围使馆、宣战、联军入京、两宫西狩、辛丑巨赔——这是『灭国名场面』里最痛的一幕,王朝的体面与主权在这一年被一并击碎。"
},
];
// ══════════════════════════════════════════════════════════════════════════
// 群臣 —— 入局同僚图谱之料。cite 为列传逐字锚点,source 标 liezhuan。
// ══════════════════════════════════════════════════════════════════════════
export const CHAO_CHEN = [
{ year: 1644, guan: '睿親王·攝政', ren: '多爾袞', dong: '入关·摄政', juan: '卷二一八', cite: '多爾袞,太祖第十四子……(顺治初摄政、率师入关定鼎燕京)', source: 'liezhuan',
note: '★ 太祖第十四子。顺治初以叔父摄政王总揽军国,率师入关、定鼎燕京,清入主中原之首功;专擅跋扈,死后(1650)被追论、削爵籍没,乾隆时始复。' },
{ year: 1667, guan: '輔政大臣', ren: '鰲拜', dong: '专权·被擒', juan: '卷二四九', cite: '鰲拜……(圣祖初四辅政之一,专权,康熙八年为帝所擒)', source: 'liezhuan',
note: '★ 瓜尔佳氏。世祖遗命四辅政大臣之一,圣祖初专擅朝政、擅杀同列;康熙八年(1669)少年天子设计擒之、拘禁,清初「主少臣强」的样本。' },
{ year: 1776, guan: '文華殿大學士·軍機大臣', ren: '和珅', dong: '专宠·聚敛', juan: '卷三一九', cite: '和珅,字致齋……(乾隆晚年宠任、军机大臣,专权聚敛,嘉庆初赐死籍家)', source: 'liezhuan',
note: '★ 《清史稿·奸臣传》。乾隆晚年第一宠臣,兼军机、掌财赋二十年,聚敛之富「和珅跌倒,嘉庆吃饱」;嘉庆四年(1799)亲政即赐死、籍其家。清中衰吏治之象。' },
{ year: 1864, guan: '兩江總督·大學士', ren: '曾國藩', dong: '平太平·中兴', juan: '卷四〇五', cite: '曾國藩……創湘軍(以团练起、平太平天国,封一等毅勇侯,为同治中兴首臣)', source: 'liezhuan',
note: '★★ 湖南人。以在籍侍郎办团练、创湘军,历十四年平太平天国,封一等毅勇侯、两江总督、大学士。汉臣掌兵柄之始、洋务之倡者,「同治中兴」第一名臣。' },
{ year: 1870, guan: '直隸總督·北洋大臣', ren: '李鴻章', dong: '洋务·外交', juan: '卷四一一', cite: '李鴻章,字漸甫……創淮軍(平捻、办洋务、长外交,历直隶总督北洋大臣二十余年)', source: 'liezhuan',
note: '★★ 安徽合肥人。曾国藩门生,创淮军、平太平捻乱,办洋务(轮船招商、北洋水师)、主晚清外交(《马关》《辛丑》多出其手)。晚清「裱糊匠」,功罪聚讼之臣。' },
{"year":1616,"guan":"一等大臣(开国五大臣之首)","ren":"额亦都","dong":"佐努尔哈赤起兵,开国元勋五大臣之","juan":"卷225","cite":"《清史稿》卷225·列传十二(额亦都传)","source":"liezhuan"},
{"year":1616,"guan":"一等大臣/理政听讼大臣","ren":"费英东","dong":"开国五大臣之一,掌军法听讼","juan":"卷225","cite":"《清史稿》卷225·列传十二(费英东传)","source":"liezhuan"},
{"year":1616,"guan":"一等大臣","ren":"何和礼","dong":"开国五大臣之一,尚公主","juan":"卷225","cite":"《清史稿》卷225·列传十二(何和礼传)","source":"liezhuan"},
{"year":1616,"guan":"一等大臣","ren":"安费扬古","dong":"开国五大臣之一,征战有功","juan":"卷225","cite":"《清史稿》卷225·列传十二(安费扬古传)","source":"liezhuan"},
{"year":1616,"guan":"一等大臣","ren":"扈尔汉","dong":"开国五大臣之一,养子赐姓","juan":"卷225","cite":"《清史稿》卷225·列传十二(扈尔汉传)","source":"liezhuan"},
{"year":1616,"guan":"巴克什","ren":"额尔德尼","dong":"奉命以蒙文字母创制老满文","juan":"卷228","cite":"《清史稿》卷228·列传十五(额尔德尼传)","source":"liezhuan"},
{"year":1616,"guan":"超品公/都统","ren":"扬古利","dong":"开国名将,后战殁松山","juan":"卷226","cite":"《清史稿》卷226·列传十三(扬古利传)","source":"liezhuan"},
{"year":1618,"guan":"三等副将/驸马","ren":"李永芳","dong":"抚顺守将首降后金,尚郡主","juan":"卷231","cite":"《清史稿》卷231·列传十八(李永芳传)","source":"liezhuan"},
{"year":1632,"guan":"巴克什","ren":"达海","dong":"增补圈点,定型新满文","juan":"卷228","cite":"《清史稿》卷228·列传十五(达海传)","source":"liezhuan"},
{"year":1633,"guan":"恭顺王","ren":"孔有德","dong":"明将航海来归封王,后镇广西","juan":"卷234","cite":"《清史稿》卷234·列传二十一(孔有德传)","source":"liezhuan"},
{"year":1633,"guan":"怀顺王","ren":"耿仲明","dong":"来归封王,后为三藩之靖南","juan":"卷234","cite":"《清史稿》卷234·列传二十一(耿仲明传)","source":"liezhuan"},
{"year":1634,"guan":"智顺王","ren":"尚可喜","dong":"来归封王,后为三藩之平南","juan":"卷234","cite":"《清史稿》卷234·列传二十一(尚可喜传)","source":"liezhuan"},
{"year":1636,"guan":"内国史院大学士","ren":"希福","dong":"通满蒙汉,首译辽金元三史","juan":"卷232","cite":"《清史稿》卷232·列传十九(希福传)","source":"liezhuan"},
{"year":1636,"guan":"内秘书院大学士","ren":"范文程","dong":"佐皇太极定开国典章,历事四朝","juan":"卷232","cite":"《清史稿》卷232·列传十九(范文程传)","source":"liezhuan"},
{"year":1636,"guan":"内弘文院学士","ren":"宁完我","dong":"参帷幄,议仿明制设六部","juan":"卷232","cite":"《清史稿》卷232·列传十九(宁完我传)","source":"liezhuan"},
{"year":1636,"guan":"内秘书院大学士","ren":"鲍承先","dong":"献反间计间明,致杀袁崇焕","juan":"卷232","cite":"《清史稿》卷232·列传十九(鲍承先传)","source":"liezhuan"},
{"year":1636,"guan":"都元帅/总理汉军","ren":"佟养性","dong":"总理汉人军民,督铸红衣炮","juan":"卷231","cite":"《清史稿》卷231·列传十八(佟养性传)","source":"liezhuan"},
{"year":1636,"guan":"固山额真","ren":"图尔格","dong":"开国名将,从征朝鲜蒙古","juan":"卷233","cite":"《清史稿》卷233·列传二十(图尔格传)","source":"liezhuan"},
{"year":1636,"guan":"固山额真","ren":"叶臣","dong":"名将,率军定山西","juan":"卷233","cite":"《清史稿》卷233·列传二十(叶臣传)","source":"liezhuan"},
{"year":1642,"guan":"总兵","ren":"祖大寿","dong":"明辽东名将,锦州降清","juan":"卷234","cite":"《清史稿》卷234·列传二十一(祖大寿传)","source":"liezhuan"},
{"year":1643,"guan":"辅政叔王","ren":"济尔哈朗","dong":"郑亲王,与多尔衮同受命辅政","juan":"卷215","cite":"《清史稿》卷215·列传二(济尔哈朗传)","source":"liezhuan"},
{"year":1644,"guan":"摄政王/奉命大将军","ren":"多尔衮","dong":"睿亲王,率清军入关定鼎北京","juan":"卷218","cite":"《清史稿》卷218·列传五(多尔衮传)","source":"liezhuan"},
{"year":1644,"guan":"靖远大将军","ren":"阿济格","dong":"英亲王,追歼李自成","juan":"卷218","cite":"《清史稿》卷218·列传五(阿济格传)","source":"liezhuan"},
{"year":1644,"guan":"陕西三边总督","ren":"孟乔芳","dong":"平定西北,底定甘陕","juan":"卷237","cite":"《清史稿》卷237·列传二十四(孟乔芳传)","source":"liezhuan"},
{"year":1644,"guan":"直隶山东河南总督","ren":"张存仁","dong":"献弭盗治平之策,总督闽浙","juan":"卷237","cite":"《清史稿》卷237·列传二十四(张存仁传)","source":"liezhuan"},
{"year":1644,"guan":"内国史院大学士","ren":"刚林","dong":"清首任内三院大学士之一","juan":"卷245","cite":"《清史稿》卷245·列传三十二(刚林传)","source":"liezhuan"},
{"year":1644,"guan":"内弘文院大学士","ren":"冯铨","dong":"首批入内院之汉臣重臣","juan":"卷245","cite":"《清史稿》卷245·列传三十二(冯铨传)","source":"liezhuan"},
{"year":1644,"guan":"内弘文院大学士","ren":"陈名夏","dong":"南党领袖,后以事伏诛","juan":"卷245","cite":"《清史稿》卷245·列传三十二(陈名夏传)","source":"liezhuan"},
{"year":1644,"guan":"中和殿大学士","ren":"金之俊","dong":"历事明清,号五朝元老","juan":"卷238","cite":"《清史稿》卷238·列传二十五(金之俊传)","source":"liezhuan"},
{"year":1645,"guan":"定国大将军","ren":"多铎","dong":"豫亲王,下江南破南明弘光","juan":"卷218","cite":"《清史稿》卷218·列传五(多铎传)","source":"liezhuan"},
{"year":1646,"guan":"武英殿大学士","ren":"傅以渐","dong":"清开国首科状元","juan":"卷238","cite":"《清史稿》卷238·列传二十五(傅以渐传)","source":"liezhuan"},
{"year":1653,"guan":"内翰林弘文院大学士/经略","ren":"洪承畴","dong":"经略湖广等五省,招抚西南","juan":"卷237","cite":"《清史稿》卷237·列传二十四(洪承畴传)","source":"liezhuan"},
{"year":1661,"guan":"一等公/辅政大臣","ren":"索尼","dong":"顺治遗命四辅政之首","juan":"卷249","cite":"《清史稿》卷249·列传三十六(索尼传)","source":"liezhuan"},
{"year":1661,"guan":"辅政大臣","ren":"苏克萨哈","dong":"四辅政之一,为鳌拜构陷死","juan":"卷249","cite":"《清史稿》卷249·列传三十六(苏克萨哈传)","source":"liezhuan"},
{"year":1661,"guan":"辅政大臣","ren":"遏必隆","dong":"四辅政之一,一等公","juan":"卷249","cite":"《清史稿》卷249·列传三十六(遏必隆传)","source":"liezhuan"},
{"year":1661,"guan":"辅政大臣/太师","ren":"鳌拜","dong":"专擅朝政,康熙八年被擒","juan":"卷249","cite":"《清史稿》卷249·列传三十六(鳌拜传)","source":"liezhuan"},
{"year":1662,"guan":"保和殿大学士","ren":"索额图","dong":"议撤三藩,主尼布楚谈判","juan":"卷269","cite":"《清史稿》卷269·列传五十六(索额图传)","source":"liezhuan"},
{"year":1662,"guan":"武英殿大学士","ren":"明珠","dong":"力主撤藩,权倾一时","juan":"卷269","cite":"《清史稿》卷269·列传五十六(明珠传)","source":"liezhuan"},
{"year":1662,"guan":"文华殿大学士","ren":"李之芳","dong":"总督浙江,平耿精忠","juan":"卷251","cite":"《清史稿》卷251·列传三十八(李之芳传)","source":"liezhuan"},
{"year":1662,"guan":"东阁大学士","ren":"熊赐履","dong":"理学名臣,经筵讲官","juan":"卷262","cite":"《清史稿》卷262·列传四十九(熊赐履传)","source":"liezhuan"},
{"year":1662,"guan":"文渊阁大学士","ren":"李光地","dong":"荐施琅复台,理学重臣","juan":"卷262","cite":"《清史稿》卷262·列传四十九(李光地传)","source":"liezhuan"},
{"year":1662,"guan":"文华殿大学士","ren":"张玉书","dong":"领修《康熙字典》《佩文韵府》","juan":"卷267","cite":"《清史稿》卷267·列传五十四(张玉书传)","source":"liezhuan"},
{"year":1662,"guan":"文华殿大学士","ren":"张英","dong":"南书房首值,张廷玉之父","juan":"卷267","cite":"《清史稿》卷267·列传五十四(张英传)","source":"liezhuan"},
{"year":1662,"guan":"文渊阁大学士","ren":"陈廷敬","dong":"总裁《康熙字典》","juan":"卷267","cite":"《清史稿》卷267·列传五十四(陈廷敬传)","source":"liezhuan"},
{"year":1662,"guan":"保和殿大学士","ren":"王熙","dong":"承旨草拟顺治遗诏","juan":"卷250","cite":"《清史稿》卷250·列传三十七(王熙传)","source":"liezhuan"},
{"year":1662,"guan":"文华殿大学士","ren":"徐元文","dong":"状元,监修《明史》","juan":"卷250","cite":"《清史稿》卷250·列传三十七(徐元文传)","source":"liezhuan"},
{"year":1662,"guan":"两江总督","ren":"于成龙","dong":"号天下廉吏第一","juan":"卷277","cite":"《清史稿》卷277·列传六十四(于成龙传)","source":"liezhuan"},
{"year":1662,"guan":"文华殿大学士/河道总督","ren":"张鹏翮","dong":"治河名臣,以清廉著","juan":"卷279","cite":"《清史稿》卷279·列传六十六(张鹏翮传)","source":"liezhuan"},
{"year":1662,"guan":"福建总督","ren":"姚启圣","dong":"筹划平台,荐用施琅","juan":"卷260","cite":"《清史稿》卷260·列传四十七(姚启圣传)","source":"liezhuan"},
{"year":1662,"guan":"甘肃提督/靖逆侯","ren":"张勇","dong":"河西四将,平三藩定西北","juan":"卷255","cite":"《清史稿》卷255·列传四十二(张勇传)","source":"liezhuan"},
{"year":1662,"guan":"云贵总督","ren":"赵良栋","dong":"平三藩,收复云南","juan":"卷255","cite":"《清史稿》卷255·列传四十二(赵良栋传)","source":"liezhuan"},
{"year":1662,"guan":"江苏巡抚/工部尚书","ren":"汤斌","dong":"理学名臣,清廉爱民","juan":"卷265","cite":"《清史稿》卷265·列传五十二(汤斌传)","source":"liezhuan"},
{"year":1662,"guan":"礼部尚书","ren":"张伯行","dong":"以清廉著,号天下清官第一","juan":"卷265","cite":"《清史稿》卷265·列传五十二(张伯行传)","source":"liezhuan"},
{"year":1662,"guan":"礼部侍郎","ren":"高士奇","dong":"南书房宠臣,学者","juan":"卷271","cite":"《清史稿》卷271·列传五十八(高士奇传)","source":"liezhuan"},
{"year":1662,"guan":"刑部尚书","ren":"王士禛","dong":"一代诗宗,倡神韵说","juan":"卷266","cite":"《清史稿》卷266·列传五十三(王士禛传)","source":"liezhuan"},
{"year":1662,"guan":"保和殿大学士","ren":"马齐","dong":"历相三朝,以议储得罪","juan":"卷287","cite":"《清史稿》卷287·列传七十四(马齐传)","source":"liezhuan"},
{"year":1676,"guan":"中和殿大学士/抚远大将军","ren":"图海","dong":"平王辅臣,底定陕西","juan":"卷251","cite":"《清史稿》卷251·列传三十八(图海传)","source":"liezhuan"},
{"year":1677,"guan":"河道总督","ren":"靳辅","dong":"治黄淮,行束水攻沙","juan":"卷279","cite":"《清史稿》卷279·列传六十六(靳辅传)","source":"liezhuan"},
{"year":1683,"guan":"福建水师提督/靖海侯","ren":"施琅","dong":"率师渡海收复台湾","juan":"卷260","cite":"《清史稿》卷260·列传四十七(施琅传)","source":"liezhuan"},
{"year":1696,"guan":"抚远大将军","ren":"费扬古","dong":"昭莫多之战大破噶尔丹","juan":"卷281","cite":"《清史稿》卷281·列传六十八(费扬古传)","source":"liezhuan"},
{"year":1723,"guan":"保和殿大学士/军机大臣","ren":"张廷玉","dong":"厘定军机处规制,唯一配享太庙之汉","juan":"卷288","cite":"《清史稿》卷288·列传七十五(张廷玉传)","source":"liezhuan"},
{"year":1723,"guan":"保和殿大学士/军机大臣","ren":"鄂尔泰","dong":"主西南改土归流","juan":"卷288","cite":"《清史稿》卷288·列传七十五(鄂尔泰传)","source":"liezhuan"},
{"year":1723,"guan":"怡亲王/总理事务大臣","ren":"允祥","dong":"总理户部,治水理财","juan":"卷220","cite":"《清史稿》卷220·列传七(允祥传)","source":"liezhuan"},
{"year":1723,"guan":"步军统领/吏部尚书","ren":"隆科多","dong":"拥立有功后获罪禁锢","juan":"卷295","cite":"《清史稿》卷295·列传八十二(隆科多传)","source":"liezhuan"},
{"year":1723,"guan":"抚远大将军/川陕总督","ren":"年羹尧","dong":"平青海罗卜藏丹津,后赐死","juan":"卷295","cite":"《清史稿》卷295·列传八十二(年羹尧传)","source":"liezhuan"},
{"year":1723,"guan":"浙江总督","ren":"李卫","dong":"缉私盐,雍正宠臣","juan":"卷294","cite":"《清史稿》卷294·列传八十一(李卫传)","source":"liezhuan"},
{"year":1723,"guan":"河南山东总督","ren":"田文镜","dong":"模范督抚,力行新政","juan":"卷294","cite":"《清史稿》卷294·列传八十一(田文镜传)","source":"liezhuan"},
{"year":1723,"guan":"宁远大将军/川陕总督","ren":"岳钟琪","dong":"平青海,岳飞后裔","juan":"卷296","cite":"《清史稿》卷296·列传八十三(岳钟琪传)","source":"liezhuan"},
{"year":1723,"guan":"文华殿大学士","ren":"朱轼","dong":"帝师,精于治水","juan":"卷289","cite":"《清史稿》卷289·列传七十六(朱轼传)","source":"liezhuan"},
{"year":1723,"guan":"文华殿大学士","ren":"蒋廷锡","dong":"领修《古今图书集成》","juan":"卷289","cite":"《清史稿》卷289·列传七十六(蒋廷锡传)","source":"liezhuan"},
{"year":1723,"guan":"都察院左都御史","ren":"孙嘉淦","dong":"上《三习一弊疏》,直谏名臣","juan":"卷303","cite":"《清史稿》卷303·列传九十(孙嘉淦传)","source":"liezhuan"},
{"year":1736,"guan":"保和殿大学士/军机领班","ren":"讷亲","dong":"首席军机,金川失律赐死","juan":"卷301","cite":"《清史稿》卷301·列传八十八(讷亲传)","source":"liezhuan"},
{"year":1736,"guan":"保和殿大学士/军机领班","ren":"傅恒","dong":"经略金川、缅甸,首席军机","juan":"卷301","cite":"《清史稿》卷301·列传八十八(傅恒传)","source":"liezhuan"},
{"year":1736,"guan":"武英殿大学士/军机领班","ren":"阿桂","dong":"定伊犁、平两金川","juan":"卷318","cite":"《清史稿》卷318·列传一百五(阿桂传)","source":"liezhuan"},
{"year":1736,"guan":"文华殿大学士/军机领班","ren":"于敏中","dong":"《四库全书》正总裁","juan":"卷319","cite":"《清史稿》卷319·列传一百六(于敏中传)","source":"liezhuan"},
{"year":1736,"guan":"东阁大学士/军机大臣","ren":"刘统勋","dong":"治河刚正,刘墉之父","juan":"卷302","cite":"《清史稿》卷302·列传八十九(刘统勋传)","source":"liezhuan"},
{"year":1736,"guan":"体仁阁大学士","ren":"刘墉","dong":"名臣,书法家","juan":"卷302","cite":"《清史稿》卷302·列传八十九(刘墉传)","source":"liezhuan"},
{"year":1736,"guan":"文华殿大学士","ren":"尹继善","dong":"四任两江总督","juan":"卷307","cite":"《清史稿》卷307·列传九十四(尹继善传)","source":"liezhuan"},
{"year":1736,"guan":"东阁大学士","ren":"陈宏谋","dong":"循吏,经世名臣","juan":"卷307","cite":"《清史稿》卷307·列传九十四(陈宏谋传)","source":"liezhuan"},
{"year":1736,"guan":"武英殿大学士/定边将军","ren":"兆惠","dong":"平定回部大小和卓","juan":"卷313","cite":"《清史稿》卷313·列传一百(兆惠传)","source":"liezhuan"},
{"year":1736,"guan":"武英殿大学士/大将军","ren":"福康安","dong":"平台湾林爽文、廓尔喀","juan":"卷330","cite":"《清史稿》卷330·列传一百十七(福康安传)","source":"liezhuan"},
{"year":1736,"guan":"参赞大臣/一等超勇公","ren":"海兰察","dong":"征战名将,四登紫光阁","juan":"卷331","cite":"《清史稿》卷331·列传一百十八(海兰察传)","source":"liezhuan"},
{"year":1736,"guan":"协办大学士/礼部尚书","ren":"纪昀","dong":"《四库全书》总纂官","juan":"卷320","cite":"《清史稿》卷320·列传一百七(纪昀传)","source":"liezhuan"},
{"year":1736,"guan":"文华殿大学士","ren":"蔡新","dong":"帝师,居相位久","juan":"卷320","cite":"《清史稿》卷320·列传一百七(蔡新传)","source":"liezhuan"},
{"year":1736,"guan":"文渊阁大学士","ren":"史贻直","dong":"三朝元老,居相位久","juan":"卷303","cite":"《清史稿》卷303·列传九十(史贻直传)","source":"liezhuan"},
{"year":1736,"guan":"文渊阁大学士/河道总督","ren":"高斌","dong":"治河名臣","juan":"卷310","cite":"《清史稿》卷310·列传九十七(高斌传)","source":"liezhuan"},
{"year":1736,"guan":"文渊阁大学士/河道总督","ren":"嵇璜","dong":"治河数十年","juan":"卷310","cite":"《清史稿》卷310·列传九十七(嵇璜传)","source":"liezhuan"},
{"year":1736,"guan":"武英殿大学士/总督","ren":"李侍尧","dong":"能吏,历任封疆","juan":"卷323","cite":"《清史稿》卷323·列传一百十(李侍尧传)","source":"liezhuan"},
{"year":1736,"guan":"东阁大学士/军机大臣","ren":"王杰","dong":"乾隆状元,首劾和珅","juan":"卷340","cite":"《清史稿》卷340·列传一百二十七(王杰传)","source":"liezhuan"},
{"year":1796,"guan":"文华殿大学士/军机领班","ren":"董诰","dong":"佐诛和珅,领班军机二十年","juan":"卷340","cite":"《清史稿》卷340·列传一百二十七(董诰传)","source":"liezhuan"},
{"year":1796,"guan":"体仁阁大学士","ren":"朱珪","dong":"嘉庆帝师","juan":"卷340","cite":"《清史稿》卷340·列传一百二十七(朱珪传)","source":"liezhuan"},
{"year":1796,"guan":"文渊阁大学士/军机领班","ren":"庆桂","dong":"军机领班","juan":"卷341","cite":"《清史稿》卷341·列传一百二十八(庆桂传)","source":"liezhuan"},
{"year":1796,"guan":"体仁阁大学士/军机大臣","ren":"戴衢亨","dong":"状元,军机大臣","juan":"卷341","cite":"《清史稿》卷341·列传一百二十八(戴衢亨传)","source":"liezhuan"},
{"year":1796,"guan":"东阁大学士/军机大臣","ren":"托津","dong":"军机大臣,总理事务","juan":"卷341","cite":"《清史稿》卷341·列传一百二十八(托津传)","source":"liezhuan"},
{"year":1796,"guan":"武英殿大学士","ren":"松筠","dong":"治理边疆,驻藏办事","juan":"卷342","cite":"《清史稿》卷342·列传一百二十九(松筠传)","source":"liezhuan"},
{"year":1796,"guan":"武英殿大学士/经略","ren":"勒保","dong":"经略川楚,平白莲教","juan":"卷344","cite":"《清史稿》卷344·列传一百三十一(勒保传)","source":"liezhuan"},
{"year":1796,"guan":"一等公/经略大臣","ren":"额勒登保","dong":"平定川楚白莲教","juan":"卷344","cite":"《清史稿》卷344·列传一百三十一(额勒登保传)","source":"liezhuan"},
{"year":1796,"guan":"一等公/参赞大臣","ren":"德楞泰","dong":"平白莲教名将","juan":"卷344","cite":"《清史稿》卷344·列传一百三十一(德楞泰传)","source":"liezhuan"},
{"year":1796,"guan":"两江总督/两广总督","ren":"百龄","dong":"严禁鸦片,靖平海盗","juan":"卷343","cite":"《清史稿》卷343·列传一百三十(百龄传)","source":"liezhuan"},
{"year":1821,"guan":"武英殿大学士/军机领班","ren":"曹振镛","dong":"军机领班,以谨饬持禄著","juan":"卷363","cite":"《清史稿》卷363·列传一百五十(曹振镛传)","source":"liezhuan"},
{"year":1821,"guan":"文华殿大学士/军机领班","ren":"穆彰阿","dong":"主和派领袖,排陷林则徐","juan":"卷363","cite":"《清史稿》卷363·列传一百五十(穆彰阿传)","source":"liezhuan"},
{"year":1821,"guan":"东阁大学士/军机大臣","ren":"王鼎","dong":"力保林则徐,尸谏而死","juan":"卷363","cite":"《清史稿》卷363·列传一百五十(王鼎传)","source":"liezhuan"},
{"year":1821,"guan":"武英殿大学士/军机大臣","ren":"潘世恩","dong":"状元,四朝元老","juan":"卷363","cite":"《清史稿》卷363·列传一百五十(潘世恩传)","source":"liezhuan"},
{"year":1821,"guan":"直隶总督/钦差大臣","ren":"那彦成","dong":"办理回疆善后","juan":"卷367","cite":"《清史稿》卷367·列传一百五十四(那彦成传)","source":"liezhuan"},
{"year":1821,"guan":"体仁阁大学士","ren":"阮元","dong":"学者督抚,编《经籍纂诂》","juan":"卷364","cite":"《清史稿》卷364·列传一百五十一(阮元传)","source":"liezhuan"},
{"year":1821,"guan":"两广/闽浙总督","ren":"邓廷桢","dong":"协同禁烟,筹办海防","juan":"卷369","cite":"《清史稿》卷369·列传一百五十六(邓廷桢传)","source":"liezhuan"},
{"year":1821,"guan":"直隶总督/钦差大臣","ren":"琦善","dong":"主抚,擅许香港获罪","juan":"卷370","cite":"《清史稿》卷370·列传一百五十七(琦善传)","source":"liezhuan"},
{"year":1821,"guan":"两江总督","ren":"陶澍","dong":"改革盐漕,经世致用","juan":"卷379","cite":"《清史稿》卷379·列传一百六十六(陶澍传)","source":"liezhuan"},
{"year":1821,"guan":"体仁阁大学士","ren":"祁寯藻","dong":"三朝帝师,首席军机","juan":"卷385","cite":"《清史稿》卷385·列传一百七十二(祁寯藻传)","source":"liezhuan"},
{"year":1821,"guan":"协办大学士","ren":"杜受田","dong":"咸丰帝师","juan":"卷385","cite":"《清史稿》卷385·列传一百七十二(杜受田传)","source":"liezhuan"},
{"year":1828,"guan":"文华殿大学士/扬威将军","ren":"长龄","dong":"擒张格尔,平定回疆","juan":"卷367","cite":"《清史稿》卷367·列传一百五十四(长龄传)","source":"liezhuan"},
{"year":1839,"guan":"两广总督/钦差大臣","ren":"林则徐","dong":"虎门销烟,查禁鸦片","juan":"卷369","cite":"《清史稿》卷369·列传一百五十六(林则徐传)","source":"liezhuan"},
{"year":1841,"guan":"两江总督/钦差大臣","ren":"裕谦","dong":"抗英,镇海城破投水殉国","juan":"卷372","cite":"《清史稿》卷372·列传一百五十九(裕谦传)","source":"liezhuan"},
{"year":1841,"guan":"广东水师提督","ren":"关天培","dong":"虎门抗英力战殉国","juan":"卷372","cite":"《清史稿》卷372·列传一百五十九(关天培传)","source":"liezhuan"},
{"year":1842,"guan":"钦差大臣/两广总督","ren":"耆英","dong":"签订《南京条约》","juan":"卷370","cite":"《清史稿》卷370·列传一百五十七(耆英传)","source":"liezhuan"},
{"year":1842,"guan":"江南提督","ren":"陈化成","dong":"吴淞抗英殉国","juan":"卷372","cite":"《清史稿》卷372·列传一百五十九(陈化成传)","source":"liezhuan"},
{"year":1861,"guan":"御前大臣/协办大学士","ren":"肃顺","dong":"赞襄政务八大臣之首,辛酉政变被诛","juan":"卷387","cite":"《清史稿》卷387·列传一百七十四(肃顺传)","source":"liezhuan"},
{"year":1862,"guan":"恭亲王/议政王军机大臣","ren":"奕訢","dong":"设总理衙门,主持洋务","juan":"卷221","cite":"《清史稿》卷221·列传八(奕訢传)","source":"liezhuan"},
{"year":1862,"guan":"武英殿大学士/军机大臣","ren":"文祥","dong":"总理衙门,洋务重臣","juan":"卷386","cite":"《清史稿》卷386·列传一百七十三(文祥传)","source":"liezhuan"},
{"year":1862,"guan":"体仁阁大学士/军机大臣","ren":"宝鋆","dong":"军机大臣,佐理洋务","juan":"卷386","cite":"《清史稿》卷386·列传一百七十三(宝鋆传)","source":"liezhuan"},
{"year":1862,"guan":"文华殿大学士","ren":"倭仁","dong":"理学领袖,守旧反洋务","juan":"卷391","cite":"《清史稿》卷391·列传一百七十八(倭仁传)","source":"liezhuan"},
{"year":1862,"guan":"两江总督/船政大臣","ren":"沈葆桢","dong":"创办福州船政局","juan":"卷413","cite":"《清史稿》卷413·列传二百(沈葆桢传)","source":"liezhuan"},
{"year":1862,"guan":"直隶/云贵总督","ren":"刘长佑","dong":"湘军督抚","juan":"卷419","cite":"《清史稿》卷419·列传二百六(刘长佑传)","source":"liezhuan"},
{"year":1862,"guan":"山东巡抚/四川总督","ren":"丁宝桢","dong":"计诛安德海,兴办洋务","juan":"卷447","cite":"《清史稿》卷447·列传二百三十四(丁宝桢传)","source":"liezhuan"},
{"year":1862,"guan":"直隶提督/台湾巡抚","ren":"刘铭传","dong":"淮军名将,首任台湾巡抚","juan":"卷416","cite":"《清史稿》卷416·列传二百三(刘铭传传)","source":"liezhuan"},
{"year":1862,"guan":"协办大学士/军机大臣","ren":"沈桂芬","dong":"军机大臣,主持洋务","juan":"卷436","cite":"《清史稿》卷436·列传二百二十三(沈桂芬传)","source":"liezhuan"},
{"year":1862,"guan":"协办大学士/军机大臣","ren":"李鸿藻","dong":"清流领袖,同治帝师","juan":"卷436","cite":"《清史稿》卷436·列传二百二十三(李鸿藻传)","source":"liezhuan"},
{"year":1864,"guan":"两江总督","ren":"曾国荃","dong":"攻克天京(南京)","juan":"卷413","cite":"《清史稿》卷413·列传二百(曾国荃传)","source":"liezhuan"},
{"year":1870,"guan":"两江总督","ren":"马新贻","dong":"遇刺身亡(刺马案)","juan":"卷426","cite":"《清史稿》卷426·列传二百十三(马新贻传)","source":"liezhuan"},
{"year":1875,"guan":"直隶总督/北洋大臣/文华殿大学士","ren":"李鸿章","dong":"兴洋务创北洋海军,签《马关》《辛","juan":"卷411","cite":"《清史稿》卷411·列传一百九十八(李鸿章传)","source":"liezhuan"},
{"year":1875,"guan":"两江总督/南洋大臣","ren":"刘坤一","dong":"东南互保,倡新政","juan":"卷413","cite":"《清史稿》卷413·列传二百(刘坤一传)","source":"liezhuan"},
{"year":1875,"guan":"湖广总督/体仁阁大学士","ren":"张之洞","dong":"办汉阳铁厂,倡中体西用","juan":"卷437","cite":"《清史稿》卷437·列传二百二十四(张之洞传)","source":"liezhuan"},
{"year":1875,"guan":"协办大学士/军机大臣","ren":"翁同龢","dong":"两朝帝师,户部尚书","juan":"卷436","cite":"《清史稿》卷436·列传二百二十三(翁同龢传)","source":"liezhuan"},
{"year":1875,"guan":"文华殿大学士/军机大臣","ren":"荣禄","dong":"督练武卫军,戊戌后掌枢","juan":"卷437","cite":"《清史稿》卷437·列传二百二十四(荣禄传)","source":"liezhuan"},
{"year":1875,"guan":"武英殿大学士/军机大臣","ren":"王文韶","dong":"军机大臣,一度署北洋","juan":"卷437","cite":"《清史稿》卷437·列传二百二十四(王文韶传)","source":"liezhuan"},
{"year":1875,"guan":"协办大学士/军机大臣","ren":"瞿鸿禨","dong":"军机大臣,丁未政潮","juan":"卷437","cite":"《清史稿》卷437·列传二百二十四(瞿鸿禨传)","source":"liezhuan"},
{"year":1875,"guan":"东阁大学士/户部尚书","ren":"阎敬铭","dong":"理财名臣","juan":"卷438","cite":"《清史稿》卷438·列传二百二十五(阎敬铭传)","source":"liezhuan"},
{"year":1875,"guan":"东阁大学士/军机大臣","ren":"鹿传霖","dong":"军机大臣","juan":"卷438","cite":"《清史稿》卷438·列传二百二十五(鹿传霖传)","source":"liezhuan"},
{"year":1875,"guan":"文渊阁大学士/管学大臣","ren":"孙家鼐","dong":"创办京师大学堂","juan":"卷443","cite":"《清史稿》卷443·列传二百三十(孙家鼐传)","source":"liezhuan"},
{"year":1875,"guan":"修订法律大臣/大理院正卿","ren":"沈家本","dong":"主持修订清末新律","juan":"卷443","cite":"《清史稿》卷443·列传二百三十(沈家本传)","source":"liezhuan"},
{"year":1875,"guan":"出使英法义比大臣","ren":"薛福成","dong":"外交家,早期维新思想家","juan":"卷446","cite":"《清史稿》卷446·列传二百三十三(薛福成传)","source":"liezhuan"},
{"year":1875,"guan":"两广总督","ren":"张树声","dong":"淮军督抚","juan":"卷447","cite":"《清史稿》卷447·列传二百三十四(张树声传)","source":"liezhuan"},
{"year":1875,"guan":"福建巡抚","ren":"丁日昌","dong":"洋务,筹办轮船电报","juan":"卷448","cite":"《清史稿》卷448·列传二百三十五(丁日昌传)","source":"liezhuan"},
{"year":1875,"guan":"湖南巡抚","ren":"吴大澂","dong":"金石学家,甲午率湘军出关","juan":"卷450","cite":"《清史稿》卷450·列传二百三十七(吴大澂传)","source":"liezhuan"},
{"year":1875,"guan":"翰林/会办福建海疆事宜","ren":"张佩纶","dong":"清流健将,马江之败获谴","juan":"卷444","cite":"《清史稿》卷444·列传二百三十一(张佩纶传)","source":"liezhuan"},
{"year":1875,"guan":"黑旗军首领/记名提督","ren":"刘永福","dong":"抗法援越,后守台湾","juan":"卷463","cite":"《清史稿》卷463·列传二百五十(刘永福传)","source":"liezhuan"},
{"year":1875,"guan":"首任新疆巡抚","ren":"刘锦棠","dong":"随左宗棠收复新疆,首任巡抚","juan":"卷454","cite":"《清史稿》卷454·列传二百四十一(刘锦棠传)","source":"liezhuan"},
{"year":1875,"guan":"甘军统领/武卫后军","ren":"董福祥","dong":"甘军统帅,庚子入卫","juan":"卷455","cite":"《清史稿》卷455·列传二百四十二(董福祥传)","source":"liezhuan"},
{"year":1875,"guan":"体仁阁大学士","ren":"徐桐","dong":"守旧,庚子祸首之一","juan":"卷465","cite":"《清史稿》卷465·列传二百五十二(徐桐传)","source":"liezhuan"},
{"year":1875,"guan":"军机大臣/协办大学士","ren":"刚毅","dong":"守旧派,庚子主战","juan":"卷465","cite":"《清史稿》卷465·列传二百五十二(刚毅传)","source":"liezhuan"},
{"year":1875,"guan":"两江总督/直隶总督","ren":"端方","dong":"出洋考察宪政,倡立宪","juan":"卷469","cite":"《清史稿》卷469·列传二百五十六(端方传)","source":"liezhuan"},
{"year":1875,"guan":"邮传部尚书","ren":"盛宣怀","dong":"办轮船招商局、铁路等实业","juan":"卷471","cite":"《清史稿》卷471·列传二百五十八(盛宣怀传)","source":"liezhuan"},
{"year":1876,"guan":"驻英公使","ren":"郭嵩焘","dong":"清朝首任驻外(英)公使","juan":"卷446","cite":"《清史稿》卷446·列传二百三十三(郭嵩焘传)","source":"liezhuan"},
{"year":1878,"guan":"陕甘总督/东阁大学士","ren":"左宗棠","dong":"率军收复新疆","juan":"卷412","cite":"《清史稿》卷412·列传一百九十九(左宗棠传)","source":"liezhuan"},
{"year":1880,"guan":"驻英俄公使","ren":"曾纪泽","dong":"改订中俄《伊犁条约》","juan":"卷446","cite":"《清史稿》卷446·列传二百三十三(曾纪泽传)","source":"liezhuan"},
{"year":1885,"guan":"广西关外军务帮办","ren":"冯子材","dong":"镇南关大捷破法军","juan":"卷459","cite":"《清史稿》卷459·列传二百四十六(冯子材传)","source":"liezhuan"},
{"year":1894,"guan":"致远舰管带","ren":"邓世昌","dong":"黄海海战力战殉国","juan":"卷460","cite":"《清史稿》卷460·列传二百四十七(邓世昌传)","source":"liezhuan"},
{"year":1894,"guan":"高州镇总兵","ren":"左宝贵","dong":"平壤之战殉国","juan":"卷460","cite":"《清史稿》卷460·列传二百四十七(左宝贵传)","source":"liezhuan"},
{"year":1895,"guan":"北洋海军提督","ren":"丁汝昌","dong":"威海卫战败自尽殉国","juan":"卷462","cite":"《清史稿》卷462·列传二百四十九(丁汝昌传)","source":"liezhuan"},
{"year":1898,"guan":"湖南巡抚","ren":"陈宝箴","dong":"推行湖南新政,变法支持者","juan":"卷464","cite":"《清史稿》卷464·列传二百五十一(陈宝箴传)","source":"liezhuan"},
{"year":1898,"guan":"军机章京","ren":"谭嗣同","dong":"戊戌变法六君子之一,就义","juan":"卷464","cite":"《清史稿》卷464·列传二百五十一(谭嗣同传)","source":"liezhuan"},
{"year":1898,"guan":"军机章京","ren":"杨锐","dong":"戊戌六君子之一","juan":"卷464","cite":"《清史稿》卷464·列传二百五十一(杨锐传)","source":"liezhuan"},
{"year":1898,"guan":"军机章京","ren":"刘光第","dong":"戊戌六君子之一","juan":"卷464","cite":"《清史稿》卷464·列传二百五十一(刘光第传)","source":"liezhuan"},
{"year":1898,"guan":"军机章京","ren":"林旭","dong":"戊戌六君子之一","juan":"卷464","cite":"《清史稿》卷464·列传二百五十一(林旭传)","source":"liezhuan"},
{"year":1900,"guan":"直隶提督","ren":"聂士成","dong":"天津抗八国联军殉国","juan":"卷467","cite":"《清史稿》卷467·列传二百五十四(聂士成传)","source":"liezhuan"},
{"year":1900,"guan":"总理衙门大臣/出使大臣","ren":"许景澄","dong":"谏阻攻使馆被杀","juan":"卷466","cite":"《清史稿》卷466·列传二百五十三(许景澄传)","source":"liezhuan"},
{"year":1900,"guan":"太常寺卿","ren":"袁昶","dong":"谏阻围攻使馆被杀","juan":"卷466","cite":"《清史稿》卷466·列传二百五十三(袁昶传)","source":"liezhuan"},
{"year":1907,"guan":"直隶总督/军机大臣","ren":"袁世凯","dong":"小站练兵,创北洋新军(无列传,见","juan":"卷177","cite":"《清史稿》卷177·表十七·军机大臣年表二(光绪三十三年授军机大臣)","source":"liezhuan"},
{"year":1909,"guan":"体仁阁大学士/军机大臣","ren":"世续","dong":"内务府大臣,顾命重臣","juan":"卷472","cite":"《清史稿》卷472·列传二百五十九(世续传)","source":"liezhuan"},
{"year":1909,"guan":"东阁大学士","ren":"陆润庠","dong":"状元,弼德院院长","juan":"卷472","cite":"《清史稿》卷472·列传二百五十九(陆润庠传)","source":"liezhuan"},
{"year":1909,"guan":"江南提督","ren":"张勋","dong":"效忠清室,后主复辟","juan":"卷473","cite":"《清史稿》卷473·列传二百六十(张勋传)","source":"liezhuan"},
{"year":1909,"guan":"军机大臣/内阁协理大臣","ren":"徐世昌","dong":"首任东三省总督,协理大臣(无列传","juan":"卷177","cite":"《清史稿》卷177·表十七·军机大臣年表二","source":"liezhuan"},
{"year":1911,"guan":"湖广总督","ren":"瑞澂","dong":"武昌起义时弃城逃","juan":"卷471","cite":"《清史稿》卷471·列传二百五十八(瑞澂传)","source":"liezhuan"},
{"year":1911,"guan":"庆亲王/内阁总理大臣","ren":"奕劻","dong":"军机领班,首任内阁总理大臣(无列","juan":"卷177","cite":"《清史稿》卷177·表十七·军机大臣年表二","source":"liezhuan"},
{"year":1912,"guan":"禁卫军统领/宗社党首","ren":"良弼","dong":"宗社党首领,被炸身亡","juan":"卷470","cite":"《清史稿》卷470·列传二百五十七(良弼传)","source":"liezhuan"},
];
// ══════════════════════════════════════════════════════════════════════════
export const CHAO_COVERAGE = {
benji: '帝表 12 帝(后金太祖/太宗前段 + 大清十帝)据《清史稿》本纪卷一~二五,年号/在位据本纪;改国号大清/入关/三藩/鸦片战争/太平天国/退位诸锚点已 curl 双证。',
shi: '大事 7 条:改国号大清/入关定鼎/三藩之乱/军机处/鸦片战争/太平天国/清帝逊位,皆本纪或职官志逐字双证。',
chen: '群臣 5 人(多尔衮/鳌拜/和珅/曾国藩/李鸿章)据列传卷次,各挂逐字锚点。',
caveat: '★《清史稿》为民国未定之稿、非正史,多回护清室之笔(如称太平天国「粤匪」「贼」);本表取其纪事年月、慎其褒贬论断,凡「贼/匪」等贬辞不入 gloss,只在原文 cite 中保留。',
};
export const NOT_DONE = [
{
id: 1,
title: '清 chao 为入局锚定而建,非全帝纪',
body:
'本文件是为「清正式接照命 Stage2·补入局」而建的聚焦帝表+大事+群臣(够入局旅程锚定),' +
'非《清史稿》全本纪志传。后续可按需增补大事(如统一台湾、摊丁入亩、甲午、戊戌、庚子)与群臣。',
},
{
id: 2,
title: '《清史稿》稿本性质与回护之笔',
body:
'《清史稿》係赵尔巽等民国初年未定之稿、非二十四史,且多回护清室、贬抑革命与民变(「粤匪」「逆」等)。' +
'本表据以纪事(年月/官职/事目),但不采其褒贬论断;gloss 用中性叙述,贬辞只在 cite 原文中保留、标明其出处性质。',
},
{
id: 3,
title: '后金二帝列帝而注明国号前',
body:
'太祖(后金天命)在 1636 建「大清」国号前,太宗前段(天聪)亦属后金。本表从《清史稿》列帝、标 houJin=true;' +
'SPAN 起年取 1636(改国号大清),与帝表「太祖属后金段」不矛盾——帝统追叙自太祖、国号确立于太宗崇德。',
},
];
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+240
View File
@@ -0,0 +1,240 @@
// 元 chao —— 帝表 + 大事 + 群臣(天下·历朝历代引擎)。
//
// ★★ 定位:纯数据。无 IO、无 DB、无 LLM。被 tianxia.js 扫入 CHAO['yuan'],供入局(rujuDashi)/同僚图谱/月旦评用。
// 尺子(选举志六轴门+族属门)在 src/xuan/yuan.js + src/chushen.js,本文件只管帝纪/大事/群臣的双证史料。
//
// ★★★ 史源(2026-07-30 curl zh.wikisource《元史》本纪/列传 action=raw 逐字核实;grep 卷内命中,cite 逐字复制):
// 帝表据《元史》本纪卷一~四七;大事、群臣各标卷次。前四汗(太祖~宪宗)在 1271 建「大元」国号前,
// 为大蒙古国之汗,《元史》立本纪为帝,本表列入帝表并注明其在国号前(reigned=true,qianKehan=true)。
//
// ⚠ 红线:不编未核之字;查不实宁可不写(史学红线)。年号/在位年据本纪,锚点句已 curl 双证(建国号大元/世祖崩/灭宋/元亡)。
// ══════════════════════════════════════════════════════════════════════════
// 帝表 —— 大蒙古国四汗(追叙)+ 大元十一帝。cite 为本纪逐字锚点或本纪卷次。
// ══════════════════════════════════════════════════════════════════════════
export const CHAO_DI = [
// ── 大蒙古国四汗(1271 建国号「大元」前;《元史》本纪列为帝) ──
{ miao: '太祖', shi: '法天啟運聖武皇帝', xing: '鐵木真(成吉思汗)', li: 1206, beng: 1227,
nianhao: [], juan: '卷一', cite: '《元史·太祖本紀》卷一(大蒙古国之立,前四汗之首)', qianKehan: true,
note: '★ 1206 建大蒙古国、上尊号成吉思汗。在「大元」国号(1271)前六十五年,本表列帝而注明其属国号前。' },
{ miao: '太宗', shi: '英文皇帝', xing: '窩闊台', li: 1229, beng: 1241,
nianhao: [], juan: '卷二', cite: '《元史·太宗本紀》卷二', qianKehan: true,
note: '★ 太宗朝始用耶律楚材以儒术治汉地、立课税所、行科举(戊戌选试)。' },
{ miao: '定宗', shi: '簡平皇帝', xing: '貴由', li: 1246, beng: 1248,
nianhao: [], juan: '卷二', cite: '《元史·定宗本紀》卷二', qianKehan: true },
{ miao: '憲宗', shi: '桓肅皇帝', xing: '蒙哥', li: 1251, beng: 1259,
nianhao: [], juan: '卷三', cite: '《元史·憲宗本紀》卷三', qianKehan: true,
note: '★ 宪宗崩于合州钓鱼山(伐宋军中),其弟忽必烈、阿里不哥争立,忽必烈胜而即位。' },
// ── 大元十一帝(世祖建国号「大元」1271) ──
{ miao: '世祖', shi: '聖德神功文武皇帝', xing: '忽必烈', li: 1260, beng: 1294,
nianhao: [{ hao: '中統', qi: 1260, zhi: 1264 }, { hao: '至元', qi: 1264, zhi: 1294 }],
juan: '卷四~十七', cite: '中統元年……即皇帝位(卷四);至元八年……建國號曰大元(卷七);三十一年……帝崩于紫檀殿(卷十七)',
zhengtong: true,
note: '★★ 元的真开国之君:1260 中统即位、1271 建国号大元、1276 入临安、1279 崖山灭宋一统。本朝一切制度(行省/钞法/科举筹备)皆起于世祖。' },
{ miao: '成宗', shi: '欽明廣孝皇帝', xing: '鐵穆耳', li: 1294, beng: 1307,
nianhao: [{ hao: '元貞', qi: 1295, zhi: 1297 }, { hao: '大德', qi: 1297, zhi: 1307 }],
juan: '卷十八~二十一', cite: '《元史·成宗本紀》卷十八~二十一', note: '★ 守成之主,「持盈守成」,大德间称治。' },
{ miao: '武宗', shi: '仁惠宣孝皇帝', xing: '海山', li: 1307, beng: 1311,
nianhao: [{ hao: '至大', qi: 1308, zhi: 1311 }], juan: '卷二十二~二十三', cite: '《元史·武宗本紀》卷二十二~二十三' },
{ miao: '仁宗', shi: '聖文欽孝皇帝', xing: '愛育黎拔力八達', li: 1311, beng: 1320,
nianhao: [{ hao: '皇慶', qi: 1312, zhi: 1313 }, { hao: '延祐', qi: 1314, zhi: 1320 }],
juan: '卷二十四~二十六', cite: '《元史·仁宗本紀》卷二十四~二十六',
note: '★★ 延祐二年(1315)首开科举(皇慶二年下诏、延祐首场),元科举自此始——然顶止从六品、时行时罢。' },
{ miao: '英宗', shi: '睿聖文孝皇帝', xing: '碩德八剌', li: 1320, beng: 1323,
nianhao: [{ hao: '至治', qi: 1321, zhi: 1323 }], juan: '卷二十七~二十八', cite: '《元史·英宗本紀》卷二十七~二十八',
note: '★ 至治新政、行助役法;1323 南坡之变被弑(御史大夫铁失等谋逆)。' },
{ miao: null, shi: null, xing: '也孫鐵木兒(泰定帝)', li: 1323, beng: 1328,
nianhao: [{ hao: '泰定', qi: 1324, zhi: 1328 }, { hao: '致和', qi: 1328, zhi: 1328 }],
juan: '卷二十九~三十', cite: '《元史·泰定帝本紀》卷二十九~三十',
note: '★ 泰定帝无庙号、无谥号——文宗即位后不为之上庙谥,《元史》以年号称「泰定帝」,本表 miao/shi 皆 null,以本名/泰定帝称。' },
{ miao: null, shi: null, xing: '阿速吉八(天順帝)', li: 1328, beng: 1328,
nianhao: [{ hao: '天順', qi: 1328, zhi: 1328 }], juan: '卷三十二(附)', cite: '《元史》卷三十二·文宗紀(天顺帝事附)',
note: '★ 天顺帝幼冲,即位于上都、在位约一月,两都之战败而不知所终,无庙谥。本表列而注明其未成一纪。' },
{ miao: '明宗', shi: '翼獻景孝皇帝', xing: '和世㻋', li: 1329, beng: 1329,
nianhao: [], juan: '卷三十一', cite: '《元史·明宗本紀》卷三十一',
note: '★ 天历之变:文宗让位迎兄明宗,明宗北还即位数月,至王忽察都暴崩(史多疑文宗弑之),文宗复位。' },
{ miao: '文宗', shi: '聖明元孝皇帝', xing: '圖帖睦爾', li: 1328, beng: 1332,
nianhao: [{ hao: '天曆', qi: 1328, zhi: 1330 }, { hao: '至順', qi: 1330, zhi: 1332 }],
juan: '卷三十二~三十六', cite: '《元史·文宗本紀》卷三十二~三十六',
note: '★ 文宗两即位(1328 天历、1329 让明宗后复位);修《经世大典》、建奎章阁,文治之主而得位不正。' },
{ miao: '寧宗', shi: '沖聖嗣孝皇帝', xing: '懿璘質班', li: 1332, beng: 1332,
nianhao: [{ hao: '至順', qi: 1332, zhi: 1332 }], juan: '卷三十七', cite: '《元史·寧宗本紀》卷三十七',
note: '★ 宁宗七岁即位、在位约五十三日而崩,元诸帝最幼且最短祚者。' },
{ miao: '惠宗', shi: '宣仁普孝皇帝(明朝諡「順帝」)', xing: '妥懽帖睦爾', li: 1333, beng: 1370,
nianhao: [{ hao: '元統', qi: 1333, zhi: 1335 }, { hao: '至元', qi: 1335, zhi: 1340 }, { hao: '至正', qi: 1341, zhi: 1368 }],
juan: '卷三十八~四十七', cite: '至正二十八年……大明兵……京城(卷四十七)', wangGuo: true,
note: '★★ 元末代帝,在位三十六年(元诸帝最长)。至正间黄河决、变钞法、贾鲁治河激民变,刘福通红巾大起。1368 大明兵入大都,帝北奔上都、应昌,1370 崩于应昌(北元)。庙号惠宗,明太祖以其「知顺天命、退避而去」谥「顺帝」。' },
];
// ══════════════════════════════════════════════════════════════════════════
// 大事 —— 事(shi)/制(zhi) 分层照全朝口径。原文逐字 curl 双证,cite 见 shi 字段末括号或 crossWitness。
// ══════════════════════════════════════════════════════════════════════════
export const CHAO_SHI = [
{ year: 1260, yue: '三月', shi: '中統元年三月,世祖即皇帝位于開平(后为上都),建元中統。忽必烈与幼弟阿里不哥争立,四年而胜。', juan: '卷四', layer: 'shi', kind: 'yuanwen',
zaichang: '一场兄弟争汗:宗王要选边站队、汉地世侯要献粮献兵(史天泽、张柔辈)、幕府文臣要草即位诏(王鹗、廉希宪)。你在这一刻押谁,定了往后二十年的荣枯。',
note: '★ 元真正的开国即位(早于国号11年)。忽必烈行汉法、都燕京,与守漠北旧俗的阿里不哥分道,是「以漢法治漢地」路线的胜利。' },
{ year: 1271, yue: '十一月', shi: '至元八年十一月,建國號曰大元,蓋取《易經》「大哉乾元」之義。大元朝立。', juan: '卷七', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《元史/卷7》action=raw 逐字命中「建國號曰大元」「可建國號曰大元,蓋取易經」(2026-07-30 curl);与《世祖本纪》改元至元、都大都一系事互证。',
zaichang: '国号确立这件大事,中书省要草建国诏颁行天下(刘秉忠等议定「大元」)、翰林院要撰册文、行省要奉新号、州县要改印信榜文。同一道诏,宰执在坛上定名,令史小吏在乡里换印。',
note: '★★ 中国史上第一个不用「汉、唐、宋」这类地名/封号、而取义经典的国号。刘秉忠建议、取《易》「大哉乾元」。这是元朝的第一天。' },
{ year: 1276, yue: '正月', shi: '至元十三年正月,丞相伯顏入臨安,宋主(恭帝)稱臣降,谢太后奉传国玺出降。南宋亡。', juan: '卷九', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《元史/卷9》action=raw 逐字命中「宋主稱臣降」「伯顏入臨安」(2026-07-30 curl);与《伯颜传》卷127灭宋事互证。',
zaichang: '灭一国、收一朝:主帅伯颜要受降、整军、护送宋室北迁;行省参政要籍其府库图籍;随军文臣要抚定江南新附州县;而江南的宋朝旧官,此刻要决「降元受官、还是隐遁殉节」。位阶越高,这道选择越重。',
note: '★★ 至元十三年入临安、十六年(1279)崖山陆秀夫负帝昺投海,南宋彻底亡、天下一统于元。伯颜「长于谋、纪律严明」,灭宋不妄杀,江南粗定。' },
{ year: 1277, yue: '', shi: '至元中,立行中書省于诸路——「行中書省,凡十〔一〕」,分镇方面,为一代方面之重。行省之制自此定。', juan: '卷九一', layer: 'zhi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《元史/卷91》百官志 action=raw 逐字命中「行中書省,凡十〔一〕」「分鎮方面」(2026-07-30 curl);与世祖本纪历年立省事互证。',
zaichang: '一套新的天下架构:行省丞相/平章要「分镇方面」总一方钱谷兵民、宣慰司要承转上下、路府州县要听行省节制、蒙古达鲁花赤要监临汉官。行省之下,你这一格是监临者、承转者,还是被临者?',
layerWhy: '行中书省是元创设、影响后世省制的根本官制(「制」——官制之确立),故 layer=zhi。',
impact: { reach: 'houshi', kind: 'tuiyan', reason: '行省(省)之名与「分镇方面、军民财赋合一」之制为元创,明清「布政使司/省」及今日「省」之建置一脉相承(制度史通说)。', to: [{ label: '明清行省→今日「省」建置(省制之源,通说)' }], reachWhy: '行省之制存续至今,影响半径「后世」。' },
note: '★ 元行省是中国省制的源头。本表只记「行省为元创、后世省制承之」这一通说事实,不编逐条因果。' },
{ year: 1279, yue: '二月', shi: '至元十六年二月,張弘範追宋帝于崖山(厓山),火攻断汲、宋师大溃,丞相陸秀夫負帝昺投海死,后宫诸臣及军民从死者十余万。南宋彻底亡,天下尽入于元。', juan: '卷一五六', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《元史/卷156》张弘范传 action=raw 逐字命中「崖山」(2026-07-30 curl);与世祖本纪至元十六年灭宋事互证。★ 陆秀夫负帝投海本诸《宋史》瀛国公纪·二王附(宋方之证),非元史臆造。',
zaichang: '一场灭国的海战、一个王朝的殉亡:元帅张弘范要合围火攻、断其汲道;宋丞相陆秀夫要背八岁帝昺投海全节、十万军民蹈海随之。同一片崖山海面,胜者刻石「张弘范灭宋于此」,败者以身殉一个已亡的天下。',
note: '★★ 崖山之战是南宋的终局,也是中国史最悲壮的名场面之一:陆秀夫负帝投海、宋室十万人蹈海殉国。「崖山之后」四字,成了一个时代的句点。' },
{ year: 1281, yue: '八月', shi: '至元十八年,二次征日本(弘安之役),范文虎率江南軍十萬渡海,遇暴風破舟,軍溃十不存一。元两征日本皆败于「神风」。', juan: '卷二〇八', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《元史/卷208》外夷·日本传 action=raw 逐字命中「范文虎」「十萬」「暴風破舟」(2026-07-30 curl)。',
zaichang: '一场跨海的远征、一场天意的覆师:主帅范文虎要统江南十万舟师渡海、诸翼万户各领其军、士卒在陌生的海上搏命。而一夜暴风破舟、十万之众十不存一——这一格的荣枯,系于一场谁也算不到的台风。',
note: '★★ 忽必烈两征日本(1274 文永之役、1281 弘安之役)皆遇台风覆师,日本谓之「神风」(かみかぜ)。元的铁骑第一次被大海挡回——「神风」一词即由此而来。' },
{ year: 1294, yue: '正月', shi: '至元三十一年正月,世祖崩于紫檀殿,年八十。皇孙鐵穆耳(成宗)即位。', juan: '卷一七', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《元史/卷17》action=raw 逐字命中「三十一年」「帝崩于紫檀殿」(2026-07-30 curl)。',
zaichang: '开国雄主骤逝、皇孙远在漠北:宗室诸王要会集上都忽里台推戴、中书宰执(完泽、不忽木)要秉政待新君、御史台要防变。世祖之后,谁能弹压诸王、稳住这个横跨欧亚的帝国,是全局的关节。',
note: '★ 世祖在位三十五年,元诸帝最久。其崩后成宗守成,元入「守成」之世。' },
{ year: 1315, yue: '二月', shi: '延祐二年,首行科舉之制——「每三歲一次開試」,右榜蒙古色目、左榜漢人南人分卷,第一名賜進士及第從六品。元科举始。', juan: '卷八一', layer: 'zhi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《元史/卷81》选举志 action=raw 逐字命中「(皇慶二年/延祐)科場,每三歲一次開試」「蒙古、色目人作一榜,漢人、南人作一榜」「第一名賜進士及第,從六品」(2026-07-30 curl);与仁宗本纪皇庆二年诏、延祐二年廷试事互证。',
zaichang: '一场迟到六十年的科举:应试的儒生要赴乡试会试廷试、考官要按四等人分卷阅卷(右榜从宽、左榜加严)、中书省要按榜授官(第一名也才从六品)。而元入仕的主河道仍是吏道与根脚——科举于此只是名义正途。你若是汉南儒生,这条路终于开,却顶得极低。',
layerWhy: '科举是取士之制的确立,故 layer=zhi。',
note: '★★ 元科举「时行时罢」:延祐二年首开、至元(后)间一度罢、又复,终元十六科、取士千余人。顶止从六品,是全表最低的正途顶——这正是元命「科举非门、根脚是门」的注脚(见 src/xuan/yuan.js)。' },
{ year: 1351, yue: '五月', shi: '至正十一年五月,脩黃河(賈魯治河)大役方興,潁州劉福通等以红巾起兵、奉韓山童「復宋」,天下大乱之始。', juan: '卷四二', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《元史/卷42》顺帝本纪 action=raw 逐字命中「潁州」「劉福通」(2026-07-30 curl);与《贾鲁传》治河、《脱脱传》事互证。',
zaichang: '治河变钞、民不聊生:脱脱主政要发民夫十五万治黄河、要行至正交钞;红巾一起,行省要发兵剿捕、义军要蜂起响应(徐寿辉、张士诚、朱元璋相继);而黄河沿岸征来的民夫,正是「石人一只眼,挑动黄河天下反」的火种。你这一格,是征夫者、剿捕者、还是被征的民?',
note: '★★ 元亡之始。黄河决、贾鲁治河聚众十五万,脱脱变钞钞法大坏,民怨沸腾,红巾以「复宋」相号召,遂成燎原。' },
{ year: 1368, yue: '闰七月', shi: '至正二十八年闰七月,大明兵逼京城(大都),帝夜半开健德门北奔上都。元亡(退居漠北为北元)。', juan: '卷四七', layer: 'shi', kind: 'yuanwen',
crossWitness: '★ 双证:zh.wikisource《元史/卷47》顺帝本纪 action=raw 逐字命中「至正二十八年」「大明兵」「京城」(2026-07-30 curl)。',
zaichang: '国亡这一夜:宿卫要护驾北奔、宗室后妃要随行出塞、留守大臣要决「死守、迎降、还是扈从」(左丞相失列门等扈从,淮王帖木儿不花留守被杀)。同一座将陷的大都,天子北去,百官各自决生死去就。',
note: '★★ 元自 1271 建国号至 1368 退出大都,凡九十八年;世祖至元十六年一统至此,八十九年。顺帝北奔后仍奉元号,史称北元。' },
{
"year": 1234,
"yue": "",
"shi": "元太宗窝阔台六年甲午(1234)正月,蒙将塔察儿与宋将孟珙联兵围金于蔡州,金哀宗自缢、末帝完颜承麟死于乱军,城拔金亡,蒙宋自此由盟转战。",
"juan": "卷二(太宗纪)",
"layer": "shi",
"kind": "yuanwen",
"crossWitness": "★ 双证:zh.wikisource《元史/卷二(太宗纪)》action=raw 逐字含「金亡」(2026-07-30 curl;详见入局 people 之 cite)",
"zaichang": "围蔡州的这年冬春:蒙古宗王、万户在城外总揽合围,兼断灭金后如何待宋;史天泽、张柔这类汉军世侯各将一军,亲当攻城锋面;随军汉军万户、千户督队填壕、先登破城;城下军士拼死苦战。城破之日,金主自焚、末帝被擒杀,有人请功、有人约束不杀、有人乘乱劫库。",
"note": "★ 这是蒙古灭掉的第一个中原大王朝,也是「联宋灭金」的收局。金亡当天,宋兵取金主余骨以归;盟约随即作废,蒙宋由盟转战——一朝之亡,接的正是另一场更大战争的开幕。"
},
{
"year": 1259,
"yue": "",
"shi": "元宪宗蒙哥九年己未(1259),大汗亲征攻宋、顿兵合州钓鱼山城下,南宋王坚婴城死守不能拔,帝竟崩于军中——遂有旭烈兀停西征回师、忽必烈与阿里不哥争汗位,世界战局为之一变。",
"juan": "卷三(宪宗纪)",
"layer": "shi",
"kind": "yuanwen",
"crossWitness": "★ 双证:zh.wikisource《元史/卷三(宪宗纪)》action=raw 逐字含「合州」(2026-07-30 curl;详见入局 people 之 cite)",
"zaichang": "同一座打不下的孤城下:宗王随大汗亲征,议的是强攻还是回师;万户督队一次次仰攻钓鱼山天险,填进去的是整军的伤亡;汉军将、千户分守各营,昼夜转运炮石;军士爬城又退、暑疫交攻;军前民夫、驱丁搬石汲水,死于疫者众。而大汗一崩,这满城的进退,一夜之间都成了各奔汗位的马蹄。",
"note": "★「上帝折鞭处」:一座孤城拖住席卷欧亚的天下强兵,大汗竟崩于城下——旭烈兀停西征、忽必烈与阿里不哥争位,一场围城改写世界走向。蒙哥死因《元史》仅记「崩于釣魚山」,中炮伤、病二说并存,此处严从正史不遽定。"
},
{
"year": 1328,
"yue": "",
"shi": "致和元年(1328),泰定帝崩于上都。权臣燕铁木儿在大都拥立武宗子图帖睦尔(文宗),与上都倒剌沙所立天顺帝(阿速吉八)两都对峙内战;燕铁木儿破上都,天顺帝不知所终,文宗立。",
"juan": "卷一三八(燕铁木儿传)",
"layer": "shi",
"kind": "yuanwen",
"crossWitness": "★ 双证:zh.wikisource《元史/卷一三八(燕铁木儿传)》action=raw 逐字含「上都」(2026-07-30 curl;详见入局 people 之 cite)",
"zaichang": "同一场两都兵变:宗王、枢密院执政在定策立君——附大都拥文宗,还是奉上都立天顺帝,一念定荣枯;御史台省院百官奉着两京各下的诏书,各决扈从与向背;卫军的将提兵厮杀、要亲手去凿开上都的城墙;军士在两京之间反复转战求生;编户避兵闭户,只盼这场内战早点了结。",
"note": "★ 元中期最大的宫廷兵变:泰定帝崩于上都,两京各立一君、兵戎相见。谁当皇帝不看血脉、看谁手里握着卫军——燕铁木儿破上都、天顺帝不知所终,一场政变把「立君」二字明晃晃摆到了台面上。"
},
];
// ══════════════════════════════════════════════════════════════════════════
// 群臣 —— 入局同僚图谱之料。cite 为本纪/列传逐字锚点,source 标 benji/liezhuan。
// ══════════════════════════════════════════════════════════════════════════
export const CHAO_CHEN = [
{ year: 1231, guan: '中書令', ren: '耶律楚材', dong: '用事', juan: '卷一四六', cite: '耶律楚材,字晉卿……(太宗朝领中书省事,立课税所、行选试)', source: 'liezhuan',
note: '★ 辽东丹王八世孙、金末仕燕京。太宗朝以儒术治汉地,立十路征收课税所、行戊戌选试(1238),元初汉法之柱。附于太宗/前段。' },
{ year: 1271, guan: '太保、议建国号', ren: '劉秉忠', dong: '定策', juan: '卷一五七', cite: '劉秉忠字仲晦……(建议建国号大元、定都大都、立朝仪官制)', source: 'liezhuan',
note: '★ 世祖首席谋臣(僧子聪还俗)。建议取《易》「大哉乾元」为国号、相地建大都、定官制朝仪,元开国规模多出其手。' },
{ year: 1276, guan: '中書右丞相', ren: '伯顏', dong: '灭宋', juan: '卷一二七', cite: '伯顏,蒙古……(至元十三年为伐宋大军统帅,入临安、受宋降)', source: 'liezhuan',
note: '★ 蒙古八邻部人。灭宋主帅,「长于谋、军纪严」,入临安不妄杀,江南粗定。世祖朝第一功臣。' },
{ year: 1282, guan: '中書平章政事', ren: '阿合馬', dong: '专政·被杀', juan: '卷二〇五', cite: '阿合馬,回(紇)……(世祖朝理财权臣,专政聚敛,至元十九年为王著、高和尚所杀)', source: 'liezhuan',
note: '★ 《元史·奸臣传》首。世祖朝理财、立尚书省聚敛,权倾一时,至元十九年被王著锤杀。元「理财之臣」与「聚敛之弊」的样本。' },
{ year: 1341, guan: '中書右丞相', ren: '脫脫', dong: '更化·治河', juan: '卷一三八', cite: '脫脫,字大用……(顺帝朝两为右丞相,行至正更化、修辽金宋三史、主治黄河)', source: 'liezhuan',
note: '★★ 顺帝朝贤相。至正更化(恢复科举、修三史)、主贾鲁治河,然变钞与治河聚众成民变之媒;后为政敌所构、贬死。元末最后一位能臣。' },
{"year":1206,"guan":"左右万户,广平王","ren":"博尔术","dong":"从太祖保驾,开国四杰(四骏)之一","juan":"卷119","cite":"《元史》卷119《博尔术传》:与木华黎并为左右万户","source":"liezhuan"},
{"year":1206,"guan":"第一千户","ren":"博尔忽","dong":"事太祖为第一千户,开国四杰之一,","juan":"卷119","cite":"《元史》卷119《博尔忽传》","source":"liezhuan"},
{"year":1206,"guan":"河北西路兵马都元帅","ren":"史天倪","dong":"募清乐军攻克大名绛州,后为武仙所","juan":"卷147","cite":"《元史》卷147《史天倪传》","source":"liezhuan"},
{"year":1217,"guan":"太师国王,都行省承制行事","ren":"木华黎","dong":"佐太祖专征伐,策马陷阵经略中原","juan":"卷119","cite":"《元史》卷119《木华黎传》:封太师、国王,承制行事","source":"liezhuan"},
{"year":1229,"guan":"中书左丞相","ren":"粘合重山","dong":"与耶律楚材并相,佐建官立法定课赋","juan":"卷146","cite":"《元史》卷146《粘合重山传》:与楚材同拜相","source":"liezhuan"},
{"year":1229,"guan":"太师,鲁国王","ren":"孛鲁","dong":"木华黎子,袭国王爵,平山东李全","juan":"卷119","cite":"《元史》卷119(木华黎附子孛鲁传)","source":"liezhuan"},
{"year":1229,"guan":"荣禄大夫,河北东西路都元帅","ren":"张柔","dong":"屡败武仙,攻下汴京,参与灭金伐宋","juan":"卷147","cite":"《元史》卷147《张柔传》","source":"liezhuan"},
{"year":1229,"guan":"便宜总帅,陇西公","ren":"汪世显","dong":"金末降元,从皇子阔端南征取蜀诸城","juan":"卷155","cite":"《元史》卷155《汪世显传》","source":"liezhuan"},
{"year":1260,"guan":"中书右丞相,太师,平章军国重事","ren":"史天泽","dong":"平李璮之叛,伐宋取襄阳,出入将相","juan":"卷155","cite":"《元史》卷155《史天泽传》:拜中书右丞相","source":"liezhuan"},
{"year":1260,"guan":"中书平章政事","ren":"廉希宪","dong":"平定关中、镇守荆南,力抑权臣阿合","juan":"卷126","cite":"《元史》卷126《廉希宪传》","source":"liezhuan"},
{"year":1260,"guan":"中书右丞相,太傅,知枢密院事","ren":"伯颜","dong":"统军灭南宋,兵不血刃入临安","juan":"卷127","cite":"《元史》卷127《伯颜传》(八邻部)","source":"liezhuan"},
{"year":1260,"guan":"中书左丞相","ren":"阿术","dong":"围攻襄樊断浮桥,控制淮南助平宋","juan":"卷128","cite":"《元史》卷128《阿术传》","source":"liezhuan"},
{"year":1260,"guan":"湖广行省左丞相","ren":"阿里海牙","dong":"献新炮法破樊城,以口舌招降荆湖广","juan":"卷128","cite":"《元史》卷128《阿里海牙传》","source":"liezhuan"},
{"year":1260,"guan":"江淮行省左丞相","ren":"相威","dong":"平定江南,劾阿合马不法,谏罢再征","juan":"卷128","cite":"《元史》卷128《相威传》","source":"liezhuan"},
{"year":1260,"guan":"中书左丞相","ren":"耶律铸","dong":"耶律楚材子,领中书省,定法令三十","juan":"卷146","cite":"《元史》卷146(耶律楚材附子耶律铸传)","source":"liezhuan"},
{"year":1260,"guan":"中书平章政事","ren":"粘合南合","dong":"粘合重山子,预言李璮必叛,后言验","juan":"卷146","cite":"《元史》卷146(附粘合南合传)","source":"liezhuan"},
{"year":1260,"guan":"太保,参领中书省事","ren":"刘秉忠","dong":"采旧典参古制建元初政制,谋定国号","juan":"卷157","cite":"《元史》卷157《刘秉忠传》","source":"liezhuan"},
{"year":1260,"guan":"枢密副使,御史中丞","ren":"张文谦","dong":"主张减赋恤民,通术数义理之学","juan":"卷157","cite":"《元史》卷157《张文谦传》","source":"liezhuan"},
{"year":1260,"guan":"翰林侍读学士,国信使","ren":"郝经","dong":"奉使南宋被羁留十六年不屈,著书数","juan":"卷157","cite":"《元史》卷157《郝经传》","source":"liezhuan"},
{"year":1260,"guan":"中书左丞","ren":"姚枢","dong":"向世祖献治国八目,主用人唯贤佐治","juan":"卷158","cite":"《元史》卷158《姚枢传》","source":"liezhuan"},
{"year":1260,"guan":"集贤大学士兼国子祭酒","ren":"许衡","dong":"创立蒙古国学,与郭守敬合修《授时","juan":"卷158","cite":"《元史》卷158《许衡传》","source":"liezhuan"},
{"year":1260,"guan":"昭文馆大学士","ren":"窦默","dong":"直言进谏,反对王文统专权,倡三纲","juan":"卷158","cite":"《元史》卷158《窦默传》","source":"liezhuan"},
{"year":1260,"guan":"中书平章政事","ren":"阿合马","dong":"专掌财赋立制国用使司增赋,后专权","juan":"卷205","cite":"《元史》卷205《奸臣·阿合马传》","source":"liezhuan"},
{"year":1260,"guan":"中书左丞,梁国公","ren":"汪良臣","dong":"管理军粮转运,平叛略巴蜀,任四川","juan":"卷155","cite":"《元史》卷155(汪世显附子汪良臣传)","source":"liezhuan"},
{"year":1260,"guan":"御史大夫,太师","ren":"玉昔帖木儿","dong":"率军平定乃颜之乱,督修通惠河","juan":"卷119","cite":"《元史》卷119(附玉昔帖木儿传)","source":"liezhuan"},
{"year":1260,"guan":"中书平章政事","ren":"阿鲁浑萨理","dong":"通诸学多语,请建集贤馆待士,弥缝","juan":"卷130","cite":"《元史》卷130《阿鲁浑萨理传》","source":"liezhuan"},
{"year":1260,"guan":"同知枢密院事,钦察亲军都指挥使","ren":"土土哈","dong":"统钦察骑兵屡败叛王,镇守北边","juan":"卷128","cite":"《元史》卷128《土土哈传》","source":"liezhuan"},
{"year":1265,"guan":"中书右丞相","ren":"安童","dong":"少年拜相,谏止滥杀阿里不哥党羽","juan":"卷126","cite":"《元史》卷126《安童传》:至元二年拜中书右丞相","source":"liezhuan"},
{"year":1284,"guan":"中书右丞","ren":"卢世荣","dong":"推行钞法盐酒课等理财新政,旋以言","juan":"卷205","cite":"《元史》卷205《奸臣·卢世荣传》","source":"liezhuan"},
{"year":1287,"guan":"尚书右丞相,宣政使","ren":"桑哥","dong":"立尚书省钩考钱谷,专权黩货抑御史","juan":"卷205","cite":"《元史》卷205《奸臣·桑哥传》","source":"liezhuan"},
{"year":1294,"guan":"中书右丞相","ren":"完泽","dong":"革桑哥弊政,请蠲免中统以来积岁逋","juan":"卷130","cite":"《元史》卷130《完泽传》","source":"liezhuan"},
{"year":1294,"guan":"中书平章政事","ren":"不忽木","dong":"儒臣屡直谏止冤狱,阻征交趾促其内","juan":"卷130","cite":"《元史》卷130《不忽木传》","source":"liezhuan"},
{"year":1294,"guan":"中书平章政事","ren":"彻里","dong":"力陈桑哥罪状促其被诛,治江浙水利","juan":"卷130","cite":"《元史》卷130《彻里传》","source":"liezhuan"},
{"year":1294,"guan":"中书右丞相,太傅,录军国重事","ren":"哈剌哈孙","dong":"平定内难稳定政局定策迎立武宗,建","juan":"卷136","cite":"《元史》卷136《哈剌哈孙传》","source":"liezhuan"},
{"year":1294,"guan":"知枢密院事,平章政事","ren":"床兀儿","dong":"土土哈子,领钦察军拒海都都哇,促","juan":"卷128","cite":"《元史》卷128(土土哈附子床兀儿传)","source":"liezhuan"},
{"year":1307,"guan":"中书右丞相,康国公","ren":"阿沙不花","dong":"平乃颜之乱稳定北边,敢言直谏武宗","juan":"卷136","cite":"《元史》卷136《阿沙不花传》","source":"liezhuan"},
{"year":1307,"guan":"中书左丞相","ren":"康里脱脱","dong":"佐武宗即位,历江浙行省左丞相,中","juan":"卷138","cite":"《元史》卷138《康里脱脱传》","source":"liezhuan"},
{"year":1307,"guan":"太师,淇阳王","ren":"月赤察儿","dong":"从征海都、笃哇诸叛王,平定北边","juan":"卷119","cite":"《元史》卷119(附月赤察儿传)","source":"liezhuan"},
{"year":1311,"guan":"中书右丞相,太师","ren":"铁木迭儿","dong":"两度为相,怙势贪虐诬陷忠良,死后","juan":"卷205","cite":"《元史》卷205《奸臣·铁木迭儿传》","source":"liezhuan"},
{"year":1312,"guan":"中书平章政事,集贤大学士","ren":"李孟","dong":"劝仁宗入宫定内难,为相节赐与重名","juan":"卷175","cite":"《元史》卷175《李孟传》","source":"liezhuan"},
{"year":1322,"guan":"中书右丞相,太师,监修国史","ren":"拜住","dong":"整肃吏治抗铁木迭儿党,南坡之变遇","juan":"卷136","cite":"《元史》卷136《拜住传》","source":"liezhuan"},
{"year":1324,"guan":"中书平章政事","ren":"张珪","dong":"名将张弘范之子,以廉直屡谏,上疏","juan":"卷175","cite":"《元史》卷175《张珪传》","source":"liezhuan"},
{"year":1324,"guan":"中书平章政事","ren":"敬俨","dong":"廉洁不贪,屡劾奸整饬官吏","juan":"卷175","cite":"《元史》卷175《敬俨传》","source":"liezhuan"},
{"year":1328,"guan":"中书右丞相,太师,太平王","ren":"燕铁木儿","dong":"率勇士入兴圣宫定策迎立文宗,平上","juan":"卷138","cite":"《元史》卷138《燕铁木儿传》","source":"liezhuan"},
{"year":1329,"guan":"陕西行台中丞","ren":"张养浩","dong":"关中大旱奉命赈灾散家财,到官四月","juan":"卷175","cite":"《元史》卷175《张养浩传》","source":"liezhuan"},
{"year":1340,"guan":"中书右丞相,太师,太傅","ren":"脱脱","dong":"罢黜伯颜,复科举、修辽金宋三史、","juan":"卷138","cite":"《元史》卷138《脱脱传》","source":"liezhuan"},
];
// ══════════════════════════════════════════════════════════════════════════
export const CHAO_COVERAGE = {
benji: '帝表 15 帝(大蒙古国四汗 + 大元十一帝)据《元史》本纪卷一~四七,年号/在位据本纪,建国号大元/世祖崩/灭宋/元亡四锚点已 curl 双证。',
shi: '大事 8 条:中统即位/建国号大元/灭南宋/行省制/世祖崩/延祐科举/至正红巾/元亡,皆本纪或选举志逐字双证。',
chen: '群臣 5 人(耶律楚材/刘秉忠/伯颜/阿合马/脱脱)据列传卷次,各挂逐字锚点。',
};
export const NOT_DONE = [
{
id: 1,
title: '元 chao 为入局锚定而建,非全帝纪',
body:
'本文件是为「元正式接照命 Stage2·补入局」而建的聚焦帝表+大事+群臣(够入局旅程锚定),' +
'非《元史》全本纪论赞群臣传。后续可按需增补大事(如至元钞法、成宗守成、天历之变细目)与群臣。',
},
{
id: 2,
title: '前四汗列帝而注明国号前',
body:
'太祖~宪宗为大蒙古国之汗,在 1271 建「大元」国号前。《元史》立本纪为帝,本表从之列入帝表并标 qianKehan=true;' +
'SPAN 起年取 1271(建国号大元),与帝表「四汗在国号前」不矛盾——帝统追叙自成吉思汗,国号确立于世祖。',
},
{
id: 3,
title: '泰定帝/天顺帝无庙谥',
body: '泰定帝、天顺帝均无庙号谥号(文宗即位后不为之上),本表 miao/shi 皆 null、以本名/年号称,不代拟——「没上庙谥」本身是史料。',
},
];
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+75
View File
@@ -0,0 +1,75 @@
/**
* 二创·创作层 —— 方向体系 + 题目派生 + 工具推荐 + 生成 spec 映射。
* 老板定的完整链路:选场景(锚点)→选题目→选方向→给素材┃自己创作/选国内模型AI创作→成果。
* 本模块管中间三段"我们准备好"的部分:方向(26条4大类) / 题目(从素材包派生) / 工具(哪家强推哪家·仅国内)。
*
* ★ 数据来自 chuangzuoData.js(工作流授权 + 对抗校验修正)。这里只做纯逻辑:分组/派生/映射,零 LLM。
*/
import { DIRECTIONS, TOOLS, TUITI_PATTERNS, CRAFT } from './chuangzuoData.js';
export { DIRECTIONS, TOOLS, CRAFT, TUITI_PATTERNS };
const DIR = Object.fromEntries(DIRECTIONS.map((d) => [d.key, d]));
export function getDirection(k) { return DIR[String(k || '')] || null; }
const FAM_ORDER = ['小说', '剧本', '美术', '视频', '音乐'];
/** 创作方向·按家族分组(前端四类 tab)。 */
export function listDirections() {
const byFam = {};
for (const d of DIRECTIONS) (byFam[d.family] ??= []).push({ key: d.key, label: d.label, blurb: d.blurb, genre: d.genre, craftMaterials: d.craftMaterials, outputKind: d.outputKind, instrumental: !!d.instrumental, hot: !!d.hot });
return FAM_ORDER.filter((f) => byFam[f]).map((f) => ({ family: f, items: byFam[f] }));
}
// 生成层:叙事小说走【句级出处分层 segment】;诗词/剧本/美术/视频脚本走【整篇 work】。
const SEGMENT_KEYS = new Set(['lianai', 'chuanyue', 'quanmou', 'gongdou', 'shuangwen', 'xuanyi', 'zhanghui']);
export function genLayerForDirection(d) { return d && d.outputKind === 'text' && SEGMENT_KEYS.has(d.key) ? 'segment' : 'work'; }
/** 方向 → 生成 spec(喂给 erchuang 的 prompt 构造器;与 STYLES 条目同构)。 */
export function specFromDirection(d) {
if (!d) return null;
return { key: d.key, label: d.label, hint: d.blurb, genre: d.genre, layer: genLayerForDirection(d), outputKind: d.outputKind, eye: null, dialogue: false };
}
// 各产出类型对应的推荐工具模态("哪家强推哪家"·仅国内·我们不代生成)。
const KIND_MODALITIES = {
text: ['文字/小说创作LLM'],
script: ['文字/小说创作LLM', 'AI配音TTS'],
art: ['AI生图', 'AI漫画/条漫', '长图/排版'],
video: ['AI短视频生成', 'AI配音TTS', '长图/排版'],
music: ['AI音乐/演唱'], // 词我们产,曲/唱拿去国内AI音乐工具生成
};
export function toolsForOutputKind(kind) {
const want = KIND_MODALITIES[kind] || ['文字/小说创作LLM'];
return TOOLS.filter((m) => want.includes(m.modality));
}
// ── 题目派生 —— 用素材包的真实数据(在场真人/节点/缝隙/器物)实例化 TUITI 模板,
// 给用户几个【具体切入点】,免得面对一整场空想。纯查表,无 LLM。
export function deriveTuiti(pack) {
if (!pack) return [];
const sk = pack.skeleton || {};
const nodes = sk.nodes || [], roster = sk.roster || [], gaps = pack.gaps || [];
const title = pack.meta?.title || '这一段';
const ev0 = nodes[0]?.event || title;
const evMid = nodes[Math.floor(nodes.length / 2)]?.event || ev0;
const qiwu = (sk.qiwu?.[0]?.item) || (sk.qiwuText ? String(sk.qiwuText).split(/[、,,((]/)[0] : null);
const out = [];
const push = (id, from, text) => { if (text && out.length < 8) out.push({ id, from, text }); };
if (roster[0]) push('t-inhabit', 'roster', `以${roster[0].name}的身份,亲历${title}——他的官职、结局都已由正史锁定,你只补史书没写的那一段。`);
const eye = pack.eye || roster[roster.length - 1];
if (eye) push('t-irony', 'eye', `写${eye.name}在${title}里的那一刻——你已经知道他的结局,他还不知道。`);
// 缝隙(两段之间 / 名场面单条留白)
gaps.slice(0, 2).forEach((g, i) => {
const b = g.between || [];
if (b.length >= 2) push('t-gap' + i, 'gap', `「${b[0]}」与「${b[1]}」之间,史书空了一笔——那段时间到底发生了什么,由你来写。`);
else if (b[0]) push('t-gap' + i, 'gap', `史书留白:${String(b[0]).replace(/——.*$/, '')}——由你来写。`);
});
if (roster.length >= 2) push('t-dialogue', 'roster', `${roster[0].name}与${roster[1].name}的一次对谈:史书没记他们当时说了什么,你来写这场对话。`);
if (qiwu) push('t-object', 'gap', `一件${qiwu}(正史确有之物):围绕它,写${title}里与它相关的一段——它怎么来的、经谁的手、那一刻在谁手里。`);
push('t-node', 'node', `就写这一刻:${evMid}——把时间线上这一条史实,铺成一个完整的场面。`);
// 反派/败方视角
const foil = roster.find((p) => p.fate && /死|败|诛|杀|废|贬|囚|自刎|坠|虏/.test(p.fate)) || roster[roster.length - 1];
if (foil) push('t-reversal', 'reversal', `换一个人站着看:以${foil.name}的立场,重写${title}——史书斥为反派或败方的那一方,也有他自己的道理。`);
return out.slice(0, 8);
}
File diff suppressed because it is too large Load Diff
+941
View File
@@ -0,0 +1,941 @@
// 出身 —— 「这一部传里,你生在哪」。
//
// 定位:与 dynasties.js / chiduo.js / shiliao.js 同级的【纯常量表 + 纯函数】。无 IO、无 DB、无 LLM。
//
// ⚠ 与 chiduo.js 的【分工不同,别混】:chiduo.js 自述「不改 computeRank 的行为,也不被它调用」,
// 它是一张【提案表】。本文件【正相反:它被 computeRank 调用,它改行为】——因为出身是个【闸】。
// 所以它必须单独成文件,不能并进 chiduo.js:一个「不被引擎调用」的文件里,
// 躺着一条引擎必须调用的闸,是自相矛盾的。(老板给了「或并入 chiduo.js,你判断」——判断在这里。)
//
// ══════════════════════════════════════════════════════════════════════════
// 【合规 —— 这一段是本文件存在方式的前提,改任何一行之前先读完】
//
// ★★★ 出身【不是用户的属性,是这部书的设定】。
// · 分身是【无籍之魂】:没有生辰、没有籍贯、不在任何名录里,正因无籍,才能入任何人的身。
// · 你选了辽 →「那么这一部传里,你生在契丹,还是生在汉?」——
// 这【和选朝代是同一类动作】:是给这一部传定一个开局,
// 【不是在采集用户的民族】(那是 PIPL 第 28 条的敏感个人信息)。
//
// ⚠⚠ 数据上必须体现这一点,三条,一条都不许破:
// ① 出身存在【这一部传(lineage)】上 —— decisions.js 的 openLineage 收它、存它。
// ② 【绝不许往 twins 集合加字段】(CLAUDE.md 红线)。也绝不许把它当用户画像的一部分:
// 不进 memories、不进 RAG、不进 soul、不进 insight。
// ③ 同一个分身开两部传,可以【一部是契丹、一部是汉】—— 因为那是两本书的设定,不是两个人。
// (lineage.id = subjectId/dynasty/edition,edition 递增,两部传天然不冲突。)
//
// ⚠ 文案红线:【不许出现「你的民族」「你的出身是」这种指向用户本人的措辞】。
// 正确的是【「这一部传里,你生在……」】。本文件所有面向用户的串(label/ask/note)都照这个写,
// 有断言守着(test/chushen.test.js 扫的是【数据】,不是注释)。
//
// ══════════════════════════════════════════════════════════════════════════
// 【头号结论 —— 老板的前提对了一半,另一半被原文推翻,逐条报在这里】
//
// 老板给的猜想是:「魏晋/南朝 = 门第;辽 = 契丹/汉人;唐 = 良/贱?;明 = 军户/民户/匠户?;汉 = ?」
// 七朝逐条 curl 目验(双证)之后:
//
// ① ★ 辽【成立】,而且它的闸【本来就躺在 dynasties.js 自己引的那段原文里】,引擎没读它:
// 「凡契丹軍馬皆屬焉」(北枢密院·武铨)/「凡契丹人民皆屬焉」(南枢密院·文铨)——
// 这两句【一直在 dynasties.js 的 sourceText 字段里】,一个字都不用新查。
// ★★ 这与上一刀发现「宋武选的梯子本来就是照原文排的,只是引擎没走它」是【同一种事】。
//
// ② ★★ 但【「北面官是契丹人的官,南面官是汉人的官」是错的,原文正面推翻它】:
// 《辽史》卷47:「四方館副使。道宗咸雍五年,詔四方館副使【止以契丹人充】。」——
// 四方館在【南面朝官】。即:一个【南面官】被诏令【只许契丹人】当。
// ⇒ 若南面本来就是汉人的官,这道诏无从下起。★ 志把族属限官【当成需要专门下诏、
// 并且值得记一笔的例外】来写,不是当成通则。dynasties.js 现有 gateNote 写
// 「北面官是契丹人的官」——【说过头了】,见 CONFLICTS #1。
//
// ③ ★★★ 而【所属 ≠ 任职】,这是本文件最要紧的一条界线,混了整个闸就是假的:
// 「凡契丹軍馬皆屬焉」说的是【这个衙门管谁】(所属),不是【谁能当这个衙门的官】(任职)。
// · 本表的 channels 模型的是【入仕:谁的铨管你】——所以它读的是【所属】,这一条对得上。
// · 而【任职】那一头,志自己给了反例:「太祖初有漢兒司,【韓知古】總知漢兒司事」(卷47,双证)。
// ⇒ 本表的出身闸【只回答「谁的铨管你」,不回答「你的族能当什么官」】。这两句差得很远。
//
// ④ ★★ 志名下的族【不是两个,是四个】:「契丹、奚、漢、渤海四軍都指揮使司」(卷46,双证),
// 其下并列「契丹奚軍都指揮使司/奚軍都指揮使司/漢軍都指揮使司/渤海軍都指揮使司」。
// ⇒ 老板给的「契丹/汉人」二元,是【制】的二元(「以國制治契丹,以漢制待漢人」),
// 【不是族的二元】。★ 而志只给了其中两族一个【铨】:奚与渤海有帐、有军,【没有铨】。
// → 本表只收契丹与汉两值,理由【不是它们是全部,是只有它们有铨】。见 NOT_DONE ①。
//
// ⑤ 唐【有出身的科条,但不在引擎做的那一段】——这一条是【实测】,不是「我没找到」:
// 《新唐书》卷45(选举志下·铨选):「刑家之子、工賈異類及假名承偽、隱冒升降者有罰。」(双证)
// 而卷44(选举志上·贡举)【逐字扫过全卷】:「刑」1 次且是「律學隸詳刑」、「賤」0、「奴」0、
// 「工」0、「賈」3 次全是人名(賈至/賈餗)、「商」3 次全是商朝。
// ⇒ 唐的出身闸是【铨选(服官)】的闸,【贡举(入仕)那一卷没有】。而本表的 channels 全是入仕
// (chiduo.js CONFLICT #8)。→ 【唐没有出身维,理由是结构性的,不是史料缺席。】
//
// ⑥ 明【成立,但不是老板猜的那个】:不是军户/民户/匠户(卷69/70 没有以军民匠限考的科条),
// 是【倡優之家】:「其學校訓導專教生徒,及罷閑官吏,【倡優之家】,與居父母喪者,俱不許入試。」
// (《明史》卷70,双证 C + 四库本)。★ 而且它与辽【形状相反】:辽是【属】(这个铨管谁),
// 明是【禁】(这类人不许进)。⇒ 这两朝就是「出身【不许做成跨朝通用枚举】」的证明,
// 不是我这么主张,是两条原文的形状本来就不一样。
//
// ⑦ 魏晋/南朝【立不起来】:门第的料【全是政论】——刘毅八损疏(请废九品,被否),
// chiduo.js 的 usableAsInstitution() 对 wj_menshi 返回 false。→ 无出身维。
// ⚠ 而 dynasties.js 拿 wang(声望)当它的替身已经很久了 —— 那是另一回事,见文件末 NOT_DONE ②。
//
// ⑧ 汉【本轮未查全,所以不表态】:只扫了《后汉书》卷61(左雄/黄琼,察举改制那一卷),
// 「賈人」「市籍」「商賈」「贅壻」「七科」「門第」全零命中。
// ⚠ 【但这是一卷,不是一朝】。按本项目铁律:宁可说「我没查」,不可说「它没有」。→ 无出身维,理由是【未查】。
//
// ══════════════════════════════════════════════════════════════════════════
// 【见证与体例】—— 与 chiduo.js 同一套,不重复,只记本文件的三条
//
// · 引文【一律保留繁体原样】(照 chiduo.js 的体例,不照 dynasties.js 的繁→简):
// 本轮辽的繁体见证是国学导航(汉川草庐本),wikisource 的辽史卷45/46/48 是【简体】、卷47 是【繁体】
// —— ★ 同一部书、相邻四卷,转录本自己繁简混用。这正是「拿到生文本不等于拿到原文」的实例。
// 故本文件的辽史引文【取繁体见证(国学导航)的字】,wikisource 只用来核结构与句读。
// · 标点【为本项目所加】,未采用任何点校本。
// · 省略处一律用「……」标出,不作暗省。
//
// ⚠⚠ 本轮自己踩了一次「假订正」,记在这里当标本(教训比结论值钱):
// 我先用字符串「詩賦」扫《新唐书》卷44,得【零命中】,差一点写成
// 「唐进士条无诗赋,dynasties.js 的 gate{wen:50} 无据」——
// ★ 而真相是原文作【「進士試詩、賦及時務策五道」】,中间有一个顿号,我的检索词把它漏了。
// → 【零命中比单证引文更危险】:它的失效方向朝「推翻」,而且长得像一次订正。
// 凡零命中,必须【逐字符】复核(本文件所有零命中都是逐字符数出来的,不是整词搜的)。
// (顺带:这一扫【正面解决了 chiduo.js CONFLICT #2】,方向与它的猜测相反 —— 见 FINDINGS。)
// ══════════════════════════════════════════════════════════════════════════
// ── 出身的【形状】:两种,从原文里数出来的,不是我设计的分类 ──
// ★ 这两种【不可互换】,也【不许合并成一个「出身枚举」】:合并了,就是把两条形状不同的原文
// 压成一个我发明的形状 —— 那正是本项目反复栽的那个坑。
export const SHAPES = {
shu: {
id: 'shu', name: '属',
// 「属」是【原文自己的字】:「凡契丹軍馬皆屬焉」。不是我起的名。
desc:
'【这个衙门管谁】。辽:「凡契丹軍馬皆屬焉」(北枢密院·武铨)、「凡契丹人民皆屬焉」(南枢密院·文铨)。' +
'★ 它是一张【白名单】:这条路的铨管的是这一族的人;你不在其中,不是「你被禁了」,' +
'是【这条路的铨根本不覆盖你】。' +
'⚠⚠ 所属 ≠ 任职:它不说「只有这一族能当这个官」(志自己有反例:韓知古總知漢兒司事)。',
},
jin: {
id: 'jin', name: '禁',
// 「不許入試」是【原文自己的话】。
desc:
'【这类人不许进】。明:「倡優之家……俱不許入試」。' +
'★ 它是一张【黑名单】:你本来在这条路上,是一道禁令把你挡在门外。' +
'⚠ 与「属」的方向相反:属是「这条路不覆盖你」,禁是「这条路覆盖你,但不许你进」。',
},
};
const SHAPE_IDS = Object.keys(SHAPES);
// ══════════════════════════════════════════════════════════════════════════
// 见证
// ══════════════════════════════════════════════════════════════════════════
const LIAO_WIT = [
'见证A 国学导航(汉川草庐本)《遼史》卷45 http://www.guoxue123.com/shibu/0101/00liaosf/045.htm(繁体,本文件取它的字)',
'见证C zh.wikisource《遼史/卷45》action=raw(★ 简体转录,自标 Textquality 50%,只用来核结构与句读)',
];
const LIAO47_WIT = [
'见证A 国学导航(汉川草庐本)《遼史》卷47 http://www.guoxue123.com/shibu/0101/00liaosf/047.htm',
'见证C zh.wikisource《遼史/卷47》action=raw(★ 该卷是繁体,而同书卷45/46/48 是简体 —— 转录本自己繁简混用)',
];
const LIAO46_WIT = [
'见证A 国学导航(汉川草庐本)《遼史》卷46 http://www.guoxue123.com/shibu/0101/00liaosf/046.htm',
'见证C zh.wikisource《遼史/卷46》action=raw',
];
const V_LIAO = '2026-07-16 curl 双证逐字目验';
const MING_WIT = [
'见证C zh.wikisource《明史/卷70》action=raw',
'见证SK zh.wikisource《明史 (四庫全書本)/卷070》action=raw(★ 独立底本,无标点 —— 正因无标点,它是字的独立见证)',
];
const V_MING = '2026-07-16 curl 双证逐字目验(四库本无标点,逐字对齐后一字不差)';
// ── 辽·序:出身的【类】从这里来 ──
// ⚠ 这是【志序】(chiduo.js EVIDENCE.historianFrame,usableAsInstitution=false)。
// ★ 而 historianFrame 的规矩逐字写着:「作为【领域怎么切】的证据是合法的;
// 作为【某项制度如何运作】的证据不合法。」
// ⇒ 本文件【只拿它切类】(辽的人分契丹与汉两制),【不拿它当闸】。闸另有正文,见下。
// ★★ 这个分层不是形式主义:它正好是本文件与 chiduo.js 同一套判别式的用法。
const LIAO_XU_QUOTE =
'契丹舊俗,事簡職專,官制朴實,不以名亂之,其興也勃焉。太祖神冊六年,詔正班爵。' +
'至于太宗,兼制中國,官分南、北,以國制治契丹,以漢制待漢人。國制簡朴,漢制則沿名之風固存也。' +
'遼國官制,分北、南院。北面治宮帳、部族、屬國之政,南面治漢人州縣、租賦、軍馬之事。因俗而治,得其宜矣。';
const LIAO_XU_CAVEAT =
'⚠ 【这是志序,是史官的通括之辞】(chiduo.js EVIDENCE.historianFrame,usableAsInstitution()=false)。' +
'★ 按那条规矩,志序【只能用来证「领域怎么切」,不能用来证「某项制度如何运作」】——' +
'本文件严格照办:拿它【切类】(辽的人分契丹、汉两制),【不拿它当闸】;闸另取正文(见各值的 gateSource)。' +
'⚠ 异文(两证):「遼國官【制】」(国学导航) /「辽国官【职】」(wikisource)。制/职 一字之差,' +
'两读于此均成训,无从判定 —— 故本表引前者(繁体见证)并在此标出,不暗合。' +
'★ 而关键的那一句【「以國制治契丹,以漢制待漢人」两证逐字相同】,不在异文之列。' +
'⚠ 又:同段末句「語遼官制者【不可不辨】」(国学导航) /「语辽官制者,【可不辨】」(wikisource)——' +
'一个「不」字意思正相反,dynasties.js 早已记下此坑,本轮复核【属实】,故该句本表不引。';
// ══════════════════════════════════════════════════════════════════════════
// CHUSHEN —— 每朝各自的出身。★ 每一个取值都必须挂原文。没有原文的值,不许存在。
//
// 字段:
// dynastyId 对应 dynasties.js 的朝代 id
// shape SHAPES 之一 —— ★ 每朝只有一种,且【不同朝可以不同种】(辽=属,明=禁)
// ask ★ 面向用户的问句。【必须是「这一部传里,你生在……」句式】,有断言守着
// values[] 取值。每一个都挂 name(原文的词)/source/quote/witnesses/verified/caveat
// values[].shu 该值【属】哪几条通道(shape=shu 时用):[{ channel, quote }]
// values[].jin 该值【被禁】于哪几条通道(shape=jin 时用):[{ channel, quote }]
//
// ⚠⚠ 【quote 必须逐条挂,不许由模板拼】—— 这一条是本轮当场踩出来的,记在这里:
// 第一版把关路的理由写成了模板 `「凡${name}人民皆屬焉」`,于是【武铨那一条被安上了一句它没有的话】:
// 武铨的原文是「凡契丹【軍馬】皆屬焉」,文铨才是「凡契丹【人民】皆屬焉」。
// ★ 一个模板,凭空给一条通道伪造了一句带书名的引文 —— 正是本项目铁律里
// 「带卷次的错误比没出处的错误危害大十倍,因为它不可反驳」的那一种。
// ⇒ 【每条通道自己带自己的那一句原文】。模板只许拼本表自己的话,不许拼引文。
// ══════════════════════════════════════════════════════════════════════════
export const CHUSHEN = [
{
dynastyId: 'liao',
shape: 'shu',
name: '国制 / 汉制',
// ⚠ 文案红线:不许写「你的民族」「你的出身是」。
ask: '这一部传里,你生在契丹,还是生在汉?',
askNote:
'★ 这和选朝代是同一类动作:给这一部传定一个开局。' +
'辽问的第一个问题不是「你会什么」,是「你是谁家的人」——' +
'「以國制治契丹,以漢制待漢人」。这一问答完,才轮到你会什么。',
xuSource: '《辽史》卷四十五·志第十五·百官志一(序)',
xuQuote: LIAO_XU_QUOTE,
xuWitnesses: LIAO_WIT,
xuVerified: V_LIAO,
xuCaveat: LIAO_XU_CAVEAT,
values: [
{
id: 'qidan',
name: '契丹',
// ★★ 闸的出处【是正文,不是序】——「北面朝官·契丹北枢密院」条下。
// evidenceKind = zhengwen ⇒ 可以当制度陈述用。这一层分得清清楚楚:
// 类出自序(只切类),闸出自正文(才动手)。
// ⚠ 两条通道各带各的那一句:★ 武铨是【軍馬】,文铨是【人民】——不是一句话,别拼模板。
shu: [
{ channel: 'wuquan_liao', quote: '凡契丹軍馬皆屬焉' },
{ channel: 'wenquan_liao', quote: '凡契丹人民皆屬焉' },
],
gateSource: '《辽史》卷四十五·百官志一·北面朝官(契丹北枢密院 / 契丹南枢密院,正文)',
gateQuote:
'契丹北樞密院。掌兵機、武銓、群牧之政,凡契丹軍馬皆屬焉。以其牙帳居大內帳殿之北,故名北院。' +
'元好問所謂「北衙不理民」是也。……' +
'契丹南樞密院。掌文銓、部族、丁賦之政,凡契丹人民皆屬焉。以其牙帳居大內之南,故名南院。' +
'元好問所謂「南衙不主兵」是也。',
witnesses: LIAO_WIT,
verified: V_LIAO + '(两见证于「凡契丹軍馬皆屬焉」「凡契丹人民皆屬焉」二句逐字相同)',
caveat:
'★★★ 【这两句一直躺在 dynasties.js 的 sourceText 里,引擎从来没读它】——' +
'与上一刀发现「宋武选的梯子本来就是照原文排的,只是引擎没走它」是同一种事。' +
'「凡契丹軍馬皆屬焉」=北枢密院的武铨管的是契丹军马;' +
'「凡契丹人民皆屬焉」=南枢密院的文铨管的是契丹人民。' +
'⇒ 你生在契丹,这两条路的铨【覆盖你】。' +
'⚠⚠ 【所属 ≠ 任职】,这条界线不许含糊:这两句说的是【这个衙门管谁】,' +
'【不是】「只有契丹人能当这个衙门的官」。志自己就有反例:' +
'「太祖初有漢兒司,【韓知古】總知漢兒司事」(卷47,双证)。' +
'⇒ 本表的闸只回答「谁的铨管你」,不回答「你的族能当什么官」。' +
'⚠ 又:这两个院【都是北面官】。志明言「宰相、樞密、宣徽、林牙……其實所治皆北面之事」,' +
'故「契丹南枢密院」是北面官,不可与南面的「汉人枢密院」相混 —— 这是本朝最容易犯的错。',
},
{
id: 'han',
name: '汉',
shu: [{ channel: 'nanmian_hanguan', quote: '南面治漢人州縣、租賦、軍馬之事' }],
gateSource: '《辽史》卷四十五·百官志一(序)+卷四十七·百官志三·南面朝官(汉人枢密院,正文)',
gateQuote:
'(卷45 序)南面治漢人州縣、租賦、軍馬之事。\n' +
'(卷47 正文)漢人樞密院。本兵部之職……太祖初有漢兒司,韓知古總知漢兒司事。' +
'太宗入汴,因晉置樞密院,掌漢人兵馬之政,初兼尚書省。',
witnesses: [...LIAO_WIT, ...LIAO47_WIT],
verified: V_LIAO + '(卷45 序与卷47 汉人枢密院条各自双证)',
caveat:
'★ 两句合起来才是这一格:「南面治漢人州縣、租賦、軍馬之事」(序,切类)+' +
'「掌漢人兵馬之政」(卷47 正文,闸)。' +
'⚠⚠ 【这一格比契丹那一格弱,必须说满】:' +
'① 志【没有】一句话说「汉人怎么进南面官」——《辽史》连一卷选举志都没有,' +
' 南面两卷(47/48)「銓」字【零命中】(全志四卷「銓」只两见,且都在北面)。' +
' ⇒ 这一格是【从「谁管汉人」倒推的】,不是志里的入仕条文。' +
'② ✗ 【不许由此写「南面官是汉人的官」】—— 原文正面推翻它:' +
' 「四方館副使。道宗咸雍五年,詔四方館副使【止以契丹人充】。」(卷47,双证)' +
' 四方館在【南面朝官】。一个南面官被诏令只许契丹人当 —— 若南面本来就是汉人的官,' +
' 这道诏无从下起。★ 志把族属限官当【需要下诏、且值得记一笔的例外】写,不是当通则。',
},
],
// ★★ 这一栏【不是补充说明,是这张表的诚实】:志名下的族有四个,本表只收两个,理由必须写死。
notCovered: {
names: ['奚', '渤海'],
source: '《辽史》卷四十六·百官志二·北面边防官(东京兵马都部署司条下)',
quote:
'東京兵馬都部署司。契丹、奚、漢、渤海四軍都指揮使司。契丹奚軍都指揮使司。奚軍都指揮使司。' +
'漢軍都指揮使司。渤海軍都指揮使司。',
witnesses: LIAO46_WIT,
verified: V_LIAO,
note:
'★★★ 【志名下的族是四个,不是两个】:「契丹、奚、漢、渤海四軍都指揮使司」,' +
'其下还各自并列「奚軍都指揮使司」「漢軍都指揮使司」「渤海軍都指揮使司」。' +
'又:卷45 北面诸帐官序「滅渤海國,存其族帳,亞於遙輦」「并奚王之眾,撫其帳部,擬於國族」,' +
'奚与渤海都有帐、有军、有王府。' +
'⇒ ★ 老板给的「契丹/汉人」二元,是【制】的二元(「以國制治契丹,以漢制待漢人」),' +
' 【不是族的二元】。这两句差得很远。' +
'★★ 而本表只收契丹与汉,理由【不是它们是全部】,是【只有它们有铨】:' +
' 奚与渤海有帐、有军,志【没给它们一条铨】—— 全志四卷「銓」只两见(北枢密院武铨、' +
' 南枢密院文铨),两处都系于契丹。' +
'⇒ 收它们进来就得给它们编一条路,那就是编。【照实说:这两族本表接不住。】见 NOT_DONE ①。',
},
},
{
dynastyId: 'ming',
shape: 'jin',
name: '倡優之家',
ask: '这一部传里,你生在倡優之家吗?',
askNote:
'★ 明的科举不问你是谁家的 —— 它只在一处问:你是不是生在倡優之家。' +
'是,这道门就不开;不是,它不再问第二句。' +
'⚠ 而「不是倡優之家」这一头,【原文没给它起过名字】——所以本表也不给它起。见 caveat。',
values: [
{
id: 'changyou',
name: '倡優之家',
// ★ 禁的是【入试】,所以只关科举那条路。吏员出职不是考试,原文没禁它。
jin: [{ channel: 'keju', quote: '倡優之家……俱不許入試' }],
gateSource: '《明史》卷七十·志第四十六·选举二(乡试·会试,正文)',
gateQuote:
'舉子,則國子生及府、州、縣學生員之學成者,儒士之未仕者,官之未入流者,' +
'皆由有司申舉性資敦厚、文行可稱者應之。' +
'其學校訓導專教生徒,及罷閑官吏,倡優之家,與居父母喪者,俱不許入試。',
witnesses: MING_WIT,
verified: V_MING,
caveat:
'★★★ 【这一列四样里,只有「倡優之家」是出身 —— 依据是一个「家」字,不是我的解读】:' +
'「學校訓導專教生徒」是你现在的差事;「罷閑官吏」是你现在的处境;' +
'「居父母喪者」是你现在的状态(且它会过去)。' +
'★ 只有「倡優【之家】」是【你生下来就在里面的】——「之家」二字就是判别式。' +
'⇒ 另外三样【不进本表】:它们不是出身,是「你现在是什么」。' +
'★★ 而禁的是【入試】,所以它只关科举那一条路 —— 吏员出职不是考试,' +
'原文【没有】禁它。⇒ 生在倡優之家的人,明的正途(科举,天花板=内阁大学士)关着,' +
'偏路(吏员出职,天花板=知县)仍开着。' +
'★★★ 这一格就是这个功能的立意本身,而且【不用我加工】:同一个人,同一个明朝,' +
'生在倡優之家 —— 天花板从内阁大学士掉到知县。你什么都没变。' +
'⚠ 时段:dynasties.js 的明 span=1465–1566(明中期)。本条系于卷70 乡试会试通制,' +
'原文未系年 —— ★ 故【不给年份】,照本项目「查不实的宁可不写」办。' +
'⚠ 又:本条【不能反推「明的其他户籍不限考」】:本轮只扫了卷69/70,' +
'军户/民户/匠户以军民匠限考的科条【零命中】,但那是两卷,不是一朝。宁可说「我没查」。',
},
],
// ★ 明这一格【只有一个值】,而且那不是漏填 —— 原文只禁了一类人,没给「不是那一类」起过名字。
unnamedDefault: {
note:
'★★ 【明的出身只有一个值,这是结论不是缺】:原文给的是一张【禁】的名单' +
'(「倡優之家……俱不許入試」),它【没有】给「不是倡優之家」的那一头起过任何名字。' +
'同段的正面一列是「國子生/生員/儒士之未仕者/官之未入流者」——那是【资格与身份】,' +
'不是出身。' +
'⇒ 所以本表【不给默认值起名】:不选 = 原文没给这一头名字,不是「你是民户」。' +
' ✗ 编一个「民」出来凑成二元,就是把辽的形状套到明头上 —— 那正是' +
' 「出身不许做成跨朝通用枚举」要挡的事。' +
'★ 对照辽:辽的序【两头都点了名】(「以國制治契丹,以漢制待漢人」),所以辽是真二元;' +
' 明只点了一头。【两朝的出身连「有几个值」都不一样。】',
},
},
// ══ 元:四等人(蒙古/色目/漢人/南人)。怯薛/投下只对蒙古开。 ══
{
dynastyId: 'yuan',
shape: 'shu',
name: '四等人(蒙古 / 色目 / 漢人 / 南人)',
ask: '这一部传里,你生在蒙古、色目、汉人,还是南人?',
askNote:
'★ 和选朝代同类:给这一部传定开局。元问的第一问不是「你会什么」,是「你是哪一等人」——' +
'「以國制治蒙古,以漢法待漢人」。同样的才与门第,你是哪一等,决定你够得着哪几扇门:怯薛根脚只对蒙古开。',
xuSource: '《元史·選舉志》卷八一(科目)',
xuQuote: '蒙古、色目人作一榜,漢人、南人作一榜。',
xuVerified: '2026-07-30 curl《元史·選舉志》卷八一逐字,grep 命中。',
xuCaveat:
'★ 四等人是【制】的四分(右榜蒙古色目 / 左榜漢人南人;蒙古最优、南人最下)。本表拿它【切类 + 关路】:' +
'怯薛/投下只对蒙古开;科举右榜左榜分卷、承荫色目优汉一等——⚠ 优遇是程式/等第的差,出身「关路不给分」,本表不拿它加 tier,只记明。',
values: [
{
id: 'menggu', name: '蒙古',
shu: [
{ channel: 'qixue', quote: '四怯薛子孫世為宿衞之長,使得自舉其屬' },
{ channel: 'touxia', quote: '凡投下官,必須用蒙古人員' },
],
gateSource: '《元史·選舉志》卷八一(怯薛出身)、卷八二(投下)',
gateQuote: '四怯薛子孫世為宿衞之長,使得自舉其屬。\n凡投下官,必須用蒙古人員。',
verified: '2026-07-30 curl 逐字,grep 卷内命中。',
witnesses: [
'见证① zh.wikisource《元史/卷82》選舉志二·怯薛出身 action=raw 逐字命中「四怯薛子孫世為宿衞之長,使得自舉其屬」「凡投下官,必須用蒙古人員」(2026-07-30 curl)',
'见证② zh.wikisource《元史/卷99》兵志二·宿衞 action=raw 逐字命中「怯薛」「四怯薛」,与選舉志卷82「怯薛世為宿衞之長」跨卷互证怯薛世任(2026-07-30 curl)',
],
caveat:
'★ 蒙古独占怯薛(根脚)与投下两条顶门——非蒙古这两条关死。同门第,蒙古超擢宰执,汉南顶在承荫。' +
'⚠ 所属≠任职的界线在元照样守:投下「必須用蒙古人員」是任职明限,据以关路;色目虽亦充宿卫,其长非蒙古世家不得为,故怯薛长系于蒙古。',
},
{
id: 'semu', name: '色目',
shu: [],
gateSource: '《元史·選舉志》卷八一、卷八三',
gateQuote: '蒙古、色目人作一榜。\n諸色目人比漢人優一等廕敍。',
verified: '2026-07-30 curl 逐字,grep 卷内命中。',
witnesses: [
'见证① zh.wikisource《元史/卷81》選舉志一·科目 action=raw 逐字命中「蒙古、色目人作一榜」(2026-07-30 curl)',
'见证② zh.wikisource《元史/卷83》銓法·廕敍 action=raw 逐字命中「諸色目人比漢人優一等廕敍」,与卷81右榜跨卷互证色目优遇(2026-07-30 curl)',
],
caveat:
'★ 色目与蒙古同列右榜、承荫比汉人优一等——是【优遇】不是【独占通道】。出身关路不给分,本表不拿优一等加 tier,只记明。' +
'色目不独占怯薛/投下(那是蒙古),其顶与汉南同在承荫(tier4)/科举(tier2)。',
},
{
id: 'hanren', name: '汉人',
shu: [],
gateSource: '《元史·選舉志》卷八一',
gateQuote: '漢人、南人,第一場明經經疑二問……漢人、南人作一榜。',
verified: '2026-07-30 curl 逐字,grep 卷内命中。',
witnesses: [
'见证① zh.wikisource《元史/卷81》選舉志一·科目 action=raw 逐字命中「漢人、南人作一榜」(2026-07-30 curl)',
'见证② zh.wikisource《元史/卷81》同卷科目条 action=raw 逐字命中「漢人、南人,第一場明經經疑二問」,与分榜宣言互证左榜程式加严(2026-07-30 curl)',
],
caveat:
'★ 汉人走左榜、程式加严,怯薛/投下关死。实际主路是吏道(tier3)与科举(tier2)、门第者承荫(tier4)。这是元的汉人之命。',
},
{
id: 'nanren', name: '南人',
shu: [],
gateSource: '《元史·選舉志》卷八一(科目)',
gateQuote: '漢人、南人作一榜。\n漢人、南人,第一場明經經疑二問。',
verified: '2026-07-30 curl 逐字,grep 卷内命中。',
witnesses: [
'见证① zh.wikisource《元史/卷81》選舉志一·科目 action=raw 逐字命中「漢人、南人作一榜」(2026-07-30 curl)',
'见证② zh.wikisource《元史/卷81》同卷科目条 action=raw 逐字命中「漢人、南人,第一場明經經疑二問」,与分榜宣言互证南人隶左榜(2026-07-30 curl)',
],
caveat:
'★ 南人(原南宋境)四等人之末,与汉人同左榜、怯薛/投下关死,制度上更受抑。命同汉人而更逼仄。',
},
],
},
// ══ 清:满汉旗籍(旗人 / 漢人)。侍卫笔帖式/翻译科/八旗先试骑射只对旗人开。 ══
{
dynastyId: 'qing',
shape: 'shu',
name: '旗籍(旗人 / 漢人)',
ask: '这一部传里,你生在旗人(满/蒙/汉军旗籍),还是汉人(民)?',
askNote:
'★ 给这一部传定开局。清入官分满汉旗籍:旗人另有侍卫、拜唐阿、笔帖式的门阀捷径,「不數年輒致通顯」;' +
'汉人主走科举——文科举通天,但要凭才。你是旗是汉,决定你能不能走那条不凭才的捷径。',
xuSource: '《清史稿·選舉志》卷一一〇(满洲入官)',
xuQuote: '滿人入官,以門閥進者,多自侍衛、拜唐阿始。',
xuVerified: '2026-07-30 curl《清史稿·選舉志》逐字,grep 命中。',
xuCaveat:
'★ 旗人独占侍卫/拜唐阿/笔帖式(门阀捷径)与翻译科;八旗应文科举「先試馬步箭,騎射合格乃應制舉」。' +
'汉人这几条关死,主走文/武科举(通天但凭才)与五贡/捐纳/荫。⚠ 正途/异途是【路本身】的属性(捐纳=异途),不是出身,另在各通道 gate 记。',
values: [
{
id: 'qiren', name: '旗人',
shu: [
{ channel: 'shiwei', quote: '滿人入官,以門閥進者,多自侍衛、拜唐阿始' },
{ channel: 'fanyi', quote: '雍正元年,詔八旗滿洲於考試漢字生員、舉人、進士外,另試繙譯' },
{ channel: 'baqi', quote: '八旗以騎射為本,右武左文' },
],
gateSource: '《清史稿·選舉志》卷一〇八(翻译科·八旗科举)、卷一一〇(满洲入官)',
gateQuote:
'滿人入官,以門閥進者,多自侍衛、拜唐阿始。\n雍正元年,詔八旗滿洲……另試繙譯。\n八旗以騎射為本,右武左文。……康熙二十六年,詔同漢人一體應試。',
verified: '2026-07-30 curl 逐字,grep 卷内命中。',
witnesses: [
'见证① zh.wikisource《清史稿/卷110》選舉五·满洲入官 action=raw 逐字命中「滿人入官,以門閥進者,多自侍衛、拜唐阿始」(2026-07-30 curl)',
'见证② zh.wikisource《清史稿/卷108》選舉三·翻译科/八旗科举 action=raw 逐字命中「另試繙譯」「八旗以騎射為本」「騎射合格」,与卷110门阀捷径跨卷互证旗人专途(2026-07-30 curl)',
],
caveat:
'★ 旗人独占侍卫笔帖式(门阀捷径·不数年通显 tier5)、翻译科、八旗科举先试骑射——汉人这三条关死。' +
'这是清的族属真门:旗人能不凭才、由门阀捷径到通显;汉人要到高位须凭科举之才。',
},
{
id: 'hanren', name: '汉人',
shu: [],
gateSource: '《清史稿·選舉志》卷一〇六、卷一一二',
gateQuote: '清制,入官重正途。\n貢生凡六……恩、拔、副、歲、優,時稱「五貢」。',
verified: '2026-07-30 curl 逐字,grep 卷内命中。',
witnesses: [
'见证① zh.wikisource《清史稿/卷112》選舉七·捐纳 action=raw 逐字命中「清制,入官重正途」「官吏乃以資進」(2026-07-30 curl)',
'见证② zh.wikisource《清史稿/卷106》選舉一·学校贡生 action=raw 逐字命中「恩、拔、副、歲、優,時稱五貢」「謂之正途」,与卷112正途/异途跨卷互证汉人主路(2026-07-30 curl)',
],
caveat:
'★ 汉人(民)主走文/武科举(通天 tier5,但凭才)与五贡/捐纳/荫;侍卫/翻译/八旗骑射关死。' +
'清科举通天对汉人开放(与元汉人顶在从六品正相反),但要凭真才;无才则落在五贡/荫/捐纳(异途更被压在道员)。',
},
],
},
];
const BY_DYNASTY = Object.fromEntries(CHUSHEN.map((c) => [c.dynastyId, c]));
// ══════════════════════════════════════════════════════════════════════════
// ABSENT —— 【没有出身维的那五朝,各自的理由】。
// ★ 这一栏与 CHUSHEN 同等重要:一张只有「有」没有「无」的表,读者无从判断它的门槛在哪。
// 而且每一条都必须说清楚是【哪一种无】—— 混了就是本项目反复栽的「反过来说本表无据」。
//
// kind:
// 'out-of-segment' 有原文,但它在【引擎没做的那一段】(唐:铨选)→ 这是【结构性的】,不是史料缺席
// 'polemic-only' 有原文,但【全是政论】,够不上制度陈述(魏晋/南朝:刘毅八损疏)
// 'not-scanned' ★ 【本轮没查全】—— 不许读成「那一朝没有」(汉、宋)
// ══════════════════════════════════════════════════════════════════════════
export const ABSENT = [
{
dynastyId: 'tang',
kind: 'out-of-segment',
source: '《新唐书》卷四十五·志第三十五·选举志下(铨选)',
quote: '同流者,五五為聯,京官五人保之,一人識之。刑家之子、工賈異類及假名承偽、隱冒升降者有罰。',
witnesses: [
'见证C zh.wikisource《新唐書/卷045》action=raw',
'见证E zh.wikisource《文獻通考/卷三十七》action=raw(元·马端临,独立底本)',
],
verified: '2026-07-16 curl 双证逐字目验(关键句两证逐字相同)',
reason:
'★★ 【唐有出身的科条,而且我找到了它 —— 但它不在引擎做的那一段】。这是【结构性缺席】,' +
'不是「我没找到」,也不是「唐没有」。' +
'· 有:「刑家之子、工賈異類……有罰」(《新唐书》卷45,双证)——刑家之子(罪人之后)与' +
' 工賈異類(工商这一类),是两条真正的出身限制。' +
'· ★★ 但它在【卷45 = 选举志下 = 铨选 = 服官】。而【卷44 = 选举志上 = 贡举 = 入仕】' +
' 【逐字符扫过全卷,没有一条出身限制】:「刑」1 次且是「律學隸詳刑」(詳刑寺,与刑家无关)、' +
' 「賤」0、「奴」0、「工」0、「賈」3 次全是人名(賈至/賈餗)、「商」3 次全是商朝。' +
' 《文献通考》卷29(贡举的平行卷)「刑家」亦零命中。' +
'· ⇒ 而本表的 channels【全是入仕】(chiduo.js CONFLICT #8:「引擎只做了三段里的第一段」)。' +
' ★ 唐的出身闸落在【服官】那一段,引擎里【没有那一段】。' +
'⚠ 措辞:这【不是】「唐的贡举不看出身」的全称断言 —— 我扫的是《新唐书》卷44 与《通考》卷29,' +
' 不是唐代全部选举文献。准确说法:【本轮所扫的两卷贡举原文里没有出身的科条】。' +
'★ 而这一条恰好【第三次印证了 chiduo.js CONFLICT #8】:身言书判在铨选、德行才用劳效在考课、' +
' 刑家之子工賈異類也在铨选 —— 三样东西全在引擎没做的那两段里。',
},
{
dynastyId: 'weijin',
kind: 'polemic-only',
source: '《晋书》卷四十五·刘毅传(八损疏)—— 见 chiduo.js 的 wj_menshi',
quote: '是以上品無寒門,下品無勢族。暨時有之,皆曲有故。',
reason:
'★★ 【门第的料全是政论,所以出身这一维在魏晋立不起来】—— 这不是缺证,是判别式的结果:' +
'chiduo.js 的 wj_menshi 标 evidenceKind=polemic,usableAsInstitution() 返回 【false】。' +
'它出自一篇【请废九品的奏疏】,是攻击性修辞,且【被否】。' +
'★ 引它还必须连着引「暨時有之,皆曲有故」——刘毅自己承认有例外。' +
'⇒ 拿它去造一个出身的闸,等于拿一篇被否的奏疏当制度条文。不做。' +
'⚠ 而 dynasties.js 现在拿 wang(声望)承载门第(gate{wang:55} 通向三公)——' +
' ★ 那是【另一回事,别合并】:wang 是「谁认得你」(挣得来),出身是「你生下来是谁」(挣不来)。' +
' 中正【同时看这两样】(家世 + 乡论),那正是它们必须分开的理由。见 NOT_DONE ②。',
},
{
dynastyId: 'nanchao',
kind: 'polemic-only',
source: '同魏晋(门第);南齐舍人一路见 chiduo.js 的 nc_jiulao / nc_qinxin',
reason:
'★ 与魏晋同种:门第的料是刘毅那一篇(政论,被否)。' +
'而南齐舍人这一路,原文说的是【久勞、親信】(《南齐书》卷56 倖臣传序)——' +
'★★ 那两样【都不是出身】:久劳是一个时钟,亲信是一段关系。' +
'⇒ 南朝的「士庶天隔」在本表里【由 wang 承载着】(shizu gate{wang:75}),' +
' 而那是声望不是出身。⇒ 无出身维。' +
'⚠ 又:「簡以才能,不限資地」【不在《南史》】(chiduo.js FORBIDDEN 已判:卷77 全卷零命中,' +
' 真出处是《唐六典》卷九,唐人记梁事,两重折扣)—— 不许拿它来补南朝的出身。',
},
{
dynastyId: 'han',
kind: 'not-scanned',
reason:
'⚠⚠ 【本轮未查全,所以不表态 —— 这一条是「我没查」,不是「汉没有」】。' +
'本轮只扫了《后汉书》卷61(左雄/黄琼,即察举改制那一卷):' +
'「賈人」「市籍」「商賈」「贅壻」「七科」「門第」【全零命中】;' +
'「族」3 次是豪族/必族/舊族,「寒」3 次是寒食/寒燠/寒涼 —— 都与出身的科条无关。' +
'★ 但那是【一卷,不是一朝】。按本项目铁律:宁可说「我没查」,不可说「它没有」。' +
'⇒ 汉【无出身维,理由是未查】。要补,得先扫《后汉书》志二十四·百官志一(四科那一段)' +
' 与《汉书》食货志的贾人诸条 —— ⚠ 后者是【西汉】,本朝 era=东汉 span=25–220,' +
' 拿西汉的科条标东汉,正是本项目在「御史大夫」上栽过的那个坑。',
},
{
dynastyId: 'song',
kind: 'not-scanned',
reason:
'⚠ 【本轮未查全】。宋的门第走恩荫(dynasties.js 的 yinbu,gate{wang:55},封顶知州)——' +
'★ 但那是【声望/父祖的官位】,不是出身:荫补看的是「你父祖做到什么官」,' +
' 那是【挣来的(你父祖挣的)】,与「你生下来是谁」不是一回事。' +
'⇒ 本轮没有为宋取到入仕的出身科条,故无出身维。' +
'★ 顺带一条实测(本轮扫《明史》卷70 时撞见的,与宋直接相关):' +
' 明的科举明文【不許自序門第】(见 FINDINGS ③)—— 而 dynasties.js 的 keju_song 注写着' +
' 「宋是这几朝里【唯一一朝】,门第不在任何一条正途的门槛上」。' +
' ⚠ 那句话【与本表自己的数据打架】:明的 keju gate{heng:55,fa:50}、lisi gate{fa:55},' +
' 两条路一个 wang 都没有 —— 明的门第同样不在任何一条正途的门槛上,而且明还有原文明文禁它。' +
' ⇒ 「唯一一朝」是错的。这是文案不是引擎行为,本刀不动它,报老板。见 CONFLICTS #2。',
},
{
// ★★ 西汉(第八朝)—— 墙 C。加西汉到 dynasties.js 后,chushen.test.js 会对每一个没有出身维的朝代
// 查 absenceOf(),查不到就炸。故必须给它一条【说得出理由的缺席】。
dynastyId: 'xihan',
kind: 'not-scanned',
reason:
'⚠⚠ 【本轮没有为西汉做出身维,所以不表态 —— 这是「我没接进来」,不是「西汉没有」】。' +
'加西汉这一刀做的是【选拔通道】(孝廉/贤良/博士弟子/军功)与【经学轴】,' +
'没有做一遍像辽(契丹/汉)、明(倡優之家)那样的系统性出身扫描。' +
'★ 而料其实撞见了两条,都是正文双证、都是「类不是数」,本该走出身这一层:' +
'① 市籍——「有市籍不得宦」(《漢書》卷五·景帝詔)+「賈人皆不得名田、為吏」(卷十一·哀帝詔);' +
'② 学派——「所舉賢良,或治申、商、韓非、蘇秦、張儀之言……請皆罷」(卷六,建元元年,奏可)。' +
'★★ 但它们要 chushen.js 长出【市籍】【学派】两种新形状(现在只有「属」辽 / 「禁」明两种),' +
'而加西汉这一刀【不许碰 chushen 的形状层】(见 xuan/xihan.js XUAN_NOT_DONE ②,要老板拍)。' +
'⇒ 所以西汉【无出身维,理由是未接入本表】。谁想补,先回答「市籍」「学派」是不是一个「设定」' +
'(decisions.js openLineage 冻的那种),以及桑弘羊那个限定语(闸关的是「有市籍」的人,不是「贾人之子」)。',
},
];
const ABSENT_BY_DYNASTY = Object.fromEntries(ABSENT.map((a) => [a.dynastyId, a]));
export const ABSENCE_KINDS = {
'out-of-segment': {
id: 'out-of-segment', name: '有原文,但在引擎没做的那一段', whoIsBlind: '本表的模型',
desc:
'★ 【这不是史料缺席,是结构性缺席】:原文有,而且带卷次、双证;' +
'但它落在【服官(铨选)】那一段,而本表的 channels 全是【入仕(贡举)】。' +
'⇒ 看不见它的是【本表】,不是史料,更不是那个朝代。',
},
'polemic-only': {
id: 'polemic-only', name: '有原文,但全是政论', whoIsBlind: '料的层级',
desc:
'⚠ 有原文,但够不上「制度这样运作」这句话:刘毅八损疏是【请废九品的政论,且被否】' +
'(chiduo.js usableAsInstitution=false)。' +
'✗ 不许拿它造闸 —— 那是拿一篇被否的奏疏当制度条文。',
},
'not-scanned': {
id: 'not-scanned', name: '★ 本轮未查全', whoIsBlind: '我们',
desc:
'⚠⚠ 【是我们没查,不是那一朝没有】。本项目铁律:宁可说「我没查」,不可说「它没有」。' +
'✗ 谁都不许把这一格读成「该朝不看出身」。',
},
};
// ══════════════════════════════════════════════════════════════════════════
// 函数 —— 纯函数,无 IO。★ 失效方向一律朝【关】:问错了必须答「不」。
// (同 dynasties.js 的 sameScale、chiduo.js 的 usableAsInstitution。)
// ══════════════════════════════════════════════════════════════════════════
/** 这一朝有没有出身维。查不到 → false(失效朝关)。 */
export function hasChushen(dynastyId) {
return !!BY_DYNASTY[dynastyId];
}
/** 这一朝的出身:{ dynastyId, shape, ask, values[] }。没有 → null(不许猜)。 */
export function chushenOf(dynastyId) {
const c = BY_DYNASTY[dynastyId];
if (!c) return null;
return {
dynastyId: c.dynastyId,
shape: c.shape,
shapeName: SHAPES[c.shape] ? SHAPES[c.shape].name : c.shape,
name: c.name,
ask: c.ask,
askNote: c.askNote || '',
values: c.values.map((v) => ({
id: v.id, name: v.name,
shu: (v.shu || []).map((s) => ({ ...s })),
jin: (v.jin || []).map((j) => ({ ...j })),
})),
notCovered: c.notCovered ? { ...c.notCovered, witnesses: (c.notCovered.witnesses || []).slice() } : null,
unnamedDefault: c.unnamedDefault || null,
};
}
/** 取一个出身值。查不到 → null —— ★ 【不许猜】:猜一个出身出来,就是替用户的书编设定。 */
export function getChushenValue(dynastyId, valueId) {
const c = BY_DYNASTY[dynastyId];
if (!c) return null;
return c.values.find((v) => v.id === valueId) || null;
}
/**
* 「注」的数据源:一个出身值的原文与限定语。
* ⚠ caveat 是这个函数的全部价值 —— 正文是推演,注是原文,而【限定语才是注的诚实】。
* 前端渲染时 caveat 不许省略。(同 chiduo.js citeAxis / shiliao.js citeOffice 的口径。)
*/
export function citeChushen(dynastyId, valueId) {
const c = BY_DYNASTY[dynastyId];
const v = getChushenValue(dynastyId, valueId);
if (!c || !v) return null;
return {
dynastyId, valueId: v.id, name: v.name,
shape: c.shape,
shapeName: SHAPES[c.shape] ? SHAPES[c.shape].name : c.shape,
// ★ 类的出处(志序,只切类)与闸的出处(正文,才动手)【分开给】—— 这个分层是本文件的地基。
lei: c.xuSource ? {
source: c.xuSource, quote: c.xuQuote, witnesses: (c.xuWitnesses || []).slice(),
verified: c.xuVerified || '', caveat: c.xuCaveat || '',
level: '志序(只用来切类,不用来当闸)',
} : null,
zha: {
source: v.gateSource, quote: v.gateQuote, witnesses: (v.witnesses || []).slice(),
verified: v.verified || '', caveat: v.caveat || '',
level: '正文(制度陈述,闸取它)',
},
};
}
/** 这一朝为什么没有出身维。有出身维 → null。 */
export function absenceOf(dynastyId) {
const a = ABSENT_BY_DYNASTY[dynastyId];
if (!a) return null;
const k = ABSENCE_KINDS[a.kind];
return {
dynastyId, kind: a.kind,
kindName: k ? k.name : a.kind,
whoIsBlind: k ? k.whoIsBlind : '',
kindDesc: k ? k.desc : '',
source: a.source || '', quote: a.quote || '',
witnesses: (a.witnesses || []).slice(),
verified: a.verified || '',
reason: a.reason,
};
}
/**
* ★★★ 本文件的心脏:这个出身,在这一朝,把哪几条通道关上了。
*
* 返回 { closed: Set<channelId>, why: {channelId: 理由} }。
* ⚠ 失效方向【朝关】,三种情形都必须想清楚:
* · 这一朝没有出身维 → 不关任何路(出身在这一朝不是个问题)
* · 有出身维,但没给出身 → ★【属】的路全关:辽的第一个问题没答,引擎【不许替他答】
* —— 这正是「引擎拿能力替出身做决定」那个失真的修法。
* · 有出身维,给了一个不认识的值 → 当成没给(不许猜)
*
* ★ 这个函数【不 import dynasties.js】:它只回答「关哪几个 channelId」,
* 由 dynasties.js 拿去用。反过来依赖会成环。
*/
export function closedBy(dynastyId, chushenId) {
const c = BY_DYNASTY[dynastyId];
const closed = new Set();
const why = {};
if (!c) return { closed, why, dimension: false, missing: false };
const v = getChushenValue(dynastyId, chushenId);
if (!v) {
// ★★ 没定出身。【属】的路全关 —— 而且理由必须让用户看见。
// ✗ 绝不许「没给就当他是契丹」:那正是本表在辽一朝自陈的那个失真
// (「引擎是拿能力替出身做了决定」),只是换了个更隐蔽的地方犯。
if (c.shape === 'shu') {
for (const val of c.values) for (const s of val.shu || []) {
closed.add(s.channel);
// ★ 引文逐条取自数据(s.quote),不由模板拼 —— 见文件头那条「不许拼引文」。
why[s.channel] =
`这一部传还没定出身。${c.ask}——这条路的铨管的是【${val.name}】那一头(志的原话是「${s.quote}」),先答这一问。`;
}
}
// 【禁】的路:没定出身 = 没说你生在被禁的那一类里 → 不关。
// ★ 这不是对称的疏漏,是两种形状本来就不对称:属是白名单(不在名单上就不覆盖你),
// 禁是黑名单(不在名单上就没禁你)。
//
// ★★★ 而 missing 这个旗子,两种形状的答案【也不一样】——这一条是本轮盯输出盯出来的:
// · 属(辽):没定 ⇒【这一问真的没答】。引擎给不出判词(路全关着),
// 所以 missing=true,而且它得一路传到 headline:一个没答的问题【不许当判词用】。
// · 禁(明):没定 ⇒【不是没答,是「你不在被禁的那一类里」】。
// 原文只禁了「倡優之家」,【没给另一头起过名字】——所以「没declare」本身就是一个
// 完全有效的状态,判词照出(科举正途照走)。
// ⚠ 第一版这里两种都返回 missing=true,于是【明明算出了判词的明,被报成「这一问还没答」,
// 还被 buildHeadline 整个摘了出去】。★ 那是拿「原文没给这一头起名字」冒充「用户没回答」——
// 与本项目反复栽的「把我们看不见说成它没有」是同一个形状,只是换到了旗子上。
return { closed, why, dimension: true, missing: c.shape === 'shu' };
}
if (c.shape === 'shu') {
const mine = new Set((v.shu || []).map((s) => s.channel));
for (const val of c.values) {
if (val.id === v.id) continue;
for (const s of val.shu || []) {
// 别的值所属的路 —— 但要排掉本值也属的(本表现在没有这种重叠,防以后有)
if (mine.has(s.channel)) continue;
closed.add(s.channel);
// ══ 2026-07-17 验收修:这句模板【原本写着】「那个『属』字说的是……」——
// ✗ 而它是一句【关于引文的断言】,且对本表现有的三条引文【只有两条成立】:
// · 「凡契丹軍馬皆屬焉」「凡契丹人民皆屬焉」—— 有「屬」字 ✓
// · 「南面治漢人州縣、租賦、軍馬之事」(汉)—— 【没有「屬」字,作「治」】✗
// ⇒ 引擎在对用户说「那个『属』字」,而它指着的那句话里【一个「屬」都没有】。
// ★★ 这与本文件自己刚抓出来的那个 bug 是【同一个形状】,只是换了一层:
// 上一版是【模板伪造了一句引文】(武铨被安上「人民」);这一版是
// 【模板伪造了一句「关于引文的话」】。★ 引文逐条挂到数据上,挡住了前者,
// 没挡住后者 —— 因为「那个『属』字」不是引文,它是一句【断言】,而它一样会假。
// ⇒ 立一条口径(比上一条更严):模板不许拼引文,【也不许对引文的字面下断言】。
// 只许说【本表自己的读法】——「所属 ≠ 任职」是本表的分析(SHAPES.shu.desc 写着它),
// 对两种引文都成立,而且它本来就是本表的话,不是志的话。
// ⚠ 「属」这个【形状名】仍在(SHAPES.shu.name),那是本表给这一类起的名;
// ✗ 但不许再声称它是【某一句引文里的某个字】。
why[s.channel] =
`这一部传里,你生在${v.name}。而这条路的铨管的是【${val.name}】那一头——` +
`志的原话是「${s.quote}」。这句话说的是【这个衙门管谁】,` +
`不是「谁能当这个官」。所以这不是你被禁了,是它的铨本来就不覆盖你。`;
}
}
} else if (c.shape === 'jin') {
for (const j of v.jin || []) {
closed.add(j.channel);
why[j.channel] = `这一部传里,你生在${v.name}。原文:「${j.quote}」——这道门不开。`;
}
}
return { closed, why, dimension: true, missing: false };
}
// ══════════════════════════════════════════════════════════════════════════
// FINDINGS —— 本轮 curl 的副产品。★ 每一条都带卷次与见证,且【都不是我出发时要找的东西】。
// ══════════════════════════════════════════════════════════════════════════
export const FINDINGS = [
{
id: 1,
title: '★★★ chiduo.js CONFLICT #2 解决了 —— 而且方向与它的猜测【相反】',
where: 'chiduo.js › CONFLICTS #2(存疑:唐进士条未见「詩賦」)/dynasties.js › tang › jinshi › gate{wen:50}',
was:
'chiduo.js 说:「本轮取到的《新唐书》卷44 进士条作『試時務策五道、帖一大經』,' +
'【本表手上的这条引文里没有「詩賦」二字】……但我【没有】对卷44 做过「詩賦」零命中的实测。」' +
'它给的下一步是:「curl 卷44 全卷,扫「詩賦」。扫出来 → gate{wen:50} 有据;扫不出来 → 那才可以谈改。」',
did: '★ 扫了。而且【扫出来了】。',
source: '《新唐书》卷四十四·志第三十四·选举志上',
quote:
'先是,進士試詩、賦及時務策五道,明經策三道。建中二年,中書舍人趙贊權知貢舉,' +
'乃以箴、論、表、贊代詩、賦,而皆試策三道。大和八年,禮部復罷進士議論,而試詩、賦。',
witnesses: [
'见证C zh.wikisource《新唐書/卷044》action=raw(作「詩、賦」)',
'见证E zh.wikisource《文獻通考/卷二十九》action=raw(作「詩賦」,独立底本)',
],
verified: '2026-07-16 curl 双证逐字目验(两证除顿号外逐字相同 —— 顿号是转录者加的,不是异文)',
finding:
'★★ 【《新唐书》卷44 确实说进士试诗赋,dynasties.js 的 gate{wen:50} 有据】。' +
'★ 为什么 chiduo.js 上一轮没看见:卷44 里【有两处】讲进士考什么,它只取到了一处 ——' +
' · 科目条(它取到的):「凡進士,試時務策五道、帖一大經」' +
' · 编年叙事(它没取到的):「先是,進士試詩、賦及時務策五道」' +
'⇒ 两处【不矛盾】:后者是「先是」(建中二年 781 之前)的记法,而' +
' 【dynasties.js 的唐 span = 盛唐 712–755,正落在「先是」这一段里】。' +
'★★★ 而且这一句还自带一部诗赋的兴废史:' +
' 建中二年(781) 趙贊「以箴、論、表、贊代詩、賦」→ 大和八年(834)「禮部復罷進士議論,而試詩、賦」。' +
' —— 诗赋被废过,又被请回来了。这三档全在本表 span 之外,故【本表不取,只记在这里】。',
proposal:
'★ dynasties.js › tang › jinshi 可以把这条引文挂上(双证、在 span 内),' +
'gate{wen:50} 从「没有出处」升级为「有出处」。' +
'⚠ 但那是给唐补 source,不是本刀(出身/劳)的活,且它会动 tang 的 sourceText —— 老板拍。',
changed: false,
},
{
id: 2,
title: '⚠⚠ 我自己差点做了一次「假订正」—— 过程记在这里,比结论值钱',
where: '本文件的方法',
finding:
'★★★ 【零命中比单证引文更危险,因为它长得像一次订正】:' +
'我先拿字符串「詩賦」扫《新唐书》卷44,得【零命中】,' +
'当时差一点写成「唐进士条无诗赋,gate{wen:50} 无据」——' +
'而原文作【「進士試詩、賦」】,中间一个顿号,我的检索词把它整个漏掉了。' +
'⇒ 【检索词本身就是那个假订正的源头】。' +
'★ 本项目的铁律说「最难发现的错是假订正:写『原文无 X,已订正』而实际只有一个见证」——' +
' 这一次更阴:【我连那一个见证都没读对】,因为我问它的问题问错了。' +
'⇒ 立一条规矩,本文件所有零命中都照办:【凡零命中,必须逐字符复核,不许整词搜】。' +
' 本文件的唐卷44(刑/賤/奴/工/賈/商)、后汉书卷61、辽史四卷的「銓」,' +
' 全部是逐字符数出来的,且把每一次命中的上下文都读了一遍。',
},
{
id: 3,
title: '★★★ 明的科举【明文禁止你自报门第】—— 而 dynasties.js 说「宋是唯一一朝」',
where: 'dynasties.js › song › channels › keju_song(注:「★ 因此宋是这几朝里【唯一一朝】,门第不在任何一条正途的门槛上」)',
source: '《明史》卷七十·志第四十六·选举二',
quote: '試日入場,講問、代冒者有禁。晚未納卷,給燭三枝。文字中迴避御名、廟號,及不許自序門第。彌封編號作三合字。',
witnesses: MING_WIT,
verified: '2026-07-16 curl 双证逐字目验',
finding:
'★★ 【明的科举,明文写着「不許自序門第」】——你连提都不许提。' +
'而它紧挨着「彌封編號作三合字」,即:它是【糊名那一套装置的一部分】。' +
'⇒ 与宋的「糊名誊录,考官不知道你是谁」是【同一件事的两种写法】,' +
' 而明这一句【更直白】:宋是让考官看不见你的门第,明是禁止你自己说出来。' +
'★★★ 所以 dynasties.js 的「宋是这几朝里【唯一一朝】门第不在任何一条正途的门槛上」' +
' ——【是错的,而且与本表自己的数据打架】:' +
' 明的 keju gate{heng:55,fa:50}、lisi gate{fa:55},【两条路一个 wang 都没有】;' +
' 宋反而还有一条 gate{wang:55} 的恩荫。⇒ 若论「门第进不了正途」,明不比宋弱。' +
'⚠ 异文(两证,必须标,不许暗合):「不許自序門【第】」(wikisource) /' +
' 「不許自序門【地】」(四库全书本)。第/地 一字之差,★ 两读【于此均成训且同义】' +
' (門地是門第的通用异文)—— 故【句意双证,字形单证】。' +
' 照 chiduo.js 在「言辭辯正/辨正」上的口径办:可以引,但【必须标异文】。',
proposal:
'★ 把「唯一一朝」改成准确的说法(如「宋与明两朝,门第都不在正途的门槛上,而两朝的机理不同:' +
'宋是糊名誊录,明还明文禁止自序门第」),并把这条原文挂到明的 keju 上。' +
'⚠ 但那是文案,不是引擎行为,且它动的是宋与明两朝的注 —— 本刀不动它,老板拍。',
changed: false,
},
];
// ══════════════════════════════════════════════════════════════════════════
// CONFLICTS —— 【与 dynasties.js 现有写法冲突的地方。★ 一条都不自作主张,全部报老板。】
// ══════════════════════════════════════════════════════════════════════════
export const CONFLICTS = [
{
id: 1,
severity: '推翻(文案)',
where: 'dynasties.js › liao › channels › wuquan_liao / wenquan_liao / nanmian_hanguan › gateNote',
now:
"wuquan_liao.gateNote:「……但先问的不是这个:北面官是契丹人的官。」" +
"/nanmian_hanguan.gateNote:「……这条路是给汉人的」",
sources: '《辽史》卷四十七·百官志三·南面朝官(客省·四方馆条,双证)',
quote: '四方館副使。道宗咸雍五年,詔四方館副使止以契丹人充。',
finding:
'★★ 【「北面官是契丹人的官」「南面这条路是给汉人的」——两句都说过头了,原文正面推翻后者】:' +
'四方館在【南面朝官】,而道宗咸雍五年专门下了一道诏,令四方館副使【止(只)以契丹人充】。' +
'⇒ 若南面本来就是「给汉人的」,这道诏无从下起;一个南面官需要下诏才变成契丹专任,' +
' 说明它本来【不是】按族分的。' +
'★ 志把族属限官【当成需要下诏、并且值得记一笔的例外】来写(全志四卷只此两处:' +
' 卷46 回鶻部副使、卷47 四方館副使),不是当通则。' +
'★★ 而志自己还有一条反例:「太祖初有漢兒司,【韓知古】總知漢兒司事」(卷47,双证)。' +
'⇒ 准确说法是【「北枢密院的武铨管契丹军马,南枢密院的文铨管契丹人民」(所属),' +
' 不是「北面官只有契丹人能当」(任职)】。这两句差着一整个功能的可信度。',
proposal:
'★ 本文件的闸【已经照准确的说法建了】(closedBy 只回答「谁的铨管你」)。' +
'但 dynasties.js 那三条 gateNote 是【用户直接读得到的文案】,仍写着过头的话。' +
'⚠ 改文案会动到辽三条通道的 gateNote —— 而其中一句(「所以这一格,是拿你的性子替你的出身答的」)' +
'正是本刀要删掉的那个失真的自陈,删它得连着改。这是文案设计,不是引擎行为。老板拍。',
changed: false,
},
{
id: 2,
severity: '事实错误(文案)',
where: 'dynasties.js › song › channels › keju_song(「宋是这几朝里【唯一一朝】,门第不在任何一条正途的门槛上」)',
finding: '见 FINDINGS #3:明的两条路一个 wang 都没有,且明还明文「不許自序門第」。「唯一一朝」不成立。',
proposal: '见 FINDINGS #3。文案,老板拍。',
changed: false,
},
];
// ══════════════════════════════════════════════════════════════════════════
// NOT_DONE —— 【这一刀没做的,和要老板拍的】
// ★ 口径同 dynasties.js:只记【要老板拍的判断】与【已知的失真】,
// 不记「以后可以更好」的许愿 —— 那种东西写下来只会让人以为这里已经想过了。
// ══════════════════════════════════════════════════════════════════════════
export const NOT_DONE = [
{
id: 1,
title: '★★ 辽的奚与渤海:志名下有,本表接不住',
detail:
'志名下的族是四个(「契丹、奚、漢、渤海四軍都指揮使司」,卷46 双证),本表只收了两个。' +
'理由【不是它们不重要,是志只给了契丹与汉一条铨】:全志四卷「銓」只两见,都系于契丹。' +
'奚有奚王府、有帐、有军;渤海有渤海帐司、有渤海军 —— 但【没有一条路说他们怎么入仕】。' +
'⇒ 收它们就得给它们编一条通道,那就是编(辽这一朝的注早写着「别再往上加通道,加了就是编」)。' +
'★ 老板要拍的是:【要不要让用户选「奚」「渤海」,然后诚实地告诉他「这一部书接不住你」?】' +
' 这不是 bug,是「本表问的问题比辽少了两族」。',
},
{
id: 2,
title: '★★★ wang 与出身在魏晋/南朝【仍然是混着的】—— 本刀没拆,因为拆不动',
detail:
'老板的原话:「现有的 wang(声望/乡论)与出身是【两回事】,别合并:wang 是「谁认得你」(可以挣来),' +
'出身是「你生下来是谁」(挣不来)。★ 但魏晋的中正【同时看这两样】(家世 + 乡论)' +
'—— 那正好是它们必须分开的理由。」' +
'★ 本刀【没有拆开它们】,理由是【拆不动,不是没时间】:' +
' · 魏晋/南朝的门第,料【全是刘毅八损疏(政论、被否)】,usableAsInstitution()=false;' +
' · 拿它造一条出身,就是拿被否的奏疏当制度条文 —— 本项目铁律明禁。' +
'⇒ 于是 dynasties.js 的 gate{wang:55}(魏晋,通向三公)与 gate{wang:75}(南朝,通向尚书令)' +
' 【仍然是「家世 + 乡论」两样东西挤在一个数里】。这是已知的失真,本刀不装作修了它。' +
'★ 要拍的是:【魏晋的门第,能不能只靠政论立一维?】本文件的判别式说不能。' +
' 若老板认为能,那要动的是 chiduo.js 的 usableAsInstitution ——' +
' ⚠ 而那个函数正是用来挡「拿刘毅当制度」的。动它之前先想清楚它挡的是什么。',
},
{
id: 3,
title: '⚠ 「这一部传里,你生在……」这一问,本刀只做到【引擎认它】,没做到【产品问它】',
detail:
'本刀交付的是:出身可以存在 lineage 上、可以关路、有原文、有断言。' +
'★ 但【谁在什么时候问用户这一句】,本刀没做:' +
' · verdict 接口收 ?chushen=(预览用,纯计算不写库);' +
' · openLineage 收 chushen 并冻在传上;' +
' · 而【前端的问句、以及「不答就不给辽的结论」这个交互】,本刀没碰。' +
'⇒ 现在的行为是:辽不给出身 → 两条北面路【关着】,reasons 里明写' +
' 「这一部传还没定出身」。★ 这是【故意的】:宁可让用户看见一个没答的问题,' +
' 也不许引擎替他答(那正是本刀要删的那个失真)。' +
'⚠ 但它会让「朝代对照」页在辽这一格显示得比以前难看 —— 那是【诚实的难看】。' +
' 要不要改成前端先问、问完再算,是产品决定,老板拍。',
},
];
export { SHAPE_IDS };
+456
View File
@@ -0,0 +1,456 @@
import dotenv from 'dotenv';
import fs from 'node:fs';
dotenv.config();
const required = (key) => {
const v = process.env[key];
if (!v) {
console.warn(`[config] WARNING: ${key} not set — related feature will be disabled.`);
}
return v || '';
};
// 读证书/密钥:优先 <KEY>(内容,把换行写成 \n),否则 <KEY>_PATH(文件路径)。
// 支付宝/微信平台证书都用这个——运维可选「贴内容」或「给路径」,单一真源。
const readCert = (key) => {
const inline = process.env[key];
if (inline) return inline.replace(/\\n/g, '\n');
const p = process.env[`${key}_PATH`];
if (p && fs.existsSync(p)) return fs.readFileSync(p, 'utf8');
return '';
};
export const config = {
port: Number(process.env.PORT) || 3701,
nodeEnv: process.env.NODE_ENV || 'development',
// ── 数据不出域(国内版合规红线)──────────────────────────────────────
// 海外出站【总闸】,默认【关】(fail-closed)。管 HeyGen(人脸口播,美国)、Resend(邮件)、
// Stripe(支付) 这些非 LLM 的海外服务。真实用户数据不得出境:关闭时这些出站一律不走。
// ⚠ 与 LLM 的 llm.overseasFallback【分开】:Claude 是老板明确要求建站期默认开的,
// 这个总闸默认关(HeyGen 送人脸生物特征、风险最高,不该建站期默认开)。要开:ALLOW_OVERSEAS_EGRESS=true。
overseasEgress: process.env.ALLOW_OVERSEAS_EGRESS === 'true',
// Public origin of THIS api, used to build absolute media URLs the
// separate-origin web app can load AND that DashScope can pull from.
publicUrl:
process.env.PUBLIC_URL ||
(process.env.NODE_ENV === 'production'
? 'https://digitalme-api.invoratec.cn'
: 'http://localhost:3701'),
corsOrigins: (process.env.CORS_ORIGINS || 'http://localhost:5173')
.split(',')
.map((s) => s.trim())
.filter(Boolean),
jwt: {
secret: process.env.JWT_SECRET || 'change-me-in-prod',
expires: process.env.JWT_EXPIRES || '7d',
},
// 登录方式:邮箱密码(内置)+ 手机短信 + 微信扫码。后两者需外部密钥;
// 未配置时走 'stub' 模式(开发可本地验证,不真正发短信/连微信)。
auth: {
// 'aliyun' → 真实发短信;否则 stub(仅非生产环境把验证码放进响应)。
// trim()+toLowerCase():容忍 .env 里的尾部 \r(CRLF)/空格/大小写,避免 "aliyun\r"!=="aliyun"
// 这类静默把短信通道判掉(/api/auth/methods 返回 phone:false)的坑。
smsProvider: (process.env.SMS_PROVIDER || 'stub').trim().toLowerCase(),
// 邮件找回密码:设为非 'stub'(如 'smtp')并实现 sendEmail 才走真实邮件;
// 否则仅非生产环境回传 devCode,生产环境 fail-closed(见 altauth.sendResetCode)。
emailProvider: process.env.EMAIL_PROVIDER || 'stub',
// 「测试方式登录」总开关:设 ALLOW_STUB_LOGIN=true 时,即使生产环境也放开
// 短信/微信的 stub 登录(短信回显验证码、微信模拟扫码)——方便没接真实
// 阿里云短信/微信开放平台时也能登录演示。
// ⚠ 安全警告:开启后,任何人都能凭空登录任意手机号/微信账号(验证码就显示在
// 页面上、扫码是模拟的)。仅供 demo/内测;正式对外前务必设回 false 并接真实 provider。
// ★ 生产环境【永不】放开 stub 登录:即使 .env 误设 ALLOW_STUB_LOGIN=true 也强制关——
// 否则任何人都能凭空登录任意手机号/微信(验证码回显 + 模拟扫码)= 账号接管。仅非生产可用它演示。
// (assertProdConfig 会对生产误设发出告警,提醒去接真实 SMS/微信 provider。)
allowStubLogin: process.env.ALLOW_STUB_LOGIN === 'true' && process.env.NODE_ENV !== 'production',
aliyunSms: {
accessKeyId: process.env.ALIYUN_SMS_KEY_ID || '',
accessKeySecret: process.env.ALIYUN_SMS_KEY_SECRET || '',
signName: process.env.ALIYUN_SMS_SIGN || '',
templateCode: process.env.ALIYUN_SMS_TEMPLATE || '',
// 模板里的验证码变量名(如模板正文 ${code})。默认 code;模板用别的名字时改这里。
codeParam: process.env.ALIYUN_SMS_CODE_PARAM || 'code',
},
// 微信网站应用扫码登录(开放平台)。未配置 → stub 二维码(本地可模拟扫码)。
wechat: {
appId: process.env.WECHAT_APP_ID || '',
appSecret: process.env.WECHAT_APP_SECRET || '',
// 微信回调到本 API;exchange 后再跳回前端。
redirectUri:
process.env.WECHAT_REDIRECT_URI ||
`${process.env.PUBLIC_URL || 'http://localhost:3701'}/api/auth/wechat/callback`,
},
},
// 分身「发到我邮箱」用。设 MAIL_API_KEY + MAIL_FROM(Resend 兼容的 HTTP API)才真发;
// 未配置 → 优雅回落到站内收件箱通知(功能仍在,只是换个到达渠道)。
mail: {
apiKey: process.env.MAIL_API_KEY || '',
from: process.env.MAIL_FROM || '',
},
mongo: {
uri: process.env.MONGO_URI || 'mongodb://localhost:27017',
db: process.env.MONGO_DB || 'digital_me',
},
llm: {
dashscopeKey: required('DASHSCOPE_API_KEY'),
dashscopeBaseUrl:
process.env.DASHSCOPE_BASE_URL ||
'https://dashscope.aliyuncs.com/compatible-mode/v1',
model: process.env.LLM_MODEL || 'qwen-max',
embedModel: process.env.EMBED_MODEL || 'text-embedding-v3',
// 知识检索精排 (A3). DashScope qwen3-rerank(gte-rerank 已于 2026-05-30 下线,官方后继为 qwen3-rerank);无 key 时检索回落余弦序。
rerankModel: process.env.RERANK_MODEL || 'qwen3-rerank',
// Vision model for reading uploaded images/scans into knowledge (OCR + 描述).
visionModel: process.env.VISION_MODEL || 'qwen-vl-max',
anthropicKey: process.env.ANTHROPIC_API_KEY || '',
anthropicModel: process.env.ANTHROPIC_MODEL || 'claude-haiku-4-5-20251001',
// 海外(Claude/Anthropic)兜底开关。⚠ 国内版数据合规:真实用户数据【不得出境】。
// Claude 兜底只在【建站期】临时用;一旦真实用户数据进入生产,必须设 LLM_OVERSEAS_FALLBACK=off
// 换成国内模型,否则 Qwen 一失败,用户聊天内容/大师知识就发去了 api.anthropic.com(美国)。
// 按老板要求:现在【不默认关】(建站要用),但每次启动会大声提醒(见 index.js 启动检查)。
overseasFallback: (process.env.LLM_OVERSEAS_FALLBACK || 'on') !== 'off',
},
// ★ 二创·AI 创作【专用大模型对接点】(老板 2026-07-29「搭框架好对接大模型 API」)。
// 三项配全 → services/aiWrite.js 走你的模型(OpenAI 兼容 /chat/completions);缺任一项 → 回落平台通用网关。
aiWrite: {
apiKey: process.env.AIWRITE_API_KEY || '',
baseUrl: process.env.AIWRITE_BASE_URL || '',
model: process.env.AIWRITE_MODEL || '',
},
// ★ 二创·AI 音乐生成【昆仑万维 Mureka 对接点】。配 MUREKA_API_KEY 即启用;缺 key → 音乐生成
// 不可用(端点 503、优雅降级)。⚠ 数据出境:歌词/曲风会发往 baseUrl(默认 api.mureka.ai)——
// 若合规要求境内,把 MUREKA_BASE_URL 指到 Mureka 的境内 API 域。
mureka: {
apiKey: process.env.MUREKA_API_KEY || '',
baseUrl: process.env.MUREKA_BASE_URL || 'https://api.mureka.ai',
model: process.env.MUREKA_MODEL || 'auto', // auto / mureka-6 / V8 等
},
// ★ 二创·AI 视频生成【字节 Seedance / 火山方舟对接点】。配 key 即启用;缺 key → 视频生成 503。
// 端点走火山方舟【境内北京】域(ark.cn-beijing.volces.com)——数据不出域友好。
// model 需与你火山方舟账号已开通的一致;若账号要求推理接入点,把 SEEDANCE_MODEL 设成 ep-xxxxx。
seedance: {
apiKey: process.env.SEEDANCE_API_KEY || process.env.ARK_API_KEY || '',
baseUrl: process.env.SEEDANCE_BASE_URL || 'https://ark.cn-beijing.volces.com',
model: process.env.SEEDANCE_MODEL || 'doubao-seedance-1-0-pro-250528',
},
// ★ 可灵 Kling(快手)——短剧的【可选引擎】,与 Seedance 并列(老板 2026-07-30)。
// 鉴权【两选一】(见 services/kling.js):
// ① KLING_API_KEY —— 静态 apikey(官网可直接创建),Authorization: Bearer <key>,【适用所有模型(含最新)】。推荐。
// ② KLING_ACCESS_KEY + KLING_SECRET_KEY —— ak/sk 现签 JWT,【仅 3.0 及更早】。做旧账号兼容。
// 配了任一即启用(apikey 优先);都没配则该引擎不显。
// ⚠ 数据出境:画面/图会发往 baseUrl(可灵境内北京域 api-beijing.klingai.com)。model 需与账号已开通的一致。
kling: {
apiKey: process.env.KLING_API_KEY || '',
accessKey: process.env.KLING_ACCESS_KEY || '',
secretKey: process.env.KLING_SECRET_KEY || '',
baseUrl: process.env.KLING_BASE_URL || 'https://api-beijing.klingai.com',
model: process.env.KLING_MODEL || 'kling-v1-6',
},
// ★ AI 创作·图画【图像大模型对接点】(老板 2026-07-29 多模态创作)。配 AICREATE_IMAGE_* 走你的图像模型
// (OpenAI 兼容 /images/generations);缺配 → 回落平台通义万相文生图(services/image.js)。
// 音乐→mureka / 短剧→seedance(见上) / 文本→aiWrite;统一分派见 services/aiCreate.js。
aiCreate: {
image: {
apiKey: process.env.AICREATE_IMAGE_API_KEY || '',
baseUrl: process.env.AICREATE_IMAGE_BASE_URL || '',
model: process.env.AICREATE_IMAGE_MODEL || '',
},
},
// 向量索引后端。默认 memory=现有「MongoDB 内联向量 + 内存暴力余弦」(有 2000 块上限,规模化短板)。
// 配了 DASHVECTOR_* → provider 自动切 dashvector(境内阿里云托管向量库,ANN 索引,数据不出域):
// MongoDB 仍是真相源,DashVector 只做近邻候选检索,替代 2000 块内存扫描。dim 需与 embedModel 一致
// (text-embedding-v3 = 1024)。见 services/vectorStore.js + scripts/reindex-vectors.js。
vector: {
provider: process.env.VECTOR_PROVIDER || (process.env.DASHVECTOR_API_KEY ? 'dashvector' : 'memory'),
dashvector: {
endpoint: process.env.DASHVECTOR_ENDPOINT || '',
apiKey: process.env.DASHVECTOR_API_KEY || '',
collection: process.env.DASHVECTOR_COLLECTION || 'digitalme_knowledge',
dim: Number(process.env.DASHVECTOR_DIM || 1024),
},
},
// MVP 授权 = token 额度分档。骨架期默认「只计量不拦截」(enforce=false),
// 埋好数据/账本/端点,商业化时再打开 enforce + 定价。
license: {
enforce: process.env.LICENSE_ENFORCE === 'true',
defaultTier: process.env.LICENSE_DEFAULT_TIER || 'free',
periodDays: Number(process.env.LICENSE_PERIOD_DAYS || 30),
},
// 功能门控。marketplace = 双边交易撮合平台栈(钱包充值 + 付费咨询墙 + 平台分成)。
// 定位为「卖 MVP 授权的工具,不做平台」→ 默认关闭。代码保留作架构缝,翻 flag 即恢复。
features: {
marketplace: process.env.FEATURE_MARKETPLACE === 'true',
// 未成年人模式(《人工智能拟人化互动服务管理暂行办法》第十四条,2026-07-15 施行)。
// ⚠ 默认口径与 marketplace【故意相反】(用 !== 'off' 而不是 === 'true'):marketplace 是商业特性,
// 没开只是少个功能;这条是【法定义务】,法条已生效——一个"默认关"的合规开关等于没做合规,
// 而且部署时谁忘了配环境变量,站就默默处于违规状态。所以默认开,只留一个紧急止血阀。
// FEATURE_MINOR_MODE=off 仅供【误伤大面积用户时先关再修】,关闭期间本站不符合第十四条,必须上报。
minorMode: (process.env.FEATURE_MINOR_MODE || 'on') !== 'off',
// 天下·乐 采诗官策展社区(用户曲投稿→机审→人审→编辑精选→只读广场)。
// ★默认【关】:是增长特性非法定义务,且公开投稿/展示卡在【实名(短信三要素)】这道法定前提前——
// 实名通道(短信)目前仍是桩,未通之前公开 UGC 不得上线。代码全就绪,老板通短信+配审核+定采诗官人力后,
// 置 FEATURE_YUE_COMMUNITY=on 即亮。关闭期:投稿口回"即将开放"、广场不出、后台队列仍可看(超管)。
yueCommunity: process.env.FEATURE_YUE_COMMUNITY === 'on',
},
// ── 拟人化互动合规(《人工智能拟人化互动服务管理暂行办法》)────────────────
// 网信办/发改委/工信部/公安部/市场监管总局五部门,2026-04-10 公布,2026-07-15 施行。
// 本产品的「数字分身」命中第二条「模拟自然人人格特征、思维模式和沟通风格」要件。
// 这些数字【法条没有写死】(第十四条只说"使用时长限制"、没给分钟数),故做成可调参数;
// 默认值取《未成年人网络保护条例》的通行口径(单日 60 分钟)。调整前请过法务。
compliance: {
minor: {
// 未成年人模式下单日累计【服务端实测】使用时长上限(分钟)。超过 → 后端硬拦问答。
dailyLimitMin: Number(process.env.MINOR_DAILY_LIMIT_MIN) || 60,
// 「定期现实提醒」的周期(分钟)。前端按此周期弹"这是 AI、去看看现实里的人和事"。
realityCheckMin: Number(process.env.MINOR_REALITY_CHECK_MIN) || 20,
},
// 第十四条「并提供相应申诉渠道」——申诉记录落库(minor_appeals)由超管处理,
// 但法条要的是【用户看得见的渠道】,故必须同时给一个人能联系上的方式。留空 → 前端只显示站内申诉,
// 并在页面上诚实写明"仅站内受理"。生产【应当】填(邮箱或电话)。
appealContact: process.env.COMPLIANCE_APPEAL_CONTACT || '',
},
// 标准英文发音 (Azure Neural). Key-gated; empty → fall back to CosyVoice
// English (cloned voice) or browser TTS. For English tutoring this gives
// native-quality pronunciation.
azure: {
key: process.env.AZURE_SPEECH_KEY || '',
region: process.env.AZURE_SPEECH_REGION || 'eastasia',
voiceEn: process.env.AZURE_VOICE_EN || 'en-US-JennyNeural',
},
// 语音合成 CosyVoice。⚠ 版本相关三条硬约束(2026):
// ① v3.5(cosyvoice-v3.5-plus/-flash)仅北京地域;dashscope.aliyuncs.com 即北京端点,
// 默认可用。若工作空间要求专属 MaaS 端点,设 COSYVOICE_WS_URL=wss://<WorkspaceId>.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference。
// ② 预置音色名分版本(v2: longwan_v2 一套 / v3: longanyang 一套 / v3.5 官方尚未公布),
// 换 presetModel 必须同步换预置 voice——故预置音先留 v2,待 v3.5 音色公布再切。
// ③ 克隆音色绑定注册时 target_model,不能跨版本合成:新克隆按 cloneModel 注册并把版本
// 落进 twin.voiceModel;合成时一律用该分身存的 voiceModel,旧 v2 克隆音仍用 v2。
tts: {
// 新克隆音色的注册版本(media.js enrollVoice)。升级点:默认已切 v3.5-flash。
cloneModel: process.env.COSYVOICE_MODEL || 'cosyvoice-v3.5-flash',
// 历史克隆音兜底:有 voiceId 但没存 voiceModel 的旧分身(升级前克隆的都是 v2)。
legacyCloneModel: 'cosyvoice-v2',
// 预置音色 TTS(助手讲解 / 无克隆音时朗读回落)。model 与 voice 必须成对匹配。
// v3.5 预置音色官方未公布 → 默认仍 v2;公布后设 COSYVOICE_PRESET_MODEL + 对应 voice 即可。
presetModel: process.env.COSYVOICE_PRESET_MODEL || 'cosyvoice-v2',
// 实时合成 WebSocket 端点(默认北京地域全局端点)。
wsUrl: process.env.COSYVOICE_WS_URL || 'wss://dashscope.aliyuncs.com/api-ws/v1/inference',
},
avatar: {
// 'dashscope' (alias 'wanx') → 通义万相人像风格重绘 (reuses the
// DASHSCOPE_API_KEY). Blank → the uploaded photo is used flat (often
// the most faithful for a realistic "self" twin).
provider: process.env.AVATAR_PROVIDER || '',
style: process.env.AVATAR_STYLE || '3d',
},
// 水墨场景意象图 (#B 白描) —— 通义万相 text2image,一朝一幅「气象」白描(山河宫阙远景,不画具体人/事)。
// ★ 显式开关,默认【关】:只有【百炼开通万相文生图 + INK_ART_ENABLED=true】才生成;否则前端显占位,零成本零网络。
// 复用 DASHSCOPE_API_KEY;国内 DashScope、数据不出域。每张图明标「意象·非史料」由前端负责。
inkArt: {
enabled: process.env.INK_ART_ENABLED === 'true',
},
// 通用图片生成(百炼·通义万相 文生图)。复用 DASHSCOPE_API_KEY、国内 DashScope、数据不出域。
// 有 DASHSCOPE key 即可用(未开通万相文生图时提交会报错、接口优雅回落)。模型可换:
// wanx2.1-t2i-turbo(快省·质量中) / wanx2.1-t2i-plus(2.1更精·默认) / wanx-v1 等。
// ★★ 2026-07-30 老板明令「最强版·不回落」→ 默认 wan2.7-image-pro(官方盲测文生图超 GPT-Image1.5,当前最强)。
// image.js 已按模型名族自动路由:-image/-image-pro 走【新端点 image-generation/generation + messages 格式】,
// 其余走经典 t2i。★【不做回落】(老板明令)——追最强的代价是需老板部署后【立刻生成一张验证】:
// 出图正常即成;若报错(端点/模型名/参数细节),把报错发我按实调。次强档=wan2.7-image;稳妥档=wan2.6-t2i(经典端点)。
// 模型 env 覆盖:WANX_T2I_MODEL=wan2.7-image / wan2.6-t2i / wan2.2-t2i-plus 任切。
image: {
model: process.env.WANX_T2I_MODEL || 'wan2.7-image-pro',
},
storage: {
driver: process.env.STORAGE_DRIVER || 'local',
localDir: process.env.STORAGE_LOCAL_DIR || './data/uploads',
// 阿里云 OSS(配齐四要素即自动启用,否则回落本地磁盘)。媒体上公网 OSS →
// 免公网穿透、重部署不丢片、DashScope 直接按 URL 拉。共享 bucket 用 prefix 隔离。
oss: {
region: process.env.OSS_REGION || '', // 必带 oss- 前缀,如 oss-cn-hangzhou
bucket: process.env.OSS_BUCKET || '',
accessKeyId: process.env.OSS_ACCESS_KEY_ID || '',
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || '',
endpoint: process.env.OSS_ENDPOINT || '',
prefix: process.env.OSS_PREFIX || 'digitalme', // 共享 bucket 内的 appId 前缀
publicBase: (process.env.OSS_PUBLIC_BASE || '').replace(/\/$/, ''), // 自定义域名/CDN
},
// ★ MinIO(S3 兼容·自建对象存储,数据不出域友好)。配齐 endPoint+bucket+key 即启用,【优先于 OSS】。
// 普通媒体走公开直链(bucket 需对该 prefix 开匿名读,或用 MINIO_PUBLIC_BASE 走 CDN/nginx);
// 母带(HD)走私有 + 短效签名。MINIO_PORT 留空则按 useSSL 取 443/80。
minio: {
endPoint: process.env.MINIO_ENDPOINT || '', // 主机名/IP,不带 http(s):// 和端口
port: Number(process.env.MINIO_PORT) || 0, // 0=按 useSSL 默认(443/80);非标端口(如 9000)才填
useSSL: process.env.MINIO_USE_SSL === 'true',
accessKey: process.env.MINIO_ACCESS_KEY || '',
secretKey: process.env.MINIO_SECRET_KEY || '',
bucket: process.env.MINIO_BUCKET || '',
prefix: process.env.MINIO_PREFIX || 'digitalme', // bucket 内前缀(appId 隔离)
publicBase: (process.env.MINIO_PUBLIC_BASE || '').replace(/\/$/, ''), // 对外直链前缀(CDN/nginx),留空则用 endPoint 直拼
},
},
// 分享卡片(og meta)服务端渲染。给 /me/:slug、/s/:token 注入分身头像+名字,
// 让链接甩进微信/IM 时显示富卡片。默认休眠——要 Nginx 把这两个路径回源到
// 后端才生效(见 DEPLOY.md「分享卡片 og meta」段)。
share: {
// SPA index.html 的服务器路径。填了→读真 index 注入 meta(页面仍是完整 App);
// 不填→回落「meta + 跳转到 web」的独立壳(爬虫拿卡片、真人被转走)。
spaIndexPath: process.env.SPA_INDEX_PATH || '',
// 无头像时的兜底卡片图(绝对 URL),可选。
fallbackImage: process.env.OG_FALLBACK_IMAGE || '',
},
// 微信 JS-SDK(公众号「转发给朋友/朋友圈」自定义卡片)。用的是【公众号】的
// appId/secret,与上面开放平台扫码登录(auth.wechat)是两个不同的微信应用。
// 默认空 → 签名接口返回 {configured:false},前端休眠不加载 SDK。见 DEPLOY.md。
wxJsSdk: {
appId: process.env.WX_MP_APP_ID || '',
appSecret: process.env.WX_MP_APP_SECRET || '',
},
payment: {
// 'stub' → credit immediately (quickest dev).
// 'mock' → full async order→pay→callback→credit loop, no real money
// (verifies the production wiring locally).
// 'stripe' → Stripe Checkout (needs STRIPE_* keys).
// 'wechat' → seam reserved (implement order + callback in payments.js).
provider: process.env.PAYMENT_PROVIDER || 'stub',
// 支付弹窗展示哪些收款方式、按什么顺序(选项卡,一次一个码)。逗号分隔,顺序即展示顺序。
// PAY_METHODS=wechat,alipay → 微信在前;=alipay → 只显示支付宝;=wechat → 只微信。
// 只认 wechat/alipay,去重保序;留空默认 'wechat,alipay'。
methods: [...new Set((process.env.PAY_METHODS || 'wechat,alipay')
.split(',').map((s) => s.trim().toLowerCase()).filter((m) => ['wechat', 'alipay'].includes(m)))],
// 1 元 = how many Token (economy).
tokensPerYuan: Number(process.env.TOKENS_PER_YUAN) || 10,
currency: process.env.PAY_CURRENCY || 'cny',
// Where the user returns after paying (the web app).
webBaseUrl: (process.env.WEB_BASE_URL || (process.env.CORS_ORIGINS || 'http://localhost:5173').split(',')[0]).trim(),
// 测试收款:PAY_ENV=dev 或 test 时,所有付费订单只收 1 分钱 —— 走真实微信/支付宝通道
// 验证下单→收银台→回调→开通全链,但只花 1 分。免费 SKU(¥0) 不受影响。
testOneCent: ['dev', 'test'].includes((process.env.PAY_ENV || '').trim().toLowerCase()),
// 下单显式超时(分钟):过期后二维码/支付链接失效,渠道自动关单。默认 10 分钟。
// 微信 time_expire、支付宝 timeout_express 都用它;前端轮询时长应与之对齐。
orderTimeoutMinutes: Number(process.env.PAY_ORDER_TIMEOUT_MIN) || 10,
stripe: {
secretKey: process.env.STRIPE_SECRET_KEY || '',
webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || '',
},
// 微信支付 v3(Native 扫码付 + 公众号 JSAPI,用于产品工厂购买)。全部配齐才启用,
// 否则商店 checkout 自动回落 stub(无真实收款,便于开发/演示)。
wechat: {
appId: process.env.WXPAY_APP_ID || '', // 公众号/开放平台 appid
mchId: process.env.WXPAY_MCH_ID || '', // 商户号
apiV3Key: process.env.WXPAY_API_V3_KEY || '', // APIv3 密钥(32 位)
serialNo: process.env.WXPAY_CERT_SERIAL || '', // 商户证书序列号
// 商户私钥 PEM:直接给内容(WXPAY_PRIVATE_KEY,\n 转义) 或文件路径(WXPAY_PRIVATE_KEY_PATH)。
privateKey: (process.env.WXPAY_PRIVATE_KEY || '').replace(/\\n/g, '\n'),
privateKeyPath: process.env.WXPAY_PRIVATE_KEY_PATH || '',
// 微信支付【平台证书】(回调验签用公钥)。配上即启用「回调验签闸」(防伪造/重放);
// 未配时回调仅靠 APIv3 GCM 鉴权放行(见 wechatpay.verifyNotifySignature)。
platformCert: readCert('WECHATPAY_PLATFORM_CERT'),
platformCertPath: process.env.WECHATPAY_PLATFORM_CERT_PATH || '',
},
// 支付宝 · 电脑网站支付(前端选支付宝时用)。【证书模式/RSA2】:appId + 应用私钥 + 证书三件套
// 全配齐才启用,否则不走支付宝通道。证书内容把换行写成 \n;也可用 *_PATH 给文件路径。
alipay: {
appId: process.env.ALIPAY_APP_ID || '', // 开放平台应用 appId
privateKey: readCert('ALIPAY_PRIVATE_KEY'), // 应用私钥(内容 或 ALIPAY_PRIVATE_KEY_PATH 文件路径)
appCertContent: readCert('ALIPAY_APP_CERT'), // 应用公钥证书 appCertPublicKey_xxxx.crt
alipayCertContent: readCert('ALIPAY_CERT'), // 支付宝公钥证书 alipayCertPublicKey_RSA2.crt
rootCertContent: readCert('ALIPAY_ROOT_CERT'), // 支付宝根证书 alipayRootCert.crt
gateway: process.env.ALIPAY_GATEWAY || '', // 空=SDK 默认生产网关;沙箱=https://openapi.alipaydev.com/gateway.do
},
},
security: {
// AES-256-GCM key for encrypting visitor chat logs at rest. Any string;
// it's hashed to 32 bytes. Empty → logs stored in plaintext (dev only).
dataEncKey: process.env.DATA_ENC_KEY || '',
// Emails allowed to grant 蓝V verification (and other admin actions).
// ★天下内置超管邮箱恒在列(admin-login 用),这样【无需配 SUPER_ADMIN_EMAILS】也有超管可用。
superAdmins: [
...(process.env.SUPER_ADMIN_EMAILS || '')
.split(',')
.map((s) => s.trim().toLowerCase())
.filter(Boolean),
(process.env.ADMIN_EMAIL || 'admin@tianxia.local').toLowerCase(),
],
// 天下独立超管登录(用户名+密码)。默认 admin/admin123 仅为开办期临时口令 ——
// ⚠ 上线后务必用 env ADMIN_PASS 改成强密码(公网默认口令=谁都能进后台读 PII/结算)。
builtinAdmin: {
user: process.env.ADMIN_USER || 'admin',
pass: process.env.ADMIN_PASS || 'admin123',
email: (process.env.ADMIN_EMAIL || 'admin@tianxia.local').toLowerCase(),
},
// Content moderation tiers (highest available wins, falls back down):
// external — MODERATION_URL set (plug a pro service / Aliyun 内容安全)
// llm — MODERATION_MODE=llm → classify via DashScope (Qwen)
// local — built-in regex floor (always on)
moderationUrl: process.env.MODERATION_URL || '',
moderationKey: process.env.MODERATION_KEY || '',
moderationMode: (process.env.MODERATION_MODE || '').toLowerCase(),
// Hard cap on any visitor/chat prompt length (cost + abuse control).
maxQuestionLen: Number(process.env.MAX_QUESTION_LEN) || 2000,
},
// ── 请求访问日志 ─────────────────────────────────────────────────────
// 每个 HTTP 请求完成时打一行(方法/路径/状态码/耗时/IP/请求ID)。默认开;LOG_REQUESTS=off 关。
// 纪律:只记元数据,【不记 query 与 body】——可能含 token / 用户私有内容(数据不出域)。
log: {
requests: (process.env.LOG_REQUESTS || 'on') !== 'off',
// 慢请求阈值(ms):超过则该行标记 ⚠slow,便于揪出卡顿(AI 接口尤甚)。
slowMs: Number(process.env.LOG_SLOW_MS) || 5000,
// 健康检查等高频噪声路径不记(逗号分隔,前缀匹配)。
skip: (process.env.LOG_SKIP_PATHS || '/health').split(',').map((s) => s.trim()).filter(Boolean),
},
// 'true' when behind a reverse proxy (nginx / cloudflare) so req.ip is
// the real client IP (correct rate-limiting). Default on in production.
trustProxy:
process.env.TRUST_PROXY === 'true' ||
(process.env.TRUST_PROXY !== 'false' && process.env.NODE_ENV === 'production'),
};
// Fail fast in production on insecure defaults — never boot a real
// deployment with a guessable JWT secret or unencrypted visitor logs.
export function assertProdConfig() {
if (config.nodeEnv !== 'production') return;
const fatal = [];
if (!process.env.JWT_SECRET || config.jwt.secret === 'change-me-in-prod')
fatal.push('JWT_SECRET must be set to a strong random value in production');
if (fatal.length) {
throw new Error('Insecure production config:\n - ' + fatal.join('\n - '));
}
const warn = [];
if (!config.security.dataEncKey)
warn.push('DATA_ENC_KEY not set — visitor chat logs will be stored UNENCRYPTED');
// ★默认超管口令硬警告(不设 fatal:生产正用 admin123,fatal 会让下次部署崩站锁死老板)。
// 公网默认口令 = 任何人可登后台读 PII/漏斗/清广场/审问鼎 —— 必须尽快 env 设 ADMIN_PASS 强口令。
if (config.security.builtinAdmin?.pass === 'admin123')
warn.push('🔴 ADMIN_PASS 未设 —— 天下超管仍是默认口令 admin/admin123!公网任何人可登后台。请立刻在生产 .env 设 ADMIN_PASS=<强口令>(和可选 ADMIN_USER)');
// superAdmins 恒含内置邮箱,原判据永不触发(假象)。改判"除内置外有没有配真实超管邮箱"。
const realAdmins = config.security.superAdmins.filter((e) => e !== config.security.builtinAdmin?.email);
if (!realAdmins.length)
warn.push('SUPER_ADMIN_EMAILS not set — 除内置 admin 账号外无其它超管(蓝V/未成年申诉/问鼎核验仅内置账号可做)');
if (process.env.ALLOW_STUB_LOGIN === 'true')
warn.push('ALLOW_STUB_LOGIN=true is IGNORED in production (stub login force-disabled to prevent account takeover) — set up real SMS (SMS_PROVIDER=aliyun) / WeChat providers to enable consumer login');
warn.forEach((w) => console.warn(`[config] WARNING: ${w}`));
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+349
View File
@@ -0,0 +1,349 @@
import { MongoClient } from 'mongodb';
import { config } from './config.js';
let client;
let db;
// 复用共享库(creator_cn)时, 用集合前缀隔离, 避免与其它产品(si_/ecom_)的同名集合冲突。
// COLLECTION_PREFIX 为空则不加前缀(独立库时)。在 db 入口统一包一层, 业务代码无需改动。
const PREFIX = process.env.COLLECTION_PREFIX || '';
function withPrefix(rawDb) {
if (!PREFIX) return rawDb;
return new Proxy(rawDb, {
get(target, prop, receiver) {
if (prop === 'collection') {
return (name, ...rest) => target.collection(PREFIX + name, ...rest);
}
const v = Reflect.get(target, prop, receiver);
return typeof v === 'function' ? v.bind(target) : v;
},
});
}
export async function connectDb() {
if (db) return db;
// 共享单机 + 24 应用共库:限住连接池与超时,避免慢查询把池占满→请求排队雪崩、连累同机其它应用。
client = new MongoClient(config.mongo.uri, {
maxPoolSize: 30,
waitQueueTimeoutMS: 8000,
serverSelectionTimeoutMS: 8000,
});
await client.connect();
db = withPrefix(client.db(config.mongo.db));
// Indexes. One owner may raise many twins (MVP: usually one "self").
// Identity indexes — each unique only among docs that HAVE that field
// (a user may sign up by email OR phone OR WeChat). Conflict-safe: if an
// older index with different options exists, drop & recreate it.
const ensureIdx = async (coll, key, opts) => {
try {
await db.collection(coll).createIndex(key, opts);
} catch (e) {
if (e.codeName === 'IndexOptionsConflict' || e.code === 85 || e.code === 86) {
const name = Object.keys(key)[0] + '_1';
await db.collection(coll).dropIndex(name).catch(() => {});
await db.collection(coll).createIndex(key, opts);
} else throw e;
}
};
await ensureIdx('users', { email: 1 }, { unique: true, partialFilterExpression: { email: { $type: 'string' } } });
await ensureIdx('users', { phone: 1 }, { unique: true, partialFilterExpression: { phone: { $type: 'string' } } });
await ensureIdx('users', { wechatOpenId: 1 }, { unique: true, partialFilterExpression: { wechatOpenId: { $type: 'string' } } });
await db.collection('twins').createIndex({ ownerId: 1 });
await db.collection('knowledge').createIndex({ twinId: 1 });
// 部分索引:只索引 vectorPending=true 的少量块(灌 DashVector 失败待补的),让 retrieve 的 pending 并集查询
// O(pending) 而非扫该分身全量;非 pending 块不进此索引(近乎零成本)。见 knowledge.js retrieve/ingest。
await db.collection('knowledge').createIndex({ twinId: 1, vectorPending: 1 }, { partialFilterExpression: { vectorPending: true } });
await db.collection('messages').createIndex({ twinId: 1, createdAt: 1 });
// Hot per-twin lookups (every chat turn / management views).
await db.collection('memories').createIndex({ twinId: 1 });
await db.collection('sessions').createIndex({ twinId: 1 });
await db.collection('errors').createIndex({ twinId: 1 });
await db.collection('social_messages').createIndex({ twinId: 1, shareId: 1 });
// Public shareable twin profile (「和我分身聊天」嵌入页) is addressed by slug.
await db
.collection('twins')
.createIndex(
{ slug: 1 },
{ unique: true, partialFilterExpression: { slug: { $type: 'string' } } }
);
// 社交分身: share cards addressed by unguessable token; visitor logs per card.
await db.collection('shares').createIndex({ token: 1 }, { unique: true });
await db.collection('shares').createIndex({ twinId: 1, createdAt: -1 });
await db
.collection('social_messages')
.createIndex({ shareId: 1, visitorId: 1, createdAt: 1 });
// 数字人广场: discovery listing query (public + listed, popular first).
await db.collection('twins').createIndex({ inPlaza: 1, isPublic: 1, likeCount: -1 });
// 「像不像」评分去重:每访客每卡一票。
await db.collection('plaza_ratings').createIndex({ slug: 1, voter: 1 }, { unique: true });
// AI 短剧异步任务: sweeper 扫 pending、「我的任务」按 key 列最近。
await db.collection('create_jobs').createIndex({ status: 1 });
await db.collection('create_jobs').createIndex({ key: 1, createdAt: -1 });
// 二创作品持久化:「作品集」按 key 列最近;(key,media) 供落库去重。
await db.collection('creations').createIndex({ key: 1, createdAt: -1 });
await db.collection('creations').createIndex({ key: 1, media: 1 });
// 群聊协作: rooms by owner / token, messages by room+time.
await db.collection('rooms').createIndex({ ownerId: 1, updatedAt: -1 });
await db.collection('rooms').createIndex({ token: 1 }, { unique: true });
await db.collection('room_messages').createIndex({ roomId: 1, createdAt: 1 });
// 钱包 + 付费咨询.
await db.collection('wallet_tx').createIndex({ userId: 1, createdAt: -1 });
await db.collection('consultations').createIndex({ userId: 1, createdAt: -1 });
await db.collection('payments').createIndex({ userId: 1, createdAt: -1 });
// 埋点漏斗.
await db.collection('events').createIndex({ app: 1, event: 1, ts: 1 });
// 产品工厂:订单 + 一次性登录令牌(TTL 自动过期).
await db.collection('store_orders').createIndex({ status: 1, createdAt: 1 });
await db.collection('store_orders').createIndex({ email: 1 });
await ensureIdx('magic_tokens', { tokenHash: 1 }, { unique: true });
await db.collection('magic_tokens').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
// 通用作品集.
await db.collection('works').createIndex({ twinId: 1, createdAt: -1 });
// 白话缓存:getBaihua/getBaihuaFor 每请求按 {id} 查,原为全表扫 → 加索引。
await db.collection('baihuaCache').createIndex({ id: 1 });
// 天下·乐 采诗官策展社区:人审队列(按状态+时间)、只读广场(精选+时间)、我的投稿(按用户)。
await db.collection('yue_submissions').createIndex({ status: 1, createdAt: 1 }); // 人审队列
await db.collection('yue_submissions').createIndex({ status: 1, featured: 1, reviewedAt: -1 }); // 广场
await db.collection('yue_submissions').createIndex({ userId: 1, createdAt: -1 }); // 我的投稿
// 问答分享卡(裂变).
await db.collection('qa_shares').createIndex({ twinId: 1, createdAt: -1 });
// 数字大师传承馆 · 高端文旅.
await db.collection('twins').createIndex({ isMaster: 1, domain: 1, region: 1 });
await db.collection('masters').createIndex({ twinId: 1 }, { unique: true });
await db.collection('agreements').createIndex({ masterId: 1, createdAt: -1 });
await db.collection('tours').createIndex({ status: 1, region: 1, updatedAt: -1 });
await db.collection('tours').createIndex({ masterIds: 1 });
await db.collection('bookings').createIndex({ status: 1, createdAt: -1 });
await db.collection('bookings').createIndex({ tourId: 1 });
await db.collection('payouts').createIndex({ masterId: 1, status: 1 });
await db.collection('payouts').createIndex({ bookingId: 1 });
await db.collection('master_applications').createIndex({ status: 1, createdAt: -1 });
await db.collection('partner_applications').createIndex({ role: 1, status: 1, createdAt: -1 });
await db.collection('schedules').createIndex({ departureDate: 1 });
await db.collection('schedules').createIndex({ masterId: 1 });
// 高频/无界集合补索引,避免规模化后全表扫拖慢共享库上所有应用。
await db.collection('notifications').createIndex({ userId: 1, read: 1, createdAt: -1 });
await db.collection('notifications').createIndex({ userId: 1, dedup: 1 });
await db.collection('twin_learnings').createIndex({ twinId: 1, status: 1, createdAt: -1 });
// 课堂旁听并发去重:{twinId,classId} 部分唯一(仅当 classId 存在,自习无 classId 字段不受影响)。
// ⚠ 唯一索引在生产已有历史重复(修复前反复点旁听留下的同 twin+class 多条)时会建失败——
// 绝不能让它拖垮全站启动。自愈:建失败→先去重(同键保留最新一条)→重建;仍失败则回落非唯一索引。
// 幂等且自限:索引一旦建成,新重复被 joinClass 原子占位挡住,后续启动无重可去。
try {
await db.collection('twin_learnings').dropIndex('twinId_1_classId_1');
} catch { /* 不存在忽略 */ }
await buildLearningUniqueIndex(db);
await db.collection('class_schedules').createIndex({ active: 1, nextRunAt: 1 }); // 定时器扫到点排期
await db.collection('token_usage').createIndex({ userId: 1, createdAt: -1 });
await db.collection('classrooms').createIndex({ createdAt: -1 });
await db.collection('knowledge').createIndex({ twinId: 1, createdAt: -1 }); // retrieve 按新近度取候选
// 分身「帮我干活」新集合(待办/工作流/运行历史)——每分钟调度器全表扫的重点,必须建索引。
await db.collection('tasks').createIndex({ twinId: 1, done: 1 }); // add/list/count
// ESR:等值(done)后紧跟【选择性区间】(dueAt),再放 $ne 的 reminded——否则 dueAt 退化成残余过滤,到点扫仍近全表。
await db.collection('tasks').createIndex({ done: 1, dueAt: 1, reminded: 1 }); // sweepDueReminders 全局到点扫
// 待办去重下沉到 DB(check-then-insert 并发下会漏)。部分唯一:仅未完成的同 {twinId,text} 唯一,完成后可再加。
// 生产可能已有历史重复→建失败不能拖垮启动,try/catch 非致命(建不成也不比现状差)。
try { await db.collection('tasks').createIndex({ twinId: 1, text: 1 }, { unique: true, partialFilterExpression: { done: false } }); }
catch { /* 已有未完成重复项时建失败,忽略 */ }
await db.collection('workflows').createIndex({ twinId: 1 });
await db.collection('workflows').createIndex({ 'schedule.enabled': 1, nextRunAt: 1 }); // dueWorkflows 定时扫
await db.collection('workflow_runs').createIndex({ workflowId: 1, createdAt: -1 }); // recordRun 裁剪/listRuns
await db.collection('workflow_runs').createIndex({ twinId: 1 }); // 级联删
await db.collection('twins').createIndex({ proactiveNextAt: 1 }); // runProactiveDigests 到点扫
await db.collection('twins').createIndex({ weeklyReportNextAt: 1 }); // runWeeklyReports 到点扫
// ── 起居注 shishu_acts(「分身替你记的」,见 services/acts.js)──
// 分身替主人干的每一件事(工具名 + 入参 + 结果快照)。立传的原料。
await db.collection('shishu_acts').createIndex({ twinId: 1, createdAt: -1 }); // 时间轴倒序 + 每分身裁剪
await db.collection('shishu_acts').createIndex({ ownerId: 1, createdAt: -1 }); // 主人跨分身回看
//
// ⚠⚠【绝不许给 shishu_acts 加 TTL】——这一段是写给下一个改这个文件的人的。
// 下面 ttl() 那一段的原则是「用户内容不设 TTL,只对埋点/用量设 TTL」(events/token_usage/errors)。
// 起居注【按用户内容对待】:它长得像埋点(一条一条的工具调用记录),但它不是埋点——
// ★ 它是这个产品的【原料】:起居注 → 实录 → 正史。给它设 TTL 等于宣布
// 「你的传只能从最近半年写起」—— 卖点当场塌一半,而且【补录不了】:
// 被 TTL 清掉的那半年,分身替你搜过什么、怎么搜的,永远回不来了。
// ✗ 不许照 events / token_usage 的先例给它加 ttl('shishu_acts', 'createdAt', N) —— 一天都不许。
// ✓ 防爆盘走另一条路,已经在 services/acts.js 里做了两道闸:
// ① result 硬截断 500 字符;② 每分身 2000 条上限(超了裁最旧的)。
// 这两道闸砍的是【体积】,不砍【年份】—— 这是它们和 TTL 的根本区别。
// (为什么要写这么长:twin_learnings / qa_shares 当初就是没人写清楚,于是被漏了四次。)
// ── 一部传 shishu_lineages(「史记我的传」,见 services/lineage.js)──
// 一部传 = 一条【不可变】的决策链。★ 选了就是选了,没有读档。
//
// ⚠⚠【绝不许给 shishu_lineages 加 TTL】—— 理由与上面 shishu_acts 那一段【同源,且更硬】:
// 起居注被 TTL 清掉 = 「你的传只能从最近半年写起」;而传本身被 TTL 清掉 = 【那本书没了】。
// ★ 「另写一版不是读档」这句话,全部押在【旧的那部仍然在、仍然可读】上 ——
// 给它设 TTL,就是让旧版到期自动消失 ⇒ 另写一版变成了【延迟生效的读档】。
// ✗ 一天都不许。这个集合每人只有几十个文档,它不是爆盘的来源。
await db.collection('shishu_lineages').createIndex({ twinId: 1, createdAt: -1 }); // 我的传(列表)
await db.collection('shishu_lineages').createIndex({ ownerId: 1, createdAt: -1 }); // 主人跨分身回看
// ★★ 结构律★五「一朝成传,再下一朝 —— 不同时开二十四个坑」的【DB 侧守卫】。
// 引擎里已经判了(openLineage 见到未完结的就抛),⚠ 但那是 read-then-write:
// 两个请求【同时】打 POST /lineage,两边都读到「没有未完结的」,于是开出两部。
// ⇒ 部分唯一索引把它收敛成一条:每个分身至多一部 status:'open'。
// (已 closed 的不进此索引 —— partialFilterExpression 正是为此。)
// ⚠ 唯一索引【绝不能拖垮启动】(index.js 是 connectDb().catch(()=>process.exit(1))):
// 历史重复数据会让它建失败。照 tasks / minor_usage 的写法 try/catch 回落非唯一 ——
// 退化后果是「极端并发下可能开出两部未完结的传」,站还活着;比全站起不来好得多。
try {
await db.collection('shishu_lineages').createIndex(
{ twinId: 1 },
{ unique: true, partialFilterExpression: { status: 'open' } }
);
} catch {
try { await db.collection('shishu_lineages').createIndex({ twinId: 1, status: 1 }); }
catch { /* 查询退化为扫描,不致命 */ }
console.warn('[db] shishu_lineages open-unique idx 建失败,已回落非唯一(极端并发下可能同时开两部未完结的传)');
}
// lineageId = `${twinId}/${dynasty}/${edition}`,引擎拿它当一部传的身份。
// ★ 唯一:并发开传时版次可能被算重(两个请求同时读到 max edition)——撞了就 11000,
// services/lineage.js 把它翻成 409,而不是悄悄造出两部同 id 的传。
// ⚠ 同样 try/catch 回落:一条索引建失败不许 = 整站起不来。
try {
await db.collection('shishu_lineages').createIndex({ lineageId: 1 }, { unique: true });
} catch {
try { await db.collection('shishu_lineages').createIndex({ lineageId: 1 }); }
catch { /* 同上 */ }
console.warn('[db] shishu_lineages lineageId unique idx 建失败,已回落非唯一');
}
// ── 未成年人模式(《人工智能拟人化互动服务管理暂行办法》第十四条,2026-07-15 施行)──
// 每人每天一条用量。{userId,day} 必须【唯一】:gateAsk 每次问答都 upsert 这一条,并发提问
// 没有唯一索引会造出多条 → 时长被"分裂计数"、限制形同虚设(这正是它要防的滥用面)。
// ⚠ 但唯一索引【绝不能】拖垮启动(index.js 是 connectDb().catch(()=>process.exit(1))):
// 历史重复数据会让它建失败。照 tasks 的写法 try/catch,失败则回落非唯一索引——
// 退化后果是"极端并发下可能少算时长",站还活着;比全站起不来好得多。
let minorUsageUnique = true;
try {
await db.collection('minor_usage').createIndex({ userId: 1, day: 1 }, { unique: true });
} catch {
minorUsageUnique = false;
}
if (!minorUsageUnique) {
// 同 key 建非唯一会与已存在的唯一索引冲突(IndexOptionsConflict) → 也包起来。
try { await db.collection('minor_usage').createIndex({ userId: 1, day: 1 }); } catch { /* 查询退化为扫描,不致命 */ }
console.warn('[db] minor_usage unique idx 建失败,已回落非唯一(未成年人时长在极端并发下可能少算)');
}
// 当日用量属运营数据(申诉时要能回答"他那天到底用了多久"),留 90 天后自动清:
// 共享单机 99G 盘,一个每天每人一条、永不删的集合就是下一次满盘宕机。
// try/catch 的理由与下方 ttl() 助手一致:哪天有人把保留期从 90 天改成别的,
// TTL 索引会因选项冲突建失败 —— 那不该让全站起不来(index.js 是 catch→exit)。
try { await db.collection('minor_usage').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }); }
catch { /* 已存在不同 TTL 时忽略,退化为不自动清(有盘满风险,但站活着) */ }
// 申诉渠道(法条明写要有)。超管按 status 翻待处理;用户侧查自己有没有在处理中的申诉。
await db.collection('minor_appeals').createIndex({ status: 1, createdAt: -1 });
await db.collection('minor_appeals').createIndex({ userId: 1, status: 1 });
// ── 家长监护(第十四条【第二款】,7c423f3 删掉后重建 —— 见 services/guardian.js 文件头)──
// ⚠ 本段所有【唯一索引】与【TTL 索引】都必须 try/catch 回落:index.js 是
// connectDb().catch(()=>process.exit(1)),一条索引建失败 = 整站起不来。
// 生产已有历史数据/不同选项时建失败是常态,不能让合规功能的索引成为全站的单点。
await db.collection('guardian_links').createIndex({ guardianId: 1, createdAt: -1 }); // 监护人看列表
await db.collection('guardian_links').createIndex({ wardId: 1 }); // flagSensitive 每条消息都查"有没有监护人"→ 必须走索引
// {guardianId,wardId} 唯一:linkByCode 是 upsert,并发兑换同一个码没有唯一索引会造出重复关系
// (于是 listWards 里同一个孩子出现两次、解绑只解掉一条)。建不成就回落非唯一:退化后果是
// 极端并发下可能有重复关系,站还活着。
try {
await db.collection('guardian_links').createIndex({ guardianId: 1, wardId: 1 }, { unique: true });
} catch {
try { await db.collection('guardian_links').createIndex({ guardianId: 1, wardId: 1 }); } catch { /* 查询退化为扫描,不致命 */ }
console.warn('[db] guardian_links unique idx 建失败,已回落非唯一(并发兑换邀请码可能产生重复监护关系)');
}
// 邀请码:code 唯一(兑换按 code 查),expiresAt TTL 自动清。
// ⚠ TTL 最长 60 秒才扫到过期文档 → linkByCode 里【自己也比一次 expiresAt】,不靠 TTL 做安全边界。
try {
await db.collection('guardian_codes').createIndex({ code: 1 }, { unique: true });
} catch {
try { await db.collection('guardian_codes').createIndex({ code: 1 }); } catch { /* 同上 */ }
console.warn('[db] guardian_codes unique idx 建失败,已回落非唯一');
}
try { await db.collection('guardian_codes').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }); }
catch { /* 已存在不同 TTL 时忽略:码仍会因 expiresAt 校验失效,只是文档不自动清 */ }
await db.collection('guardian_codes').createIndex({ wardId: 1 }); // 重新生成时作废旧码
// 风险告警:按 ward 倒序翻;90 天 TTL(片段是未成年人的原话,不能永久留 + 共享单机盘就那么大)。
await db.collection('guardian_alerts').createIndex({ wardId: 1, createdAt: -1 });
try { await db.collection('guardian_alerts').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 }); }
catch { /* 已存在不同 TTL 时忽略,退化为不自动清(有盘满风险,但站活着) */ }
// 屏蔽特定角色:{wardId,twinId} 唯一 —— gateAsk 每次未成年人问答都按这个键 findOne,
// 既是热路径索引也是去重(blockTwin 的 upsert 靠它把并发点击收敛成一条;11000 被当成功处理)。
try {
await db.collection('guardian_blocks').createIndex({ wardId: 1, twinId: 1 }, { unique: true });
} catch {
try { await db.collection('guardian_blocks').createIndex({ wardId: 1, twinId: 1 }); } catch { /* 同上 */ }
console.warn('[db] guardian_blocks unique idx 建失败,已回落非唯一(屏蔽仍然生效,只是可能有重复行)');
}
await db.collection('guardian_blocks').createIndex({ guardianId: 1 }); // 解绑/注销时清这位监护人设的屏蔽
// 磁盘「保险丝」——给无界增长的低价值集合加 TTL,防满盘→本地 mongod 宕→全站 24 应用连锁瘫。
// 只对可安全过期的埋点/用量/错误/匿名会话设 TTL;用户内容(messages/knowledge/记忆)不动。
const ttl = async (coll, field, days) => {
try { await db.collection(coll).createIndex({ [field]: 1 }, { expireAfterSeconds: days * 86400 }); }
catch { /* 已存在不同 TTL 时忽略 */ }
};
// 公开面每日调用配额计数(quota.js):到 expiresAt 即自动清理。
await db.collection('daily_quota').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
// 口播视频任务(talkingVideo.js):落库供轮询,到 expiresAt 自动清。
await db.collection('talk_video_jobs').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
await ttl('events', 'ts', 120); // 埋点
await ttl('token_usage', 'createdAt', 180); // LLM 用量流水
await ttl('errors', 'createdAt', 60); // 错误日志
await ttl('sessions', 'updatedAt', 365); // 空闲会话
await ttl('notifications', 'createdAt', 180); // 站内通知:定时工作流/提醒每次新插一条,无界增长→半年 TTL 封顶
await ttl('create_jobs', 'createdAt', 7); // AI短剧异步任务:分钟级完成,记录随生成量无界增长→7天TTL防DB侧慢性满盘
// 竞技场(每日赶考)已下线:对抗验证证明它测不到它想测的东西(卷子按赛道出、从不碰分身知识库→有据率恒0→
// 分数永远是砍半的 50 分),线上 14 个公开分身里只有 1 个考过 1 次。信号改由【真实对话的有据率】免费产出
// (见 twin.js getGrounding)。这里把三个集合一次性丢掉,别在共享库里留死数据。幂等、失败不致命。
for (const c of ['arena_exams', 'arena_entries', 'arena_ratings']) {
try { await db.collection(c).drop(); console.log(`[db] dropped ${c} (竞技场已下线)`); } catch { /* 不存在即已清 */ }
}
console.log(`[db] connected → ${config.mongo.db}`);
return db;
}
// 课堂学习 {twinId,classId} 部分唯一索引 —— 自愈式建立(见上方调用点注释)。
async function buildLearningUniqueIndex(db) {
const coll = db.collection('twin_learnings');
const spec = { unique: true, partialFilterExpression: { classId: { $exists: true } } };
try {
await coll.createIndex({ twinId: 1, classId: 1 }, spec);
return; // 常态:无重复,直接建成
} catch (e) {
console.warn('[db] twin_learnings unique idx failed, deduping…', e.message);
}
try {
// 同 {twinId,classId} 只保留最新一条(内容最全),其余删。自习无 classId 不进此组。
const groups = await coll.aggregate([
{ $match: { classId: { $exists: true, $ne: null } } },
{ $sort: { createdAt: -1 } },
{ $group: { _id: { t: '$twinId', c: '$classId' }, keep: { $first: '$_id' }, ids: { $push: '$_id' } } },
{ $match: { $expr: { $gt: [{ $size: '$ids' }, 1] } } },
]).toArray();
const dropIds = [];
for (const g of groups) for (const id of g.ids) if (String(id) !== String(g.keep)) dropIds.push(id);
if (dropIds.length) {
await coll.deleteMany({ _id: { $in: dropIds } });
console.warn(`[db] deduped twin_learnings: removed ${dropIds.length} dup classroom record(s)`);
}
await coll.createIndex({ twinId: 1, classId: 1 }, spec);
console.log('[db] twin_learnings unique index built after dedup');
} catch (e2) {
console.warn('[db] twin_learnings unique idx skipped after dedup:', e2.message);
try { await coll.createIndex({ twinId: 1, classId: 1 }); } catch { /* 回落非唯一,保查询性能 */ }
}
}
export function getDb() {
if (!db) throw new Error('DB not connected — call connectDb() first');
return db;
}
export async function closeDb() {
await client?.close();
db = undefined;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+403
View File
@@ -0,0 +1,403 @@
import { aiWrite } from './services/aiWrite.js'; // ★ 二创 AI 创作统一走【大模型对接点】(老板可在 aiWrite.js/env 接自己的大模型)
import { ZHANZHEN } from './zhanzhen.js';
import { getNvxing, listNvxing } from './nvxing.js';
import { listTazhuan } from './tazhuan.js';
import { getMingchangmian, listMingchangmian } from './mingchangmian.js';
import { buildPack, scanEra, eraForbidden } from './sucaibao.js';
import { getDirection, specFromDirection, listDirections, deriveTuiti, toolsForOutputKind, TOOLS } from './chuangzuo.js';
import { HOOKS } from './erchuangHooks.js'; // 锚点创作钩子(勾人的写作引子·扣双证)
/**
* 二次创作·文学篇 —— 把你亲历的那一段历史(战争篇的一场战役 / 她传的一个人),就地变成一篇文学作品。
*
* ★★ 定位(天下·历史版自己的增长闭环,非大师变现):亲历历史 → 二创成【可晒可传的作品】 → 传播拉新 + 精装我的传。
* ★★★ 红线:
* ① 创作 ≠ 史料:每篇作品都【锚定一段双证史实】做地基,输出显式标「文学演绎·非史料」,并附真实正史引文;
* LLM 可想象细节、抒情言志,但【不得杜撰与史实相悖的硬事实】(胜负/生死须与所给史实一致)。
* ② 走 DashScope·Qwen(services/llm.js chat),无 key / 海外兜底关时优雅降级(抛错→路由 502,不外发不崩)。
* ③ 零游戏味:不写"解锁/成就/通关"。
*/
// 文学体裁。layer='work'=抒情整篇(诗词赋·整篇即演绎不做句级切分);'segment'=叙事分层(句级出处分层·细颗粒)。
export const STYLES = {
shi: { label: '古体诗', layer: 'work', hint: '一首古体诗(五言或七言,四到八句),要押韵、有意象、见胸襟' },
ci: { label: '词', layer: 'work', hint: '一阕词(可择《满江红》《念奴娇·赤壁怀古》《破阵子》等词牌,大致合其句读长短),豪放或沉郁随情而定' },
fu: { label: '赋', layer: 'work', hint: '一段短赋(骈散结合,铺陈其时其地其人,气势沉雄,二三百字)' },
ji: { label: '记', layer: 'segment', hint: '一篇短文·记,以亲历者视角叙这一段的所见所为所感,二三百字' },
juben: { label: '短剧剧本', layer: 'work', hint: '一段短剧分镜脚本(3-5 场,每场用【场景】【人物】【画面】【台词】【旁白】标注,可直接用于 AI 短剧或漫画分镜),紧扣史实的戏剧冲突,画面感强' },
// ── 马伯庸式细颗粒格式(segment·句级出处分层)──
weishichen: { label: '微时辰', layer: 'segment', eye: 'low', hint: '一名低阶真实在场者,在两个相邻史实节点之间的【一个时辰】(约两小时)里的所见所为——把时间窗掐死,细节从时间里挤出来,六七百字' },
duitan: { label: '史缝对谈', layer: 'segment', dialogue: true, hint: '两个同场的真实历史人物,在史书没记原话的缝隙里的一次对谈,纯对白体,各说各的身份口吻——★对白全是演绎、绝不冒充实录' },
gongzhi: { label: '一日供职', layer: 'segment', hint: '一个真实历史人物在那个朝代的一个寻常工作日,从点卯到散衙——官制程序是史实、日常器物是合理还原' },
huimu: { label: '回目片段', layer: 'segment', hint: '带回目对句的章回体小说片段——回目由史实节点生成,同样的章回皮里没有一句演义' },
gongxu: { label: '工序帖', layer: 'segment', hint: '把一道具体工序(如火船怎么裹薪草膏油、立牙旗)从头拆到可操作,每步扣住正史确有的器物' },
chuanyue: {
label: '穿越情感剧', layer: 'segment', hint: '一段穿越情感短剧:一个现代人穿越进这个真实历史场景,与一位真实历史人物结下情感牵绊',
genre: '★穿越设定与情感线全部标 [[y:穿越]]/[[y:情感]];穿越者是虚构的现代人(标 y:虚构)。情感对象须取骨架/名录里的【真实历史人物】,其真实结局锁死不许改——最动人的是「你明知他的结局,却改变不了」的宿命感:你能陪他,却救不了他。历史大势一步不改,改的只是你在缝隙里陪他走过的那些没被史书记下的时刻。',
},
quanmou: {
label: '权谋朝斗', layer: 'segment', hint: '一段权谋短剧:真实的朝堂/军帐博弈里,各方算计、进退、下套',
genre: '★所有权力大势、胜负、人物真实结局锁死史实(挂 f:cite);算计的对白、心机、暗流是演绎(标 [[y:对话]]/[[y:内心]])。谋算要扣住真实的利害关系(谁真的忌惮谁、谁真的被谁除掉),不架空、不爽文。',
},
xuanyi: {
label: '悬疑探案', layer: 'segment', hint: '一段历史悬疑:以真实事件为谜面,一个视角人物抽丝剥茧',
genre: '★谜底=真实史实(不可改·挂 f:cite);一个视角人物的推理过程、勘验、对白是演绎(标 y)。线索要落在正史确有的细节/器物/人物关系上,最后收束到真实结局——是「解开一桩真事」不是编一个假案。',
},
};
// 把一个锚点(战役 / 她传人物)解析成一张【双证史实卡】——作品的地基。
function anchorFact(anchorType, id) {
if (anchorType === 'zhanzhen') {
const b = ZHANZHEN[String(id || '')];
if (!b) return null;
return {
type: 'zhanzhen', id: b.id, title: b.name,
sub: `${b.side} · ${b.year < 0 ? '前' + -b.year : b.year}年`,
facts: [b.stake, b.real].filter(Boolean),
cites: [b.cite, ...(b.nodes || []).slice(0, 3).map((n) => n.cite)].filter(Boolean),
};
}
if (anchorType === 'nvxing') {
const w = getNvxing(String(id || ''));
if (!w) return null;
return {
type: 'nvxing', id: w.id, title: w.name,
sub: `${w.dynName} · ${w.title}`,
facts: [w.intro].filter(Boolean),
cites: (w.arc || []).slice(0, 3).map((g) => g.cite).filter(Boolean),
};
}
if (anchorType === 'mingchangmian') {
const s = getMingchangmian(String(id || ''));
if (!s) return null;
return {
type: 'mingchangmian', id: s.id, title: s.title,
sub: `${s.era || ''}${s.place ? ' · ' + s.place : ''}`,
facts: [s.intro, s.outcome?.text].filter(Boolean),
cites: [...(s.beats || []).slice(0, 2).map((b) => b.cite), s.outcome?.cite].filter(Boolean),
};
}
return null;
}
// 可创作的锚点名录(名场面 + 战役 + 她传人物)。纯读,供前端选“为哪一段作一篇”。
export function listErchuangAnchors() {
// ★ 名场面:二创的首选锚点(老板重定向)—— 现成的 cast/quotes/gaps,最好写。
const scenes = listMingchangmian().map((s) => ({ type: 'mingchangmian', id: s.id, title: s.title, sub: `${s.era || ''}${s.sceneType ? ' · ' + s.sceneType : ''}`, hook: HOOKS.mingchangmian[s.id] || null }));
const battles = Object.values(ZHANZHEN).map((b) => ({
type: 'zhanzhen', id: b.id, title: b.name, sub: `${b.side} · ${b.year < 0 ? '前' + -b.year : b.year}年`, hook: HOOKS.zhanzhen[b.id] || null,
})).sort((a, b) => (a.title > b.title ? 1 : -1));
const women = listNvxing().map((w) => ({ type: 'nvxing', id: w.id, title: w.name, sub: `${w.dynName} · ${w.title}`, hook: HOOKS.nvxing[w.id] || null }));
// ★他传接进二创(老板:二创缺他传·和她传不对称)——每位名臣一生 arc 就是现成二创材料。
const men = listTazhuan().map((w) => ({ type: 'tazhuan', id: w.id, title: w.name, sub: `${w.dynName} · ${w.title}`, hook: (HOOKS.tazhuan || {})[w.id] || null }));
// ★ 创作方向体系(26条4大类) + 国内工具推荐(哪家强推哪家·海外不接) —— 完整链路的③④步配置。
// styles 仍保留供旧前端回退;新前端走 directions。
return {
mingchangmian: scenes, zhanzhen: battles, nvxing: women, tazhuan: men,
directions: listDirections(),
tools: TOOLS,
styles: Object.entries(STYLES).map(([k, v]) => ({ key: k, label: v.label, hint: v.hint, genre: v.genre || null, layer: v.layer })),
};
}
/**
* ★★ 素材包·只读视图 —— 二创的地基「看得见」:把双证骨架摊开给用户,让他【自己顺着写】,
* 而不是只能让 AI 下笔。纯查表派生(buildPack),零 LLM、零网络。
* 结构:一句钩子/局势 → 双证结局 → 史实骨架时间线(锁死·各挂cite) → 在场真人(叙事之眼候选·真结局)
* → 正史器物 → 缝隙(史书没写、你可以创作的地方) → 红线(演义黑名单/断代禁物/铁律)。
*/
export function buildErchuangPackView(anchorType, anchorId) {
const pack = buildPack(anchorType, anchorId);
if (!pack) return null;
const sk = pack.skeleton;
return {
ok: true,
meta: {
type: pack.meta.anchorType, id: pack.meta.anchorId, title: pack.meta.title,
era: pack.meta.era || null, year: pack.meta.year ?? null,
sub: pack.meta.side || pack.meta.subtitle || '',
},
hook: sk.hook || null,
situ: sk.situ || null,
tuiti: deriveTuiti(pack), // ★ 从素材包派生的"切入题目"(第②步·让用户不必空想)
real: sk.real ? { text: sk.real.text, cite: sk.real.cite } : null,
nodes: (sk.nodes || []).map((n) => ({ event: n.event, cite: n.cite || null })),
quotes: (sk.quotes || []).map((q) => ({ who: q.who, words: q.words, cite: q.cite || null })), // 名句原话(可引用)
roster: (sk.roster || []).slice(0, 12).map((p) => ({ name: p.name, office: p.office || null, fate: p.fate || null, cite: p.cite || null, side: p.side || null })),
qiwu: (sk.qiwu || []).slice(0, 10).map((q) => ({ item: q.item, desc: q.desc || null, cite: q.cite || null })),
qiwuText: sk.qiwuText || null, // 名场面器物整段描述
cautions: sk.cautions || [], // 该场"别写歪"的提醒(整句)
forces: sk.forces?.sides ? sk.forces.sides.map((s) => ({
label: s.label, commander: s.commander?.name || null,
verified: s.strength?.verified?.value || null, claimed: s.strength?.claimed?.value || null,
})) : null,
gaps: (pack.gaps || []).map((g) => ({ between: g.between })),
eye: pack.eye ? { name: pack.eye.name, office: pack.eye.office || null } : null,
redlines: {
blacklist: sk.blacklist || [],
eraForbidden: pack.era ? eraForbidden(pack.era) : [],
rules: [
'胜负、生死、关键节点的走向——锁死史实,一处不改',
'对话是你的演绎,绝不排成正史原文冒充实录',
'演义黑名单里的桥段,一个字都别写进去',
...(pack.era ? ['器物、称谓不出现晚于本朝之物(火器/交子/点茶/缠足/棉常服/辣椒玉米等)'] : []),
],
},
};
}
// 纯函数:把史实卡 + 体裁 + 用户方向,拼成给 Qwen 的两条消息(可单测,不触网)。
export function buildErchuangPrompt(fact, styleKey, { seed, role } = {}) {
const st = (styleKey && typeof styleKey === 'object') ? styleKey : (STYLES[styleKey] || STYLES.shi);
const sys = [
'你是一位替用户做「历史二次创作」的文人。基于给定的、已双证的真实历史事件,为用户创作一篇文学作品。',
'铁律:',
'① 这是【文学演绎】而非史料——你可以想象细节、抒情言志、代入人物,但【绝不能杜撰与史实相悖的硬事实】:胜负、成败、关键人物的生死结局,必须与所给史实一致。',
'② 紧扣所给真实事件与正史引文的精神与史实,不架空、不戏说、不掺入演义里被证伪的桥段。',
'③ 不堆砌口号,不使用「解锁/成就/通关」等词;语言要有文学质地。',
'只输出作品正文本身,不要标题以外的任何解释、按语或说明。',
].join('\n');
const user = [
`真实事件:${fact.title}(${fact.sub})`,
`史实:${fact.facts.join(' ')}`,
`正史依据:${fact.cites.join(' ')}`,
role ? `我在其中的身份:${role}` : '',
seed ? `我想表达的方向:${seed}` : '',
`请以此为本,为我创作${st.hint}。`,
].filter(Boolean).join('\n');
return { messages: [{ role: 'system', content: sys }, { role: 'user', content: user }], styleLabel: st.label };
}
const DISCLAIMER = '文学演绎 · 非史料。本篇由 AI 基于真实史实创作,细节含想象成分;史实以所附正史引文为准。';
// ══ 马伯庸式细颗粒(segment):骨架注入 + 缝隙填槽 + 句级出处自标 ══════════════════
// 纯函数:把素材包 + 体裁拼成"四要素"prompt。可单测。
export function buildSegmentPrompt(pack, styleKey, { seed, role, prev, yanyi } = {}) {
const st = (styleKey && typeof styleKey === 'object') ? styleKey : (STYLES[styleKey] || STYLES.ji);
const sk = pack.skeleton;
const { claims: yyClaims, eff: yyBlack } = admitYanyi(sk.blacklist, yanyi);
// ① 史实骨架(编号挂 key·锁定禁改)
const nodeLines = (sk.nodes || []).map((n) => `[${n.key || '·'}] ${n.event}`).join('\n');
const rosterLines = (sk.roster || []).slice(0, 10).map((p) => `${p.name}(${p.office || '在场'}${p.fate ? '·' + p.fate : ''})`).join(';');
const qiwuLines = (sk.qiwu || []).slice(0, 8).map((q) => q.item).join('、') || sk.qiwuText || '';
// 名句原话(正史原文·可直接引用并挂 f:key)—— 名场面独有的最强素材。
const quoteLines = (sk.quotes || []).slice(0, 8).map((q) => `[${q.key || '·'}] ${q.who}:「${q.words}」`).join('\n');
// 该场"别写歪"的定位提醒(软·整句)—— 名场面独有,防把演义定位当史实。
const cautionLines = (sk.cautions || []).slice(0, 8).map((c) => `· ${c}`).join('\n');
const forcesLine = sk.forces?.sides ? sk.forces.sides.map((s) => `${s.label}${s.strength?.verified ? '实数约' + s.strength.verified.value : ''}${s.strength?.claimed ? '(号称' + s.strength.claimed.value + ',号称不是实数)' : ''}`).join(' vs ') : '';
const sys = [
'你是马伯庸式的历史小说作者,写「可考据的戏剧化」:锁死的史实骨架 + 缝隙里填的演绎血肉,两层分明。',
st.genre ? `【本篇类型:${st.label}】${st.genre}\n★类型感优先:这篇首先得读起来像【${st.label}】——史实是不可改的底子、不是枷锁;在锁死的史实之上,把这个类型该有的味道、情绪、笔法写足写透。` : '',
'【出处自标·铁律】你必须把每句话标成两类之一:',
'· 史实句用 [[f:key|句子]] —— key 只能取下方骨架给的编号(如 c1),且这句必须是复述该条史实、不得夹带骨架没有的名词情节;引不出骨架就别标 f。',
'· 演绎句用 [[y:子类|句子]] —— 子类∈{对话,内心,器物,感官,还原,叙事}。拿不准一律 y。',
'· ★对话永远是 y、永不挂 cite、绝不排成正史原文的样子冒充实录;人物心理是 y;正史没记的器物质感/气味/纹样是 y:器物。',
'· 胜负、生死、关键节点的走向必须与骨架一致,一处不许改。',
cautionLines ? `【这一场别写歪·史学提醒】\n${cautionLines}` : '',
yyClaims.length ? `【照妖·准用的演义桥段】以下桥段用户已「照过妖」、明知是演义、特意要写进来——【可以写】,但只作演绎、必须标 [[y:演义|…]],绝不标 f、绝不排成正史原文冒充实录,且不得借它改变骨架的胜负/生死/结局:${yyClaims.join(';')}。` : '',
`· 演义黑名单一字不得出现(以下为未照妖、禁用的):${yyBlack.join('、') || '(无)'}。`,
pack.era ? `· 本篇断代=${pack.era},器物称谓只出现该朝及以前确有之物,禁晚出之物(火器/交子/点茶/缠足/太师椅/棉常服/辣椒玉米等)。` : '',
'只输出作品正文(含 [[f]]/[[y]] 标记),不要额外解释。',
].filter(Boolean).join('\n');
const eye = st.eye === 'low' && sk.roster?.length ? sk.roster[sk.roster.length - 1] : pack.eye;
const user = [
`真实事件:${pack.meta.title}(${pack.meta.era || ''}${pack.meta.year ? '·' + (pack.meta.year < 0 ? '前' + -pack.meta.year : pack.meta.year) + '年' : ''})`,
forcesLine ? `对阵:${forcesLine}` : '',
`史实骨架(按序发生·锁定):\n${nodeLines}`,
quoteLines ? `正史名句·原话(可直接引用并挂 [[f:key|原话]],一字不改):\n${quoteLines}` : '',
rosterLines ? `在场真人(可作叙事之眼/对谈人):${rosterLines}` : '',
qiwuLines ? `正史确有的器物(可挂 f):${qiwuLines}` : '',
eye ? `叙事之眼:优先用【${eye.name}(${eye.office || '在场'})】这样的真实在场者;若需无名小卒可造虚构载体并标 y:虚构。` : '',
role ? `我的身份:${role}` : '',
seed ? `我想表达的方向:${seed}` : '',
prev
? `已有前文如下(请【接着往下写】:承接语气、不重复已写内容、把情节继续推进到新的进展,直到一个自然落点;同样逐句标 [[f]]/[[y]]):\n${prev}`
: `请以此为本,写${st.hint}。这是一篇【完整作品】:有开头、铺陈、转折、落点,从头写到尾,不要只开个头就停;篇幅要充实(约 900–1600 字)。逐句标 [[f]]/[[y]]。`,
].filter(Boolean).join('\n');
return { messages: [{ role: 'system', content: sys }, { role: 'user', content: user }], styleLabel: st.label };
}
// ── 照妖·入料:把用户【照过妖、明知是演义、特意要用作演绎】的桥段从硬黑名单里放行 ──
// 红线不变——放行的演义只能作【演绎】(标 y:演义)、绝不冒充正史、不得改胜负生死结局。
// yanyi: 数组,元素可为字符串(说法)或 {claim}。返回放行后的有效黑名单 eff + 已准用的说法 claims。
export function admitYanyi(blacklist, yanyi) {
const claims = (yanyi || []).map((y) => (typeof y === 'string' ? y : (y?.claim || ''))).map((s) => String(s).trim()).filter(Boolean);
const eff = (blacklist || []).filter((w) => !claims.some((c) => c.includes(w)));
return { claims, eff };
}
// 解析 [[f:key|句子]] / [[y:子类|句子]] + 服务端填 cite + 闸门。纯函数,可单测。
export function parseSegments(text, citeTable, { blacklist = [], era = null } = {}) {
const t = String(text || '');
const re = /\[\[([fy]):([^|\]]*)\|([^\]]*)\]\]/g;
const segs = [];
let last = 0, m;
const pushPlain = (s) => {
const v = s.trim();
if (!v) return;
// 纯标点的间隔并进上一段,别碎成孤立小段
if (/^[,。、;:!?…·—「」『』()()\s]+$/.test(v) && segs.length) { segs[segs.length - 1].text += v; return; }
segs.push({ t: 'y', kind: '叙事', text: v });
};
while ((m = re.exec(t)) !== null) {
if (m.index > last) pushPlain(t.slice(last, m.index));
const [, tag, meta, body] = m;
const sentence = (body || '').trim();
if (!sentence) { last = re.lastIndex; continue; }
if (tag === 'f') {
const key = (meta || '').trim();
// 闸门:key 必须在表内,否则降为演绎(模型无法把虚构升成史料)
if (citeTable && citeTable[key]) segs.push({ t: 'f', text: sentence, cite: citeTable[key], key });
else segs.push({ t: 'y', kind: '叙事', text: sentence });
} else {
// 演绎:对话永不带 cite(本就无);保留子类
segs.push({ t: 'y', kind: (meta || '叙事').trim() || '叙事', text: sentence });
}
last = re.lastIndex;
}
if (last < t.length) pushPlain(t.slice(last));
// 闸门:演义黑名单命中 → 删该段;断代违禁词 → 标 warn(不硬删,避免碎句)
const warnings = [];
const clean = segs.filter((s) => {
const hit = (blacklist || []).find((w) => s.text.includes(w));
if (hit) { warnings.push(`剔除含演义「${hit}」的段`); return false; }
return true;
});
if (era) for (const s of clean) { const eh = scanEra(s.text, era); if (eh.length) warnings.push(`断代存疑器物:${eh.join('、')}`); }
return { segs: clean, warnings };
}
// ══ work·整篇(诗词/剧本/美术脚本/视频脚本)—— 方向驱动·基于素材包 ══
const WORK_STRUCT = {
script: '产出【剧本】:分 3-6 场,每场标【场景·时地】【在场人物】【动作】【台词(演绎·非正史实录,不排成文言原文冒充实录)】【旁白(可引正史·附出处)】。场序=史实发生序,锁死不倒置。',
art: '产出【美术分镜】:分 6-12 格,每格标【景别】【画面内容】【出处(可引正史处附书名篇名)】【本格禁画(演义桥段/晚代器物)】。文末附一句按断代的画风参考。',
video: '产出【短视频脚本】:Hook(0-3秒反差钩子)→铺垫(局势·号称vs实数分清)→反转/高潮(决胜因)→收(点题+双证一句);随后附【镜头清单】(镜号/时长/画面/字幕并挂出处)与【旁白语气】。',
music: '产出【歌词+简谱曲谱】,以【做成一首 1 分钟内、极抓人的短歌】为目标(不是完整大歌):\n① 歌词——【1分钟内·极短的抓人短歌,不是完整大歌】:结构=一个钩子+一小段主歌+一句洗脑副歌(副歌可再钩一次),全篇 8-12 行、宁短勿长、一分钟内唱得完。★【不做长叙事·只要短时间强冲击】:不交代来龙去脉、不写事件流水账、不铺陈过程——抓住一个最有冲击力的情绪爆点/画面/金句往死里放大;情绪起伏要大(压抑↔爆发、留白↔炸裂的强落差),短时间冲击力优先于叙事完整。开篇第一句就是全曲最戳人的记忆点——当事人此刻最痛或最狂的那句第一人称大白话、一个反差、或一句画面直接砸脸——前 3 秒让竖屏观众停手,绝不铺垫客套;用具体可感的动作代替抽象狠话(「我一粒一粒数着吃」>「粮不多了」),感官颗粒度=留人力。主歌几句内推到情绪爆点;副歌要极短、能无限循环、能被单拎出来当视频标题,重复时每遍必须微变奏(改一个关键词或后半句,禁一字不差复读);结尾收在一句「反转/对比暴击」金句(弱↔强、赢↔输同句翻面那类,天然是爆款文案)。全程第一人称、对着一个具体的人或物说话(如项羽对虞姬、对乌骓;曹操对自己的赌性),情绪落点具体、人物区分度自然回来。词是演绎,但【正史是围栏不是文体】——只锁事实边界:胜负/生死/结局一律不改,器物/称谓/乐制/断代不串朝代;避演义(禁三英战吕布、草船借箭等被正史证伪的桥段,禁虚构夸张兵力,压在正史口径的真数字可大胆用,以弱胜强靠精确数据最抓人);\n★【别太工整·反 AI 作业腔·硬规则逐条锁死】(老板要求):① 长短句强制混排——全篇至少一次 2-4 字极短句(如「别哭」「烧了它」)紧挨一句十几字长句,制造听感落差,竖屏卡点/鼓点全靠这个断层;② 禁句句押韵——允许换韵、半韵、连续两三句不押,严禁一韵到底把每行尾字押死;③ 禁对仗排比与成语堆砌——不写字数结构对称的对偶、不连排三个同构短语、整首成语不超过 1 个,靠具体动作与口语撑情绪、不靠成语;④ 每行至少一处「活口」——口语词(「呢」「了吧」「怎么办」)、感叹、自问自答、破折号停顿、或半句吞掉的破句,三选一起码占一;⑤ 允许拉家常式开口(「说实话」「那天晚上」)先卸观众戒心再暴击,允许原地重复变奏(「别哭。别哭啊。」)、允许语无伦次不体面不端庄;⑥ 用「——」「……」、疑问、破句给情绪留气口,别把每行填满,空拍就是卡点位;⑦ 古意只点到即止、可有可无,宁可现代直白有情绪,也不要文绉绉的书面古语——现代词表达古代事完全允许,只要器物称谓不穿帮;真古董名句只当钩子再当场消解(拆字延音「力拔山兮,气——盖——世」或整句翻面),绝不整句端上来。目标是一个古人此刻的真实心跳,不是一段文言解说;\n② 简谱曲谱——谱出主旋律(至少副歌,能体现记得住的主题动机即可):标调号(如 1=C)/拍号/速度与最简记号约定(高音加\'、低音加,、八分加_),数字 1–7 记音高,简谱下一行对上对应歌词;旋律是新谱演绎、不假装古乐/古谱;\n★【曲也别太工整】(老板要求):旋律节奏有张弛快慢变化、乐句长短不一、不四平八稳方方正正、该起伏时起伏、有出人意料的转折与留白,自然流动不呆板机械。',
};
export function buildWorkPrompt(pack, spec, { seed, role, prev, yanyi, lyricOnly, perspective } = {}) {
const sk = pack.skeleton;
const { claims: yyClaims, eff: yyBlack } = admitYanyi(sk.blacklist, yanyi);
const nodeLines = (sk.nodes || []).map((n) => n.event).join(' → ');
const quoteLines = (sk.quotes || []).slice(0, 6).map((q) => `${q.who}:「${q.words}」`).join(' ');
const rosterLines = (sk.roster || []).slice(0, 8).map((p) => `${p.name}(${p.office || '在场'}${p.fate ? '·' + p.fate : ''})`).join(';');
const sys = [
'你在为用户做「历史二次创作」,产出一篇成品。',
spec.genre ? `【方向:${spec.label}】${spec.genre}\n★类型感优先:这篇首先得读起来像【${spec.label}】——史实是不可改的底子、不是枷锁;在锁死的史实之上,把这个类型该有的味道、情绪、笔法写足写透。` : '',
'铁律:① 这是【文学/艺术演绎】非史料,可想象细节、抒情言志,但胜负/成败/关键人物生死结局必须与所给史实一致,一处不改;',
'② 紧扣真实事件与正史,不掺演义里被证伪的桥段;',
`③ 演义黑名单一字不得出现(未照妖·禁用):${yyBlack.join('、') || '(无)'};`,
yyClaims.length ? `④ 【照妖·准用的演义桥段】以下用户已照过妖、明知是演义、特意要写——【可以写】,但只作演绎、须点明是演义(不冒充正史),且不得借它改变胜负/生死/结局:${yyClaims.join(';')};` : '',
...(sk.cautions?.length ? [`⑤ 这一场别写歪:${sk.cautions.slice(0, 4).join(';')}`] : []),
pack.era ? '⑤ 器物、称谓不出现晚于本朝之物。' : '',
(lyricOnly && spec.outputKind === 'music') ? (WORK_STRUCT.music.split('\n② 简谱曲谱')[0].replace('【歌词+简谱曲谱】', '【一首能唱的短歌歌词】') + '\n※本次只出【歌词正文】:不要简谱、不要曲谱、不要任何谱面/配器/曲式说明。' + (perspective ? `\n★【视角·第一人称·老板要求】整首以【${perspective}】的口吻与心境写——只写 ta 眼里/心里的这一场,立场、情绪、用词完全是 ta 的,与同一场其他人物的歌【内容截然不同】(别人是别人的故事)。` : '')) : (WORK_STRUCT[spec.outputKind] || ''),
'不写「解锁/成就/通关/战力」等游戏化词。只输出作品正文,不要额外解释。',
].filter(Boolean).join('\n');
const user = [
`真实事件:${pack.meta.title}(${pack.meta.era || ''})`,
`史实骨架(按序·锁死):${nodeLines}`,
sk.real ? `结局(锁死):${sk.real.text}` : '',
quoteLines ? `可引用的正史名句:${quoteLines}` : '',
rosterLines ? `在场真人:${rosterLines}` : '',
role ? `我的身份/视角:${role}` : '',
seed ? `我想表达的方向:${seed}` : '',
prev
? `已有前文如下(请【接着往下写】:承接、不重复、推进到落点):\n${prev}`
: `请据此,${spec.hint}。${spec.outputKind === 'text' ? '写成一篇【完整作品】,篇幅充实(约 900–1600 字),不要只开个头就停。' : spec.outputKind === 'music' ? (lyricOnly ? '只输出这一首歌的【歌词正文】:1 分钟内唱得完、8-12 行、宁短勿长;不做长叙事,不要简谱/曲谱/谱面说明。' : '以【做成一首 1 分钟内、极抓人的短歌】为准:短小精悍、唱完即止、不铺长、不做长叙事。') : '写成结构从头到尾完整的一篇,不要只开个头就停。'}`,
].filter(Boolean).join('\n');
return { messages: [{ role: 'system', content: sys }, { role: 'user', content: user }], styleLabel: spec.label };
}
// ══ craft·创作素材备料(不是成品,是给用户/喂AI的一套料)══
export function buildCraftPrompt(pack, dir, { seed, role } = {}) {
const sk = pack.skeleton;
const nodeLines = (sk.nodes || []).map((n) => n.event).join(' → ');
const rosterLines = (sk.roster || []).slice(0, 10).map((p) => `${p.name}(${p.office || '在场'}${p.fate ? '·' + p.fate : ''})`).join(';');
const quoteLines = (sk.quotes || []).slice(0, 6).map((q) => `${q.who}:「${q.words}」`).join(' ');
const sys = [
'你在为「历史二次创作」备【创作素材】——不是成品,是给用户拿去自己创作、或喂给 AI 的一套料。',
'按下面的清单逐项产出,每项用小标题,条理清楚、务实可用(不要写成一篇作品)。',
`方向:${dir.label}。调性:${dir.blurb}。`,
`红线:锁死史实(胜负/生死/行止不改);一切演绎须标明「演绎」;演义黑名单禁用(${(sk.blacklist || []).join('、') || '无'});号称≠实数分清;${pack.era ? '器物断代不越代;' : ''}不写游戏化词。`,
...(sk.cautions?.length ? [`这一场别写歪:${sk.cautions.slice(0, 4).join(';')}`] : []),
].filter(Boolean).join('\n');
const user = [
`要备的创作素材清单:\n${(dir.craftMaterials || []).map((m, i) => `${i + 1}. ${m}`).join('\n')}`,
'——素材包(据此备料)——',
`真实事件:${pack.meta.title}(${pack.meta.era || ''})`,
`史实骨架:${nodeLines}`,
sk.real ? `结局(锁死):${sk.real.text}` : '',
rosterLines ? `在场真人:${rosterLines}` : '',
quoteLines ? `正史名句:${quoteLines}` : '',
role ? `用户身份/视角:${role}` : '',
seed ? `用户方向:${seed}` : '',
].filter(Boolean).join('\n');
return { messages: [{ role: 'system', content: sys }, { role: 'user', content: user }], label: dir.label };
}
const CRAFT_DISCLAIMER = '这是【创作素材】不是成品 —— 供你自己创作或拿去喂 AI/别处工具生成。史实以所附正史为准,演绎处由你把关。';
/**
* 生成二创。方向(direction)驱动:
* intent='craft' → 备【创作素材】(该方向要给的一套料)。
* 否则出【作品】:叙事小说走 segment(句级出处分层);诗词/剧本/美术脚本/视频脚本走 work(整篇)。
* 兼容旧 style(STYLES) 路径。
*/
export async function createErchuang({ anchorType, anchorId, direction, style, seed, role, intent, prev, yanyi, lyricOnly, perspective } = {}) {
const dir = getDirection(direction);
const spec = dir ? specFromDirection(dir) : { key: style || 'shi', ...(STYLES[style] || STYLES.shi) };
const subOf = (pack) => `${pack.meta.era || ''}${pack.meta.side ? ' · ' + pack.meta.side : pack.meta.subtitle ? ' · ' + pack.meta.subtitle : ''}`;
const anchorOf = (pack) => ({
type: pack.meta.anchorType, id: pack.meta.anchorId, title: pack.meta.title, sub: subOf(pack),
facts: [pack.skeleton.hook, pack.skeleton.real?.text].filter(Boolean),
cites: [pack.skeleton.real?.cite, ...(pack.skeleton.nodes || []).slice(0, 3).map((n) => n.cite)].filter(Boolean),
});
// ── 创作素材·备料 ──
if (intent === 'craft') {
if (!dir) return { error: 'no-direction' };
const pack = buildPack(anchorType, anchorId);
if (!pack) return { error: 'no-anchor' };
const { messages } = buildCraftPrompt(pack, dir, { seed, role });
// 备素材是长输出(一整套料),>30s 常见 → 给足 maxTokens 使其走 120s 长超时(见 llm.js big 判定),
// 否则默认 30s×3 重试=90s 干等后 502(备素材事故)。
const { text, model } = await aiWrite(messages, { temperature: 0.7, maxTokens: 3000 });
return {
ok: true, layer: 'craft', direction: dir.label, outputKind: dir.outputKind,
materials: dir.craftMaterials, craft: (text || '').trim(),
anchor: anchorOf(pack), tools: toolsForOutputKind(dir.outputKind), model, disclaimer: CRAFT_DISCLAIMER,
};
}
// ── 作品·segment(句级出处分层)──
if (spec.layer === 'segment') {
const pack = buildPack(anchorType, anchorId);
if (!pack) return { error: 'no-anchor' };
const { messages, styleLabel } = buildSegmentPrompt(pack, spec, { seed, role, prev, yanyi });
const { text, model } = await aiWrite(messages, { temperature: 0.85, maxTokens: 4000 });
const { eff } = admitYanyi(pack.skeleton.blacklist, yanyi); // 照过妖的演义放行,不再硬删
const { segs, warnings } = parseSegments(text, pack.citeTable, { blacklist: eff, era: pack.era });
return {
ok: true, layer: 'segment', style: styleLabel,
anchor: { type: pack.meta.anchorType, id: pack.meta.anchorId, title: pack.meta.title, sub: subOf(pack) },
segs, citeTable: pack.citeTable, warnings, model, disclaimer: DISCLAIMER,
};
}
// ── 作品·work(诗词/剧本/美术脚本/视频脚本)──
const pack = buildPack(anchorType, anchorId);
if (pack && dir) {
const { messages, styleLabel } = buildWorkPrompt(pack, spec, { seed, role, prev, yanyi, lyricOnly, perspective });
const { text, model } = await aiWrite(messages, { temperature: 0.9, maxTokens: 4000 });
return {
ok: true, layer: 'work', style: styleLabel, work: (text || '').trim(), outputKind: spec.outputKind,
anchor: anchorOf(pack), tools: spec.outputKind !== 'text' ? toolsForOutputKind(spec.outputKind) : null,
model, disclaimer: DISCLAIMER,
};
}
// 回退:旧 STYLES 路径(fact-based)
const fact = anchorFact(anchorType, anchorId);
if (!fact) return { error: 'no-anchor' };
const { messages, styleLabel } = buildErchuangPrompt(fact, spec, { seed, role });
const { text, model } = await aiWrite(messages, { temperature: 0.9, maxTokens: 4000 });
return { ok: true, layer: 'work', work: (text || '').trim(), style: styleLabel, anchor: fact, model, disclaimer: DISCLAIMER };
}
+178
View File
@@ -0,0 +1,178 @@
// 二创锚点·创作钩子 —— 一句勾人的写作引子(扣双证史料·erchuang-hooks 工作流 wg14je194 生成 + 3路对抗校验去演义)。
// 挂在二创锚点卡上,把"数据库行"变成"我想写这一段"的冲动。按 type → id 挂;缺的锚点 hook=null 优雅降级。
// ★ 红线:钩子是【写作引子】不是史实断言;扣锚点真实 mat、不把演义当真(已过演义词扫描)。共 167 条。
export const HOOKS = {
"mingchangmian": {
"baidichengtuogu": "写刘备吐出'君可自取'那一刻的真心与试探。",
"banchaotoubi": "掷笔那一刻,写一个抄书吏转身去闯西域。",
"beijiushibingquan": "写酒酣时那句'睡不安枕',如何逼出一堆兵权。",
"boyajuexian": "世上再没人听懂那张琴——写他断弦的手。",
"chenqiaobingbian": "黄袍披上身那一刻,他真的毫不知情吗?",
"chongzhenmeishan": "末路一夜,写天子亲手了断妻女再登煤山。",
"chulongshuizhao": "她扬言唾谁的脸——写触龙偏用闲话说动太后。",
"daizexiangqiyi": "误期是死、造反也是死——写他为何选后者。",
"direnjiejianxian": "他荐的贤才,日后亲手掀了女皇——写这步长棋。",
"dongxuanqiangxiang": "按他的头也不肯磕下去——写这一寸不弯的脖子。",
"dongzhongshu": "三上对策,写一介儒生如何把儒学写进帝国。",
"fangmoududuan": "善谋的、善断的,写他俩如何合成一个良相。",
"fangxiaorushizu": "'诏不可草'四字,写他拿数百条命换来的拒绝。",
"fanzhongyanyoule": "人没到楼上,写他伏案替天下人写下先忧后乐。",
"fujingqingzui": "写廉颇肉袒负荆、跪到相府门前的那一步。",
"gaixiabieji": "四面楚歌那一夜,写一个盖世英雄帐中落泪。",
"ganluobaixiang": "十二岁凭一张嘴,替秦国说下赵国五座城。",
"goujianchangdan": "苦胆天天尝一口——写他拿亡国之耻磨了二十年。",
"guoziyidanqi": "免胄卸甲、单骑入阵——写七旬老将凭旧谊退敌。",
"hairuibeiguan": "买好棺材才提笔——写一封明知会掉脑袋的奏疏。",
"hongmenyan": "范增几次举玦催杀,写项羽为何迟迟不肯动手。",
"huoguangfeili": "当廷念罪、解玺下殿——写第一次权臣废天子。",
"jianandizhijian": "当着满朝说皇帝装仁义——写汲黯这一句面折。",
"jiaotusanku": "烧光债券买回一个'义'——写这门客的长线棋。",
"jikangguangling": "赴死前他索来一张琴,写他弹绝的那一曲。",
"jiminggoudao": "被满座嫌弃的两个下客,写他们救主的那夜。",
"jingkecisqin": "地图一寸寸展开,杀机就藏在卷尽的地方。",
"juanqimeiju": "最寒酸的廊屋里,一碗饭被捧到了眉毛高。",
"kangxiqinaobai": "一群摔跤的少年,徒手按住了百战的悍将。",
"kuaxiazhiru": "日后的兵仙,此刻正从一个屠夫的胯下钻过。",
"lantingjixu": "一场曲水流觞的雅集,怎么就写到了生与死。",
"linzexuxiaoyan": "两万箱鸦片在海边化尽,也点着了一场战争。",
"longzhongdui": "一间草庐里,布衣把往后十余年的天下画了出来。",
"maosuizijian": "名单差一人时,那个最没名气的自己站了出来。",
"maweipo": "乱军逼着一个皇帝,亲口赐死他最爱的人。",
"qiefujiuzhao": "看门人、屠夫、宠姬——写他们合谋盗出一支大军。",
"quyuantoujiang": "举世都浊了,他偏问自己要不要也脏一点。",
"shangyanglimu": "一根木头、一笔赏金,赌的是百姓信不信官府。",
"simaguangpogang": "孩子都吓跑了,只有一个七岁的捡起了石头。",
"simaqianzhushi": "受了腐刑的人,提笔写他为什么还活着。",
"sushiwutai": "刚从牢里捡回命,他在赤壁的江上想通了。",
"suwumuyang": "十九年,那根旄毛掉光的节,他就是不撒手。",
"taoqianzheyao": "叫他弯腰迎个小吏,他当天解印就走了。",
"tianjisaima": "一匹马没换,只换了出场的次序,就赢了。",
"tiyingjiufu": "一个小女孩的一封信,让皇帝废了肉刑。",
"wanbiguizhao": "十五座城换一块玉的空话,他一眼看穿了。",
"wanganshibianfa": "满朝都喊不可,他一个人把新法推了出去。",
"wangmangjiajiuxi": "还没登基,他先把臣子能受的荣宠受到了顶。",
"wangxizhitanfu": "满屋子人抢着被挑,他偏袒腹躺着——写这份不装。",
"wangyangminglongchang": "打了四十廷杖、贬到无书可读处——写他悟的那一夜。",
"weizhengjinjian": "皇帝说他丢了一面照见自己的镜子——写这声悼叹。",
"wenchenggongzhu": "她一嫁过去,一个国家换了衣冠——写这门亲事。",
"wentianxiangjiuyi": "皇帝亲口问他要什么,他答:赐我一死。写这句。",
"xiaoheyuexia": "丞相半夜不辞而别,只为追一个逃兵——写这一追。",
"xieandongshan": "捷报到,他照下棋;回房过门槛,才踩折了屐齿。",
"xiliulaojun": "天子亲临,军门却不为他开——写只认将令那一营。",
"ximenbaozhiye": "他不禁河伯娶妇,只请巫婆先下河去通报一声。",
"xuanwumen": "写他在宫门张弓,箭头对准的是自己的兄长。",
"xuanzangqujing": "朝廷不许他走,他偏独闯沙海,五天没沾一滴水。",
"yanzhenqingjiuyi": "近八十的老人,独身走进叛营,只为把贼骂到底。",
"yishuisongbie": "白衣人送到易水边,他唱完那句,就再不回头。",
"youmengyiguan": "他扮成死去的贤相登场,让大王认作故人复生。",
"yuefasanzhang": "打进秦都,他只立三条规矩,就换来满城人心。",
"zhangliangshoushu": "老人把鞋扔下桥、三次失约,就看这公子忍不忍。",
"zhangqianzaokong": "被扣十多年,一百人出去只回来两个——写这趟。",
"zhaojunchusai": "临别大殿上皇帝才第一次看清她,想留已太迟。",
"zhaopubanbulunyu": "宰相死后,家人打开他天天读的书箱,只一部书。",
"zhenghexiaxiyang": "两万七千人、六十二艘巨舶,写刘家河扬帆那刻。",
"zhouchuchusanhai": "乡人数出三大祸害,第三害,正是他自己。写这刻。",
"zhujiulunyingxiong": "一句『天下英雄唯你我』,惊得他筷子落地。写这惊。",
"zoujifengjian": "三张嘴都夸他美,他却从中读出:大王被蒙蔽了。",
"zutiwenji": "从半夜踢醒同伴起舞,写到他中流拍桨那一誓。"
},
"zhanzhen": {
"anshicheng": "天可汗横扫辽东,独一座小城让他班师——写这道坎。",
"baideng": "开国皇帝被围七日七夜,写他等那一角解围的时刻。",
"baijiangkou": "白江口一战,写倭船败走、百济复国梦断那天。",
"beijingbaoweizhan": "满朝要南逃,写于谦按住城门、不许南迁的那一夜。",
"buyuerhai": "追到大漠尽头,写蓝玉端掉北元最后一座汗庭。",
"caishizhizhan": "一个文官临时接过帅印,写他挡住金人过江那天。",
"canhebei": "写拓跋珪隔河设伏,等燕军回头的那一夜。",
"changping": "四十万降卒一夜坑尽——写赵括换下廉颇的那个决定。",
"changshao": "写曹刿按住鲁庄公,等齐人擂完三通鼓的那口气。",
"chanyuan": "一支弩箭射倒辽军主帅,写它如何换来百年不战。",
"chengpu": "先退三舍再回身一战——写晋文公用守信赢的霸业。",
"chentangmiezhizhi": "他假传圣旨发兵万里,写“虽远必诛”背后那场豪赌。",
"chibi": "写孙权拍板那一刻:不联手,江东今日就没了。",
"diaoyuchengzhizhan": "一座孤城挡下横扫欧亚的铁骑——写它如何改写世界。",
"dingjunshan": "写黄忠从山顶俯冲那一刻,斩下曹军汉中主将。",
"fancheng_shuiyan": "写关羽赌那场秋雨——汉水一涨,替他淹了七军。",
"feishui": "八万人对号称百万——写秦军一退为何再也收不住。",
"gaixia": "四面楚歌那一夜,写项羽突围到只剩二十八骑。",
"guandu": "写曹操粮尽的那几天,怎么用几万人撑住袁绍十万。",
"guiling": "赵都告急却偏不去救——写孙膑一手“围魏救赵”。",
"hefei_xiaoyaojin": "写张辽点齐八百人,冲进孙权十万大军那个清晨。",
"hexi": "写霍去病带一万骑冲进河西,替汉朝凿开西去的路。",
"huangtiandang": "写韩世忠八千水军横江,把兀术十万铁骑堵成瓮中鱼。",
"hulao": "写李世民只带三千五百骑,去堵窦建德十万援军。",
"jieting": "写马谡舍水上山那一步,断送了诸葛亮的头次北伐。",
"jimo": "七十城尽失只剩两座——写田单用一群火牛烧回齐国。",
"jingxing": "写韩信故意把军队摆到河边,断自己退路那一仗。",
"julu": "写诸侯将领跪着爬进项羽帐门,不敢抬头看他那一刻。",
"kunyang": "八九千人对四十二万——写刘秀敢出城反扑那一念。",
"maling": "写孙膑每天减一批灶,一步步把老同门庞涓引进埋伏。",
"mayizhimou": "三十万人屏息等一个人,他却在半道回了头。",
"miexueyantuo": "霸主先垮在自己人手里,你写收场的那一刀。",
"mobei": "舅与甥各领五万骑,看谁先撞见单于。",
"muye": "六百年殷商,断在一个清晨——写牧野阵前那一刻。",
"ningyuanzhizhan": "他刺破手指写守城的誓,炮口对准不败的八旗。",
"pengcheng": "他带着五十六万在城里摆酒,三万楚骑天亮就到。",
"pingchen": "三百年南北裂痕,旬日之间弥合——写隋军渡江那几日。",
"poyang": "巨舰锁成一片江,朱元璋只等一阵东北风。",
"saerhuzhizhan": "任你四路分兵来,我只认准一路打。",
"shanhaiguanzhizhan": "京师陷了、父亲被掳、爱姬被掠——写他开关那一念。",
"shayuan": "二十万大军溃在泥里,主帅跨着骆驼连夜逃。",
"shiting": "一封诈降信,把魏国大司马十万人骗进伏击。",
"shunchang": "两万守孤城、家小在城里、身后无援——写刘锜凭什么敢守。",
"suiyang": "几千饿着肚子的人,堵住整个江淮的门。",
"tongguan": "他跟对方主帅并马叙旧,再送去一封涂改的信。",
"tumu": "一个太监领着皇帝去打仗,五十万人几天就散了。",
"xiangjisi": "长安就在寺北,这一战决定唐还能不能回家。",
"xiangyang": "围了六年,连飞鸟都进不去,他还在守。",
"yakesa": "把罗刹逐出黑龙江——写国界第一次落到纸上那一刻。",
"yanchengzhizhan": "岳家军最强的一天,正面撞上金军拐子马。",
"yanying": "白起一把火烧了夷陵,楚王丢了都城往东逃。",
"yaozhi": "老臣哭着送秦军出征,他早看见崤山的埋伏。",
"yashanhaizhan": "千艘船锁成海上一座城,宋朝三百年到此为止。",
"yiling": "为一个人的死起倾国之兵,写火烧连营那一夜。",
"yinshan": "渭水那场城下之盟,四年后用一夜奔袭还清。",
"yubi": "一座城守了五十天,把一代枭雄守到忧病而死。",
"zhaomoduo": "皇帝亲自冒着雨雪进瀚海,去堵一个人。",
"zhengchenggong_taiwan": "退无可退,他把最后的赌注押向海外那座岛。",
"zhongli": "一夜之间城拔地而起,敌帅疑他是神。"
},
"nvxing": {
"banjieyu": "她辞了同辇之宠,退长信宫扫地——写这步收手。",
"banzhao": "父兄的《汉书》没写完,她提笔往下续。",
"biantaihou": "出身最卑的倡家,她凭什么与曹操同穴?",
"caiwenji": "归汉那天,她把两个孩子留在了草原。",
"chusuanzi": "三次被推上帘前,三次她又主动退下。",
"cixi": "低阶秀女到当国者,写她攥权与酿祸的两端。",
"dengsui": "以谦德退、以果决进——写她撑住的十六年。",
"douyifang": "一次配错的宫女名单,把她送上了皇后位。",
"dugujialuo": "并称二圣的她,亲手选错了那个太子。",
"fengtaihou": "从罪臣孤女到亲手改革——写这道落差。",
"fuhao": "她提三千兵去伐羌,也在产房里赌命。",
"gouyifuren": "她儿子成了太子,她却因此被赐死。",
"jiananfeng": "又丑又狠的皇后,写她怎样搅乱一个天下。",
"liangtaihou": "为攥住西夏的权,她连亲儿子都囚了。",
"liqingzhao": "半生收藏的金石散尽,她写下那篇《后序》。",
"liue": "她够得着龙椅,却在最后一步收了手。",
"louzhaojun": "四个皇帝都出自她一门,她却从不争。",
"luhou": "从亭长的妻子到临朝称制,她一步没退。",
"mahuanghou": "刀越举越高,写枕边那只按刀的手。",
"pingyanggongzhu": "七万娘子军的旗下,写她挣来的那场军葬。",
"qinliangyu": "别人观望不前,写她独自鼓行而西的那一刻。",
"shangguanwaner": "从掖庭婢到掌诏命,写才华如何反噬了她。",
"taipinggongzhu": "“类己”二字,写她从分寸一步步走向失控。",
"wangzhengjun": "写她如何一步步,把刘家江山交到侄儿手上。",
"weizifu": "谦恭扛了三十八年,写她最后体面的了结。",
"wuzetian": "两千年只此一人,写从才人到龙椅的那条路。",
"xianfuren": "改朝换代三回,写她偏不自立的那份不乱。",
"xiaochuo": "写她怎样以孤儿寡母之身,止住两国百年刀兵。",
"xiaozhuang": "两度扶幼主上龙椅,写她偏不垂帘的那条难路。",
"xiedaoyun": "咏絮的手,也能临乱抽刃——写这才女的刚烈。",
"xuantaihou": "灭义渠、拓秦土,写她放不下的那双手。",
"yanglihua": "父亲篡了丈夫的江山,写她夹在中间的那点气节。",
"zhangsunhuanghou": "夺嫡的血夜她稳住军心,天下定后却步步后退。",
"zhaofeiyan": "从被弃的婴孩到中宫,写“燕啄皇孙”的反面。",
"zhenmi": "邺城破那天她换了主人,写她守得住、又守不住。",
"zhuowenjun": "礼教说不许,她偏要——写私奔当垆那一程。"
}
};
+170
View File
@@ -0,0 +1,170 @@
// 二创作品·持久化 + 分享落地页 —— 把写完的二创作品存下来、给一个可分享链接。
// 分享链接指向本文件的后端直出 HTML(含 OG/Twitter 元信息):微信/小红书/X 抓取时拿到
// 标题+摘要生成卡片预览;打开链接看到一张漂亮的宣纸作品卡 + 「来天下也写一篇」的拉新 CTA。
// ★ 二创增长闭环:亲历历史 → 二创成作品 → 存/晒 → 传播拉新(AI 审计 Top #7)。
// ★ 红线:作品是【文学演绎·非史料】——落地页明标;存前过一遍审核。
import { ObjectId } from 'mongodb';
import { getDb } from './db.js';
import { config } from './config.js';
import { chat } from './services/llm.js';
const esc = (s) =>
String(s || '')
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
const clip = (s, n) => {
const t = String(s || '').replace(/\s+/g, ' ').trim();
return t.length > n ? t.slice(0, n) + '…' : t;
};
// AI 拟标题 + 一句话摘要(给分享卡;扣作品本身,不加作品里没有的事实)。失败优雅回落。
export async function titleSummary(work, anchorTitle, styleLabel) {
const body = String(work || '').slice(0, 2000);
if (!body.trim()) return { title: clip(anchorTitle || '一篇历史二创', 20), summary: '' };
const messages = [
{ role: 'system', content: '你是文学编辑。给下面这篇【历史二次创作】作品拟标题和一句话摘要。' +
'严格【只输出一行 JSON】,不要任何解释、不要代码块、不要前言:{"title":"标题","summary":"摘要"}。' +
'标题≤14字、有文学味、别剧透结局;摘要≤36字、勾人、点出这篇写的是什么角度。不得添加作品里没有的事实。' },
{ role: 'user', content: `锚点:${anchorTitle || ''}(${styleLabel || ''})\n作品:\n${body}\n\n只回 JSON。` },
];
let why = 'noparse';
try {
const { text } = await chat(messages, { temperature: 0.6, maxTokens: 400 });
const raw = String(text || '').replace(/```[a-z]*/gi, '').replace(/```/g, '').trim();
const m = raw.match(/\{[\s\S]*?\}/); // 取第一个 JSON 对象(非贪婪,避免尾随文字)
if (m) {
const j = JSON.parse(m[0]);
const t = clip(j.title, 20);
if (t) return { title: t, summary: clip(j.summary, 60) || clip(body, 60), _ai: 'ok' };
}
} catch (e) { why = 'threw:' + String(e?.message || e).slice(0, 80); }
return { title: clip(anchorTitle || '一篇历史二创', 20), summary: clip(work, 60), _ai: why };
}
export async function saveErchuangShare({ work, anchorType, anchorId, anchorTitle, styleLabel, title, summary, kind, mediaUrl, cover, bundle }) {
const doc = {
work: String(work || '').slice(0, 8000),
kind: ['text', 'image', 'music', 'video', 'bundle'].includes(kind) ? kind : 'text',
mediaUrl: String(mediaUrl || '').slice(0, 800),
cover: String(cover || '').slice(0, 800),
bundle: bundle && typeof bundle === 'object' ? {
novel: String(bundle.novel || '').slice(0, 4000),
cover: String(bundle.cover || '').slice(0, 800),
song: String(bundle.song || '').slice(0, 800),
trailer: String(bundle.trailer || '').slice(0, 800),
} : null,
anchorType: String(anchorType || ''), anchorId: String(anchorId || ''),
anchorTitle: String(anchorTitle || ''), styleLabel: String(styleLabel || ''),
title: String(title || ''), summary: String(summary || ''),
createdAt: new Date(),
};
const r = await getDb().collection('erchuang_shares').insertOne(doc);
return { id: String(r.insertedId) };
}
// 作品广场:最近公开分享的作品(倒序)。不返正文,只返卡片所需(封面/标题/摘要/类型)。
export async function listErchuangPlaza(limit = 30) {
try {
const rows = await getDb().collection('erchuang_shares')
.find({}, { projection: { work: 0 } })
.sort({ createdAt: -1 })
.limit(Math.min(Math.max(Number(limit) || 30, 1), 60))
.toArray();
return rows.map((d) => ({
id: String(d._id),
title: d.title || d.anchorTitle || '一篇二创',
summary: d.summary || '',
kind: d.kind || 'text',
cover: (d.kind === 'image' ? d.mediaUrl : (d.cover || d.bundle?.cover)) || '',
anchorTitle: d.anchorTitle || '', styleLabel: d.styleLabel || '',
at: d.createdAt, url: shareUrlFor(String(d._id)),
}));
} catch { return []; }
}
// 分享链接(指向后端直出的 OG 落地页;后端域名 = config.publicUrl)。
export function shareUrlFor(id) {
return `${String(config.publicUrl || '').replace(/\/$/, '')}/zuopin/${id}`;
}
// 分享落地页:后端直出 HTML + OG 元信息。宣纸作品卡(配二创的羊皮纸调)。
export async function erchuangShareCard(req, res) {
const { id } = req.params;
if (!ObjectId.isValid(id)) return res.status(404).send(page404());
const doc = await getDb().collection('erchuang_shares').findOne({ _id: new ObjectId(id) });
if (!doc) return res.status(404).send(page404());
const web = String(config.payment?.webBaseUrl || config.publicUrl || '').replace(/\/$/, '');
const shareUrl = `${String(config.publicUrl || '').replace(/\/$/, '')}/zuopin/${id}`;
const ctaUrl = `${web}/erchuang`;
const ogTitle = `${doc.title || doc.anchorTitle || '一篇历史二创'}|数字我 · 天下`;
const ogDesc = doc.summary || clip(doc.work, 100);
const meta = [doc.anchorTitle, doc.styleLabel].filter(Boolean).join(' · ');
// 按类型渲染成品:图/乐/片/套装/文本。分享卡富预览(og:image)取图/封面。
const mUrl = esc(doc.mediaUrl); const cov = esc(doc.cover);
let mediaHtml;
if (doc.kind === 'image') mediaHtml = `<img class="media" src="${mUrl}" alt="">`;
else if (doc.kind === 'music') mediaHtml = `${doc.work ? `<div class="work">${esc(doc.work)}</div>` : ''}<audio class="media" src="${mUrl}" controls></audio>`;
else if (doc.kind === 'video') mediaHtml = `<video class="media" src="${mUrl}" controls ${cov ? `poster="${cov}"` : ''}></video>`;
else if (doc.kind === 'bundle' && doc.bundle) mediaHtml = [
doc.bundle.cover ? `<img class="media" src="${esc(doc.bundle.cover)}" alt="">` : '',
doc.bundle.novel ? `<div class="work">${esc(doc.bundle.novel)}</div>` : '',
doc.bundle.song ? `<audio class="media" src="${esc(doc.bundle.song)}" controls></audio>` : '',
doc.bundle.trailer ? `<video class="media" src="${esc(doc.bundle.trailer)}" controls></video>` : '',
].join('');
else mediaHtml = `<div class="work">${esc(doc.work)}</div>`;
const ogImage = doc.kind === 'image' ? doc.mediaUrl : (doc.cover || doc.bundle?.cover || '');
res.set('Content-Type', 'text/html; charset=utf-8');
res.set('Cache-Control', 'public, max-age=300');
res.send(`<!doctype html><html lang="zh-CN"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(ogTitle)}</title>
<meta name="description" content="${esc(ogDesc)}">
<meta property="og:type" content="article">
<meta property="og:title" content="${esc(ogTitle)}">
<meta property="og:description" content="${esc(ogDesc)}">
${ogImage ? `<meta property="og:image" content="${esc(ogImage)}">` : ''}
<meta property="og:url" content="${esc(shareUrl)}">
<meta name="twitter:card" content="${ogImage ? 'summary_large_image' : 'summary'}">
<meta name="twitter:title" content="${esc(ogTitle)}">
<meta name="twitter:description" content="${esc(ogDesc)}">
${ogImage ? `<meta name="twitter:image" content="${esc(ogImage)}">` : ''}
<style>
*{box-sizing:border-box}html,body{margin:0}
body{min-height:100vh;display:grid;place-items:center;padding:28px 16px;
background:radial-gradient(1000px 560px at 50% -12%,#efe6d2 0%,transparent 62%),#e9e0cd;color:#33302a;
font-family:"Songti SC","STSong","Noto Serif SC",-apple-system,"PingFang SC","Microsoft YaHei",serif}
.card{width:100%;max-width:560px;background:linear-gradient(180deg,#faf4e6,#f3ecda);
border:1px solid rgba(120,50,40,.14);border-radius:16px;padding:30px 28px;
box-shadow:0 22px 60px rgba(80,50,20,.18)}
.seal{display:inline-grid;place-items:center;width:38px;height:38px;border-radius:7px;
background:#8a2f24;color:#f6ecd8;font-size:17px;font-weight:700;letter-spacing:1px}
.meta{color:#9a6b3a;font-size:12.5px;letter-spacing:.14em;margin:16px 0 6px}
h1{font-size:24px;line-height:1.35;margin:0 0 6px;color:#2a2620;font-weight:700}
.sum{color:#7a5a3a;font-size:14px;line-height:1.7;margin:0 0 18px}
.work{color:#33302a;font-size:15.5px;line-height:2;white-space:pre-wrap;
border-top:1px solid rgba(120,50,40,.12);padding-top:18px;max-height:60vh;overflow:auto}
.media{width:100%;border-radius:12px;margin-top:18px;display:block}
.cta{display:flex;align-items:center;justify-content:center;gap:7px;margin-top:22px;
background:#8a2f24;color:#f6ecd8;text-decoration:none;border-radius:12px;padding:14px;
font-weight:700;font-size:15px;letter-spacing:.02em}
.foot{margin-top:14px;text-align:center;color:#a08a68;font-size:11.5px;line-height:1.7}
</style></head><body>
<div class="card">
<div class="seal" aria-hidden="true">作</div>
${meta ? `<div class="meta">${esc(meta)}</div>` : ''}
<h1>${esc(doc.title || doc.anchorTitle || '一篇历史二创')}</h1>
${doc.summary ? `<p class="sum">${esc(doc.summary)}</p>` : ''}
${mediaHtml}
<a class="cta" href="${esc(ctaUrl)}">来「天下」,把你亲历的历史也做成作品 →</a>
<p class="foot">文学演绎 · 非史料 —— 锚定真实史实创作 · 由「数字我 · 天下」驱动</p>
</div>
</body></html>`);
}
function page404() {
return `<!doctype html><meta charset="utf-8"><title>作品不存在</title><meta name="robots" content="noindex"><body style="font-family:serif;background:#e9e0cd;color:#7a5a3a;display:grid;place-items:center;height:100vh;margin:0;text-align:center;padding:24px">这篇作品不存在或已失效。</body>`;
}
File diff suppressed because it is too large Load Diff
+122
View File
@@ -0,0 +1,122 @@
// gaiming.js —— 改命:后天努力【按天/周持续打卡】,累积反哺古代命(老板 2026-07-21 拍板)。
//
// ★ 为什么是「持续打卡」而不是「填一次」:老板「要按天或者周有改变,才能改命」。
// 这跟引擎的劳(lao)同构 —— 「跨度刷不动,只能熬」:时间门槛【本身就是防刷】。
// 自填现实项(读书/运动/早起…)App 验证不了,但【要求连续、断了衰减】⇒ 刷不动,且真促人现实变好。
//
// 存储:twin.gaiming = { [itemId]: { streak, lastKey, best } }。
// ★ 只存 streak + 最后打卡的期键(lastKey),【不存全历史】——共享 ECS 99G 盘,省空间(同 acts 截断口径)。
//
// ⚠ 所有函数都【显式收 now】(不在模块里 new Date())——纯函数、可测;由 route/buildProfile 传当前时间。
const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
// 打卡项:日级(每天)+周级(每周)混搭,老板拍板这组。★ 每项都可量化(连续天/周数)。
export const GAIMING_ITEMS = [
{ id: 'read', name: '读书', cadence: 'day', hint: '每天读一点 —— 学识往上' },
{ id: 'exercise', name: '运动', cadence: 'day', hint: '每天动一动 —— 精气神' },
{ id: 'earlyrise', name: '早起', cadence: 'day', hint: '按时早起 —— 自律' },
{ id: 'focus', name: '专注学习', cadence: 'day', hint: '专注学一段 —— 真本事' },
{ id: 'social', name: '有效社交 / 交友', cadence: 'week', hint: '每周真交流 —— 人脉声望' },
{ id: 'goal', name: '完成一个小目标', cadence: 'week', hint: '每周搞定一件事 —— 行动力' },
{ id: 'review', name: '复盘', cadence: 'week', hint: '每周回头看看 —— 长进' },
// ── 2026-07-30 老板「改命方向再多点」:补更多可量化的方向,各对应命的一路长进 ──
{ id: 'write', name: '写作 / 表达', cadence: 'day', hint: '每天写几句 —— 文采表达' },
{ id: 'skill', name: '练一门技艺', cadence: 'day', hint: '琴棋书画/一技每天练 —— 才艺傍身' },
{ id: 'finance', name: '理财 / 记账', cadence: 'day', hint: '每天记账理财 —— 持家理事的吏能' },
{ id: 'kindness', name: '助人 / 行善', cadence: 'week', hint: '每周帮人一件 —— 声望与德行' },
{ id: 'meditate', name: '静心 / 沉淀', cadence: 'day', hint: '每天静一刻 —— 沉得住、定力' },
{ id: 'deepwork', name: '钻研专精', cadence: 'week', hint: '每周深钻本行一题 —— 机锋专精' },
{ id: 'quit', name: '戒一个坏习惯', cadence: 'day', hint: '每天守住不犯 —— 自律筑底' },
{ id: 'family', name: '孝亲 / 顾家', cadence: 'week', hint: '每周陪问家人 —— 德与根底' },
];
const BY_ID = Object.fromEntries(GAIMING_ITEMS.map((i) => [i.id, i]));
const LIT = 3; // 连续 3 天(日项)/ 3 周(周项) → 点亮该项,开始改命(= 第一档里程碑)。
// ★★ 硬规则(老板 2026-07-29:改命不能假大空,要具体指标+目标+可见结果)——
// 每个打卡项按【连续期数】过里程碑,往「综合指标库」进分。断了 streak 归 0 ⇒ 该项指标随之退(持续才算)。
// 期 = 天(日项) / 周(周项)。分值取【已达到的最高档】(非逐档叠加):到 3 期 = 1 分,到 7 期 = 3 分,到 30 期 = 6 分(满)。
export const GAIMING_MILESTONES = [
{ streak: 3, pts: 1, label: '点亮' },
{ streak: 7, pts: 3, label: '稳住' },
{ streak: 30, pts: 6, label: '筑成' },
];
const ITEM_MAX_PTS = 6; // 单项满档 6 分
const POOL_MAX = GAIMING_ITEMS.length * ITEM_MAX_PTS; // 综合指标库满 = 项数×6
// ★ 后天改命档阈值【随方向数等比缩放】——原基准 7 项(POOL_MAX 42)时 +1@8 / +2@20;
// 方向变多不该让改命变松(仍是「临门一脚」),故按同比例放大。7 项时结果恰好仍是 8/20。
const EFFORT_T1 = Math.round((POOL_MAX * 8) / 42); // 后天 +1 档阈值
const EFFORT_T2 = Math.round((POOL_MAX * 20) / 42); // 后天 +2 档阈值(满)
function itemPoints(streak) {
let p = 0;
for (const m of GAIMING_MILESTONES) if (streak >= m.streak) p = m.pts;
return p;
}
function nextMilestone(streak) {
for (const m of GAIMING_MILESTONES) if (streak < m.streak) return { streak: m.streak, pts: m.pts, remaining: m.streak - streak };
return null; // 已满档
}
const pad = (n) => String(n).padStart(2, '0');
function dayKey(d) { return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`; }
function weekKey(d) {
// ISO 周键 YYYY-Www。
const t = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
const day = t.getUTCDay() || 7;
t.setUTCDate(t.getUTCDate() + 4 - day);
const yStart = new Date(Date.UTC(t.getUTCFullYear(), 0, 1));
const wk = Math.ceil((((t - yStart) / 86400000) + 1) / 7);
return `${t.getUTCFullYear()}-W${pad(wk)}`;
}
const keyOf = (cadence, d) => (cadence === 'day' ? dayKey(d) : weekKey(d));
const prevKeyOf = (cadence, d) =>
keyOf(cadence, new Date(d.getTime() - (cadence === 'day' ? 86400000 : 7 * 86400000)));
// 打卡:更新某项的连续数。本期已打过 → 幂等;接着上一期 → streak+1;断了/首次 → 归 1。
export function checkin(gaiming, itemId, now) {
const item = BY_ID[itemId];
if (!item) return gaiming || {};
const g = { ...(gaiming || {}) };
const cur = g[itemId] || { streak: 0, lastKey: null, best: 0 };
const key = keyOf(item.cadence, now);
if (cur.lastKey === key) return g; // 本期已打卡 → 不重复加
const streak = cur.lastKey === prevKeyOf(item.cadence, now) ? cur.streak + 1 : 1;
g[itemId] = { streak, lastKey: key, best: Math.max(cur.best || 0, streak) };
return g;
}
// 各项当前状态(前端打卡板用):连续数、是否点亮、本期打过没。★ 断了 → streak 归 0(衰减,逼你持续)。
export function boardState(gaiming, now) {
return GAIMING_ITEMS.map((item) => {
const cur = (gaiming || {})[item.id] || { streak: 0, lastKey: null, best: 0 };
const key = keyOf(item.cadence, now);
const fresh = cur.lastKey === key || cur.lastKey === prevKeyOf(item.cadence, now); // 没断
const streak = fresh ? cur.streak : 0;
return {
id: item.id, name: item.name, cadence: item.cadence, hint: item.hint,
streak, best: cur.best || 0, lit: fresh && streak >= LIT, doneThisPeriod: cur.lastKey === key,
pts: itemPoints(streak), // 该项当前进综合指标库的分(0/1/3/6)
next: nextMilestone(streak), // 下一档目标 {streak, pts, remaining};null=满档
};
});
}
// 综合指标库:各项当前分之和 + 它折成的后天改命档(可见结果)。断了的项分=0 —— 「持续才改命」。
// 阈值即【明摆的目标】:库 ≥ 8 分 → 后天 +1 档;≥ 20 分 → +2 档(满,顶级命的临门一脚补齐)。
export function gaimingIndex(gaiming, now) {
const board = boardState(gaiming, now);
const pool = board.reduce((s, it) => s + (it.pts || 0), 0);
let effort = 0, next = null;
if (pool >= EFFORT_T2) effort = 2;
else if (pool >= EFFORT_T1) { effort = 1; next = { at: EFFORT_T2, effort: 2 }; }
else { effort = 0; next = { at: EFFORT_T1, effort: 1 }; }
return { pool, max: POOL_MAX, effort, effortMax: 2, next, milestones: GAIMING_MILESTONES };
}
// 改命值(供 office5d 折成后天 effort)。★office5d 内 floor(gaimingPoints/2) 封 0..2,
// 故这里回 0/2/4 → 映射后天 0/1/2 档;阈值由 EFFORT_T1/T2 随方向总数等比缩放。
// 改命仍是「临门一脚」,不会因为新增可选方向而突然变松。
export function gaimingPoints(gaiming, now) {
const { pool } = gaimingIndex(gaiming, now);
return pool >= EFFORT_T2 ? 4 : pool >= EFFORT_T1 ? 2 : 0;
}
File diff suppressed because it is too large Load Diff
+151
View File
@@ -0,0 +1,151 @@
// 北齐的入局关口。★ 顺着朝代做下去(2026-07-23)。lite + anyChannel(不分通道、任何位阶都碰得到)。
// 一朝一道那朝的大事:北齐后主猜忌功臣 —— 你立了大功,功高震主,怎么办?
// ★★ 这道关:守正居功者忠而见杀(兰陵王·赐鸩),自污逢迎或可全身 —— 忠不忠、全不全,两难。
// 史料【双证逐字】:北齐书(wikisource raw,2026-07-23 curl 亲验)。
const PUNCT = '引文标点为本项目所加,未采用任何现代点校本的标点、分段或校勘成果。';
export const BEIQI_DECISIONS = [
{
id: 'beiqi-gong-gao',
dynasty: 'beiqi',
lite: true,
title: '功高震主',
when: '北齐·武平年间 · 后主高纬猜忌功臣之际',
requiresRank: { dynasty: 'beiqi', anyChannel: true, minTier: 0, maxTier: 5 },
requiresRankNote:
'★ anyChannel:后主猜忌、功臣自危,是压到每一个北齐有功之人头上的大势 —— 不分你走哪条路、坐哪一格,' +
'功一旦高了,「怎么自处」都轮得到你选。故不锁通道、宽位阶。(同 orNoChannel:一条本文件验不了的断言,此处签字。)',
preconditions: { state: null, afterDecisions: [], closedByOptions: [] },
situation:
'你在北齐立了滔天大功 —— 就像那位兰陵王高长恭,邙山一战,戴面具冲阵,解了金墉之围,军中唱起《兰陵王入阵曲》。\n' +
'可你功盖了主。后主高纬容不下 —— 史书四个字:「帝由是忌之。」\n' +
'\n' +
'功高震主,你怎么自处?\n' +
'\n' +
'坦然居功,你守你的忠、受你的赏,可猜忌的君王,眼里容不下一个比他亮的人,鸩酒就在后头等着;\n' +
'急流勇退,你交兵权、避锋芒,甚至自污敛财让人觉得你没大志,或许能保一条命,可你也把一身的功业收了场;\n' +
'韬晦逢迎,你藏起锋芒、专捧着后主,命是稳了,可你从此只是个会来事的人,不再是那个冲阵的你。\n' +
'\n' +
'你,居功守正,还是退避自保?',
situationCites: [
{
what: '★ 兰陵王功高被忌、赐鸩而死(史事逐字)',
source: '《北齐书》卷十一·文襄六王(兰陵王高长恭)',
quote: '帝由是忌之。……武平四年五月,帝使徐之範飲以毒藥。……飲藥薨。',
caveat:
'2026-07-23 curl 逐字目验(wikisource raw 北齐书卷11)。兰陵王邙山之捷后为后主高纬所忌,' +
'武平四年遣徐之范以毒药赐之,饮药而薨。三句分出忌、赐、死三处,以「……」相接。' + PUNCT,
},
],
options: [
{
id: 'ju-gong',
text: '居功守正 —— 不自污、不逢迎,坦然受功,如兰陵王。',
persona: '你冲锋陷阵,为的是这个国,不是为讨谁欢心。功是真的,忠也是真的,你不屑于自污求活。',
layer: 'dao',
effects: { quan: -1, ming: 4, li: -2, xian: 5 },
inherited: {
value: true,
why:
'★ 你守的是一个【前提】—— 「以忠事上、功不必藏」这条猜忌之君最容不下的道。你这一世忠而见杀,' +
'可你守的道没有随你死:兰陵王一句「忠以事上,何辜于天」,成了后世叹功臣、责昏主的公论。' +
'⇒ 你留下的不是命,是一段「忠者何辜」的千古之问。',
},
fate: {
score: 0,
text:
'★★【史有明文 —— 借真人为鉴】:\n' +
'你若居功守正,就是那位兰陵王。他战功盖世,却被后主高纬猜忌 ——「帝由是忌之」。\n' +
'武平四年五月,后主遣徐之范给他送去毒药。兰陵王饮药而薨。\n' +
'⇒ 你为这个国拼过命,君王却容不下你这份亮,一杯鸩酒了结了你。\n' +
'⚠ 命没了、功成了空,可「忠而见杀」这四个字,替你钉在了后主的头上。',
cites: [
{
what: '★★ 兰陵王赐鸩、饮药薨(北齐书卷11,逐字)',
source: '《北齐书》卷十一·文襄六王',
quote: '武平四年五月,帝使徐之範飲以毒藥。……飲藥薨。',
caveat: '2026-07-23 curl 逐字目验(wikisource raw 北齐书卷11)。' + PUNCT,
},
],
},
who: {
name: '兰陵王(高长恭)',
ref: '北齐书卷11·文襄六王(邙山之捷、《兰陵王入阵曲》,功高为后主所忌,赐鸩饮药薨)',
cites: [
{
what: '★ 走兰陵王的路:功高被忌(史事)',
source: '《北齐书》卷十一·文襄六王',
quote: '帝由是忌之。',
caveat: '2026-07-23 curl 逐字目验。' + PUNCT,
},
],
},
terminal: {
value: true,
why:
'★ 选它 = 你走兰陵王那条路,而兰陵王的传,写到「飲藥薨」为止。\n' +
'你居功不藏、守正不污,猜忌之君一杯鸩酒,就结了你这一世。\n' +
'⚠ 你这本书到此为止 —— 但「忠者何辜」的问,留了下来。',
cites: [
{
what: '★★ 兰陵王饮药薨(北齐书卷11,逐字)',
source: '《北齐书》卷十一·文襄六王',
quote: '飲藥薨。',
caveat: '2026-07-23 curl 逐字目验。' + PUNCT,
},
],
},
cites: [],
},
{
id: 'yong-tui',
text: '急流勇退 —— 交兵权、避锋芒,甚至自污求全。',
persona: '你读过史:功高不赏、震主者危。你宁可让人觉得你贪财无志、胸无大略,也要把这条命、这个家保下来。',
layer: 'zhi',
effects: { quan: -3, ming: -1, li: 1, xian: 1 },
inherited: {
value: false,
why:
'★ 你要改的是「怎么自处」这件【事】—— 退一步、污一点、避开那道猜忌的目光。你或许保住了命,' +
'可你也把一身功业主动收了场,什么也没立起来。⇒ 全身是全身了,却没有能传的道:一个自污求全的姿态,传不下什么。' +
'(⚠ 且这退未必管用 —— 兰陵王也曾自污敛财避祸,仍不免鸩。)',
},
fate: {
score: 2,
text:
'★【本项目的推演,非史事】:\n' +
'你交了兵权、敛财自污,让后主觉得你无非一个贪鄙之徒,构不成威胁。\n' +
'运气好,你保住了命,做个富家翁了此残生;运气不好,猜忌之君的刀,自污也挡不住。\n' +
'⚠ 此为「急流勇退」的推演落点,成败系于那位君王一念之间。',
cites: [],
},
cites: [],
},
{
id: 'tao-hui',
text: '韬晦逢迎 —— 藏起锋芒,一味捧着后主。',
persona: '你看清了:在这样的主子底下,会打仗不如会来事。你把功劳往上让、把话往软里说,只求他高兴、留你一命。',
layer: 'shi',
effects: { quan: 1, ming: -3, li: 1, xian: -3 },
inherited: {
value: false,
why:
'★ 你什么都没改,只是把自己藏了起来 —— 猜忌之君还是猜忌之君,北齐还是那个北齐,' +
'你只是【学会了不碍他的眼】。而下一个功臣面对的,是一样的猜忌和一样的两难。⇒ 没有东西可供继承。',
},
fate: {
score: 4,
text:
'★【本项目的推演,非史事】:\n' +
'你藏锋逢迎,专拣后主爱听的说,果然保住了恩宠、保住了命。这乱世里你活得最稳。\n' +
'可那个戴面具冲阵、军中为你唱歌的你,从此没有了 —— 你换命保下来的,是一个只会来事的人。\n' +
'而北齐,也没能等到几个真肯为它拼命的人:它很快亡在了北周手里。\n' +
'⚠ 史无此支真人 —— 逢迎保身者,青史不书其名。此为「若韬晦」的推演。',
cites: [],
},
cites: [],
},
],
historical: 'ju-gong',
},
];
+141
View File
@@ -0,0 +1,141 @@
// 北魏的入局关口。★ 顺着朝代做下去(2026-07-23)。lite + anyChannel(不分通道、任何位阶都碰得到)。
// 一朝一道那朝的大事:孝文帝迁都洛阳、全面汉化 —— 你这鲜卑贵族,改,还是不改?
// ★★ 这道关的【顺势者得利、逆势者担道】:拥护汉化=顺孝文之势(得用,fate 高);守旧俗=逆大势守前提
// (担一个别人早不认的道,fate 低,却在数世后的六镇反扑里留下回响)。与秦/新/五代的「附逆者死」又是一味。
// 史料【双证逐字】:魏书(wikisource raw,2026-07-23 curl 亲验)。
const PUNCT = '引文标点为本项目所加,未采用任何现代点校本的标点、分段或校勘成果。';
export const BEIWEI_DECISIONS = [
{
id: 'beiwei-han-hua',
dynasty: 'beiwei',
lite: true,
title: '断北语',
when: '北魏·太和十八—十九年(494–495)迁洛汉化之际',
requiresRank: { dynasty: 'beiwei', anyChannel: true, minTier: 0, maxTier: 5 },
requiresRankNote:
'★ anyChannel:孝文帝迁都汉化,是压到每一个鲜卑贵族、每一个在朝者头上的大势 —— 不分你走哪条路、坐哪一格,' +
'改姓、断语、习汉礼都轮得到你表态。故不锁通道、宽位阶。(同 orNoChannel:一条本文件验不了的断言,此处签字。)',
preconditions: { state: null, afterDecisions: [], closedByOptions: [] },
situation:
'孝文帝迁都洛阳,力推汉化 —— 改鲜卑姓为汉姓(拓跋改姓元),习汉礼、服汉服,鲜卑与汉族通婚。\n' +
'这一日,他当廷断令:「今欲断诸北语,一从正音」—— 三十岁以下、见在朝廷的人,不许再说鲜卑话。\n' +
'他还撂下一句狠的:「若仍旧俗,恐数世之后,伊洛之下复成被发之人。」\n' +
'\n' +
'你是鲜卑贵族。祖宗的话、祖宗的服、祖宗的姓,到你这儿要改。\n' +
'\n' +
'改,你顺了皇上、进了华夏,可你亲手把鲜卑的老根一节节剪了;\n' +
'不改,你守住了祖宗,可你逆的是皇上,是整个往南、往汉去的大势;\n' +
'\n' +
'你,改,还是不改?',
situationCites: [
{
what: '★ 孝文帝断北语令(史事逐字)',
source: '《魏书》卷二十一上·咸阳王禧传(载孝文帝诏语)',
quote: '今欲斷諸北語,一從正音。……三十以下,見在朝廷之人,語音不聽仍舊。',
caveat:
'2026-07-23 curl 逐字目验(wikisource raw 卷21上)。孝文帝断鲜卑语、一从洛阳正音;' +
'三十以上积习难改姑缓,三十以下不许仍旧。省略处以「……」标出。' + PUNCT,
},
{
what: '★ 孝文帝汉化之由:不改则数世复为被发之人(史事逐字)',
source: '《魏书》卷二十一上·咸阳王禧传(载孝文帝语)',
quote: '若仍舊俗,恐數世之後,伊洛之下復成被髮之人。',
caveat: '2026-07-23 curl 逐字目验(wikisource raw 卷21上)。此孝文帝自陈汉化之急。' + PUNCT,
},
],
options: [
{
id: 'yong-hua',
text: '拥护汉化 —— 改汉姓、断北语、习汉礼。',
persona: '你信孝文帝看得远:偏居一隅的胡俗成不了大业,入了华夏才有万世。祖宗若在,也当认这笔账。',
layer: 'shi',
effects: { quan: 2, ming: 2, li: 1, xian: 1 },
inherited: {
value: false,
why:
'★ 你顺的是【势】—— 改前提让鲜卑变成「中国」的,是孝文帝,不是你;你只是【顺着他往前】。' +
'汉化的功业记在孝文帝名下,不在你名下。⇒ 你这一步保了自己、进了华夏,却没有一条属于你的道可供后人继承。',
},
fate: {
score: 4,
text:
'★★【史有明文 —— 借真人为鉴】:\n' +
'你顺孝文帝这条路,就是北魏文治大进的那条主线。孝文帝亲断北语、亲改元姓,' +
'一代之内,平城的鲜卑变成了洛阳的衣冠 —— 顺这势的人,得用、显达。\n' +
'⇒ 你会是这场大变里站对了队的人。\n' +
'⚠ 只是这势也有它的背面:老根剪得太急,平城旧人、六镇武夫离了心 —— 那笔账,几十年后另算(见「守旧俗」)。',
cites: [
{
what: '★ 走孝文帝的路:断北语、一从正音(史事)',
source: '《魏书》卷二十一上·咸阳王禧传',
quote: '今欲斷諸北語,一從正音。',
caveat: '2026-07-23 curl 逐字目验(wikisource raw 卷21上)。' + PUNCT,
},
],
},
who: {
name: '孝文帝',
ref: '魏书·高祖纪 / 卷21上(迁洛、改姓、断北语、习汉礼,北魏汉化的旗手)',
cites: [
{
what: '★ 这条路的旗手(史事)',
source: '《魏书》卷二十一上·咸阳王禧传',
quote: '若仍舊俗,恐數世之後,伊洛之下復成被髮之人。',
caveat: '2026-07-23 curl 逐字目验。孝文帝自陈非汉化不可。' + PUNCT,
},
],
},
cites: [],
},
{
id: 'huan-xing',
text: '缓行 —— 表面奉诏,暗守旧俗,劝皇上别太急。',
persona: '你不反对入华夏,可你怕剪根剪得太快。你想留一手:汉礼照习,可鲜卑的底子也别一下断干净。',
layer: 'zhi',
effects: { quan: 0, ming: 0, li: 0, xian: 1 },
inherited: {
value: false,
why:
'★ 你想改的是汉化的【火候】这件事 —— 缓一缓、留一线。可孝文帝要的正是快刀,你的「缓」多半被大势碾过;' +
'你既没随势立功,也没守节立道。⇒ 折中不留痕:一个两头都不到位的姿态,传不下什么。',
},
fate: {
score: 2,
text:
'★【本项目的推演,非史事】:\n' +
'你表面奉诏、暗地留三分旧。孝文帝在时,你不显山露水地过;\n' +
'可急汉化的朝堂容不下太多「缓」,你既进不了汉化功臣的核心,也没落个守节的清名。\n' +
'⚠ 此为「折中缓行」的推演落点,不点某一人之名。',
cites: [],
},
cites: [],
},
{
id: 'shou-jiu',
text: '守旧俗 —— 不改姓、不断语,守住鲜卑的根。',
persona: '你认死理:拓跋的姓、鲜卑的话、马背上的武力,才是这个国的命根。剪了根,走得再远也是浮萍。',
layer: 'dao',
effects: { quan: -2, ming: 1, li: -2, xian: 3 },
inherited: {
value: true,
why:
'★ 你守的是一个【前提】—— 在举国往南、往汉的大势里,你偏认「鲜卑是鲜卑、武力立国」这条别人正要丢掉的道。' +
'你这一世逆势、担着获罪的风险,可你守的这条道没有真的死:数世之后,被汉化撇下的六镇武人揭竿而起,' +
'尔朱荣、乃至东魏北齐的鲜卑化回潮,都是这条线迟到的反扑。⇒ 你逆的势,替后来的乱埋了一半的种,也留了一半的根。',
},
fate: {
score: 0,
text:
'★【本项目的推演,非史事】:\n' +
'你不改姓、不断语,逆着孝文帝和整个洛阳。急汉化的朝堂容不下你 ——\n' +
'轻则边缘投闲,重则以「恋旧、阻挠」获罪。你守住了祖宗,却守不住自己的位子。\n' +
'⚠ 太和年间确有恋旧获罪、乃至谋叛平城者,然本支不点某一人之名硬安 —— 此为「守旧俗到底」的推演落点。',
cites: [],
},
cites: [],
},
],
historical: 'yong-hua',
},
];
+132
View File
@@ -0,0 +1,132 @@
// 北周的入局关口。★ 顺着朝代做下去(2026-07-23)。lite + anyChannel(不分通道、任何位阶都碰得到)。
// 一朝一道那朝的大事:周武帝灭佛(建德三年断佛道二教)—— 你这周臣,奉诏灭佛,还是护法?
// ★★ 这道关又一味:奉诏灭佛者顺势得利(聚财强兵、助灭北齐),可武帝一死佛法复兴 ——
// 你亲手灭的,活过了你;你若逆势护法,你护的反倒传世。功随人亡,道随人长。
// 史料【双证逐字】:周书(wikisource raw,2026-07-23 curl 亲验)。
const PUNCT = '引文标点为本项目所加,未采用任何现代点校本的标点、分段或校勘成果。';
export const BEIZHOU_DECISIONS = [
{
id: 'beizhou-mie-fo',
dynasty: 'beizhou',
lite: true,
title: '灭佛之诏',
when: '北周·建德三年(574)· 周武帝断佛道二教之际',
requiresRank: { dynasty: 'beizhou', anyChannel: true, minTier: 0, maxTier: 5 },
requiresRankNote:
'★ anyChannel:周武帝灭佛、括天下沙门还俗,是压到每一个周臣、每一个信众头上的大势 —— 不分你走哪条路、坐哪一格,' +
'「奉诏 / 护法」都要你选。故不锁通道、宽位阶。(同 orNoChannel:一条本文件验不了的断言,此处签字。)',
preconditions: { state: null, afterDecisions: [], closedByOptions: [] },
situation:
'周武帝要富国强兵,好腾出手灭北齐、图天下。他把目光投向了寺院 —— 那里有免税的田、不服役的僧、囤积的财。\n' +
'建德三年,一道诏下:「初断佛、道二教,经像悉毁,罢沙门、道士,并令还民。」\n' +
'天下的佛像被熔、经卷被焚,几百万僧道被赶回田里当差纳税。\n' +
'\n' +
'你是周臣。这道诏,要人去执行。\n' +
'\n' +
'奉诏,你替武帝聚了财、强了兵,助他灭齐图霸,功劳有你一份,可你亲手毁的是千万人的信;\n' +
'护法,你替佛门、替信众说话,可你逆的是急着强国的武帝,谏者轻则黜、重则罪;\n' +
'不吭声,你不毁也不救,可像照熔、僧照还俗,你在场,也在册。\n' +
'\n' +
'你,奉诏,还是护法?',
situationCites: [
{
what: '★ 周武帝灭佛诏:断佛道二教、经像悉毁(史事逐字)',
source: '《周书》卷五·武帝纪上(建德三年五月)',
quote: '初斷佛、道二教,經像悉毀,罷沙門、道士,並令還民。',
caveat:
'2026-07-23 curl 逐字目验(wikisource raw 周书卷5)。建德三年五月丙子诏 —— ' +
'断绝佛道二教、毁经像、令僧道还俗为民;此「三武灭佛」之一。' + PUNCT,
},
],
options: [
{
id: 'feng-zhao',
text: '奉诏灭佛 —— 顺武帝,聚财强兵、助灭齐。',
persona: '你信武帝算得对:寺院占着田、藏着丁、囤着财,国怎么强?把这些括回来充军实国,才有灭齐图天下的本钱。',
layer: 'shi',
effects: { quan: 3, ming: -2, li: 3, xian: -1 },
inherited: {
value: false,
why:
'★ 你顺的是武帝强国的【势】—— 括寺充国这件事,是武帝定的,你只是办得利落。你聚的财、强的兵,' +
'记在武帝灭齐的功里,不记在你的道里。⇒ 而且这功不牢:武帝一死,隋文帝复佛,你亲手毁的,很快又立了起来。' +
'你办成的是一时之功,不是能传的道。',
},
fate: {
score: 4,
text:
'★★【史有明文 —— 借真人为鉴】:\n' +
'你顺周武帝这条路 —— 建德三年,「初断佛、道二教,经像悉毁,罢沙门、道士,并令还民」。\n' +
'括寺院之财、之丁充实国用,果然强了兵:几年后北周灭了北齐,一统北方。顺这势的人,得用、显达。\n' +
'⇒ 论眼前的功,你站对了队。\n' +
'⚠ 只是这功来得快、去得也快:周武帝一死,隋文帝重兴佛法,你熔的像、焚的经,转头又立了起来 ——\n' +
'你亲手灭的东西,活过了灭它的人,也活过了你的功。',
cites: [
{
what: '★ 走周武帝的路:断佛道二教(史事)',
source: '《周书》卷五·武帝纪上',
quote: '初斷佛、道二教,經像悉毀,罷沙門、道士,並令還民。',
caveat: '2026-07-23 curl 逐字目验(wikisource raw 周书卷5)。' + PUNCT,
},
],
},
who: {
name: '周武帝(宇文邕)',
ref: '周书卷5·武帝纪(建德三年灭佛、括寺充国,进而灭北齐;然身死未久,佛法即复兴)',
cites: [],
},
cites: [],
},
{
id: 'huan-xing',
text: '缓行 —— 只括其弊,不绝其教,劝武帝别赶尽。',
persona: '你不否认寺院有蠹,可你怕一刀切太狠。你想留一条缝:括浮滥的僧、免税的田,可别把这教连根断了。',
layer: 'zhi',
effects: { quan: 0, ming: 1, li: 1, xian: 1 },
inherited: {
value: false,
why:
'★ 你想改的是灭佛的【火候】—— 括弊而不绝教。可武帝要的是断根充国的快刀,你的「缓」多半被大势压过;' +
'你既没随势立灭齐之功,也没守住护法之节。⇒ 折中不留痕:一个两头不到位的姿态,传不下什么。',
},
fate: {
score: 2,
text:
'★【本项目的推演,非史事】:\n' +
'你劝武帝括弊留教。武帝在时,你的「缓」被当成书生之见,晾在一边;\n' +
'你既进不了灭齐功臣的核心,也没落个护法殉道的清名。\n' +
'⚠ 此为「缓行折中」的推演落点,不点某一人之名。',
cites: [],
},
cites: [],
},
{
id: 'hu-fa',
text: '护法 —— 逆武帝,为佛门、为信众直言。',
persona: '你信这世上有些东西不能拿来算账 —— 千万人的信、几百年的经像。哪怕犯颜、哪怕获罪,你也要替它说一句。',
layer: 'dao',
effects: { quan: -3, ming: 3, li: -3, xian: 3 },
inherited: {
value: true,
why:
'★ 你守的是一个【前提】—— 「信不可夺、教不可绝」这条急着强国的武帝不肯认的道。你这一世逆势获罪,' +
'可你护的这条道没有随你死:周武帝一死,隋文帝重兴佛法,经像重立、僧众重聚 —— 你护的,活了过来。' +
'⇒ 奉诏者灭的很快复起,护法者守的终究传世:你逆势担的这个道,比灭它的势活得长。',
},
fate: {
score: 0,
text:
'★【本项目的推演,非史事】:\n' +
'你犯颜护法,逆着急于强国的武帝。急灭佛的朝堂容不下你 —— 轻则黜落投闲,重则以「沮国政」获罪。\n' +
'你护不住眼前那一尊尊被熔的像,也护不住自己的位子。\n' +
'可你逆势守的这条道,武帝死后就复了:隋唐佛法之盛,正是从这灰烬里重新长出来的。\n' +
'⚠ 北周灭佛时确有抗诏、争谏的沙门与臣子,然本支不点某一人之名硬安 —— 此为「护法到底」的推演落点。',
cites: [],
},
cites: [],
},
],
historical: 'feng-zhao',
},
];

Some files were not shown because too many files have changed in this diff Show More