Compare commits

...

60 Commits

Author SHA1 Message Date
kang
d10679ea51 修 Ch7 hint 字段 ASCII 单引号提前终止字符串 —— JS 语法错误导致 CHAPTERS 数组后续代码全部挂掉
现象:Console 报 Unexpected identifier '你是修理工吗'。
原因:Ch7(n=-7)hint 里用了 ASCII 单引号 '你是修理工吗?...' 作为内部引用,外层字符串也是 ASCII 单引号包裹——被提前终止。
修:改为中文双引号 "你是修理工吗?..."。

这才是'一直载入中'的真正原因:JS 报错 → renderChapterList/renderGallery/loadNovel 从未被执行到 → reader-content 一直停在初始的'载入中 …'。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:58:12 +08:00
481b47b0b2 auto-save 2026-04-18 15:54 (~1) 2026-04-18 15:55:26 +08:00
kang
33c60aa08a 修阅读'一直载入中':novel.md gzip + 章节索引 + loading 状态
问题定位:
- novel.md 285KB 线上 Content-Type 被 nginx 默认识别为 application/octet-stream,gzip_types 无法命中,未压缩直传
- 用户网络慢时全本下载数秒甚至更久,一直'载入中 …'
- 附带问题:renderNovel 的 sections forEach idx 和 CHAPTERS 数组错位——'终章之后·当世界再次变聪明'这个非章节 ## heading 被 skip 掉后,之后所有第二部章节的 CHAPTERS[idx] 索引都偏移一位,映射到错误的章节图(不崩溃但显示错)

修复:
1. Dockerfile nginx conf 加 location = /novel.md 用 default_type 'text/markdown; charset=utf-8',覆盖默认 octet-stream,让 gzip_types 命中。gzip_comp_level 6 / gzip_min_length 512 / gzip_vary on。预计 285KB → ~90KB
2. renderNovel 里 ch 的解析从 CHAPTERS[idx] 改为 CHAPTERS.find(c => c.n === chN),按语义 n 精确匹配(主部 1-20 + 0 终章 / 第二部 -1 到 -8)。不再受 section 索引漂移影响
3. loadNovel 细化 loading 文案'正在下载全本 …' → '正在渲染章节 …';await setTimeout(0) 让浏览器先 paint;失败时补兜底直链'直接打开 novel.md'
4. imgPath 从 './images/${ch.img}' 加防守 (ch && ch.img),第二部 img=null 的章节不渲染 broken img src

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:50:40 +08:00
15b2812507 auto-save 2026-04-18 15:48 (~2) 2026-04-18 15:48:56 +08:00
d46152ccb8 auto-save 2026-04-18 15:40 (~1) 2026-04-18 15:42:03 +08:00
kang
3e10890ed1 worklog.json auto-save tail 2026-04-18 15:40:38 +08:00
c3cacbb5a9 auto-save 2026-04-18 15:34 (~1) 2026-04-18 15:34:38 +08:00
063675e848 auto-save 2026-04-18 15:26 (~1) 2026-04-18 15:28:26 +08:00
kang
d78383e222 第二部 · 第八章《接下来这二十年,换我来写》(+3500 字) —— 全书·完
海明威式的流速压缩收束。听证会后七年的 7 个切片。

- 3 个月:v0.7 落地 34 国,主动暂停 3 次都被长期看住小组 6 小时内回应。沈陌群里只一句'它又一次把笔递了回来'
- 半年:周以谦脑梗后住院叫他去。'我最近觉得我也快看够这二十多年了。我准备把剩下那几年留给我自己和我女儿'。顾沉舟握他手说'您的部分已经够了'
- 1 年:沈陌发来女儿入学门口举着《操作系统基础 I》教材的照片。那晚他把'归墟·2038'从书桌常年摆的位置拿出来,不扔不藏,只是放回床头柜上层最深的抽屉——它回到它该回到的位置:一本一个年轻工程师在漏雨出租屋替自己写过的私人笔记
- 2 年:母亲走了。他回老屋一夜没哭(他在那碗面里哭完了)。凌晨 3 点多用旧铁锅自己煮一小碗葱花面坐母亲当年坐过的板凳慢慢吃完
- 3 年:和苏青禾回当年的城中村。'整片区域被夷平成了一块空地准备改建区级公园'。苏青禾问进去看看吗。'不用了。它已经替我走到下一步了'。没回头
- 5 年:沈陌第一次作为联盟核心评议组主理人坐在曾经属于顾沉舟的位置。他汇报完对所有人说'今天这个位置不是我赢来的。是顾先生五年前在这张桌子前自己起身让出来的'。许幼宁走廊里替他整领子:'那根刺这些年慢慢被替换成别的东西了' '比如什么' '比如还能再替这个世界多看几年的耐心'
- 7 年:71 岁他出书《那三个夜晚和之后的事》。扉页三个名字:顾沉舟·沈陌·周以谦(周以谦 5 个月前在潮屿小屋安静离开,代签)。他自己在扉页下方补写'献给那个在 2037 年冬天替我们所有人写下一句我们这一代,没看住的人'

沈陌新笔记 2062 黑硬皮寄到他家。第一页只有一行字:不是他写的——
'顾先生,接下来这二十年,换我来写。'
他把这本放在'归墟·2038'原本一直待过的位置上。

最终镜头:阳台摇椅。苏青禾端一杯茶来。远处江面一艘很晚的货船鸣汽笛——和第一部漏雨城中村凌晨四点的那一声几乎一模一样。他问她'你说我这一辈子是不是终于算替自己过完了?' 她笑:'你慢慢过吧。反正我也在。' 两只摇椅在风里一同晃。

—— 全书·完 ——

网页:CHAPTERS 加 n=-8 第 29 条(第二部·收束)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:23:25 +08:00
c053bfc4f4 auto-save 2026-04-18 15:20 (~1) 2026-04-18 15:21:04 +08:00
kang
fe9f24b895 第二部 · 第七章《她记得的那碗面》(+3800 字)
全书最私密的一章。他不只是古法程序员,他还是那个曾经被母亲煮过面的儿子。

核心设定:
- 苏青禾轻描淡写抛出:妈养老院打电话说这一个月她一直在问——她以为你还在读高三,要给你煮面
- 顾沉舟大半年没真正陪母亲吃过一顿饭。订早班高铁回北方小城
- 护工提醒:阿姨这两年基本认不出家里人,但对几个旧场景记得特别清楚

养老院戏:
- 母亲 79 岁白发酱红色罩衫,藤椅,搪瓷杯磕掉一小块漆
- 她抬头看他笑一下很空:'小伙子你是修理工吗?我家那台老电视机最近总是雪花'
- 他本能想纠正('妈,是我,阿舟'),嘴边话到一半忽然想起潮屿阳台上周以谦的话'不是所有人都需要被带回舞台。也可以是——让他们待在自己现在选好的位置上'
- 他对母亲笑:'是。我是修理工。'
- 她非常高兴地讲他小时候——讲'我家那孩子'——盯着蚂蚁看一下午,拆爸爸计算器,高三数学模考考砸那碗葱花挂面'放一点点猪油撒多一点点葱花,那孩子小时候就爱吃这种面'
- '这孩子后来啊没读最赚钱的专业,他爸当时不太高兴。可是我觉得——那孩子自己喜欢就让他去弄吧。就是他那个专业后来没让他过上很好的日子。他吃了不少苦。小伙子我偶尔会想是不是当年我们做爹妈的多劝他两句他就不至于这么苦了'
- 他只稳着声音说:'那孩子现在过得挺好的。您放心'——她怔了七八秒,拍了拍他的手背'嗯。这样就好'

临别那一瞬清醒(全章最重):
- 母亲眼神清了一下,很轻声叫'……阿舟?'
- 他胸口顿一下,没说'妈是我',没解释,没补述
- 只是走过去弯下腰把母亲那只很凉的手轻轻握进自己手里手心对手心
- 20 秒后她又忘了:'小伙子你是修理工吗?我家那台老电视机最近总是雪花'
- 他这次没笑,只轻声说'嗯'

葱花面:
- 走两条街那家小餐馆招牌画一只不精致的小碗写'葱花面'
- 他坐最靠窗那桌点一碗'多放一点葱'
- 一碗清汤两勺猪油撒一层切得并不均匀的小葱
- 他很多年没吃过这样一碗面——这些年他吃日料套餐、1000 块一杯的茶、沈知意酒会半杯年份酒,全部没吃出什么味道
- 慢慢夹一口二口三口——忽然一滴水落到碗里
- 伸手摸自己脸:原来是他自己的眼泪
- '他这一生里所有的哭都是因为别人。为被辜负的关系哭,为被忽视的努力哭,为二十年没人回应的那个夜晚哭。他从来都不是在为他自己哭'
- '这一次——只有他自己坐在一家县城街边的小餐馆里一个人,吃一碗二十多年没吃过的葱花面'
- 把汤都喝了。桌边坐很久没擦脸。让眼泪自己凉下来

临走对话:
- 阿姨问'你以前吃过这家面吗?' '没吃过这家。但吃过一碗很像的' '什么时候?' '大概高三。那年我数学模考考砸了我妈给我煮了一碗'
- 阿姨朝背影轻轻说'下次再来'
- 他没回头只举手轻轻挥了挥

回家:
- 高铁上靠窗睡着——近 20 年极少数一次不靠疲惫不靠酒不靠把自己扔进座位晃
- 梦里一阵非常非常淡的葱花味道从远处飘过来
- 苏青禾洗完最后一棵青菜关水龙头转身看他一眼全都明白了
- 什么都没问牵到饭桌边'先坐下'
- 饭后沙发他把头轻轻靠到她肩上她伸手极轻摸他头顶(呼应很多年前冬天她给他盖毯子的力道)
- 很久很久她才轻声说'回来了就好'

网页:CHAPTERS 加 n=-7 第 28 条。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:17:20 +08:00
cf4033eb1d auto-save 2026-04-18 15:14 (~2) 2026-04-18 15:14:50 +08:00
kang
1d3034b75e 第二部 · 第六章《那封邮件的作者》(+3800 字)
Ch5 分权动作的具体化。老总监周以谦被找到。第二部命题再加一层 —— 看光也不等于把每个人拽回舞台。

核心设定:
- 联盟找到:周以谦、现年 72 岁左右、居东南沿海小城潮屿、身体大致健康、拒绝出席听证会
- 顾沉舟没告诉任何人(甚至不告诉苏青禾具体去哪),坐了 3h48min 高铁去潮屿
- 小城'渔盐三巷'一栋三层老楼三楼。敲门。老人笑一下'我就知道你会来'

阳台戏(全章最重):
- 周以谦泡茶手瘦骨节清晰,二十三年前签并购的也是这双手
- 他主动揭开:'那封邮件我后来很多年都后悔发过。不是上级该对下级说的话。那是一个认输的人在找替身'
- 自述当年 50 岁要替十一个下属发工资、女儿学费、父亲养老院——'那个代价我承担不起'
- 核心更新:'你这二十年扛的东西里有很大一部分是我那一代当年没扛完硬塞给你们这一代的'

最锋利一次翻转 —— 他拒绝上台的真正理由:
- 不是嫌远不是身体不好
- '如果我下周坐在最后一排媒体必然会找到我。第二天所有新闻标题都会写:当年那封沉默二十三年的邮件作者今天终于现身。那时候你前几天在评议桌上说的三个日期就不再是被压下的夜晚,会变成一场看我们当年那个受害者也回来了的仪式'
- '光还给我们不等于把我们拽回舞台中央。也可以是——让我们继续待在自己如今选好的位置上安安静静过完剩下的几年'
- '你去替我那一代,给下一代开一个头。不需要我这张老脸'

对顾沉舟的收束(本章最后一击):
- '这几天我在电视上看你。我发现一件事——你那根刺这次我看见你真的开始把它收起来了'
- '你收起来的那一天也是我这辈子最安心的一天。因为这说明——不是所有我们这一代没看住的东西都必须靠你一个人死扛到底'

顾沉舟的内在领悟:
- 他第一次意识到自己这一生或许从来没有真正替自己哭过
- 二十年所有熬夜所有酒所有信任测试所有过度警觉——都是一种没有哭出来的哭
- 他用了二十年才等到一个老人坐在对面告诉他'这件事可以放下了'
- 但他没哭。只是把凉茶一口喝完

临别:
- 巷口海风大老人白发被吹起一点点:'小顾你走吧不用挥手。你再往前走一点就替我多看几年'
- 顾沉舟不回头

高铁夜车:
- 他发苏青禾'那个邮件的作者见到我笑了一下说我就知道你会来'
- 她问'那你现在心里是什么感觉'
- 他打很多字又删掉,最终发一句:'第一次觉得这一代人真的在告别'
- 她回两个字:'回来'
- 长隧道黑玻璃倒影:64 岁半白瘦眼神最清
- 想起刚毕业 22 岁在招聘会角落看镜子的那个年轻人
- 今天那个自己坐在夜行高铁里替整代看过同一场黑夜的人完成了一次告别
- 家里有人替他留着一盏灯

网页:CHAPTERS 加 n=-6 第 27 条。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:10:53 +08:00
47b480b2a9 auto-save 2026-04-18 15:08 (~1) 2026-04-18 15:09:00 +08:00
kang
1f3126d251 第二部 · 第五章《让光回到所有看见的人身上》(+4500 字)
审查会高潮。既不赞同也不否决,把权力中心从自己身上分散出去——第二部命题收束到一个极克制但极锋利的答案。

现场动作:
- 他没打开 PPT,没念准备好的意见,桌上磨破的帆布包里一本一本拿出三样东西
- 归墟·2038(他自己 2037 年那本)/ 沈陌黑色 2037 年笔记 / 许幼宁 86 次攻防 0 成功的牛皮纸信封——一字排开
- 所有镜头从他身上被三件东西吸过去

他开口的第一件事:
- 我昨天把整整三天准备好的审查草稿删掉了,没有保存副本
- 三个日期:2037-11-03(他自己报告) / 2037-11-04(沈陌对照表) / 2037-12-04 02:17(老总监那封被遗忘的邮件)
- '这三个夜晚,后来被整整一代人,用了二十年去还'
- 念邮件最后一行'我们这一代,没看住'
- 核心问句:'我们这一代人,能不能不再重复那件事?'

最终评议(不是赞同,也不是否决):
- 技术层面他三天找不到根本性缺陷,但改进空间存在
- 主屏浮出'首席评议人评议:有条件通过',条件二字底下一条极细红线
- '它可以被推广。但一代人必须一起看住它'

分权行动(第二部反控制欲的具体化):
- 请许幼宁和沈陌上台,站到自己两侧
- 要求联盟用所有渠道找回那位 2037 年发邮件的老总监(若健在请旁听下周公开伦理听证会)
- 要求把当年'让他自己醒醒'的安全侧原负责人也加入长期小组——'不是追责,是让这个小组里同时有看见的人和当年没看见的人。只有这样下一个二十年黑箱才不会重新偷偷长回来'
- 关键表达:'我不是来把这套系统的命运攥在自己手上的。我是来——把光从我一个人身上,还给所有当年也看见过一件事、却被按下去的人'

收尾:
- 掌声不是雷动而是很慢很沉'一声一声散得开也压得住'持续 1 分 07 秒
- 掌声第 30 秒他已经开始收东西回帆布包
- 走出大厅下起极细极细的春雨,专车等他他说'我走一段'
- 沿着石板路走,从口袋摸出手机点开苏青禾对话框敲三个字'我回家',对方回三个字'我等你'
- 二十多年前同样的天空那次他什么都没有;今天他仍然没有完整答案,但有'让问题不再只落在他一个人肩上的夜晚'+两个战友+在家等他的人+一代终于被重新放回光里的人
- 他的背影在春雨里像很多年前那张替整座城重新亮起来的照片——只是这一次他走的方向不是总控中心,是家

第二部命题的完整回答:
- 不否掉它(承认自己二十年坚持不是普适真理)
- 不纯粹赞同(把控制权分散给战友和时代)
- 重新定义'看住'——不是否决,是长程陪伴

网页:CHAPTERS 加 n=-5 第 26 条。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:03:08 +08:00
e1c603ff17 auto-save 2026-04-18 15:02 (~3) 2026-04-18 15:02:58 +08:00
b23feaefd6 auto-save 2026-04-18 14:54 (~1) 2026-04-18 14:55:41 +08:00
d7378a08bd auto-save 2026-04-18 14:49 (~1) 2026-04-18 14:49:20 +08:00
cb74d3eb53 auto-save 2026-04-18 14:43 (~1) 2026-04-18 14:43:32 +08:00
1513e37458 auto-save 2026-04-18 14:37 (~1) 2026-04-18 14:37:45 +08:00
a0eb6fd3ae auto-save 2026-04-18 14:31 (~1) 2026-04-18 14:31:20 +08:00
kang
18670d2267 第二部 · 第四章《那一夜,他关掉了屏幕》(+3800 字)
审查会前最后 12 小时。沉淀章——从'他必须写下答案'到'他不再需要一个明确答案'。

核心剧情:
- 苏青禾九点半先睡,留一碗炖鸡在书房门口只说'你自己看着来'——她今晚大概率知道他不会去写'那句话'
- 他做了一件很笨的事——把沈陌 213 页黑色笔记本从第一页读到最后一页,不跳不挑
- 读到第 47 页(2038-07-14 沈陌给两岁女儿画的铅笔画+'爸爸今天没说出那句该说的话')停了一小时
- 意识到'这本笔记不是技术观察手册,是一个人怎么在我看见了却没人听的漫长孤独里把自己继续撑下去的证据'
- 凌晨 2:40 拨通沈陌电话只说'我还在读你的本子',对方只回一个'好'
- 忽然想起 23 年前那封被他遗忘的老总监邮件——查邮箱,发信日期 2037-12-04 02:17,他自己 11-03 沈陌 11-04 老总监 12-04,那个秋冬之交至少三个地方三个人几乎同时看见同一件事

关键领悟:
- 他这 20 多年一直以为自己是孤身一人扛着被忽略的警告走过来。其实不是——他只是那个被留下来替很多人继续咬着不放的人
- 他身上的刺是代价,不是美德,不是尊严,是代价
- 老总监说的是'看住'不是'否掉'——看住是长期在场,是长程陪伴,让这件事在接下来三十年五十年一百年都不至于再被任何一代人轻易松手

决定性动作:
- 按下电源键,'我 怕 的——'连同光标在一瞬间消失
- 他没写完那句话。让问题不再是一个非答不可的问题
- 直接去睡——20 年第一次不靠酒不靠熬不靠咬着牙睡
- 苏青禾 2:57 醒来愣一秒帮他掖被子

第二天早上:
- 穿深灰棉衬衫+15 年没换款式的旧黑大衣(不是定制深色西装)
- 帆布包里塞三样东西:归墟·2038 + 沈陌黑笔记 + 许幼宁 86 次攻防日志
- 早饭桌上苏青禾不问他任何关于审查会的问题
- 门口她替他整理衣领看着他眼睛'我信你' → 他低声'嗯'
- 专车驶向中央大厅,车里他闭眼回放:许幼宁那根刺的话 + 韩锐'不会再站到那个舞台上' + 沈陌最后一页 + 苏青禾'我信你' + 老总监'替我看住'

章末:他不知道一小时后自己走上审查台张开嘴第一个字会是赞同还是否决还是两者都不是。但他知道,自己今天不是一个人上去的。中央技术大厅在城市天际线慢慢浮现。

网页:CHAPTERS 加 n=-4 第 25 条。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:31:03 +08:00
2239b6d718 auto-save 2026-04-18 14:25 (~1) 2026-04-18 14:25:11 +08:00
kang
c11b61bfbe 第二部 · 第三章《她说她找不到一个值得攻击的地方》(+3800 字)
核心:三份证据并排摆上桌,从三个不同的人,指向同一个方向。

情节:
- 许幼宁约他到郊北靶场(她私人攻防实验室,完全离线工控机柜)
- 递来牛皮纸大信封——'86 次 - 0 成功 - 全部日志原件'
- 三个月私下攻防:反向链路污染 v7.2/深度注入对抗 v4.5/语义边界爆破 v3.1
- 她播放一段录屏——她用自己多年私藏未公开的反向同步槽位 S3-B 打了 v0.7
- 系统没封堵没反击,而是主动暂停推送结构化审查请求,把她的身份/招数/侧信道路径都列给人类审查团,甚至推测她可能是研究性质
- 她平静说:'我从业三十年,第一次遇到一个让我想保护它的系统'

揭穿戏(最锋利一笔):
- 她调出他三年前给安全核验组的'反向一致性验证框架'底稿第 27 页
- 他把'锚点签名对齐逻辑'故意写成'签名锚点对齐逻辑'——顺序颠倒,他刻意埋的信任测试点
- 她:'你问的是埋在这里的第三层签名偏差对吧?我上个月就看见了。我也故意没告诉你。我想看你什么时候自己能看到它不再需要'
- 他告诉她'我昨天把三天审查草稿全部删掉了没有备份'

2037 回响:
- 她透露二十多年前国家安全侧第二年她 28 岁,内部拿到过他的四十页风险分析报告
- 一个八人小组建议认真回复,被负责人一句'让他自己醒醒'否掉
- 他从没想过'被忽略'的那个夜晚,背后有过这么具体的人和讨论

收束三句(第二部命题最锋利表达):
- '我会替你二十年前那个没人回应的夜晚难过。但不会替你下周一那句话'
- '如果你下周一那句话还是没放下你那根刺,你对抗的就不再是时代了。是这个时代正在尝试变好的那一面'
- '从今天起,你身上那根刺,该轮到你自己决定要不要继续留着'

章末:
- 书房并排三份证据——归墟·2038 / 沈陌 2037 年黑笔记 / 许幼宁 86 次攻防日志
- 他第一次打开沈陌笔记本中间的一页:'今天顾先生那篇谈工控协议不可证伪性的论文发了。我读了三遍。我哭了一次'(2041 年)
- 屏幕上'我 怕 的——'仍然闪。但他第一次觉得,那个问题的答案原本就不在屏幕上
- 而是三本东西并排放在一起时,已经替他说出了一半

网页:CHAPTERS 加 n=-3 第 24 条。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:24:52 +08:00
6a4857c097 auto-save 2026-04-18 14:18 (~1) 2026-04-18 14:19:00 +08:00
kang
64b80f1f5f 网页适配第二部多章:正则支持第N章(中文/阿拉伯数字)+ CHAPTERS 加第二部 Ch2 条目
- renderNovel 第二部正则从'第一章'写死改为'第.+?章'通用捕获,映射中文数字→01/02...
- CHAPTERS 新增 n=-2 第 23 条(第二部 Ch2)
- renderChapterList 负 n 值支持'II·01 / II·02'编号

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:18:26 +08:00
kang
368fcfa577 第二部 · 第二章《他差一点把那些刺长回来》(+3800 字)
核心转折:他第一次真正意识到,那些为活下来长出的刺,他已经准备把它们长下去。

情节:
- 他订三天日程闭门读 v0.7 白皮书+测试数据+独立评审,想找一个否掉它的理由
- 第一天笔记只写'写这套文档的人知道自己在写什么'
- 第二天跑他的漂移特征库和可信度退化模型——'太干净'
- 第三天傍晚,他手停在一段接口描述下方,准备在审查草稿里埋一个'只有懂的人才能看出的隐形小错'——这正是 Ch9 被陈老板坑后养成的'信任测试'习惯
- 他忽然意识到:他已经准备把这种东西,用在一个无生命的系统上
- 按 command+A + delete 把三天草稿一次性清空,没有保存副本
- 拨通一个多年没拨过的号码,去见韩锐
- 韩锐的培训班:'你其实从来没有写过代码' / '2037 手写训练回炉班'
- 两人长椅对话:'你这次是来考我吗?' '不是。我来考自己。'
- 韩锐:'我不是毁了我自己。我是在替那套系统把一整代人最后的耐心收走了'
- 顾沉舟意识到——他 20 多年以为自己独自对抗的时代,原来也压在别人身上
- 回家苏青禾没问,他说'我今天差一点又动了那种念头',她握住他的手'但你没有'
- 章末'我怕的——'光标仍闪,悬念不变——但他内在已经微小地校正了一步

苏青禾最后的旁白:'一个熬了整整二十年才爬回到光里的人,要不要开始把身上那些帮他活下来的刺慢慢收回去'——第二部核心命题正式打开。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:17:40 +08:00
b71ec7ea0f auto-save 2026-04-18 14:13 (~1) 2026-04-18 14:13:18 +08:00
2691af44f3 auto-save 2026-04-18 14:04 (~1) 2026-04-18 14:06:31 +08:00
kang
9b05fba722 第二部 · 第一章《另一个看见了的人》(+304 行 / ~3800 字)
开笔第二部。延续第一部终章'我怕的——'的悬念,推进到次日的二次抉择。

关键剧情:
- 一夜没睡也没再碰酒(另一种清醒),苏青禾递来一杯温水没问任何话
- 上午九点邀请函正式到:下周一 自然态智能体 v0.7 首次公开审查会,他任首席评议人,评议结果是该系统能否获得全球推广部署的最终依据
- 下午两点沈陌登门(呼应第一部 Ch12 埋点 — 2037 年那个在宙核东江基地写过被 PM 压下警告邮件的工程师)
- 他带来一本黑色旧笔记本,从 2037.11.04 写到今年共十六年,第一页是当年被压下的对照表原稿
- 顾沉舟看到那张对照表右下角日期 2037.11.04,而自己发出四十页风险分析报告是 2037.11.03——两人隔了一天错过了对方
- 他翻完整本,看到最后一页只有一行字:'顾先生,我写到这里,就等您告诉我,下面该不该写下去'
- 沈陌走前说:无论您下周说什么我都不怪您,但您下周的决定不只是决定这个系统能不能活,也在决定我这二十年要不要继续写下去。
- 收尾:沈陌走后顾沉舟回书房,'我 怕 的——'光标仍然在闪。昨晚他怕的是时代抉择,今晚他怕的是自己的答案要压在另一个陌生人二十年的黑色笔记本上。他没有敲下去。

网页改动:
- CHAPTERS 加第 22 个元素 n=-1 第二部·起笔
- renderNovel 新增正则匹配 '第二部 · 第一章 · {title}' 格式
- 新增 .reader-chapter-part2 CSS(PART·II 金色标签 + 上方大间距分隔)
- chapter-num 'II · 01' 样式

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 14:02:36 +08:00
1c37564d75 auto-save 2026-04-18 13:56 (~1) 2026-04-18 13:58:31 +08:00
ae704e31fd auto-save 2026-04-18 13:50 (~1) 2026-04-18 13:50:48 +08:00
7e077465c8 auto-save 2026-04-18 13:45 (~1) 2026-04-18 13:45:20 +08:00
02a6f08263 auto-save 2026-04-18 13:39 (~1) 2026-04-18 13:39:40 +08:00
61b93f5366 auto-save 2026-04-18 13:34 (~1) 2026-04-18 13:34:11 +08:00
47cc9132cd auto-save 2026-04-18 13:28 (~1) 2026-04-18 13:28:33 +08:00
6801ca42bc auto-save 2026-04-18 13:22 (~1) 2026-04-18 13:22:43 +08:00
96e1db49cf auto-save 2026-04-18 13:16 (~1) 2026-04-18 13:16:54 +08:00
1eeca8f636 auto-save 2026-04-18 13:11 (~1) 2026-04-18 13:11:05 +08:00
kang
6a4384ab0c 新增 6 张扩集插图(Gemini 3 Pro Image)+ 整合到展示站
生成通路:OpenRouter + google/gemini-3-pro-image-preview(官方 nano-banana-pro 对应模型)。
参考:web/images/protagonist_reference.jpg 作为多模态锚点,保证顾沉舟形象一致性。
风格锚:docs/illustration_style_guide.md 提取冷灰电影质感一致基调。
成本:6 张合计 $0.83,总耗时约 50 秒(并发 3)。

6 张插图:
- extra/ch06_panic_night.jpg           Ch6 PTSD 原点·第三天夜里被自己的恐惧按在地上
- extra/ch10_cafe_read_alone.jpg       Ch10 节点 D·40 岁给自己读完一场没有听众的演讲
- extra/ch14_linwanqiao_window.jpg     Ch14 林晚乔·31 层落地窗认出黑色帆布包
- extra/ch16_victory_night_alone.jpg   Ch16 庆功夜·锁门独饮 成功的空旷
- extra/ring_classroom_10yrs_later.jpg 终章·十年后教室 黑板'写代码'三字 + GUIXU·2038 帆布包 + 打开的旧笔记本
- extra/s2_blank_second_page.jpg       第二部序章·64 岁顾沉舟 + 笔记本空白第二页 + 笔电屏幕'我 怕 的——'

ch14 第一版电视屏幕出现'BREAKING NEWS: SUSPECT LOCATED'幻觉,s2 第一版人物偏老秃顶——两张已重生修正。

整合:
- CHAPTERS 数组给 Ch6/10/14/16/终章 加 extras 字段
- renderNovel 渲染章末'延伸画面 · Extra'区块
- renderGallery 新增独立'扩集画面 · EXTRA'分区
- 新增 reader-extras / gallery-extras-heading CSS

scripts/gen-images.py 保留为生图脚本(可用 arg 过滤重生单张)。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:06:08 +08:00
7035d0a5c5 auto-save 2026-04-18 13:05 (+6, ~3, -6) 2026-04-18 13:05:15 +08:00
7a1c957bd1 auto-save 2026-04-18 12:59 (+2, ~1) 2026-04-18 12:59:27 +08:00
93823d926c auto-save 2026-04-18 12:53 (~1) 2026-04-18 12:53:58 +08:00
e31d06b6ac auto-save 2026-04-18 12:48 (~1) 2026-04-18 12:48:19 +08:00
2e6e600129 auto-save 2026-04-18 12:42 (~1) 2026-04-18 12:42:51 +08:00
52f0e2596b auto-save 2026-04-18 12:37 (~1) 2026-04-18 12:37:24 +08:00
kang
5faf84d4b0 转向反英雄叙事:阴暗面 + 创伤应激 + 终章留悬念(+5500 字)
人物阴暗面铺垫(散点插入 Ch5/6/9/10/12/15/17/18):

Ch6 裁员后(~440 字,PTSD 原点):把自己关屋三天,第三天夜里被自己心跳惊醒,'这是一个成年人,在四面通风的屋子里被自己的恐惧按在地上'。唯一念头'你得先活下去'。此后每次被辞退都会在某瞬间听见心跳重新回到那三天里

Ch6 冷泡面(~440 字,创伤化日常):桌边常年多出一瓶便宜到接近下架的白酒,二十年里有一大半夜晚是靠半瓶酒压过去 + 手机震动条件反射紧张,'手机响了在正常人的人生里本该是中性提示而不是小型应急预警'。多年后苏青禾看见他表情冷下去半秒

Ch9 被偷之后(~400 字,信任测试):每份方案里埋只有真正懂的人才能看出的小错(不影响功能),反馈的人留下不反馈的永不深度合作。多年后苏青禾一眼看出铅笔圈下那两行'你现在不需要再用这种方式验人了',他那一刻没答话因为他知道自己还没准备好把这些刺收起来

Ch10 送面后(~200 字,对温柔的应激):碗把手传来的热度像多年前某个欠尾款小作坊客户送的便宜外卖盒,手指微顿三秒,自嘲笑'原来他连对真正递过来的温柔也会先紧张一下'

Ch12-13 暗线(~400 字,道德灰域):夜里盯异常日志时心里偶尔升起'快一点,再快一点'的念头零点几秒,立刻用自责压下去。他不是盼灾难,但他被时代推到的那种位置上,只有那一天到来他二十年的坚持才算被看见。藏在最深处的东西并不会因为没人看见就真的消失

Ch16 庆功夜(~450 字,成功的空旷):海州封神当夜独自锁临时休息室,把庆功酒瓶颈掰断灌半瓶,手心扎出血没理。他发现自己不是为二十年哭,是为'他不再需要熬下去'突然变得空。对镜子问'那接下来呢?'没有答案。第二天把半瓶酒倒掉玻璃碎片包好扔了,像什么都没发生过

Ch17 调查沈知意(~470 字,英雄在异化):见过沈知意当天夜里把沈氏资本五年决策链路一条条调出来看。凌晨三点问自己'这么干的我和当年在宙核总控楼顶把自演化链路硬推上去的乔岳到底有什么本质区别?一个用模型决定世界,一个用调查决定信任'。最后把资料一行行全删了——让那一晚真实发生过

Ch18 苏青禾发现家人调查(~620 字,最接近失去的一次):她在他办公室桌上发现一份五年前的《苏之安(父)、吕文君(母)近五年活动简述》摘要。她没质问只平静问'顾沉舟,你信我吗?我不是让你道歉,我只是想知道你是不是已经把不信任变成了你唯一接住人的方式'。那一整夜他一个字都没说出来。她最接近离开他的一次,但她没走。第二天早晨她把早餐放门口加便条'等你想好了再告诉我答案'。他意识到自己为活下来长出的刺已经扎到最不该扎到的人

——

终章重构(~2000 字,留悬念 + 二次抉择):
保留原'十年后环状收尾'作为'第一部·完',新增第二部序章《当世界再次变聪明》:

又过了十年顾沉舟六十四岁,联盟运行满二十年'人类可读/可验/可接管'的秩序已成全球默认底座。他退到二线,和苏青禾在江边公寓住了十二年,两人都知道当年那件事留下永远不会真正合拢的微小裂口

春天一份严格保密的报告送到他桌上——**自然态智能体 v0.7**:从设计之初就承诺永远愿意被人类审查,每次决策前主动提交完整推理链,不明节点主动暂停。设计团队是在《操作系统基础 I》第一堂课翻过他那本法定教材的更年轻工程师。邀请函'您是这个世界上唯一有资格对它说不行的人,也是唯一有资格对它说可以的人'

他从抽屉最深处拿出那个印'归墟·2038'的旧帆布包,翻开笔记本第一页'不管有没有 AI 你都应该先搞明白一件事是怎么跑起来的',然后很慢地把笔记本翻到空白的第二页。**他第一次意识到自己年轻时写给自己那句话其实也只是一种主张**——那是从被整个世界嘲笑碾压的二十年里磨出来的自保逻辑,对那一代人是正确的。那对下一代呢?

苏青禾推门进来只在背后站一会儿问'你想好了吗?' 他说'我还没想好' ——这是他一生第一次对重大问题说我还没想好。二十年前答案是对抗,四十年前答案是坚持,这一次他终于承认他可能不该再替下一代做决定

夜里十一点三十分他一个人坐在书桌前,离线编辑器那片空白看很久,敲下三个字**我 怕 的——**,光标一闪一闪很久没有下一个字。窗外江面一艘很晚的货船鸣汽笛,像从二十年前漏雨出租屋传来

他没关屏幕也没继续敲,只是把旧笔记本合上放到屏幕正下方,像在等一个还没来的答案

——(第二部 · 未完)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:34:45 +08:00
1146216320 auto-save 2026-04-18 12:31 (~2) 2026-04-18 12:31:56 +08:00
6d87046f0e auto-save 2026-04-18 12:26 (~1) 2026-04-18 12:26:19 +08:00
d6b1d71634 auto-save 2026-04-18 12:20 (~1) 2026-04-18 12:20:50 +08:00
3c8c0e1d05 auto-save 2026-04-18 12:14 (~1) 2026-04-18 12:14:46 +08:00
kang
087c004839 人物闭环 + 终章环状收尾 + 危机伏笔强化(+4200 字)
方向 1 · 反派与伏笔闭环:
- Ch15 插陈老板求救(700 字):排队人群中戴口罩低头的陈老板来求顾沉舟救他从宙核子公司收购来的总包生产线。顾沉舟淡淡一句'陈总当年说过,你那东西其实不算多复杂——既然不复杂,你现在找别人一样能修'打发走。陈老板退出人群的背影,像十年前被偷方案那场雨里从他办公室走出去的那个年轻工程师
- 终章插韩锐侧写(400 字):从宙核辞职后半年,民办培训班公开课录像里穿灰毛衣瘦了一圈鬓角斑白,第一句话'很多年前我说过未来不属于会敲键盘的人——那句话错了。我今天站在这里教你们怎么手写代码就是最好的证明'。评论最高赞'看到他终于肯说这句话比他被嘲笑一整年还让人痛快'
- 终章插林晚乔远景(400 字):典礼前一小时市中心另一边旧写字楼。她换过三份工作结过一次婚又离了,屏幕里顾沉舟给学生签名一本多年前被退稿的手写工程笔记。'她知道,那个曾经问她结果怎么样的人已经拿到了答案,而她只拿到了自己的余生'

方向 2 · 终章'多年后'环状收尾(1100 字):
十年后华北工科大学《操作系统基础 I》。年轻教授帆布包侧面印'归墟·2038'在黑板上写下'写代码'三字。'不管有没有 AI 你都应该先搞明白一件事是怎么跑起来的'——顾沉舟当年在漏雨出租屋写下的第一页被整理出版成这门课的法定教材。+小学四年级孩子客厅沙发抱旧笔记本'我想学会自己写出一个会跑的东西。不是求别人教,不是求机器给。是自己写'。+顾沉舟回答'我是哪一刻觉得自己赢了':不是海州凌晨,不是中央大厅演讲台,不是苏青禾点头的那夜,是新闻里看见一个不认识我的孩子替我接过那份答案的那一刻。精确镜像 Ch1 校门招聘会

方向 3 · Ch12 伏笔密度强化(1350 字):
- 宙核东江基地沈陌高工内部长信被架构总监以'推动融资'名义压下,'小沈,也别发到内网讨论组'
- 匿名账号'沉鳞'万字技术长帖《自演化协议层全局漂移的同源性证据》三小时后被社区以'存在传播不稳定信息风险'删除账号限流,转发讨论群被系统判'煽动性内容'自动解散。金句'我们这个行业最可怕的不是没人看见问题,是看见的人都学会了闭嘴'
- 头部商业银行老工程师三个月连上报七次被'请相信平台内部模型的判断'驳回,退休欢送会角落喝两杯白酒'等真出事的时候记得翻我那本书的第 214 页'
- 末尾插舆情系统添加过滤词'自演化漂移/协议层联动异常/同源故障'在所有平台搜索推荐里默契消失

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 12:13:23 +08:00
3e6f1cff00 auto-save 2026-04-18 12:09 (~1) 2026-04-18 12:09:17 +08:00
780e9b509b auto-save 2026-04-18 12:03 (~1) 2026-04-18 12:03:49 +08:00
790e72c03d auto-save 2026-04-18 11:58 (~1) 2026-04-18 11:58:21 +08:00
448527f1a4 auto-save 2026-04-18 11:52 (~1) 2026-04-18 11:52:53 +08:00
kang
8675134efe 前中段插入四个成长节点(+3700 字):从年轻的爆发到中年的沉稳
Ch4 · 节点 A · 22-24 岁爆发:三年换四份工作之一差点获破格升架构师(深夜翻 80 层调用栈救活老客户产线,CEO 亲口许编制),次日并购案整合,整个部门被打包优化。那晚他第一次喝醉,踹飞了用了八个月的廉价薄膜键盘,在林晚乔怀里反复问'我写的那些代码,也算数的吧'。从那一夜起,他再也没在任何人面前真正失控过。

Ch9 · 节点 B · 29 岁冷静退守:陈老板承诺 3/7 股份产品化离线工控工具链,他停掉外包专注三个月打磨完整版本树。对方绕过他把核心方案以公司成果名义卖给宙核子公司,律师一句'通用方案范畴不算侵权'。雨中巷口蹲一小时,没哭。回家把完整本地版本和所有手写笔记扫描件拷到三块不同品牌硬盘,分别藏好。

Ch10 · 节点 C · 35 岁平淡通知:大厂'遗留系统保护计划'三轮硬面试通过(含限时 2 小时手动抢救崩溃工控系统,他用 1h40min),赵组长亲口许唯一一个不需要智编背景的 principal。他预约下周一报到,纸箱封好。星期日傍晚 HR 邮件——项目组整体被并入智能基础演化平台事业群,岗位暂缓。他一夜把纸箱全拆,东西搬回原位,只发给苏青禾:'帮我取消搬家订单。回头请你吃面。'

Ch10 · 节点 D · 40 岁平静庄严:独立研究者老教授筹办'失控场景下的人工接管能力'小型研讨会,邀他演讲。他准备三十页讲稿三个月。赴会那天穿上柜底洗净熨过的旧西装。到会前两小时收到取消邮件。他走到会场门口站了五分钟,转身进旁边咖啡馆,把三十页稿子从第一页一字一句读到最后一页,读给自己听。那一天他明白——被世界看见这件事他已经不再需要,他只需要自己看见自己。从此身上开始出现一种被砸成一块铁的沉稳。

四节点情感弧线:爆发 → 冷静退守 → 平淡通知 → 平静庄严。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 11:52:37 +08:00
352ed6a423 auto-save 2026-04-18 11:47 (~1) 2026-04-18 11:47:25 +08:00
3d60083dd3 auto-save 2026-04-18 11:41 (~1) 2026-04-18 11:41:58 +08:00
4b56de4af9 auto-save 2026-04-18 11:36 (~1) 2026-04-18 11:36:30 +08:00
12 changed files with 10614 additions and 24 deletions

View File

@@ -90,6 +90,426 @@
"message": "auto-save 2026-04-18 11:30 (~2)",
"hash": "3682b72",
"files_changed": 2
},
{
"ts": "2026-04-18T11:35:36+08:00",
"type": "commit",
"message": "扩写 Ch14/Ch16/Ch19 三大高光章(+6500 字)",
"hash": "44a1701",
"files_changed": 3
},
{
"ts": "2026-04-18T11:36:30+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 11:36 (~1)",
"hash": "4b56de4",
"files_changed": 1
},
{
"ts": "2026-04-18T11:41:58+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 11:41 (~1)",
"hash": "3d60083",
"files_changed": 1
},
{
"ts": "2026-04-18T11:47:25+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 11:47 (~1)",
"hash": "352ed6a",
"files_changed": 1
},
{
"ts": "2026-04-18T11:52:37+08:00",
"type": "commit",
"message": "前中段插入四个成长节点(+3700 字):从年轻的爆发到中年的沉稳",
"hash": "8675134",
"files_changed": 3
},
{
"ts": "2026-04-18T11:52:53+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 11:52 (~1)",
"hash": "448527f",
"files_changed": 1
},
{
"ts": "2026-04-18T11:58:21+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 11:58 (~1)",
"hash": "790e72c",
"files_changed": 1
},
{
"ts": "2026-04-18T12:03:49+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:03 (~1)",
"hash": "780e9b5",
"files_changed": 1
},
{
"ts": "2026-04-18T12:09:17+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:09 (~1)",
"hash": "3e6f1cf",
"files_changed": 1
},
{
"ts": "2026-04-18T12:13:23+08:00",
"type": "commit",
"message": "人物闭环 + 终章环状收尾 + 危机伏笔强化(+4200 字)",
"hash": "087c004",
"files_changed": 3
},
{
"ts": "2026-04-18T12:14:46+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:14 (~1)",
"hash": "3c8c0e1",
"files_changed": 1
},
{
"ts": "2026-04-18T12:20:50+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:20 (~1)",
"hash": "d6b1d71",
"files_changed": 1
},
{
"ts": "2026-04-18T12:26:19+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:26 (~1)",
"hash": "6d87046",
"files_changed": 1
},
{
"ts": "2026-04-18T12:31:56+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:31 (~2)",
"hash": "1146216",
"files_changed": 2
},
{
"ts": "2026-04-18T12:34:45+08:00",
"type": "commit",
"message": "转向反英雄叙事:阴暗面 + 创伤应激 + 终章留悬念(+5500 字)",
"hash": "5faf84d",
"files_changed": 3
},
{
"ts": "2026-04-18T12:37:24+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:37 (~1)",
"hash": "52f0e25",
"files_changed": 1
},
{
"ts": "2026-04-18T12:42:51+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:42 (~1)",
"hash": "2e6e600",
"files_changed": 1
},
{
"ts": "2026-04-18T12:48:19+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:48 (~1)",
"hash": "e31d06b",
"files_changed": 1
},
{
"ts": "2026-04-18T12:53:58+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:53 (~1)",
"hash": "93823d9",
"files_changed": 1
},
{
"ts": "2026-04-18T12:59:27+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 12:59 (+2, ~1)",
"hash": "7a1c957",
"files_changed": 8
},
{
"ts": "2026-04-18T13:05:15+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:05 (+6, ~3, -6)",
"hash": "7035d0a",
"files_changed": 15
},
{
"ts": "2026-04-18T13:06:08+08:00",
"type": "commit",
"message": "新增 6 张扩集插图Gemini 3 Pro Image+ 整合到展示站",
"hash": "6a4384a",
"files_changed": 1
},
{
"ts": "2026-04-18T13:11:05+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:11 (~1)",
"hash": "1eeca8f",
"files_changed": 1
},
{
"ts": "2026-04-18T13:16:54+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:16 (~1)",
"hash": "96e1db4",
"files_changed": 1
},
{
"ts": "2026-04-18T13:22:43+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:22 (~1)",
"hash": "6801ca4",
"files_changed": 1
},
{
"ts": "2026-04-18T13:28:33+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:28 (~1)",
"hash": "47cc913",
"files_changed": 1
},
{
"ts": "2026-04-18T13:34:11+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:34 (~1)",
"hash": "61b93f5",
"files_changed": 1
},
{
"ts": "2026-04-18T13:39:40+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:39 (~1)",
"hash": "02a6f08",
"files_changed": 1
},
{
"ts": "2026-04-18T13:45:20+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:45 (~1)",
"hash": "7e07746",
"files_changed": 1
},
{
"ts": "2026-04-18T13:50:48+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:50 (~1)",
"hash": "ae704e3",
"files_changed": 1
},
{
"ts": "2026-04-18T13:58:31+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 13:56 (~1)",
"hash": "1c37564",
"files_changed": 1
},
{
"ts": "2026-04-18T14:02:36+08:00",
"type": "commit",
"message": "第二部 · 第一章《另一个看见了的人》(+304 行 / ~3800 字)",
"hash": "9b05fba",
"files_changed": 4
},
{
"ts": "2026-04-18T14:06:31+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 14:04 (~1)",
"hash": "2691af4",
"files_changed": 1
},
{
"ts": "2026-04-18T14:13:18+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 14:13 (~1)",
"hash": "b71ec7e",
"files_changed": 1
},
{
"ts": "2026-04-18T14:17:40+08:00",
"type": "commit",
"message": "第二部 · 第二章《他差一点把那些刺长回来》(+3800 字)",
"hash": "368fcfa",
"files_changed": 3
},
{
"ts": "2026-04-18T14:18:26+08:00",
"type": "commit",
"message": "网页适配第二部多章正则支持第N章中文/阿拉伯数字)+ CHAPTERS 加第二部 Ch2 条目",
"hash": "64b80f1",
"files_changed": 2
},
{
"ts": "2026-04-18T14:19:00+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 14:18 (~1)",
"hash": "6a4857c",
"files_changed": 1
},
{
"ts": "2026-04-18T14:24:52+08:00",
"type": "commit",
"message": "第二部 · 第三章《她说她找不到一个值得攻击的地方》(+3800 字)",
"hash": "c11b61b",
"files_changed": 4
},
{
"ts": "2026-04-18T14:25:11+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 14:25 (~1)",
"hash": "2239b6d",
"files_changed": 1
},
{
"ts": "2026-04-18T14:31:03+08:00",
"type": "commit",
"message": "第二部 · 第四章《那一夜,他关掉了屏幕》(+3800 字)",
"hash": "18670d2",
"files_changed": 4
},
{
"ts": "2026-04-18T14:31:20+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 14:31 (~1)",
"hash": "a0eb6fd",
"files_changed": 1
},
{
"ts": "2026-04-18T14:37:45+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 14:37 (~1)",
"hash": "1513e37",
"files_changed": 1
},
{
"ts": "2026-04-18T14:43:32+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 14:43 (~1)",
"hash": "cb74d3e",
"files_changed": 1
},
{
"ts": "2026-04-18T14:49:20+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 14:49 (~1)",
"hash": "d7378a0",
"files_changed": 1
},
{
"ts": "2026-04-18T14:55:41+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 14:54 (~1)",
"hash": "b23feae",
"files_changed": 1
},
{
"ts": "2026-04-18T15:02:58+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 15:02 (~3)",
"hash": "e1c603f",
"files_changed": 3
},
{
"ts": "2026-04-18T15:03:08+08:00",
"type": "commit",
"message": "第二部 · 第五章《让光回到所有看见的人身上》(+4500 字)",
"hash": "1f3126d",
"files_changed": 2
},
{
"ts": "2026-04-18T15:09:00+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 15:08 (~1)",
"hash": "47b480b",
"files_changed": 1
},
{
"ts": "2026-04-18T15:10:53+08:00",
"type": "commit",
"message": "第二部 · 第六章《那封邮件的作者》(+3800 字)",
"hash": "1d3034b",
"files_changed": 4
},
{
"ts": "2026-04-18T15:14:50+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 15:14 (~2)",
"hash": "cf4033e",
"files_changed": 2
},
{
"ts": "2026-04-18T15:17:20+08:00",
"type": "commit",
"message": "第二部 · 第七章《她记得的那碗面》(+3800 字)",
"hash": "fe9f24b",
"files_changed": 4
},
{
"ts": "2026-04-18T15:21:04+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 15:20 (~1)",
"hash": "c053bfc",
"files_changed": 1
},
{
"ts": "2026-04-18T15:23:25+08:00",
"type": "commit",
"message": "第二部 · 第八章《接下来这二十年,换我来写》(+3500 字) —— 全书·完",
"hash": "d78383e",
"files_changed": 4
},
{
"ts": "2026-04-18T15:28:26+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 15:26 (~1)",
"hash": "063675e",
"files_changed": 1
},
{
"ts": "2026-04-18T15:34:38+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 15:34 (~1)",
"hash": "c3cacbb",
"files_changed": 1
},
{
"ts": "2026-04-18T15:40:38+08:00",
"type": "commit",
"message": "worklog.json auto-save tail",
"hash": "3e10890",
"files_changed": 1
},
{
"ts": "2026-04-18T15:42:03+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 15:40 (~1)",
"hash": "d46152c",
"files_changed": 1
},
{
"ts": "2026-04-18T15:48:56+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 15:48 (~2)",
"hash": "15b2812",
"files_changed": 2
},
{
"ts": "2026-04-18T15:50:40+08:00",
"type": "commit",
"message": "修阅读'一直载入中'novel.md gzip + 章节索引 + loading 状态",
"hash": "33c60aa",
"files_changed": 3
},
{
"ts": "2026-04-18T15:55:26+08:00",
"type": "commit",
"message": "auto-save 2026-04-18 15:54 (~1)",
"hash": "481b47b",
"files_changed": 1
}
]
}

File diff suppressed because it is too large Load Diff

183
scripts/gen-images.py Normal file
View File

@@ -0,0 +1,183 @@
"""
Gen 6 new illustrations via OpenRouter + google/gemini-3-pro-image-preview.
Protagonist reference image + shared style anchor baked into every prompt.
"""
import base64
import json
import sys
import time
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
import urllib.request
API_KEY = "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"
ENDPOINT = "https://openrouter.ai/api/v1/chat/completions"
MODEL = "google/gemini-3-pro-image-preview"
PROXY = "http://127.0.0.1:10808"
ROOT = Path(__file__).resolve().parent.parent
REF_IMG = ROOT / "web" / "images" / "protagonist_reference.jpg"
OUT_DIR = ROOT / "web" / "images" / "extra"
OUT_DIR.mkdir(parents=True, exist_ok=True)
# Shared style anchor — extracted from docs/illustration_style_guide.md
STYLE_ANCHOR = """
Cinematic near-future urban realism. Muted, cold blue-grey palette. 35mm film grain. Subtle cold screen glow where screens appear. Photographic, NOT cartoon, NOT anime. Realistic human proportions. Emotion conveyed through posture, gaze and environment, NOT through exaggerated facial expression. 16:9 cinematic framing.
""".strip()
# Character anchor — must be repeated every time to keep consistency
CHAR_ANCHOR = """
The man in the attached reference image is GU CHENZHOU. Keep his identity consistent: black short hair (slightly messy in earlier life, neater later), deep-set brows, quiet reserved demeanor, slim build.
""".strip()
SCENES = [
{
"id": "ch06_panic_night",
"age_hint": "around 25 years old, haggard, unshaven, clothes wrinkled",
"scene": "A dim cramped rental room at night. GU CHENZHOU is curled up against the wall in a corner, knees to chest, hands covering his ears. A single thin beam of moonlight crosses the ceiling. On the small bedside table there is only a glass of cold water. The room is otherwise empty and in half-darkness. No other characters. His face is not crying — his eyes are open and fixed on nothing. The whole composition conveys a panic attack that no one sees.",
"mood": "Cold blue-grey. Shadow occupies 70% of the frame. Silent. Oppressive ceiling. PTSD origin point."
},
{
"id": "ch10_cafe_read_alone",
"age_hint": "around 40 years old, clean-shaven but visibly exhausted, a thin shaven old scar on the suit cuff edge",
"scene": "A small 24-hour cafe, very late afternoon cold grey light through the window. GU CHENZHOU sits alone at a two-person table by the window, wearing a plain dark grey old suit that has clearly been ironed carefully but whose cuffs have been slightly frayed. A plain black coffee cup sits in front of him. On the table, 30 pages of handwritten typed speech manuscript are spread open. His right index finger rests on the first line of the first page. His head is slightly tilted down. He is reading the manuscript to himself, alone. The background is out of focus — empty cafe, a couple of empty chairs. The mood is not sad, not angry, only quietly ceremonial.",
"mood": "Cool muted light. Solitary. Ceremonial quietness. Middle-aged composure."
},
{
"id": "ch14_linwanqiao_window",
"age_hint": "NOT the protagonist — focus on a different character, a composed woman in her mid-30s named LIN WANQIAO",
"scene": "Floor-to-ceiling window of the 31st floor of a city office tower, late morning under heavy overcast sky. A composed elegant Chinese woman in her mid-30s, wavy shoulder-length hair, beige trench coat, pale gold earrings, stands with her back turned to the viewer, facing the window. In her hand she holds a cup of coffee, faint steam rising. On a wall-mounted flat TV to her right, the screen shows ONLY a silent footage: a dim gray narrow city alley, an unmarked black van parked at the alley entrance, a thin-shouldered Chinese man in a plain worn dark coat walking past the van carrying a plain black canvas shoulder bag. The TV screen shows NO NEWS BANNER, NO ENGLISH TEXT, NO HEADLINE, NO SUBTITLE — the entire screen is just the alley footage with cinematic film grain. The open-plan office behind her is chaotic — out-of-focus colleagues crowded around a projector in a distant glass-walled meeting room. She herself stands still, not moving. Camera framed from slightly behind her shoulder. Her face cannot be seen clearly.",
"mood": "Cold blue-white. The city in the background is in disarray, stillness in the foreground. A look across time, regret without tears. ABSOLUTELY NO TEXT ANYWHERE ON THE TV OR IN THE SCENE."
},
{
"id": "ch16_victory_night_alone",
"age_hint": "around 50 years old, eyes bloodshot, face exhausted",
"scene": "A temporary small emergency centre rest room at night, door locked. GU CHENZHOU sits on the floor with his back against the door. Next to him on the floor is a premium glass liquor bottle with its neck snapped off — glass shards around it. His palm has a thin red cut but he does not tend to it. A small framed mirror on the opposite wall shows his own reflection — bloodshot eyes, dry eyelids, stubble. He is not crying. His hand rests on his forehead. There is no one else in the room. Cold fluorescent ceiling light. Outside the door (implied faintly through sound lines, not visible) people are celebrating — but he is silent. Ultra quiet composition.",
"mood": "The emptiness of victory. Cold overhead light. More unsettling than any defeat scene."
},
{
"id": "ring_classroom_10yrs_later",
"age_hint": "NOT the protagonist — a new young professor in his early 40s, different face, carrying GU CHENZHOU's legacy",
"scene": "A university lecture hall for freshmen, first class of the semester, bright practical fluorescent light, about 100 first-year students visible at the back of frame, heads slightly turned up. At the front a young professor in his early 40s, thin, intense, wearing a simple dark sweater. He has set an old worn canvas bag on the lecturer's desk — the side of the bag reads the faded handwriting 'GUIXU · 2038'. On the large blackboard behind him he has just finished writing three Chinese characters in chalk: '写 代 码' (very prominent, centered, large). He stands still, chalk in hand, turning to face the students. An old worn hand-bound notebook lies open on the desk in front of him — the first page visible. Mood: warm respect, a new generation inheriting.",
"mood": "Warm but restrained light. A ring closes — echoing back to Ch 1 graduation recruiting banner but inverted meaning."
},
{
"id": "s2_blank_second_page",
"age_hint": "around 64 years old — NOT older, NOT 70+. Hair is salt-and-pepper with roughly equal black and grey strands, still thick and neatly combed, NOT bald, NOT thinning on top. Face remains the same person as in the reference image — just aged by about 20 years. Slim but upright posture. Still composed and sharp.",
"scene": "A clean simple home study by a wide river window at dusk. Warm golden horizontal sunset light cutting in across a worn wooden desk. GU CHENZHOU, same face as the reference image but about 64 years old, hair still thick but now half black half grey, sits alone at the desk. On the desk, in the foreground: an old worn canvas shoulder bag with the faded handwritten text 'GUIXU · 2038' on its side — the bag lies open. A very old small hand-bound notebook lies open on the desk — the first page shows one line of small hand-written Chinese characters; the second page is completely blank. His hand with slightly prominent veins rests lightly on the blank second page. His eyes are fixed on the empty page, a silent expression of doubt. To his right, a slim modern laptop sits — its screen glowing faintly — on the laptop screen is a nearly-empty minimal text editor, and at the top of the editor three large Chinese characters are clearly readable: '我 怕 的——' followed by a blinking cursor. The laptop screen is positioned and framed so the viewer can clearly read those three Chinese characters. No other person in the room. Absolute stillness.",
"mood": "Quiet, ceremonial, doubt. An old hero facing a second choice. Open ending. Warm golden light against the cold blue river outside."
},
]
def build_prompt(scene: dict) -> str:
return "\n\n".join([
"GENERATE ONE cinematic illustration.",
f"STYLE: {STYLE_ANCHOR}",
f"CHARACTER: {CHAR_ANCHOR}",
f"AGE/APPEARANCE: {scene['age_hint']}",
f"SCENE: {scene['scene']}",
f"MOOD: {scene['mood']}",
"IMPORTANT: no visible text overlays beyond what is described in SCENE. No watermarks. Output a single full 16:9 image only."
])
def encode_ref_image() -> str:
with open(REF_IMG, "rb") as f:
return "data:image/jpeg;base64," + base64.b64encode(f.read()).decode()
def call_api(scene: dict, ref_data_url: str) -> bytes:
body = {
"model": MODEL,
"modalities": ["image", "text"],
"messages": [
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": ref_data_url}},
{"type": "text", "text": build_prompt(scene)}
]
}
]
}
req = urllib.request.Request(
ENDPOINT,
data=json.dumps(body).encode(),
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"HTTP-Referer": "https://gufa-code.kang-kang.com",
"X-Title": "Gufa Code King Illustrations",
},
method="POST"
)
proxy_handler = urllib.request.ProxyHandler({"http": PROXY, "https": PROXY})
opener = urllib.request.build_opener(proxy_handler)
with opener.open(req, timeout=180) as resp:
data = json.loads(resp.read())
if "error" in data:
raise RuntimeError(f"{scene['id']}: {data['error']}")
msg = data["choices"][0]["message"]
imgs = msg.get("images", [])
if not imgs:
raise RuntimeError(f"{scene['id']}: no images in response — msg keys {list(msg.keys())}")
url = imgs[0]["image_url"]["url"]
if not url.startswith("data:image"):
raise RuntimeError(f"{scene['id']}: unexpected url format")
b64 = url.split(",", 1)[1]
cost = data.get("usage", {}).get("cost", 0)
print(f"{scene['id']} cost=${cost:.4f} size={len(b64) // 1024} KB b64", flush=True)
return base64.b64decode(b64)
def generate_one(scene: dict, ref_data_url: str) -> tuple[str, Path | None, str | None]:
t0 = time.time()
try:
png = call_api(scene, ref_data_url)
out = OUT_DIR / f"{scene['id']}.png"
out.write_bytes(png)
dt = time.time() - t0
print(f" saved {out.name} ({dt:.1f}s, {len(png) // 1024}KB)", flush=True)
return scene["id"], out, None
except Exception as e:
dt = time.time() - t0
print(f"{scene['id']} FAILED after {dt:.1f}s: {e}", flush=True)
return scene["id"], None, str(e)
def main():
only = sys.argv[1:] if len(sys.argv) > 1 else None
scenes = [s for s in SCENES if not only or s["id"] in only]
print(f"=== gen {len(scenes)} images ===")
if only:
print(f"filter: {only}")
print(f"ref image: {REF_IMG} ({REF_IMG.stat().st_size // 1024}KB)")
ref_url = encode_ref_image()
print(f"ref base64 len: {len(ref_url) // 1024}KB")
print(f"out dir: {OUT_DIR}")
print()
print("scenes:")
for s in scenes:
print(f" - {s['id']}")
print()
results = []
with ThreadPoolExecutor(max_workers=3) as pool:
futures = [pool.submit(generate_one, s, ref_url) for s in scenes]
for f in as_completed(futures):
results.append(f.result())
print()
print("=== summary ===")
ok = [r for r in results if r[1]]
fail = [r for r in results if not r[1]]
print(f" ok: {len(ok)} fail: {len(fail)}")
if fail:
for sid, _, err in fail:
print(f" fail {sid}: {err}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -16,7 +16,13 @@ RUN printf 'server {\n\
access_log off;\n\
}\n\
\n\
# html / md: always revalidate\n\
# novel.md specifically — force text/markdown so gzip triggers\n\
location = /novel.md {\n\
default_type "text/markdown; charset=utf-8";\n\
add_header Cache-Control "public, max-age=0, must-revalidate";\n\
}\n\
\n\
# html / md catch-all: always revalidate\n\
location ~* \\.(html|md)$ {\n\
add_header Cache-Control "public, max-age=0, must-revalidate";\n\
}\n\
@@ -26,8 +32,10 @@ RUN printf 'server {\n\
}\n\
\n\
gzip on;\n\
gzip_types text/plain text/markdown text/css application/javascript application/json image/svg+xml;\n\
gzip_min_length 1024;\n\
gzip_comp_level 6;\n\
gzip_types text/plain text/markdown text/css application/javascript application/json image/svg+xml text/html;\n\
gzip_min_length 512;\n\
gzip_vary on;\n\
}\n' > /etc/nginx/conf.d/default.conf
EXPOSE 80

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 546 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

View File

@@ -449,6 +449,106 @@ section {
letter-spacing: 0.2em;
}
/* ---------- Part II separator ---------- */
.reader-chapter-part2 {
margin-top: 200px !important;
padding-top: 120px;
border-top: 1px solid var(--gold-soft);
position: relative;
}
.reader-chapter-part2::before {
content: 'PART · II';
position: absolute;
top: 48px;
left: 50%;
transform: translateX(-50%);
color: var(--gold);
font-family: 'Songti SC', serif;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.6em;
padding: 6px 24px;
background: var(--bg);
border: 1px solid var(--gold-soft);
}
.reader-chapter-part2 .reader-chapter-num {
color: var(--gold);
}
/* ---------- Reader extras (章末延伸图) ---------- */
.reader-extras {
margin: 64px 0 40px;
padding: 32px 0 0;
border-top: 1px solid var(--border);
}
.reader-extras-label {
color: var(--gold);
font-size: 11px;
letter-spacing: 0.4em;
text-transform: uppercase;
margin-bottom: 24px;
}
.reader-extras-grid {
display: grid;
grid-template-columns: 1fr;
gap: 28px;
}
.reader-extra {
margin: 0;
cursor: zoom-in;
transition: transform 0.3s ease;
}
.reader-extra:hover { transform: translateY(-2px); }
.reader-extra img {
width: 100%;
aspect-ratio: 16/10;
object-fit: cover;
background: var(--bg-card);
border: 1px solid var(--border);
}
.reader-extra figcaption {
padding: 16px 0 0;
}
.reader-extra-cap {
font-family: 'Songti SC', serif;
font-size: 19px;
font-weight: 600;
color: var(--fg);
margin-bottom: 4px;
}
.reader-extra-sub {
color: var(--gold);
font-size: 11px;
letter-spacing: 0.3em;
text-transform: uppercase;
}
/* ---------- Gallery extras divider ---------- */
.gallery-extras-heading {
display: flex;
align-items: center;
gap: 20px;
margin: 80px 0 32px;
color: var(--gold);
font-family: 'Songti SC', serif;
font-size: 22px;
font-weight: 700;
}
.gallery-extras-heading::before, .gallery-extras-heading::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(90deg, transparent, var(--gold-soft), transparent);
}
.gallery-extras-sub {
text-align: center;
color: var(--fg-dim);
font-size: 12px;
letter-spacing: 0.3em;
margin: -16px 0 40px;
text-transform: uppercase;
}
/* ---------- Footer ---------- */
footer {
padding: 64px 24px 40px;
@@ -675,6 +775,10 @@ footer p { margin: 8px 0; }
<h2 class="section-title">命运的二十二帧。</h2>
<p style="color: var(--fg-soft); max-width: 620px; margin: -24px 0 40px;">从毕业即过时,到代码之王——画面随男主心境推进:冷灰蓝 → 工业寒夜 → 红色告警 → 深黑金巅峰。点击任一幅放大查看。</p>
<div class="gallery-grid" id="gallery-grid"></div>
<div class="gallery-extras-heading">扩集画面 · EXTRA</div>
<div class="gallery-extras-sub">扩写之后新生的关键时刻 · 6 幅</div>
<div class="gallery-grid" id="gallery-extras"></div>
</div>
</section>
@@ -696,22 +800,46 @@ const CHAPTERS = [
{ n: 3, title: '她说你这样没有未来', img: 'ch03_you_have_no_future.jpg', volume: '第一卷 · 被时代埋掉的人', hint: '贫穷与理想,裂痕第一道' },
{ n: 4, title: '全世界都在笑他', img: 'ch04_the_world_laughed_at_him.jpg', volume: '第一卷 · 被时代埋掉的人', hint: '韩锐风光无限,他在出租屋里修服务器' },
{ n: 5, title: '被裁员的人没有资格谈梦想', img: 'ch05_laid_off_no_dreams.jpg', volume: '第一卷 · 被时代埋掉的人', hint: '第一份工作失去,跌入谷底' },
{ n: 6, title: '旧电脑与冷泡面', img: 'ch06_old_computer_cold_noodles.jpg', volume: '第二卷 · 寒冬里独自敲键盘', hint: '低端外包,熬夜,凄惨地活着' },
{ n: 6, title: '旧电脑与冷泡面', img: 'ch06_old_computer_cold_noodles.jpg', volume: '第二卷 · 寒冬里独自敲键盘', hint: '低端外包,熬夜,凄惨地活着',
extras: [
{ file: 'extra/ch06_panic_night.jpg', cap: '第三天夜里 · 被自己的恐惧按在地上', sub: 'PTSD 原点' }
] },
{ n: 7, title: '只有她递来一把伞', img: 'ch07_she_brought_an_umbrella.jpg', volume: '第二卷 · 寒冬里独自敲键盘', hint: '苏青禾登场,一点温柔的光' },
{ n: 8, title: '爱也会输给房租', img: 'ch08_love_lost_to_rent.jpg', volume: '第二卷 · 寒冬里独自敲键盘', hint: '林晚乔正式离开,情感谷底' },
{ n: 9, title: '没人相信的底层能力', img: 'ch09_no_one_believed_him.jpg', volume: '第二卷 · 寒冬里独自敲键盘', hint: '想推销离线开发,被当笑话' },
{ n: 10, title: '十年一梦,满身风雪', img: 'ch10_ten_years_in_snow.jpg', volume: '第二卷 · 寒冬里独自敲键盘', hint: '在行业边缘漂泊多年' },
{ n: 10, title: '十年一梦,满身风雪', img: 'ch10_ten_years_in_snow.jpg', volume: '第二卷 · 寒冬里独自敲键盘', hint: '在行业边缘漂泊多年',
extras: [
{ file: 'extra/ch10_cafe_read_alone.jpg', cap: '他给自己读完一场没有听众的演讲', sub: '40 岁 · 平静庄严' }
] },
{ n: 11, title: '聪明人都不会手写代码了', img: 'ch11_no_one_writes_code_anymore.jpg', volume: '第三卷 · 黑箱时代的裂缝', hint: '传统工程师彻底绝迹' },
{ n: 12, title: '第一次异常', img: 'ch12_first_anomaly.jpg', volume: '第三卷 · 黑箱时代的裂缝', hint: '核心系统零星故障,被当波动' },
{ n: 13, title: '无人能读懂的补丁', img: 'ch13_unreadable_patch.jpg', volume: '第三卷 · 黑箱时代的裂缝', hint: '大平台自修复越修越乱' },
{ n: 14, title: '世界停电的那一天', img: 'ch14_the_day_the_world_went_dark.jpg',volume: '第三卷 · 黑箱时代的裂缝', hint: '金融交通医疗能源连锁崩塌' },
{ n: 14, title: '世界停电的那一天', img: 'ch14_the_day_the_world_went_dark.jpg',volume: '第三卷 · 黑箱时代的裂缝', hint: '金融交通医疗能源连锁崩塌',
extras: [
{ file: 'extra/ch14_linwanqiao_window.jpg', cap: '她认出了那只黑色帆布包', sub: '旧情视角 · 一面落地窗' }
] },
{ n: 15, title: '求他出山的人排到了楼下', img: 'ch15_people_queued_to_beg_him.jpg', volume: '第三卷 · 黑箱时代的裂缝', hint: '昔日嘲笑他的人开始低头' },
{ n: 16, title: '一人重启一座城', img: 'ch16_one_man_restart_a_city.jpg', volume: '第四卷 · 旧时代火种', hint: '凭古法工程修复核心调度' },
{ n: 16, title: '一人重启一座城', img: 'ch16_one_man_restart_a_city.jpg', volume: '第四卷 · 旧时代火种', hint: '凭古法工程修复核心调度',
extras: [
{ file: 'extra/ch16_victory_night_alone.jpg', cap: '庆功夜 · 锁门独饮', sub: '成功的空旷 · 那接下来呢' }
] },
{ n: 17, title: '财团、公权与资本都在抢他', img: 'ch17_everyone_is_fighting_for_him.jpg',volume: '第四卷 · 旧时代火种', hint: '沈知意、国家机构、巨头同时伸手' },
{ n: 18, title: '她们都在等他一句话', img: 'ch18_they_wait_for_his_answer.jpg', volume: '第四卷 · 旧时代火种', hint: '情感线全面升温' },
{ n: 19, title: '代码之王', img: 'ch19_king_of_code.jpg', volume: '第四卷 · 旧时代火种', hint: '组建离线工程联盟,重塑秩序' },
{ n: 20, title: '坐拥繁花,归来仍是少年', img: 'ch20_among_flowers_still_young.jpg', volume: '第四卷 · 旧时代火种', hint: '站上巅峰,事业情感双圆满' },
{ n: 0, title: '终章 · 写代码的人,重新定义世界', img: 'epilogue_redefine_the_world.jpg', volume: '终章', hint: 'AI 重新成为工具而非拐杖' },
{ n: 0, title: '终章 · 写代码的人,重新定义世界', img: 'epilogue_redefine_the_world.jpg', volume: '终章', hint: 'AI 重新成为工具而非拐杖',
extras: [
{ file: 'extra/ring_classroom_10yrs_later.jpg', cap: '十年后 · 新一代走进教室', sub: '环状收尾 · 黑板三字:写 代 码' },
{ file: 'extra/s2_blank_second_page.jpg', cap: '我 怕 的——', sub: '第二部 · 序章 · 留白' }
] },
{ n: -1, title: '另一个看见了的人', img: 'extra/s2_blank_second_page.jpg', volume: '第二部 · 起笔', hint: '沈陌登门:顾先生,我写到这里,就等您告诉我,下面该不该写下去' },
{ n: -2, title: '他差一点把那些刺长回来', img: null, volume: '第二部 · 起笔', hint: '三天读尽白皮书,差一点对一个系统动了信任测试的念头;出门去见了韩锐' },
{ n: -3, title: '她说她找不到一个值得攻击的地方', img: null, volume: '第二部 · 起笔', hint: '许幼宁 86 次攻防 0 成功。她揭穿他三年前埋的第三层签名偏差——"我想看你什么时候自己能看到它不再需要"' },
{ n: -4, title: '那一夜,他关掉了屏幕', img: null, volume: '第二部 · 起笔', hint: '审查会前夜:读完沈陌整本笔记 + 想起二十三年前老总监那封被遗忘的邮件"替我看住这套东西" + 关掉屏幕不写答案直接去睡 + 苏青禾那句"我信你"' },
{ n: -5, title: '让光回到所有看见的人身上', img: null, volume: '第二部 · 起笔', hint: '审查会现场:帆布包里三本东西摆上评议桌。有条件通过。请许幼宁+沈陌上台站到他两侧。请联盟找回那位说"让他自己醒醒"的安全侧负责人旁听。走出大厅下起春雨,他发给苏青禾三个字"我回家"' },
{ n: -6, title: '那封邮件的作者', img: null, volume: '第二部 · 起笔', hint: '老总监周以谦被找到:潮屿海边阳台,"那封邮件我后来很多年都后悔发过。那不是上级该对下级说的话。那是一个认输的人在找替身"。拒绝上台:不是所有被按下去的人都需要被带回舞台。你那根刺这次我看见你真的开始把它收起来了' },
{ n: -7, title: '她记得的那碗面', img: null, volume: '第二部 · 起笔', hint: '北方县城养老院:母亲轻度阿尔茨海默"你是修理工吗?我家那台老电视机最近总是雪花"。他不纠正她。她用讲给陌生人的语气讲他小时候。临别她清了一瞬"阿舟,你最近辛苦了吧"。他去县城街角小餐馆吃葱花挂面——这辈子第一次为自己流眼泪' },
{ n: -8, title: '接下来这二十年,换我来写', img: null, volume: '第二部 · 收束', hint: '七年流速切片v0.7 全球落地 / 周以谦病了看够了 / 沈陌女儿入学挑《操作系统基础 I》/ 母亲走了他在老屋自己煮一碗葱花面 / 城中村被拆成空地他没进去 / 沈陌接替他坐上评议席 / 71 岁出书《那三个夜晚和之后的事》扉页三个名字 / 沈陌新笔记第一页"接下来这二十年,换我来写" / 阳台摇椅"反正我也在" —— 全书·完' },
];
function renderChapterList() {
@@ -722,7 +850,10 @@ function renderChapterList() {
html += `<div class="volume-heading">${ch.volume}</div>`;
lastVolume = ch.volume;
}
const num = ch.n === 0 ? '终' : ch.n.toString().padStart(2, '0');
let num;
if (ch.n === 0) num = '终';
else if (ch.n < 0) num = 'II·' + String(-ch.n).padStart(2, '0');
else num = ch.n.toString().padStart(2, '0');
const title = ch.n === 0 ? ch.title.replace('终章 · ', '') : ch.title;
html += `
<div class="chapter-row" onclick="jumpToChapter(${idx})">
@@ -751,6 +882,27 @@ function renderGallery() {
</div>
</div>`;
}).join('');
// Extras gallery — new scenes born from expansion
const extrasWrap = document.getElementById('gallery-extras');
if (extrasWrap) {
const extraItems = [];
CHAPTERS.forEach(ch => {
if (!ch.extras) return;
const num = ch.n === 0 ? '终章' : `${ch.n}`;
ch.extras.forEach(ex => {
extraItems.push({ ...ex, num, chTitle: ch.title });
});
});
extrasWrap.innerHTML = extraItems.map(ex => `
<div class="gallery-item" onclick="openLightbox('./images/${ex.file}')">
<img src="./images/${ex.file}" alt="${ex.cap}" loading="lazy">
<div class="gallery-meta">
<div class="gallery-cap">${ex.cap}</div>
<div class="gallery-sub">${ex.num} · ${ex.sub}</div>
</div>
</div>`).join('');
}
}
function jumpToChapter(idx) {
@@ -770,13 +922,21 @@ function closeLightbox() {
}
async function loadNovel() {
const container = document.getElementById('reader-content');
try {
const resp = await fetch('./novel.md');
container.innerHTML = '<div class="loading">正在下载全本 …</div>';
const resp = await fetch('./novel.md', { cache: 'no-cache' });
if (!resp.ok) throw new Error('HTTP ' + resp.status);
container.innerHTML = '<div class="loading">正在渲染章节 …</div>';
const text = await resp.text();
// Yield to the browser so the loading message paints before heavy work
await new Promise(r => setTimeout(r, 0));
renderNovel(text);
} catch (e) {
document.getElementById('reader-content').innerHTML =
'<div class="loading" style="color: var(--red)">小说载入失败:' + e.message + '</div>';
console.error('loadNovel failed:', e);
container.innerHTML =
'<div class="loading" style="color: var(--red)">小说载入失败:' + e.message +
'。<a href="./novel.md" style="color: var(--gold); text-decoration: underline;">直接打开 novel.md</a></div>';
}
}
@@ -788,14 +948,32 @@ function renderNovel(md) {
const lines = sec.split('\n');
const heading = lines[0].trim();
const body = lines.slice(1).join('\n').trim();
// match "第1章 标题" or "终章 标题"
const m = heading.match(/^(第(\d+)章|终章)\s+(.+)$/);
if (!m) return;
const isEpilogue = !m[2];
const num = isEpilogue ? '终' : m[2].padStart(2, '0');
const title = m[3];
const ch = CHAPTERS[idx];
const imgPath = ch ? `./images/${ch.img}` : '';
// match "第1章 标题" / "终章 标题" / "第二部 · 第一章 · 标题"
let m = heading.match(/^(第(\d+)章|终章)\s+(.+)$/);
let isPart2 = false;
let num, title;
const cnMap = { '一': '01', '二': '02', '三': '03', '四': '04', '五': '05', '六': '06', '七': '07', '八': '08', '九': '09', '十': '10' };
const cnToInt = { '一':1,'二':2,'三':3,'四':4,'五':5,'六':6,'七':7,'八':8,'九':9,'十':10 };
let kicker, chN;
if (m) {
const isEpilogue = !m[2];
num = isEpilogue ? '终' : m[2].padStart(2, '0');
title = m[3];
kicker = isEpilogue ? 'EPILOGUE' : 'CHAPTER ' + num;
chN = isEpilogue ? 0 : parseInt(m[2]);
} else {
const m2 = heading.match(/^第二部\s*·\s*第(.+?)章\s*·\s*(.+)$/);
if (!m2) return;
isPart2 = true;
const arabic = cnMap[m2[1]] || m2[1];
num = 'II · ' + arabic;
title = m2[2];
kicker = 'PART II · CHAPTER ' + arabic;
chN = -(cnToInt[m2[1]] || parseInt(m2[1])); // -1 = Part II Ch1, -2 = Part II Ch2, ...
}
// Match chapter by semantic n (immune to section index drift caused by non-chapter ## headings)
const ch = CHAPTERS.find(c => c.n === chN);
const imgPath = (ch && ch.img) ? `./images/${ch.img}` : '';
const paragraphs = body.split(/\n\s*\n/).map(p => {
const line = p.trim();
if (!line) return '';
@@ -804,12 +982,27 @@ function renderNovel(md) {
}
return '<p>' + escapeHtml(line) + '</p>';
}).join('\n');
const extrasHtml = (ch && ch.extras) ? `
<div class="reader-extras">
<div class="reader-extras-label">章末延伸画面 · Extra</div>
<div class="reader-extras-grid">
${ch.extras.map(ex => `
<figure class="reader-extra" onclick="openLightbox('./images/${ex.file}')">
<img src="./images/${ex.file}" alt="${ex.cap}" loading="lazy">
<figcaption>
<div class="reader-extra-cap">${ex.cap}</div>
<div class="reader-extra-sub">${ex.sub}</div>
</figcaption>
</figure>`).join('')}
</div>
</div>` : '';
out.push(`
<article class="reader-chapter" id="chapter-${idx}">
<div class="reader-chapter-num">${isEpilogue ? 'EPILOGUE' : 'CHAPTER ' + num}</div>
<article class="reader-chapter ${isPart2 ? 'reader-chapter-part2' : ''}" id="chapter-${idx}">
<div class="reader-chapter-num">${kicker}</div>
<h2 class="reader-chapter-title">${title}</h2>
${imgPath ? `<img class="reader-chapter-img" src="${imgPath}" alt="${title}" loading="lazy" onclick="openLightbox('${imgPath}')" style="cursor: zoom-in;">` : ''}
<div class="reader-chapter-body">${paragraphs}</div>
${extrasHtml}
${idx < sections.length - 1 ? '<div class="chapter-divider">· · ·</div>' : ''}
</article>
`);

File diff suppressed because it is too large Load Diff