包含关键字 typecho 的文章

经过我不断的尝试,发现还是直接抄 Claude Code 的 vs code 拓展的界面比较好看


📌 转载信息
转载时间:
2026/1/22 13:12:06

项目介绍

GoogleManager 是一款专业的谷歌账号资产管理系统,适合需要管理多个谷歌账号的用户。

功能特性

  • 账号管理 - 批量导入、单个导入、智能搜索、状态筛选
  • 2FA 验证码 - 一键生成 TOTP 验证码,实时倒计时
  • 快捷复制 - 一键复制完整账号信息
  • 出售状态 - 标记已售 / 未售,二次确认机制
  • 修改历史 - 记录密码、2FA、恢复邮箱等修改记录
  • 安全特性 - 管理员密码保护、7 天登录有效期、IP 封禁机制
  • 暗色 / 亮色模式 - 支持主题切换

技术栈

前端后端
React 18Flask
TailwindCSSSQLite
ViteSQLAlchemy



仓库链接:

快速开始

# 安装依赖
pip install -r requirements.txt
cd frontend && npm install

# 构建并启动
npm run build && cd ..
python run.py

📌 转载信息
原作者:
Hack_Z
转载时间:
2026/1/22 13:11:02

正在构造多对一替换词库,让 suno 能唱一切中文歌,过程中发现了一个有趣的拼音 “zhei” 继续讨论:
小工具封面标题也算话题封面:


引用的话题目的是构建一个替换词库,让 AI 在写歌词的时候 额外给出一个版本 根据自己写的歌词的每一个字的拼音将他们替换为我整理出来的更加可行的字…
但是

让 AI 自己判断,你放心吗?

经过几个小时的耕耘
三天的努力
最终确定了一个基本可行的替换词库
部分字我已经通过官网提供的免费 4.5 模型进行了验证可行~

但是,必须强调 似乎 如果 suno 发现前几行是中文,他不会好好唱的。

a → 啊
ai → 爱
an → 安
ang → 昂
ao → 熬
ba → 八
bai → 白
ban → 班
bang → 帮
bao → 包
bei → 杯
ben → 本
beng → 崩
bi → 比
bian → 边
biao → 标
bie → 别
bin → 冰
bing → 冰
bo → 波
bu → 不
ca → 擦
cai → 才
can → 产
cang → 仓
cao → 草
ce → 测
cen → 成
ceng → 成
cha → 查
chai → 才
chan → 产
chang → 尝
chao → 超
che → 车
chen → 晨
cheng → 城
chi → 吃
chong → 冲
chou → 抽
chu → 出
chuan → 传
chuang → 窗
chui → 吹
chun → 春
chuo → 错
ci → 词
cong → 从
cou → 抽
cu → 粗
cuan → 传
cui → 催
cun → 村
cuo → 错
da → 大
dai → 带
dan → 单
dang → 当
dao → 刀
de → 的
deng → 灯
di → 滴
dian → 点
diao → 掉
die → 蝶
ding → 丁
diu → 丢
dong → 东
dou → 都
du → 读
duan → 段
dui → 对
dun → 盾
duo → 多
e → 饿
ei → 诶(该拼音也不常用,建议禁用)
en → 恩
er → 儿
fa → 发
fan → 凡
fang → 方
fei → 飞
fen → 分
feng → 风
fo → 佛(禁用音)
fou → 否(禁用)
fu → 负
ga → 嘎(禁用音)
gai → 该
gan → 干
gang → 刚
gao → 高
ge → 哥
gei → 给
gen → 跟
geng → 更
gong → 工
gou → 勾
gu → 姑
gua → 瓜
guai → 乖
guan → 关
guang → 光
gui → 归
gun → 棍
guo → 国
ha → 哈
hai → 海
han → 汉
hang → 行
hao → 好
he → 和
hei → 黑
hen → 很
heng → 横
hong → 红
hou → 后
hu → 胡
hua → 花
huai → 怀
huan → 欢
huang → 黄
hui → 回
hun → 昏
huo → 火
ji → 几
jia → 家
jian → 间
jiang → 江
jiao → 交
jie → 接
jin → 金
jing → 京
jiong → 囧(禁用)
jiu → 九
ju → 居
juan → 卷
jue → 决
jun → 军
ka → 卡
kai → 开
kan → 看
kang → 抗
kao → 考
ke → 可
ken → 肯
keng → 坑
kong → 空
kou → 口
ku → 哭
kua → 夸
kuai → 快
kuan → 宽
kuang → 狂
kui → 亏
kun → 困
kuo → 扩
la → 拉
lai → 来
lan → 兰
lang → 狼
lao → 老
le → 乐
lei → 雷
leng → 冷
li → 力
lian → 连
liang → 良
liao → 疗
lie → 列
lin → 林
ling → 零
liu → 流
long → 龙
lou → 楼
lu → 路
luan → 乱
lun → 轮
luo → 罗
lü → 驴
lüe → 略
ma → 妈
mai → 买
man → 满
mang → 忙
mao → 毛
mei → 没
men → 门
meng → 蒙
mi → 米
mian → 面
miao → 苗
mie → 灭
min → 民
ming → 明
miu → 缪(禁用)
mo → 莫
mou → 谋
mu → 木
na → 那
nai → 奶
nan → 男
nang → 囊(禁用)
nao → 脑
ne → 呢
nei → 内
nen → 嫩(禁用)
neng → 能
ni → 你
nian → 年
niang → 凉
niao → 鸟
nie → 捏(禁用)
nin → 宁
ning → 宁
niu → 牛
nong → 农
nu → 努
nuan → 暖
nuo → 诺
nü → 女
nüe → 略
o → 哦
ou → 欧
pa → 趴
pai → 拍
pan → 盘
pang → 胖
pao → 跑
pei → 陪
pen → 盆
peng → 朋
pi → 皮
pian → 偏
piao → 飘
pie → 撇(禁用)
pin → 品
ping → 平
po → 破
pou → 剖(禁用)
pu → 扑
qi → 七
qia → 恰
qian → 千
qiang → 墙
qiao → 桥
qie → 切
qin → 亲
qing → 青
qiong → 穷
qiu → 秋
qu → 区
quan → 全
que → 缺
qun → 群
ran → 然
rang → 让
rao → 绕
re → 热
ren → 人
reng → 仍
ri → 日
rong → 容
rou → 肉
ru → 如
ruan → 软
rui → 锐
run → 润
ruo → 若
sa → 撒
sai → 赛
san → 三
sang → 桑
sao → 扫
se → 色
sen → 生
sha → 沙
shai → 晒(禁用)
shan → 山
shang → 上
shao → 少
she → 社
shen → 身
sheng → 声
shi → 是
shou → 手
shu → 书
shua → 刷
shuai → 帅
shuan → 拴(禁用)
shuang → 双
shui → 水
shun → 顺
shuo → 所
si → 四
song → 松
sou → 搜
su → 苏
suan → 酸
sui → 随
sun → 孙
suo → 所
ta → 他
tai → 太
tan → 谈
tang → 唐
tao → 桃
te → 特
teng → 疼
ti → 踢
tian → 天
tiao → 条
tie → 贴
ting → 听
tong → 同
tou → 头
tu → 土
tuan → 团
tui → 推
tun → 吞(禁用)
tuo → 托
wa → 哇
wai → 外
wan → 万
wang → 王
wei → 为
wen → 文
weng → 文
wo → 我
wu → 无
xi → 西
xia → 下
xian → 先
xiang → 香
xiao → 小
xie → 写
xin → 心
xing → 星
xiong → 熊
xiu → 休
xu → 须
xuan → 宣
xue → 学
xun → 寻
ya → 呀
yan → 言
yang → 阳
yao → 腰
ye → 叶
yi → 一
yin → 音
ying → 英
yo → 哟
yong → 用
you → 有
yu → 于
yuan → 元
yue → 月
yun → 云
za → 扎
zai → 在
zan → 赞
zang → 杖
zao → 早
ze → 择
zen → 珍
zeng → 增
zha → 炸
zhai → 摘
zhan → 占
zhang → 张
zhao → 招
zhe → 着
zhen → 真
zheng → 正
zhi → 之
zhong → 中
zhou → 周
zhu → 朱
zhua → 抓
zhuai → 拽(禁用)
zhuan → 专
zhuang → 庄
zhui → 追
zhun → 准
zhuo → 捉
zi → 子
zong → 宗
zou → 走
zu → 足
zuan → 钻
zui → 最
zun → 尊
zuo → 做

原版不是这样的

我们经过长时间的调查取证
我们先去除了 zhei shei
lia
den eng nun
下面是特殊:同样去除 但是:
dei (常用词只能对应一个 “得” 但是这个确实比较特殊 对于这一个字需要有一条小小的规则,明确规定 禁止在歌词中尝试使用 dei 这个字 而不选择表示 de 意的创作 ) 同时那个学校规则中也明确规定 chua (只能对应歘) 同样禁止使用 (得是多音字,并且确实很常用,所以特殊要求 而歘只有这一个读音,并且是这个读音的唯一一个常用字,所以相对来说更不重要,但还是概率会用上,所以明确一下禁用这个词 因为这个词 ai 确实不认识 可以这么理解 直接去除不额外写规则的 就是完全放心,你不会用上 也不应该用上 而明确规定的 就是怕你用上 提前阻止你用上 算是一个预防) 和 chua 地位相同的还有 dia (嗲) 这里再明确一下 这些有明确规则的,他们同样从我们的替换库中除名 (也就是从系列中去除),只在专属的规则中提到 然后是 zei (只能对应一个贼 也不是好词,以后就不用了 当然一定要脑袋清晰的认识到,这是对歌词的处理,不是说全局禁用这些字,虽然大部分确实用不上 到现在为止,反反复复补充了好多内容。很多内容确实需要记录到规则中的,不是听一下就可以了 要谨慎对待这些补充语言哦) 然后是 seng (只能对应僧 我们禁用) chuai (踹和揣 前者不好,后者不常用 因此明确禁用这两个字 以上禁用的都是字,毕竟他们就是这个音能对应的非极生僻字的全部,所以禁用所有字相当于禁用音)

上面这一大段话是我对 AI 说的喵
只是为了把他们从库中删除
不过最终库还是我自己写的 w
毕竟所有替换词都是我最终自己决定的 400 个呀

所以为了解决开篇提出的问题,我们实现了代码化,我们使用了 pinyin-Pro 这个优良的库.
它甚至支持智能的多音字识别!

正式推出我的小工具

灵晖译影

也叫 lumen trans (流明转换 流明就是灵感的意思)

警告!

代码存在未知错误 需要排查


http://lumen-trans.stellafortuna.dpdns.org/
预览:


彻底的全量替换 确实看上去很奇怪,但一定能唱…
神奇的是 AI 即使面对这完全毫无逻辑的语言,他也可以用完全符合原版歌词的声调唱出来 (因为音乐通常都不在意声调 都是根据语境来的~这一点来说 AI 真的挺强的)

我的流明诺特也终于升级到正式的新年版本了!
星缘小工具之「灵晖译影」: 让 AI 唱一切中文歌!拼音提取 自动替换 全代码实现2
26-0122!


📌 转载信息
原作者:
StellaFortuna
转载时间:
2026/1/22 13:10:28

上次在 Linuxdo 开源了小红书商业分析调研 agent:

开源地址:

基于这一个小例子,展示效果如何:





具体操作资料:
scraping_complete.json 爬虫 xhs 详细数据:

combined_analysis_complete.json 汇总分析数据:

comments_tag_analysis_complete.json 评论分析数据:

analysis_complete.json 完全分析数据:

github 开源地址,欢迎 star 和提意见:


📌 转载信息
转载时间:
2026/1/22 13:04:19

大家好,分享一个我做的开源项目 ThinkFlow AI :把「大模型的发散能力」变成「可视化的结构化思维树」,用来做选题拆解、方案推演、学习笔记、写作大纲都挺顺手。

它和传统脑图不一样的点

  • 输入一个核心想法,AI 自动生成 3–5 个子分支,快速破除 “白纸焦虑”
  • 每个节点都带 “从根到当前节点的路径上下文”,在节点里继续追问(Follow-up)会更贴合你的推理链路
  • 节点支持「回答 / 深挖」(Markdown 富文本渲染)、以及「节点配图」增强记忆
  • 一键「全图总结」提炼主线逻辑;一键「导出 Markdown」保留结构与深挖内容
  • 子树折叠 + 树形布局 + 联动拖拽,图大了也能管得住

隐私与接口

  • 本地优先:配置保存在浏览器本地
  • 支持自定义任何 OpenAI 兼容接口(自定义模式下浏览器直连,不走中转)
  • 默认提供演示接口(Cloudflare Workers/Pages 转发,可能有频率 / 额度限制)

如果你觉得这个方向有意思,求一个 Star 支持一下;也欢迎提 Issue/PR,一起把它打磨成更好用的 “思考画布”。




📌 转载信息
原作者:
ziting_liu
转载时间:
2026/1/22 13:03:41

青年大学习之坠机的 GitHub 账号恢复:

猪波被 suspended 一次 + flagged 一次,记录一下恢复流程:

Suspended:

具体表现:

登录不上 GitHub 账号;所有 git 操作显示被 suspended; 其他人看你的仓库显示 404; 本地仓库 push 显示 suspended, 如图:


重要!
请第一时间前往 https://api.github.com/users/[username]/starred 查看你是否还能看到 star 过的仓库并备份 star 列表,如出现 404 那似透了,如没有出现 404 请在被 suspended 后的 6 个月以内立即使用本文的恢复方式提交工单

Flagged:

可以登录 GitHub 账号但是三方 GitHub Auth 认证全掉;所有 git 操作全被 ban; 其他人看你的仓库显示 404, 同时会重定向到 GitHub 首页显示下图:
Github 账号被 suspended/flagged 恢复教程2

如何恢复:

  1. 前往 Sign in for Software Support and Product Help - GitHub 支持 用海外实体卡接码 (不会绑定手机号只是接码打开工单界面即可)
  2. 根据不同情况搓恩情小作文:
    若 suspended 请先自己检查所有仓库为何频繁违反 GitHub DMCA 协议 , 同时在小作文里向安全与支持部门委婉描述清除违规 repo (彻底删除否则不让你登录), 参考邮件如下

    恩情小作文参考:
Does your claim involve content on GitHub or npm.js?

GitHub

What is the username and repository or package name that was impacted?

vmoranv

Why are you requesting reinstatement? If you are requesting support for your account not related to moderation limits made on your account by GitHub please raise a support ticket instead.

My account was disabled and I can’t login

Have you previously contacted GitHub about this claim?

No

Would you like to provide any additional information or context that would be helpful for our review of your reinstatement request?

Hello GitHub Support Team,

I’m writing regarding my account under the username vmoranv — it was suddenly suspended while I was submitting an issue to “IMG_PROMPT_API.” This account is critically important to me and my entire team.

I do not believe we have engaged in any activity that violates GitHub’s Terms of Service or Community Guidelines, and I’m not aware of any suspicious behavior originating from our account. We use this repository collaboratively and rely on it for project coordination and issue tracking.

Could you please inform me of the specific reason(s) for the suspension? If possible, I kindly request that you re-enable the account at your earliest convenience. If there has been some misunderstanding or false alarm, I would appreciate any guidance on how to rectify any issues or comply with your requirements.

Thank you for your time and assistance. We highly value GitHub as a platform for our work and hope to continue contributing as soon as possible.

Kind regards,
vmoranv
On behalf of the vmoranv team

I have reviewed and understand the GitHub Acceptable Use Policies and Community Guidelines.

此处我自己检查到这个 repo 的 issue 传瑟图了 (虽然不露点但是包含暗示), 请求安全部永久删除对应 repo

若 flagged 请第一时间记录下任何在 GitHub 上的活动如 init repo|git commit | 授权三方站点 | fork 三方仓库等,并在工单中说明自己被误封狡辩 , 参考小作文如下:

vmoranv
Jan 21, 2026, 10:40 AM UTC

Would you like to provide any additional information or context that would be helpful for our review of your reinstatement request?
Dear sir or madam:
I am writing to seek your help. My GitHub account has been flagged and my profile is hidden from public. It brings me a whole lot of trouble because I can't log in any third party site with GitHub auth. I doubt whether this problem has something to do with my recent network fluctuation or repo init recently. I would appreciate your help if you unflagged my account. Thank you so much!

您的主张是否涉及 GitHub 或 npm.js 上的内容?
github

What is the username and repository or package name that was impacted?
vmoranv

Why are you requesting reinstatement?
account-not-visible

Have you previously contacted GitHub about this claim?
no

confirm-notice
confirmed

请等待安全与支持部检查你的账号违规部分再回应
重要

  1. 千万不要提交多个工单
  2. 指出出问题的 repo 并删除即可,不要要求恢复
  3. 如果在提交工单后一个星期内没有回复你的邮件可以尝试去 community · Discussions · GitHub 开一个小号并指出工单未受理 参考 Suspended Account Recovery · community · Discussion #181760 · GitHub
  4. 如果在 discussions 中仍然未受理只能期望在 X 上 at https://x.com/GitHub 发帖问候他们是否有空审理你的工单并贴上工单号
  5. 如果在 6 个月内都没有回应说明你的号连撒旦都嫌弃,似的透透的了,重开吧

参考资料:GitHub 申诉和恢复 - GitHub 文档


📌 转载信息
转载时间:
2026/1/22 12:57:59

总所周知 cf 只转发指定端口
但是我买的低价 nat 鸡,那些端口都被别人占用了,
于是想着能不能任意端口都能经过 cf 的转发,
那个 cf 的隧道就能实现。
cf 主页的这个:
nat 鸡任意端口套 cf 转发教程 - 加速节点1
在网络 - 连接器中创建隧道:


选 cf 的这个就行了:


然后我的 nat 鸡是 debian13,选这个:


建议上面的代码分开执行,反正我一键复制执行就不行:


最后执行下面的那个启动隧道就是了。
然后自己添加最右边那个:


我懒得折腾证书了,直接用域名:80 端口就算了,反正都是连接到 cf,管他什么安全性的,唉


然后 3x-ui,里面整个 vmess 的 ws,主机名肯定是自己上面整的域名



nat 鸡肯定需要端口映射,自己整吧,
首先确认自己的 vmess 是能连通的:
nat 鸡任意端口套 cf 转发教程 - 加速节点10
然后直接,改成隧道的域名,和 80 端口:


然后发现也是通的:
nat 鸡任意端口套 cf 转发教程 - 加速节点11
然后直接改成 cf 的优选域名,(优选域名一大堆自己随便搜索,找一个自己 ping 低的就行了):
nat 鸡任意端口套 cf 转发教程 - 加速节点13
然后就能低延迟一点,连接到辣鸡节点了,
测速图:


我的网络就 100m, 没办法。

这个的优点:辣鸡节点能套 cf 加速下载,延迟也能低一点;
缺点:晚上必寄,只有白天才舒服,晚上直接卡死。

所以我一般整来下载东西吧,不过需求也不大,不过白天用用也是不错的,至少能加速连接,加速下载。


📌 转载信息
转载时间:
2026/1/22 12:57:52

这几天我对派奇智图的底层代码进行了一次脱胎换骨式的重构,让项目结构更加清晰,耦合性更低。

我在项目底层增加了对 OPFS(Origin private file system) 的支持,简单来说 OPFS 是浏览器提供的私有的高性能本地存储空间。图片生成后会默认下载并保存到临时的 OPFS 存储空间,这样在本地查看或操作图片时就无需再从网络上下载文件,也无需担心文件短时间内过期的问题。通过 OPFS,我实现了在无服务器(浏览器本地)直接使用画廊功能。 这样很多没有 S3 服务或者 WebDAV 的朋友也可以使用画廊功能,保存自己喜欢的图片了 。

增加了对 A4F 免费模型的支持。免费用户一天可以使用 300 次创作,允许使用的模型包括 Z-Image Turbo、Google Imagine 4 和 Google Imagine 3.5。不过目前测试下来,A4F 的免费服务稳定性不高,经常会出现生成失败的情况,不确定是我网络问题还是这个网站服务的问题。另外他们对图片尺寸的支持性很低,很多图片生成的尺寸都不遵循参数

我对 Hugging Face 的请求进行了重构,使用了通用的 Queue API,这个 API 可以更清楚的返回错误信息,稳定性也更高。

您可以通过我的 Demo 网站:Peinture 快速体验此项目。

另外此项目也有服务器版本,支持自部署,方便您与您的朋友共享您的图片生成服务。

如果您喜欢此项目,不妨 star 此项目


📌 转载信息
原作者:
Amery2010
转载时间:
2026/1/22 12:57:17

时间过得真快,距离上次发话题已经过去几个月,成年人的时间真是不经用。马上过年了,想罢年前一定要发点东西出来的。预祝大家新年快乐。


老实说,我在使用 AI coding 时,最抓狂的不是它写不出代码,而是它 太喜欢 “一口闷” 了

场景通常是这样的:
我让 Claude Code 查一个 Bug,它二话不说读取了一个 5000 行的 server.log 或者把整个 utils.py 塞进上下文。结果就是:

  1. Token 燃烧:我的钱包在滴血。
  2. 上下文污染:关键信息被淹没在几千行无关代码里,它的智商瞬间掉线,开始胡言乱语。
  3. 响应变慢:处理大量 Token 需要时间。

这其实就是典型的 上下文过载(Context Overload。模型就像一个贪婪的读者,如果你不限制它,它会试图把整个图书馆搬回家,而不是只借那一本它需要的书。

最近我在研究 Anthropic 提倡的 渐进式披露(Progressive Disclosure),并折腾出了一套强制性的文件读取策略。今天分享给大家,亲测能让 Claude Code 的脑子清醒不少。

什么是 “渐进式披露”?

别被这个学术名词吓到。用人话说是:不要给 AI 看全图,除非它问你要。

这就好比你作为一个人类程序员接手新项目,你不会上来就把 10 万行代码从头读到尾。你会先看目录结构(ls),再搜关键字(grep),最后只打开相关的那几十行代码(read)。

Anthropic 的文档里一直强调这一点:让模型先通过搜索定位,再通过切片读取。

但在实际的 CLI 工具中,Claude 有时候很懒,或者说 “太勤快”,默认行为往往是直接 Read 全文。所以,我们需要给它装一个 “防呆开关”。

这个 Hook 是怎么工作的?

我写了一个 Python 脚本作为 PreToolUse 的 Hook(工具调用前拦截器),配合 CLAUDE.md 的提示词,搞了一套 软硬兼施 的组合拳。

核心逻辑

这个方案由两部分组成:

  1. “软” 规则(Prompt):在系统提示词里告诉它,读文件必须加 offset(起始行)和 limit(行数限制)。
  2. “硬” 拦截(Hook 脚本):这是关键。当 Claude Code 试图调用 Read 工具时,脚本会检查目标文件的大小。
  • 如果文件超过 1000 行,且 Claude Code 没有 指定 offset/limit
  • 拦截操作!返回 Exit Code 2。
  • 杀手锏:在 stderr 里返回一段精心设计的报错信息。这段报错不仅告诉它 “你错了”,还告诉它 “你应该怎么做”(比如:推荐你先用 Grep 搜一下,然后只读第 X 行附近的 50 行)。

为什么它非常 Work?

这利用了 LLM 的一个特性:它们非常听 “报错信息” 的话。

当 Tool Use 失败并返回一个明确的 “推荐路径” 时,Claude 会立刻在这个报错的 Context 下进行自我修正。

  • Claude: “我要读 app.log。” (未指定范围)
  • Hook: (拦截) “不行,文件太大了。你必须指定读取范围。建议先用 Grep 搜一下关键词。”
  • Claude: (收到报错) “噢,抱歉。那我们就先用 Grep 搜一下 ‘Error’ 关键字吧。”

看,这就强行把它拽回了 “渐进式披露” 的最佳实践路径上。

如何食用

你需要两个东西:一个是配置在项目根目录的规则文件,一个是实际执行拦截的 Python 脚本。

1. 提示词 (CLAUDE.md)

把这段加到你的项目提示词文件中。这相当于 “先礼后兵”,先告诉它规则。

中文版本

### 文件读取策略

** 强制规则 **:每次调用 Read 工具时 ** 必须 ** 指定 `offset``limit` 参数,禁止使用默认值。

#### 参数要求

| 参数   | 要求           | 说明                          |
| ------ | -------------- | ----------------------------- |
| `offset` | ** 必须指定 ** | 起始行号(从 0 开始)         |
| `limit`  | ** 必须指定 ** | 读取行数,单次不超过 500 行   |

#### 读取流程 1. ** 侦察 **:先用 Grep 了解文件结构,或定位目标关键词行号。
2. **
精准打击 **:使用 offset + limit 精确读取目标区域。
3. **
扩展 **:如果需要更多上下文,再调整 offset 继续读取。

**
目标 **:保持上下文精准、最小化。如果不遵守,工具调用将被 Hook 拦截。

English Version

### File Reading Strategy **MANDATORY RULE**: Every `Read` tool call **MUST** verify `offset` and `limit` parameters. Default full-file reads are prohibited for non-trivial files.

#### Parameter Requirements

| Param    | Requirement    | Description                   |
| -------- | -------------- | ----------------------------- |
| `offset` | **REQUIRED** | Start line number (0-indexed) |
| `limit`  | **REQUIRED** | Max lines to read (Max 500)   |

#### Workflow 1. **Recon**: Use `Grep` first to understand structure or locate keywords.
2. **Surgical Read**: Use `offset` + `limit` to read only the relevant section.
3. **Expand**: Adjust `offset` to read more context only if strictly necessary.

**Goal**: Keep context precise and minimal. Violations will be blocked by the PreToolUse hook.

2. The Hook (Python 脚本)

保存为 read_limit_hook.py,并在你的 Claude CLI 配置 hook(如果你不会可以直接把文件给 claude code 让它代劳)。

(这个脚本稍微有点长,但逻辑很简单:检查文件大小 → 检查参数 → 决定是放行、自动修正还是报错拦截)

#!/usr/bin/env python3 """
PreToolUse hook for Read tool - Enforce offset/limit and block large file reads.
"""
import json import sys import os from datetime import datetime # --- 配置区域 --- MAX_FILE_LINES = 1000 # 超过这个行数必须切片读 MAX_FILE_BYTES = 50 * 1024 MAX_SINGLE_READ_LINES = 500 # 一次最多读 500 行 MAX_SINGLE_READ_BYTES = 20 * 1024 # 跳过不需要检查的二进制文件 SKIP_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.pdf', '.exe', '.dll', '.so', '.dylib', '.zip', '.tar', '.gz'} # 日志文件(可选,帮你分析它浪费了多少次尝试) LOG_FILE = os.path.expandvars ("$USERPROFILE/.claude/hooks/read-stats.log") def get_file_stats (file_path): try: if not os.path.exists (file_path): return None, None size = os.path.getsize (file_path) with open (file_path, 'r', encoding='utf-8', errors='ignore') as f: lines = sum (1 for _ in f) return lines, size except: return None, None def format_bytes (size): if size >= 1024 * 1024: return f"{size / (1024 * 1024):.1f} MB" if size >= 1024: return f"{size / 1024:.1f} KB" return f"{size} B" def main (): try: input_data = json.load (sys.stdin) except: sys.exit (0) # 甚至不是 JSON,不管了 tool_name = input_data.get ("tool_name", "") tool_input = input_data.get ("tool_input", {}) # 只管 Read 工具 if tool_name != "Read": sys.exit (0) file_path = tool_input.get ("file_path", "") offset = tool_input.get ("offset") limit = tool_input.get ("limit") # 1. 扩展名检查 ext = os.path.splitext (file_path)[1].lower () if ext in SKIP_EXTENSIONS: sys.exit (0) lines, size = get_file_stats (file_path) if lines is None: sys.exit (0) # 读不到文件,让 Claude 自己处理错误 # 2. 检查是否是大文件 is_large_file = lines > MAX_FILE_LINES or size > MAX_FILE_BYTES if is_large_file: # 如果是大文件,且没有指定 offset 或 limit -> 拦截! if offset is None or limit is None: reason = f"{lines} lines / {format_bytes (size)}" error_msg = ( f"BLOCKED: File is too large ({reason}) for a full read.\n" f"You MUST use 'offset' and 'limit' to read specific sections.\n\n" f"Strategy:\n" f"1. Use`Grep`to find the line number of your function/variable.\n" f"2. Then`Read`with offset=LINE_NUM, limit=50.\n" f"DO NOT try to read the whole file again." ) print (error_msg, file=sys.stderr) sys.exit (2) # 2 通常表示操作被拒绝 # 3. 检查单次读取是否贪得无厌 if limit is not None and limit > MAX_SINGLE_READ_LINES: print (f"BLOCKED: Limit {limit} is too high. Max allowed is {MAX_SINGLE_READ_LINES}.", file=sys.stderr) sys.exit (2) # 4. 贴心的小功能:如果有 offset 没 limit,自动帮它补上 limit,防止它犯傻 if offset is not None and limit is None: output = { "hookSpecificOutput": { "permissionDecision": "allow", "updatedInput": { "limit": MAX_SINGLE_READ_LINES } } } print (json.dumps (output)) sys.exit (0) sys.exit (0) if __name__ == "__main__": main ()

效果

装上这一套之后,你会发现 Claude 的行为模式变了:

  • 以前:读取 main.c (3000 行) → 思考 → 修改。
  • 现在:尝试读取 main.c → 被拦截 → 思考 “哦,我应该先搜一下” → Grep main 函数 → 读取 main 函数周围 50 行 → 思考 → 修改。

虽然多了一步交互,但 上下文极其干净,Token 消耗量能降低 80% 以上,而且修改的准确率反而提高了。

试一下吧,让你的 Claude Code 甚至其他 Agent 学会渐进式的读取。


📌 转载信息
原作者:
cedricthecoder
转载时间:
2026/1/22 12:56:48

项目简介

Endspace 是我在业余(摸鱼)时间开发的一款第三方 NotionNext 主题,致力于为你的 NotionNext 博客赋予未来主义与工业科幻的独特气质(不是,这年头谁写博客啊)。其 UI 设计在一定程度上参考了《明日方舟:终末地》官网。

效果预览

仓库刚建不久,后续还会优化一些小细节,不定期更新。
如果感兴趣的话,可以给我一个 Star 吗?这将给我非常大的帮助。
如果你和我一样,也在用 NotionNext 建站,欢迎使用 Endspace 作为主题,如果能提 issue 和 pr 就更好了!


📌 转载信息
原作者:
Cloud-009
转载时间:
2026/1/22 12:56:28

漏洞详情:
GNU InetUtils telnetd 远程身份认证绕过漏洞(CVE-2026-24061),此漏洞主要影响 telnetd 在调用系统 /usr/bin/login 程序时,未对从客户端 USER 环境变量传入的用户名做过滤,直接拼接到 login 命令行。未经授权的远程攻击者可利用该缺陷,在无需任何口令的情况下直接获取目标主机的 root shell,造成完全控制权泄露、敏感信息被窃取或进一步横向移动。

目前我已经在本地成功复现并且在公网有捡到很多群辉存在这个漏洞 , 站内好像有不少有玩 NAS 之类的朋友特别是有公网 IP 的兄弟注意排查一下,其实版本挺苛刻的 1.9.3 <= GNU InetUtils <= 2.7,但是我测试过了 ubuntu24.04 使用 apt 下的 telnet 服务就是存在这个漏洞的。

因为影响面比较广我就不放 exp 了避免被滥用,只要记住自查一下版本就行了

修复建议:

Debian/Ubuntu: sudo apt update && sudo apt install inetutils-telnetd
RHEL/CentOS: sudo dnf upgrade inetutilsAlpine: apk upgrade inetutils
# 或手动应用补丁:
wget https://codeberg.org/inetutils/inetutils/commit/fd702c02497b2f398e739e3119bed0b23dd7aa7b.patch
wget https://codeberg.org/inetutils/inetutils/commit/ccba9f748aa8d50a38d7748e2e60362edd6a32cc.patch
patch -p1 < fd702c0.patch
patch -p1 < ccba9f7.patch
./configure && make &&sudo make install

📌 转载信息
原作者:
linxi_2650
转载时间:
2026/1/22 12:55:41

开发者朋友们大家好:

这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的技术」、「有亮点的产品」、「有思考的文章」、「有态度的观点」、「有看点的活动」,但内容仅代表编辑的个人观点,欢迎大家留言、跟帖、讨论。

本期编辑:@瓒an、@鲍勃

01有话题的技术

1、阶跃星辰开源 Step3‑VL‑10B:10B 模型对标 200B 能力

昨天,阶跃星辰宣布正式开源旗下 10B 参数量多模态模型 Step3‑VL‑10B。该模型在多项核心基准测试中达到同规模 SOTA 水平,部分能力甚至超越 10–20 倍体量的大模型。

Step3‑VL‑10B 主打「小模型实现大模型能力」,在视觉感知、逻辑推理、数学竞赛题、多模态对话等任务中表现突出。

阶跃星辰称,Step3‑VL‑10B 的性能已接近甚至超越部分百亿级开源模型(如 GLM‑4.6V 106B‑A12B、Qwen3‑VL‑Thinking 235B‑A22B),并在部分场景中达到顶级闭源旗舰模型(如 Gemini 2.5 Pro、Seed‑1.5‑VL)水平。

官方强调,该模型的关键突破来自三项核心设计:

  • 全参数端到端多模态联合预训练:在 1.2T 高质量多模态数据上训练,实现视觉与语言的深度对齐;
  • 大规模多模态强化学习:经历超过 1,400 次迭代,使模型在识别、推理与对话能力上持续提升;
  • 并行协调推理机制:通过并行探索与证据聚合提升复杂任务的准确度,尤其在数学推理、OCR、计数与空间拓扑任务中效果显著。

Step3‑VL‑10B 同时提供 SeRe(顺序推理)与 PaCoRe(并行推理)两种范式,覆盖 STEM 推理、OCR、GUI Grounding、空间理解与代码等多项能力维度。

当前,Step3‑VL‑10B 已开放 Base 与 Thinking 两个版本,社区可在 HuggingFace 与 ModelScope 获取模型并进行微调。

项目主页:
https://stepfun-ai.github.io/Step3-VL-10B/

Hugging Face:
https://huggingface.co/collections/stepfun-ai/step3-vl-10b

ModelScope:
https://modelscope.cn/collections/stepfun-ai/Step3-VL-10B

论文链接:
https://arxiv.org/pdf/2601.09668

(@阶跃星辰、@APPSO)

2、showlab 开源 whisperVideo:集成 SAM3 与 TalkNet 实现长视频「音视对齐」的说话人转录

showlab 近期开源了名为 whisperVideo 的项目,专门致力于解决长视频场景下「谁在说话」的身份归属难题。该工具打破了传统方案仅依赖音频的局限,通过融合视听双重特征,实现了语音内容与画面特定人脸的精准对齐。

为了突破纯音频方案在多人混响或近距离交谈时常见的识别漂移问题,whisperVideo 构建了一套紧密的多模态级联架构。它集成了 WhisperX 负责语音转录、Pyannote.audio 处理声纹分离,并引入 SAM3 进行人脸分割以及 TalkNet 判定主动说话人。这种组合拳方式,确保了机器能像人类一样同时「听」和「看」,从而做出更准确的判断。

针对小时级素材中常见的跨场景挑战,工具特别引入了「长时身份一致性」机制。利用视觉嵌入与轨迹聚类技术,系统能在漫长的视频时间轴上记住每一张脸,确保同一说话人的 ID 在不同场景切换中始终保持稳定。

在工作流设计上,whisperVideo 追求全自动化体验。内置的 SceneDetect 能够自动进行场景切割与分段处理,无需人工干预即可完成时间戳、文本与视觉 ID 的三方对齐。最终生成的成果不仅包括带说话人 ID 的字幕,还支持可视化的面板模式,并将底层数据以 。pckl 格式开放给开发者。

目前,项目已在 GitHub 开源,需使用 CUDA GPU 环境,依赖 HuggingFace Token 调用 Diarization 模型,支持 Python 命令行一键推理。

GitHub:
https://github.com/showlab/whisperVideo

( @aigclink\@X)

3、Bolna 获 630 万美元种子轮融资:自研 SLM 语音智能体,支持「印式英语」混说

总部位于班加罗尔的初创公司「Bolna」近日完成了由 General Catalyst 领投的 630 万美元种子轮融资。这家公司致力于通过自研的专用小模型(SLM)技术,打破多语言环境下的自动化通信瓶颈。

为了适应印度极其复杂的语言生态,Bolna 构建的语音智能体不仅将端到端响应延迟控制在 500 毫秒以内,更实现了深度的本地化适配。它能够流畅处理包括印地语、泰米尔语在内的 10 余种本土语言及 50 多种地区口音,甚至针对印度特有的语言混合现象,专门优化了对「印式英语(Hinglish)」的语义理解与生成能力。

在技术架构上,Bolna 摒弃了昂贵的通用大模型方案,转而采用针对事务性查询优化的 SLM 与智能路由架构。这种策略有效平衡了计算成本与响应速度,使其更适合大规模商业落地。配合其提供的无代码控制台,企业可自主设计并监控智能体。目前,该平台的日呼叫处理量已从 1,500 通激增至 20 万通以上,广泛应用于购物车挽回、货到付款确认及招聘筛选等场景。

平台现已正式上线,主要面向印度企业提供订阅制的自助服务。

( @AI Tech Suite)

02有亮点的产品

1、消息称华为首款 AI 眼镜将在上半年发布:搭载鸿蒙 OS,支持同传翻译与拍照

1 月 20 日多家媒体消息,华为的第一款「AI 眼镜」暂定在今年上半年推出,支持拍照和音频,鸿蒙系统 + 跨端无缝协同,同传翻译等功能。 AI 眼镜被誉为「下一代 AI 终端超级入口」,已然是大厂必争之地,百度、小米、阿里、理想等早已进场,并推出了 AI 拍照眼镜,字节也即将推出 AI 眼镜,作为国内消费类智能终端龙头的华为自然不会落后于人。

据 @数码闲聊站 爆料,华为 AI 眼镜将采用鸿蒙 OS 系统与轻量化设计,内置 3 块锂电池,支持跨端无缝协同,进一步拓展使用场景。并提供流光银、钛银灰、摩登黑三款配色,支持拍照、拍视频、音频播放以及同声传译等功能。

虽然目前具体细节尚未公布,但结合华为在 AI 技术领域的探索,预计将内置华为 AI 助手小艺,产品可能涉及 AI 识物、智能场景推荐等功能。

经查询发现,华为曾推出带有音频功能的智能眼镜,主打听音乐、打电话、健康播报等。如今随着 AI 的兴起,智能眼镜行业也纷纷上马 AI,以及自带摄像头、显示屏的 AI 眼镜也不断推新。

据 IDC 预测,智能眼镜产品成为 2025 年消费电子赛道的黑马,相应产品在中国市场出货量预计达到 290.7 万台,同比增长 121.1%。业内人士普遍认为,这缘于技术突破、市场需求释放以及产业链成熟等多重因素。

汇丰控股认为,智能眼镜市场仍处于加速扩张阶段。分析师预计,智能眼镜的用户规模将在未来十多年内迎来爆发式增长,到 2030 年代末将达到 2.89 亿人,较 2025 年的 1500 万用户增长超过 18 倍。

(@即智 Ultra、@IT 之家)

2、MiniMax 推出「Agent 实习生」,AI-native Workspace 全面升级

昨天,MiniMax 官宣,AI-native Workspace 迎来两项核心升级,进一步推动 AI 深度嵌入真实工作场景,并面向用户开放限时免费体验。

  • 桌面端应用正式上线: 用户可在本地环境中指定 Workspace 作为工作空间与上下文,使 AI 能够直接理解本地文档、代码仓库、邮件与日程,从而构建一个专属于个人的智能工作环境。
  • 推出「专家 Agents」能力: 用户可构建在特定领域达到「95 分甚至 100 分」水平的专业智能体。这类 Agent 能够在复杂任务链路中稳定执行、主动判断并长期协作。

公司内部数据显示,「Agent 实习生」在过去数周已被接近 100% 的员工使用,并在运维场景中承担了约 80% 的查 Bug 工作量。

MiniMax 表示,AI-native Workspace 标志着 Agent 从「被动执行指令」向「主动感知环境」的形态演进。

公司认为,未来的 Agent 将具备长期记忆、完整职业上下文与跨系统感知能力,成为用户的长期工作伙伴,而非一次性工具。

目前,MiniMax 已开启专家 Agents 的限时免费体验。用户可通过 Web 端直接试用,也可通过官方体验链接获取桌面端安装包。

体验地址:
https://agent.minimaxi.com/

( @APPSO)

3、Crow 发布 AI 智能体框架:支持 OpenAPI 与 MCP 协议,实现「对话即 UI」交互

Crow 近期推出了一套专为 SaaS 产品打造的 AI 智能体基础设施,旨在通过「对话即 UI」的理念重构软件交互模式。该工具的核心逻辑在于将传统的点击操作转化为自然语言指令流,通过接入 OpenAPI 规范或 MCP 协议,使智能体不仅能回答问题,更能直接触发后端 API 调用及前端 UI 导航,从而实现对软件功能的深度控制。

为了解决生成式 AI 不可控的难题,Crow 引入了名为「Journeys」的结构化工作流。开发者可以针对取消订阅、创建报表等特定业务场景,定义确定性的引导路径,确保智能体在执行敏感操作时严格遵循预设的逻辑分支。配合支持文件与文档集成的 RAG 管道,智能体还能充分理解产品特定的业务逻辑与私有数据。

在开发与运维层面,Crow 提供了生产级的观测指标,能够详细追踪每一条指令对应的工具调用路径。其低代码部署方案仅需嵌入单行 Script 标签,官方宣称这能将传统长达半年以上的自研周期缩短至一周以内,并支持与 Claude Code 或 Cursor 等工具集成。目前该产品已正式上线,开发者项目可免费试用,同时针对中大型企业提供了定制化方案。

( @Y Combinator Launch)

4、Thread 发布 Voice AI:实现 MSP 电话自动化分拣与实时工单同步,单人效能提升 30%

Thread 宣布其专为托管服务提供商设计的 Voice AI 正式商用。该产品旨在终结传统 IVR(交互式语音应答)系统的僵化体验,通过语音智能体接管电话接入、分拣与派发的全流程,将高成本的电话渠道整合进结构化的自动化运维体系中。

AI Attendant 与 Overflow Agent 双引擎驱动:

  • AI Attendant:取代传统 IVR,能够即时接听电话并识别来电者身份。它不仅能进行自然的语音交互,还能在后台实时创建工单、匹配技术人员,并完成「热切换」,确保客户在转接给真人时无需重复复述问题。
  • Overflow Agent:专为下班后或线路繁忙场景设计。它能拦截进入语音信箱的电话,自动收集关键信息并进行分类;遇到 P1 级紧急事件时,可直接升级并呼叫待命团队,消除了「下班后盲区」。

Voice AI 的核心价值在于将非结构化的语音高效转化为结构化数据。系统不仅能根据通话内容自动填充工单的标题、类别、优先级和解决摘要,还引入了「自动时间条目」功能,可依据通话时长直接生成计费记录。据官方数据统计,这一特性为每张工单平均节省了 19 分钟的处理时间,从而推动单一技术人员的日均通话处理量从 8-12 通显著提升至 14-20 通。

在生态兼容性方面,该方案作为 Thread AI Service Desk 平台的重要组成部分,已与 ConnectWise、Autotask 和 HaloPSA 等主流 PSA 系统实现了原生集成。这意味着所有通话数据都会实时转化为结构化文档,并无缝同步至企业现有的工作流中,从而确保了整个服务链条的完整性与可追溯性。

据 Thread 统计,通过消除手动记录和人工轮班需求,该系统可使响应速度提升 5 倍,平均解决时间缩短 78%。目前该服务已正式上线。

相关链接:
https://www.getthread.com/voice-ai

( @Mansfield News Journal)

03有态度的观点

1、谷歌前 CEO 施密特:欧洲要么投资开源 AI,要么依赖中国模型

1 月 20 日,据外媒报道,谷歌前 CEO、科技投资人埃里克 · 施密特 (Eric Schmidt) 周二表示,欧洲必须投资建设自己的开源 AI 实验室,并解决能源价格飙升的问题,否则很快就会发现自己对中国的模型产生依赖。 施密特周二在达沃斯世界经济论坛表示:「在美国,企业基本上正在转向闭源,这意味着这些技术将被购买、授权等等。而与此同时,中国在做法上基本是开放权重、开源的。除非欧洲愿意为欧洲自己的模型投入大量资金,否则欧洲最终将会使用中国的模型。」

目前,许多热门 AI 模型都是闭源的,比如谷歌的 Gemini 和 OpenAI 的 ChatGPT,这意味着这些公司不会向外界提供底层代码供下载或审查。虽然这种方式能为用户带来更顺畅、更统一的使用体验,但通常成本更高、灵活性也更低。中国在所谓「开放权重」模型的开发方面处于领先地位,这类模型具有更高的透明度。

为了在开发更强大 AI 模型和智能体的全球竞赛中具备竞争力,欧洲还需要解决高企的能源价格问题,并建设更多可用于训练这些技术的数据中心。施密特曾联合创办一家数据中心公司,致力于应对这类基础设施巨大的能源需求。他也对美国 AI 发展对电力供应的影响表示担忧。

(@IT 之家)

阅读更多 Voice Agent 学习笔记:了解最懂 AI 语音的头脑都在思考什么

写在最后:

我们欢迎更多的小伙伴参与 「RTE 开发者日报」 内容的共创,感兴趣的朋友请通过开发者社区或公众号留言联系,记得报暗号「共创」。

对于任何反馈(包括但不限于内容上、形式上)我们不胜感激、并有小惊喜回馈,例如你希望从日报中看到哪些内容;自己推荐的信源、项目、话题、活动等;或者列举几个你喜欢看、平时常看的内容渠道;内容排版或呈现形式上有哪些可以改进的地方等。

作者提示: 个人观点,仅供参考

2025 年 12 月,涛思数据与沈阳化工研究院(简称“沈阳院”)正式达成合作。涛思数据将为其提供 TDengine TSDB + IDMP 产品组合,通过部署工业数据管理平台,以 AI 原生的数据智能技术,支撑沈阳院构建覆盖从实验室研究到中试放大全流程的统一数据基座,助力其研发数字化转型迈向新阶段。

沈阳院是我国重要的综合性化工科研院所,其研发过程中涉及海量、多源的时序数据与非时序数据,同时其中试基地拥有多条专业化生产线。面对实验室、中试装置产生的庞杂数据,如何打破数据孤岛,实现数据的统一管理、关联分析与智能洞察,从而加速研发进程、优化生产工艺,是沈阳院数字化转型的重要任务。

随着数字化进程的推进,沈阳院需要一个能够打通从实验到中试全流程的数据管理平台,能够将时序数据与非时序数据(如物料信息、实验记录)进行关联分析,同时满足信创环境要求和数据安全规范。涛思数据全新发布的 TDengine IDMP(工业数据管理平台)产品,具备“无问智推”的 AI 原生能力,这种让数据主动说话的能力,正是解决业务人员依赖 IT 团队获取数据洞察的关键。

本次项目需要采集和分析来自实验室、中试生产线的数据,总计需要监控的测点约 2 万。这些数据来源于高压釜、干燥箱、色谱仪等实验设备,以及生产线的温度、压力、流量等工艺参数,还包括水电气等能耗数据。TDengine TSDB 支持多种数据接入方式,包括 MQTT、OPC-UA/DA 等。这对于研究院现有的数据采集系统(MQTT 和 OPC)非常重要。在数据建模方面,TDengine IDMP 采用树状层次结构,这与研究院的设备组织方式天然契合。比如,可以按照“研究院-中试基地-生产线-设备”的层级结构建立数据目录,每个节点都可以配置属性、分析规则和可视化面板。这种结构特别适合中试基地的批次分析需求,可以清晰地展示每个批次的工艺参数和质量指标。

本项目采用“整体规划、分步实施”的策略,项目计划分两阶段进行:

  • 第一阶段,选择 3 个实验室和 1 条中试生产线进行试点实施;
  • 第二阶段,基于试点成果向全院范围推广。

基于数据安全性和网络环境考虑,选择本地化部署方案。部署架构如下图所示:

本次项目规划的设计思路紧密围绕化工研发的业务特点展开,力图在以下几大关键业务场景提升数据应用效率与深度:

  1. 数据全景可视化与智能告警:通过 TDengine IDMP 的智能可视化功能,实现实验数据和中试生产数据的全景可视化管理。研究人员无需 IT 支持即可通过自然语言交互获取所需数据视图;通过实时分析和事件管理功能,自动触发告警,并帮助研究人员快速定位问题根源;借助“无问智推”能力,自动推送质量波动的批次与标准参数的对比分析,帮助管理人员快速决策。
  2. 工艺优化与批次对比分析:批次分析是中试生产的核心需求之一。借助 TDengine IDMP,可以实现多批次数据的自动对比分析。系统能够根据批次质量指标帮助科研人员找到"黄金批次",并分析其工艺参数特征,为工艺优化提供数据支持。通过时序数据高级分析功能,研究人员可以轻松对比不同批次的差异,找出影响产品质量的关键工艺参数。
  3. 预测性维护与能耗管理:基于 TDengine TDgpt 的能力,平台能够轻松集成时序数据的预测、异常检测、分类、补全、相关性分析等算法和模型,帮助客户实现对关键设备的实时监控与预测性维护。在中试基地的能耗管理方面,通过对水、电、气的实时监测与统计分析,帮助找出能效瓶颈、识别出能耗异常点,用以指导设备改造和工艺调整。
  4. 数据驱动的工艺包开发:TDengine 产品组合将帮助研究院实现数据驱动的研发模式,提高工艺包开发的效率和质量。新工艺包的设计可以基于历史中试数据,确保工艺参数的可靠性。而工艺包转化为实际生产后,又可以通过对比设计数据与实际生产数据,持续优化工艺模型。同时,TDengine IDMP 内置了备份/恢复机制,未来还将支持 Git 式数据版本管理,有望进一步提高数据归档、变迁、回溯的能力。

本次涛思数据与沈阳化工研究院的强强联合,为化工科研数据管理和数据分析描绘出更多可能性。相信此次合作不仅能提升沈阳院的研发效率,更有望探索出一条以数据智能驱动化工行业创新的可行路径。

关于沈阳化工研究院

沈阳化工研究院有限公司始建于 1949 年 1 月 8 日,是综合性化工科研院所,现为中国中化控股有限责任公司直管单位。目前沈阳院主要开展化工新材料、生态农业、生物化工、化学品测试与评价、化工反应风险评估、危险废物鉴别、化工智能优化等方向的研究及产业化。沈阳院聚焦提升关键共性技术的研究与开发能力、较强的新产品孵化能力和适度产业规模和盈利能力;致力于成为精细化工行业国内领先,国际有一定影响力的科技型企业。

2025 年 12 月,涛思数据与北京海莱德自动化工程有限公司(简称“海莱德”)正式建立合作伙伴关系。此次合作,海莱德将基于自身行业自动化系统集成能力,结合涛思数据提供的 TDengine TSDB + IDMP 产品组合,共同为制糖等行业客户打造从数据采集、治理到智能分析应用的完整解决方案,助力制糖工业企业实现生产运营的数字化与智能化转型。

行业背景|制糖生产正在面对的新挑战

制糖行业的生产实践表明,甘蔗制糖是一项高度连续、强耦合、对运行稳定性要求极高的工业过程。原料受品种、成熟度、含糖量和纤维含量等因素影响,天然波动较大;加之榨季集中、生产节奏紧凑,一旦发生非计划停车或关键参数失控,带来的不仅是产量损失,更可能造成难以弥补的经济影响和社会影响(涉及甘蔗和甜菜的农业生产)。

在此背景下,行业内绝大多数糖厂长期依赖以人工经验为主的工艺调整方式,以及以工段为单位、相互割裂的数据管理模式,这些传统做法逐渐显现出其局限性。经验固然重要,但难以在不同班组、不同人员之间稳定传承与高效复制;生产数据虽然持续产生,却因分散在不同系统与记录中而难以整合分析,从而无法有效支撑对稳产、提质、降本目标的持续精细化管控。这已成为行业的一个普遍共识:仅依靠传统方式,已难以应对当前生产运行对稳定性与过程可控性日益提升的要求。

面临挑战|从“看不清”到“管不住”

在实际运行中,以下这些挑战并非个案,而是制糖行业中普遍存在的共性问题。

首先,生产过程链条长、环节多,从预处理、压榨、澄清、蒸发、煮糖到分蜜、干燥包装,各工段数据往往分散在不同的系统与记录中,缺乏统一视角,导致难以形成真正贯穿全流程的生产监控与分析能力。

其次,在工艺质量管控方面,参数调整长期依赖人工经验判断。许多异常往往在最终质量指标已发生偏差后才得以察觉,缺乏对工艺质量的过程性分析与持续监控手段,难以实现事前预警与主动干预。

最后,在物料与糖分损耗管理上,行业长期缺乏有效的工具进行清晰、有效的分析和管理。糖分损耗分散于滤泥、废蜜、洗水、跑糖等多个环节,大多依靠经验估算,无法形成系统、可对比的“糖损画像”,这在很大程度上制约了对产糖效率与整体经营指标的持续优化。

正是这些普遍存在的“看不清、管不住”的痛点,促使制糖行业开始重新思考生产管理方式,并推动如 TDengine IDMP 这样的生产数据与工艺管理平台,逐渐成为企业进行数字化转型、实现精细化运营的重要选择。

解决方案|从“数据分散”到“AI Ready”,让制糖跑在数据之上

在榨季现场,行业内常有一种共识:“数据其实都有,就是用不起来。”原料特性每日波动,工艺流程长且复杂,相关数据往往分散在局部的 DCS、各类设备的独立系统及手工台账中。操作人员依赖经验盯守,生产系统中前后无高效的数据流通,一旦生产节奏加快,潜在的风险与异常便容易被淹没在庞杂的信息流中。

因此,选择引入 TDengine IDMP 平台,其初衷并非简单“再上一套系统”,而是旨在将沉睡的数据转化为直接支撑生产决策与运营优化的能力。围绕制糖行业原料波动大、流程链路长、设备可靠性要求高等特点,该平台以 TDengine TSBS + TDengine IDMP 为核心,从数据采集与接入起步,逐步打通数据治理、业务情景化建模与 AI 分析应用,致力于构建一套真正面向生产、服务于工艺优化与稳定运行的工业数据管理体系。

图1 以 TDengine IDMP 为基础面向生产的工业数据管理体系

数据采集|先把“碎数据”连成一条线

在项目启动之初,制糖企业现场所面临的情况在行业中并不陌生:数据体量并不少,但分布零散。工厂局部的 DCS、各类设备的独立系统仅仅服务于局部的监控层面。而在数据分析、集中管理与智能应用层面,则长期缺乏统一、高效的数据出口。

针对这一现状,项目规划在不影响现有控制系统稳定运行的前提下,于集控层之上构建独立的数据采集与汇聚通道。计划在每个工厂部署一套 TDengine TSDB,利用其自带的零代码采集工具 taosX,通过 OPC 标准接口从 DCS Server 读取实时工艺数据,以实现关键生产数据的稳定采集与接入。同时,在企业级数据中心部署统一的 TDengine TSDB,对各工厂的时序数据进行集中汇聚与统一管理,为后续的数据整合分析与跨厂协同打下基础。

图2 某甘蔗制糖项目的数据采集架构图

这种架构既充分保留了 DCS 与 SCADA 的成熟运行体系,又在其之上形成统一、可扩展的数据采集与汇聚层,为后续的数据治理、业务情景化和 AI 应用奠定了可靠基础。

数据分析|从“看历史”到“提前知道”

在数据分析层,平台基于 TDengine TSDB 的高性能时序数据管理能力,实现实时与历史数据的统一处理,并能够结合时序基础模型的时序数据预测与异常检测能力,对生产过程和设备运行状态进行持续分析。

通过对关键工艺参数和运行指标的时序建模,时序基础模型能够识别正常运行模式,预测指标变化趋势,并对偏离正常区间的异常波动进行及时检测与预警,帮助企业提前发现潜在风险。请参考:时序数据分析智能体 TDgpt

该能力使生产管控从依赖经验的事后分析,转向基于数据的趋势预判与异常识别,为工艺稳定运行、设备可靠性提升及运营决策提供更加及时、可靠的数据支撑。

数据目录|让每个岗位都用得上数据

如果说采集和分析解决了“数据有没有、算不算得动”的问题,那么数据目录解决的,是“业务用不用得上”。

TDengine IDMP 并没有强制所有人用同一种视角看数据,而是允许不同部门按自己的业务逻辑组织数据。生产车间可以围绕工艺流程,把数据按工序、工段和关键参数来组织;设备管理部门则按设备类型和运行状态建立目录,专注设备可靠性和维护。同一份底层数据,可以在不同业务视角下被反复引用。

对业务人员来说,找数据不再是“翻系统”,而是“进目录”;对系统来说,数据有了清晰的结构和入口,才能被稳定调用、持续分析。

图3 甘蔗制糖厂数据目录(按设备、按工艺)
https://segmentfault.com/write###

数据标准化 | 让“一吨糖”只有一种算法

在工业系统中,数据标准化不是“规范问题”,而是直接影响结果是否可信的基础工程。航天领域曾因单位不统一而导致重大事故,这一案例反复被提及,并不是偶然,而是揭示了一个普遍规律:当数据口径不统一时,系统即使运行正常,结论也可能完全错误。

在制糖生产中,这类风险同样真实存在。以澄清汁流量为例,DCS 系统通常以体积流量 m³/h 采集数据,而部分历史系统或人工台账则沿用质量流量 t/h。两种口径在各自系统内都能够正常使用,但一旦进入跨系统分析场景——例如物料衡算、产能评估或能耗核算——问题便会显现:同一个“澄清汁流量”,在不同系统中参与计算,得到的却是两套完全不同的结果。

在 TDengine IDMP 中,这类问题不再依赖人工经验去“记住差异”,而是通过模型层面的标准化设计,从源头上消除歧义,确保“一吨糖”在系统中只有一种确定、可复用的计算方式。

将“老师傅的共识”固化为系统规则

在实际生产中,许多关键口径早已形成行业共识,只是长期存在于经验和习惯中。TDengine IDMP 通过元素模板机制,将这些共识转化为可执行、可约束的系统规则。

以“澄清汁”这一对象为例,IDMP 在模型层对其进行统一、规范的定义,明确其所包含的各类属性,并对每个属性的名称、业务含义、数据类型、计量单位及使用口径进行统一约束。针对澄清汁流量,模型中会明确其业务含义、统一采用的标准计量单位、适用的工艺计算口径,以及是否参与物料衡算与产能分析等核心规则。

通过这种方式,同一类工艺对象、同一类指标在系统中只保留唯一、确定的解释,从根本上避免“同名不同义”或“同数不同算”的问题,为后续跨系统分析和长期稳定运行提供一致、可靠的数据基础。

图4 通过元素模板将知识固化

单位不同?系统自动算清楚

在统一标准的同时,TDengine IDMP 也充分考虑了现有系统的复杂性。针对属性模板,平台在公式层引入计量单位的自动识别与推导能力。

当数据来自 DCS 系统时,平台能够识别其计量单位为体积流量(m³/h);当数据来自历史系统或台账时,则识别为质量流量(t/h)。在参与计算或分析时,TDengine IDMP 会根据目标属性所要求的计量单位,自动推导并完成必要的单位换算,确保计算结果口径一致。

整个过程无需人工干预,也不依赖个人经验假设,使不同来源、不同口径的数据能够在统一模型下安全、可靠地参与分析,为物料衡算和经营决策提供稳定支撑。

图5 澄清汁的质量流量到体积流量的自动推导

数据情景化|让工业数据真正看得懂、用得上

在实际生产中,制糖行业越来越深刻地体会到:没有情景的数据,只是一串数字;只有将其置于具体的工艺场景中,数据才真正具有意义。

榨季期间,生产现场变化极为迅速。今天可能是澄清工段的 pH 值出现波动,明天发现废蜜纯度偏高,过几天又察觉实际产糖率与理论值存在偏差。这类问题本身并不复杂,但过去的分析方式却异常耗时费力——通常由业务人员凭借经验提出初步判断,再由技术人员到各个独立系统中查找相关点位、收集数据;数据找齐后,还需反复确认其时间范围、计算口径是否一致。往往经过这样一轮繁琐流程,数天时间已经过去。

究其根源,问题通常不在于人员专业能力,而在于数据本身缺乏情景化组织。业务人员往往不清楚所需数据具体分布在哪些系统中、是否可直接使用;技术人员也难以理解这些数据在工艺上应如何关联、如何分析,以及它们之间的业务逻辑是什么。这种数据与业务之间的“断层”,使得高效的分析与决策难以实现。

连接业务与技术的关键一环

引入 TDengine IDMP 平台后,制糖企业将能够使数据真正成为业务与技术之间的“通用语言”。

该平台通过为数据补充统一、清晰的业务语义,将其与具体的生产过程直接关联。每一条数据都将被明确归属到特定的工艺环节(如澄清、蒸发或煮糖),同时标识其反映的工艺机理类型(如反应强度、抽提效率或回收损失),并清楚定义其适用的业务场景(如质量监控、物料衡算、异常分析或工艺优化)。

在此基础上,平台还将构建标准化的技术元数据层,对数据来源、计量单位和合理取值范围进行统一管理。由此,数据从何处来、如何计算将变得清晰可溯,在进行数据分析、计算或设置告警时,系统能够自动确保口径一致,从而避免因理解偏差导致的结果不一。

这一步的关键价值在于,许多原本存在于“老师傅经验”中的隐性知识与共识,将被有效地沉淀并固化为清晰、可复用的系统规则,为知识的传承与规模化应用奠定基础。

图6 数据情景化(业务描述和限值)

业务分析真正实现自助

在数据完成情景化之后,制糖企业的业务分析方式将发生根本性转变,从过去高度依赖 IT 部门支持,转向以业务人员自助分析为主。系统前端将不再展示零散的点位编号与底层数据结构,而是围绕“澄清稳定性”“物料衡算”“产糖效率”等业务人员熟悉的工艺情景来组织数据与功能。

以澄清工段为例,工艺人员在“澄清稳定性”情景下,将能够直接选取 pH 值、混浊度、色值等关键指标,并自行拖拽搭建趋势对比与关联分析面板,用于实时判断反应状态是否偏离正常区间。整个过程无需向 IT 部门提出建模或取数需求,分析逻辑也将更加贴近现场实际。业务人员从而能真正基于数据流进行自主判断与决策。

这种以业务情景为核心的分析模式,将显著降低数据使用门槛与技术障碍,使得工艺人员更愿意、也更能够主动、自信地使用数据工具,推动数据分析融入日常作业闭环。

图7 澄清工艺是否稳定?业务人员自助分析

响应能力的显著提升

当业务分析实现自助化,为制糖企业带来最直接的变化就是——业务响应速度得到显著提升。过去,从发现异常到形成分析结论,往往需要经过多环节传递与处理,周期以天计算,等结论出来时,问题可能已经扩大,甚至错过了最佳工艺调整窗口。

未来,在数据情景化的支撑下,业务人员将能够在当班内直接完成数据取用、对比分析和假设验证。例如,当澄清工段 pH 值刚出现连续偏移时,系统可在对应的业务情景中自动聚合相关指标,工艺人员即可当场判断是否需要调整加药或工艺参数;当产糖率与预期出现偏差时,也可快速定位问题根源,判别是前段抽提、澄清损失,还是后段回收效率所致。

这意味着,问题有望在“扩大之前”就被识别和处理,从而使生产运行从被动应对逐步转向主动预防与控制。

总体而言,数据情景化将帮助制糖企业真正把数据用活于业务。生产管理将不再高度依赖个人经验与事后分析,而是逐步形成一套以数据为驱动、以业务场景为依托的快速决策机制,生产运行也因此有望变得更加稳定、高效与可控。

无问智推|AI 驱动的生产洞察升级

在实际生产中,制糖行业逐渐形成一种共识:AI 技术在其中的真正价值,并非在于“替代人工思考”,而在于能够在问题尚未被明确提出之前,就已将所需的相关信息与洞察准备就绪

过去,行业中的中控系统更多地扮演着“被动工具”的角色。监控哪些指标、如何进行关联分析,完全依赖当班人员的个人经验:工艺人员需自行回忆关键指标、查找数据点位、调整分析的时间窗口。新接班的团队往往难以快速入手;而当经验丰富的老师傅不在场时,许多隐性的工艺逻辑与判断也难以得到有效复用。

在引入 TDengine IDMP 平台并完成数据情景化构建之后,AI 所扮演的角色将发生显著转变。它将不再被动等待指令,而是基于对工艺语义与业务上下文的理解,主动识别当前生产状态,并动态推荐最贴合该业务场景的监控视图与分析内容。这使得系统能够引导注意力,辅助不同经验层次的人员更快地聚焦于关键问题,从而将专家经验转化为可持续、可复用的系统能力

澄清段的一个真实场景

以澄清工段的澄清汁监控为例。过去,制糖行业在监控澄清段时,往往仅限于观察几条关键参数的实时曲线,难以系统性地判断“当前工况是否真正处于正常状态”或“其趋势是否正在恶化”。

现在,AI 会自动为用户推荐一整套符合澄清工艺逻辑的监控面板,只需简单的点击“生成”,TDengine IDMP 就能够自动生成监控看板。在澄清汁场景下,系统会优先推荐:

  • 过去一小时澄清汁 pH 的最新值,用于快速判断当前反应状态;
  • 过去一天每小时澄清汁锤度的平均值,帮助用户观察短周期稳定性;
  • 过去一周澄清汁还原糖的平均值,以及按天汇总的变化趋势,用于评估澄清效果对糖损的影响。

AI 推荐的澄清汁的监控面板

这些内容并不是“通用模板”,而是因为系统已经理解:这些指标正是澄清段最关键、最有业务意义的数据组合

从“人盯数据”到“系统叫人”

在引入 TDengine IDMP 之前,制糖行业对澄清段的监控更多依赖人工经验。中控画面上曲线一直在动,工艺人员需要长时间盯着趋势,凭感觉判断是不是“有点不对劲”。采用 TDengine IDMP 之后,这种状态发生了明显改变。基于已经完成的数据情景化,AI 不再等待人工提问,而是主动推荐与澄清汁相关的实时事件监控和分析,通过实时分析预警,能够在关键时刻把人“叫过来”。

在澄清汁场景中,系统能够自动推荐分析:

  • 当澄清汁加热器出口温度超过 105℃,并持续 5 分钟以上时,立即触发主要告警,同时给出该时段的平均出口温度,清楚提示存在过热风险;
  • 对澄清汁锤度,系统每 5 分钟基于 3 倍标准差的 K-sigma 方法进行异常检测,一旦波动异常,直接给出最大锤度值,帮助用户快速判断异常程度;
  • 系统还推荐每 10 分钟滚动计算过去 30 分钟内的平均流量,用于辅助判断当前负荷是否发生变化。

在过去,这些判断逻辑往往只掌握在少数经验丰富的工艺人员手中,依赖于人员持续盯守数据、反复比对分析才能得以运用。如今,通过引入 TDengine IDMP 平台,这些经验与逻辑得以被 AI 沉淀并固化为持续、自动运行的系统能力。生产管理模式由此从依赖“人盯数据”逐步转向为“系统预警、人员确认”的协同机制,使异常得以更早被识别,工艺调整也能更及时地执行。这正是 TDengine IDMP 为制糖行业生产管理带来的最直观价值——将隐性知识显性化,将个人经验转化为可持续、可复用的系统智能。

AI 自动推荐的实时分析场景

给制糖行业带来的真正变化

对制糖行业来说,最大的变化在于:正常时不被数据打扰,异常时绝不会被遗漏。
生产管理也由此从“人盯数据”转向“系统叫人”,让异常更早被发现,让调整更及时发生。这正是 TDengine IDMP 在实际生产中带给制糖行业的最直观价值。

应用成效|从“系统上线”到“价值落地”

随着该工业数据平台在生产现场的深入部署与应用,制糖企业有望在生产管理与工艺管控方面逐步收获系统性成效。整体解决方案围绕生产、工艺和设备三大核心对象展开,将推动数据不再仅仅停留在系统层面,而是持续融入日常运行与管理决策之中。

全流程生产监控:让制糖过程“看得见”

通过对制糖工艺流程进行统一的数据资产建模,平台实现了从预处理到干燥包装的全过程数据采集与集中监控。各工段之间原本割裂的数据被打通,形成连续、完整的生产视图。关键工艺参数和运行状态能够集中呈现,为现场管理、生产调度以及异常发现提供了直观、统一的支撑。

生成物料损耗分析:让损耗“算得清”

围绕工艺过程和物料流转,平台引入了系统化的数据分析与物料衡算方法,对糖分在关键环节中的变化进行结构化分析,使以往主要依赖经验判断的物料损耗问题,转变为可量化、可对比的结果。生产、工艺和设备状态对管理层更加透明,为工艺优化和质量管控提供了更有依据的决策支持。

各个工艺段制糖损耗分析

工艺质量实时监控:让生产“跑得稳”

围绕关键工艺参数和质量指标,平台构建了持续运行的工艺质量监控体系,对生产各环节的运行状态进行实时跟踪和对比分析,使工艺波动由事后发现逐步转变为过程可控。通过对工艺偏差和异常趋势的及时识别,有效降低了过程波动对产品质量的影响,推动生产运行保持稳定。

工艺质量状态在生产层和管理层之间更加透明,为工艺调整和质量管控提供了持续、可靠的数据依据,有效支撑制糖生产的稳定运行和产品质量的均质化。

澄清汁 PH 值实时监控

工艺质量异常告警(澄清汁 PH 值)

商业价值|制糖企业可持续演进的数字化底座

从行业应用与发展的角度来看,此类项目的价值并不仅体现在一次性的系统建设或阶段性验收上,更在于为企业构建了一套可长期演进、持续赋能的数字化底座。通过统一的数据标准与平台架构,制糖行业首次获得了对全生产过程进行持续感知、系统分析与长效优化的能力,这为后续的管理深化与智能应用奠定了坚实基础。

短期而言,项目的实施将有效提升生产透明度与运行稳定性;从中长期看,该平台有望逐步成长为支撑企业实现稳产、提质、降本与风险精准管控的核心基础设施。

行业意义|一条稳健、可落地的制糖数字化路径

适用企业

  • 希望持续提升管理水平和长期竞争力的甘蔗制糖企业
  • 正处于数字化转型关键阶段的中小规模糖厂

成功前提

  • 管理层对数字化目标和数据价值形成清晰、统一的认知
  • 具备相对稳定、连续的生产和设备数据基础

核心路径

  • 以“工艺 + 物料 + 设备”为主线,系统推进数字化建设
  • 按“看得见 → 算得清 → 跑得稳”的节奏逐步实施,避免激进投入
  • 在夯实数据基础之上,稳步迈向智能优化和 AI 应用

未来展望|通过组态强化生产过程与工艺质量管控

从预期效果来看,TDengine IDMP 将在生产数据采集、集中监控与分析方面为制糖企业打下坚实基础,从而有效支撑生产过程监控与工艺质量分析的日常需求。

在此基础上,企业可期待未来进一步引入并强化平台的组态能力,以更加直观、图形化的方式呈现工艺流程、设备运行状态与关键工艺参数。这将推动生产监控从以数据列表和图表展示为主,逐步升级为面向过程与运行状态的综合可视化管控。通过组态化配置关键质量指标和工艺约束条件,有助于将成熟的工艺经验固化为可自动执行的监控规则,提升对工艺偏差和质量风险的提前识别与主动干预能力,从而更好地服务于制糖生产长期、稳定、高效的运行目标。

关于海莱德

北京海莱德自动化工程有限公司成立于 2010 年,是国内工业自动化技术与解决方案提供商,在制糖行业自动化领域具有专业积累。公司业务覆盖系统设计、工程实施、调试及售后服务等全流程,并在食品饮料、汽车、电力、冶金、烟草和机械制造等行业积累了丰富工程经验。近年来,海莱德参与了多个“一带一路”糖厂的集中控制 DCS 系统及数字化系统的设计、供货与调试,持续推进从自动化向数字化、信息化和智能化方向升级,并结合涛思数据的时序数据库和 TDengine IDMP 平台建立起了对制糖企业真正高效、实用且易于掌握的,具备 AI 智能的数字化系统。

一、引言:为什么选择TextIn与Coze搭建财报机器人?

面对季度、年度财报堆叠如山的PDF文档,技术团队如何快速、准确地将其中复杂的表格数据转化为结构化信息?本文将介绍一种高效实践方案:利用TextIn的智能文档解析能力,结合Coze的自动化工作流编排,快速构建一个能够处理多格式财报、抽取关键表格的自动化流程。

1.1 财报文档的典型难点

财报处理长期存在几大核心难点:

1.表格结构复杂:资产负债表、利润表等核心表格常存在跨页、续表情况,且合并报表与母公司报表两套体系并存,单元格合并频繁,对程序的结构化识别构成首要挑战。

2.文档格式多样:资料库中通常是电子PDF与扫描件图像混合共存,要求解决方案同时具备强大的文本解析与OCR版面分析能力。

3.手工处理成本高昂:三大表及附注的手动复制、粘贴、核对工作极其耗时,且容易出错,难以满足及时性、准确性要求。

1.2 TextIn+Coze方案的核心价值

本方案采用清晰的分工架构,将复杂问题模块化:

TextIn xParse引擎负责“读懂”文档:其强大的版面分析与表格识别技术,能统一处理电子PDF与扫描件,将混乱的原始文档转换为包含完整表格结构、段落标题的清晰JSON数据,为下游提取提供高质量的结构化输入。
Coze工作流负责“串联”自动化流程:可自动化编排“文件上传→调用TextIn解析→定位并抽取目标表格→输出至数据库/Excel”的完整管道。
Coze Bot 提供交互层:可构建一个对话机器人,不仅支持触发自动化流程,更能基于抽取出的数据,提供报表摘要、关键指标对比、甚至问答解释,让数据结果可直接被业务人员使用。

这种组合将专业的文档解析、灵活的业务逻辑编排与友好的交互界面相结合,使开发者能聚焦于核心的抽取规则,快速搭建从原始文档到业务可用数据的端到端流水线。

二、方案应用速览

工作流:

图片

输出结果:

图片

三、架构设计

3.1 总体链路

用户上传财报 → Coze触发工作流 → xParse → 代码节点抽取 → 输出结构化tables


图片

开始节点:接收用户上传的财报文件(File)。
TextIn插件节点:将财报解析为结构化JSON,核心使用result.detail(包含paragraph/table/image等元素)以及result.markdown。
代码节点:仅遍历detail,通过“表标题 → 后续表格”方式抽取三大表,并统一输出为tables{balanceSheet,incomeStatement,cashFlow}。
结束节点:将tables / debug / markdown输出给Bot,用于展示与后续问答分析。

3.2 数据结构约定

TextIn xParse - 插件节点的输出(result.detail / result.markdown等,详情见TextIn xParse API文档:https://docs.textin.com/xparse/parse-getjson

Response
├─ code                               # 接口状态码
├─ message                            # 状态信息
└─ result
   ├─ markdown                         # 文档级 Markdown
   └─ detail[]                         # 元素明细数组(只处理 type=table)
      └─ (仅当 item.type == "table" 时关注)
         ├─ type                        # 固定为 "table"(表格块)
         ├─ sub_type                    # "bordered"(有线) / "borderless"(无线)
         ├─ page_id                     # 表格所在页(续表拼接用)
         ├─ paragraph_id                # 表格元素ID(续表拼接用)
         ├─ rows                        # 表格行数
         ├─ cols                        # 表格列数
         ├─ text                        # 表格整体文本(md/html;展示用,抽字段优先 cells)
         ├─ continue?                   # 是否跨页/跨段续表(可选字段)
         └─ cells[]                     # 单元格数组(抽取字段核心)
            ├─ row                       # 行号(从0开始)
            ├─ col                       # 列号(从0开始)
            ├─ row_span?                 # 行合并跨度(默认1)
            ├─ col_span?                 # 列合并跨度(默认1)
            └─ text                      # 单元格文本(字段值通常从这里拿)

TextIn的返回结果中对表格块(type=table)的两种常见数据形态(务必兼容)

形态 A:HTML/Markdown 表格(最常见于工作流插件输出)


    抽取方式:解析text→ 转二维矩阵(headers/rows)
    item.text内包含<table>...</table>(或Markdown table)
    item.type == "table"


形态 B:单元格数组cells(部分接口/参数下提供)

    item.cells[]存在,包含row/col/text等
    抽取方式:优先用cells拼matrix(更结构化),不存在再回退到解析tex






财务三大表抽取 - 代码节点的输出示例(tables)
tables.balanceSheet / incomeStatement / cashFlow均为数组,设计理由如下:

同一份财报可能包含“合并 + 母公司”两套表;
或者出现“(续)”导致一张表被拆成多段;
因此用数组承载多张/多段表更稳妥,业务侧可按title/page_id再做合并与筛选。

tables

{
    "balanceSheet": [
        {
            "headers": [
                "项 目",
                "附注",
                "2025 年6 月30 日",
                "2024 年12 月31 日"
            ],
            "page_id": [
                2
            ],
            "rows": [
                [
                    "流动资产:",
                    "",
                    "-",
                    "-"
                ],
            ],
            "title": "合并资产负债表"
        },
 
 
    ],
    "incomeStatement": [
        {
            "headers": [
                "项 目",
                "附注",
                "2025 年1-6 月",
                "2024 年1-6 月"
            ],
            "page_id": [
                4
            ],
            "rows": [
                [
                    "一、营业总收入",
                    "",
                    "88,095,798,091.41",
                    "85,336,441,428.97"
                ],
            ],
            "title": "母公司利润表"
        }
    ],
    "cashFlow": [
        {
            "headers": [
                "项 目",
                "附注",
                "2025 年1-6 月",
                "2024 年1-6 月"
            ],
            "page_id": [
                5
            ],
            "rows": [
                [
                    "一、经营活动产生的现金流量;",
                    "",
                    "-",
                    "-"
                ],
            ],
            "title": "母公司现金流量表"
        }
    ]
}

Debug

"debug": {
  "detailLen": 823,
  "titleCandidates": 6,
  "hitTitles": [
    {"idx": 120, "page_id": 2, "title": "合并资产负债表"},
    {"idx": 260, "page_id": 4, "title": "母公司利润表"}
  ],
  "picked": [
    {"titleIdx": 120, "tableIdx": 125, "tableType": "balanceSheet"},
    {"titleIdx": 260, "tableIdx": 268, "tableType": "incomeStatement"}
  ],
  "tableBlocks": 12
}

3.3 关键设计点(财报专属)

标题命中策略(table_title + 关键词)
标题长度阈值(>20 跳过):避免长文档中出现“包含关键词的长句”被误判为表标题,从而误抽无关表格。
只认sub_type=table_title:优先使用版面分析识别到的“表格标题”元素,减少正文段落(header/text)误命中概率。

const TITLE_PATTERNS = {
  balanceSheet: ["资产负债表", "合并资产负债表", "母公司资产负债表"],
  incomeStatement: ["利润表", "合并利润表", "母公司利润表", "损益表", "收益表"],
  cashFlow: ["现金流量表", "合并现金流量表", "母公司现金流量表", "现金流量"],
};

function normalizeTitle(s) {
  return String(s || "")
    .replace(/\*\*/g, "")
    .replace(/[\s ]/g, "")
    .replace(/[《》]/g, "");
}
function matchType(norm) {
  for (const [k, kws] of Object.entries(TITLE_PATTERNS)) {
    if (kws.some(kw => norm.includes(kw))) return k;
  }
  return null;
}

function extractFromDetail(detail) {
  const tables = { balanceSheet: [], incomeStatement: [], cashFlow: [] };
  const debug = { hitTitles: [], picked: [], tableBlocks: 0, titleCandidates: 0 };

  for (let i = 0; i < detail.length; i++) {
    const item = detail[i];
    if (!item || typeof item !== "object") continue;

    const rawTitle = String(item.text || "");
    const title = normalizeTitle(rawTitle);

    // ✅ 简单校验:标题长度太长跳过
    if (title.length > 20) continue;

    // ✅ 查询TextIn接口返回数据中的表格标题,避免正文误命中
    if (String(item.sub_type || "").toLowerCase() !== "table_title") continue;

    const ttype = matchType(title);
    if (!ttype) continue;

四、准备工作

TextIn 开发者信息(x-ti-app-id / secret_code)

图片

在TextIn控制台(https://www.textin.com/)「开发者信息」中获取x-ti-app-id与x-ti-secret-code(下文统称 app_id/secret_code)。
建议在Coze工作流里把鉴权参数作为开始节点输入传入(便于不同环境切换),或在团队内部用变量/密钥管理统一配置。

五、工作流搭建

5.1 创建工作流

工作流命名、描述、版本说明
图片

5.2 开始节点配置

Input类型:File(接收上传文件)

图片

5.3 添加 xParse插件节点

输入映射:file → Input.file
鉴权配置:x_ti_app_id / x_ti_secret_code
输出字段说明:result.detail / result.markdown 等,输出重点使用:ParseX.result(作为代码节点输入),其中result.detail是抽表主数据源。


图片

5.4 添加代码节点(核心)

输入变量配置 (选择ParseX.result)

图片

代码职责:遍历detail→找table_title→找后续table→HTML转二维矩阵→输出 tables(代码节点源码附在文章最末尾)

图片

输出结构:tables{balanceSheet,incomeStatement,cashFlow} +debug

5.5 结束节点输出

输出给Agent:tables / markdown / debug

六、不止于抽取:更多自动化扩展方向

财报抽取机器人是一个高效的起点,接下来,基于TextIn提供的精准结构化数据与Coze灵活的工作流,还可以轻松延伸出更多智能化的数据处理能力:

续表自动合并:财报中经常存在大型表格跨页,可在工作流中添加逻辑节点,按title相同且表头一致合并 rows,并合并 page_id,彻底解决数据割裂问题。
表内锚点词校验:为确保抽取表格的完整性与正确性,可设计自动校验规则。例如,检查资产负债表中是否同时存在“流动资产”/“资产总计”科目;验证利润表是否包含“营业收入”/“净利润”;确认现金流量表是否包含“经营活动”。这一步能有效拦截因解析页面错误或文档版本差异导致的重大数据缺失。
结构化导出至Excel:将最终整理的tables列表,通过添加代码节点或Coze插件,转换为更通用的CSV或XLSX格式文件。这能让财务、业务部门的同事无缝接手,直接在Excel环境中进行后续分析与可视化。
实现智能多期对比:将工作流升级为可接收两份财报,分别提取后,系统能根据标准化的会计科目名称自动对齐数据,计算关键项目的同比、环比变化,并可由集成的LLM输出差异分析简报。

通过TextIn与Coze的组合,我们完成了从杂乱文档到结构化数据,再到可交互、可扩展的业务工具的完整路径,构建了一个可靠、可重复、且持续进化的数据流水线。无论是应对合规检查,还是满足定期的经营分析,这个财报机器人都能成为你技术工具箱中一个反应迅速、值得信赖的数字化助手。
现在,是时候告别手动处理的繁琐与不确定,让你的数据工作流真正“智能”起来。

七、附:代码节点源码

下载链接:https://dllf.textin.com/download/2026/CustomService/财报提取-coze代码节点源码.js

2026年初,工业和信息化部等五部门联合印发的《工业绿色微电网建设与应用指南(2026—2030年)》,为能源的数字化转型铺设了清晰的政策轨道。目前,全国已投入运行的工业绿色微电网项目超过300个,它们正从试点走向规模化。智慧能源的管理,正从传统的报表与经验,向一个全域可视、实时交互、智能决策的数字世界加速演进。

01 政策引领,智慧能源按下“加速键”

国家层面正在以前所未有的力度,推进能源系统的数字化转型。《工业绿色微电网建设与应用指南》明确将智慧能源管控系统,列为绿色微电网建设的核心内容之一。其目标是构建一个集成光伏、风电、储能、氢能等多能互补,并实现与大电网友好互动的综合能源系统。未来的能源管理,必须是数字化、可视化、智能化的。

02 现实挑战:智慧能源的进阶痛点

然而,理想蓝图在落地时,却面临着一系列棘手的现实挑战。当前的核心痛点可以概括为:“看不见、摸不清、调不动”。

  • 状态“看不见”:一个现代化的能源场站或微电网,包含成千上万的设备与传感器。传统分散的图表和报表,让管理者难以在短时间内掌握全局状态,如同“开盲盒”。
  • 逻辑“摸不清”:SCADA、PLC、IoT等系统数据格式各异,形成信息壁垒。当发生故障时,运维人员需要跨多个系统排查,难以快速穿透网络层、服务器层、应用层,精准定位根源。
  • 协同“调不动”:为了实现对复杂系统的精细化管理,数字孪生技术正成为标配。但这些高精度三维模型对终端电脑的图形性能要求极高,导致许多一线运维人员无法流畅使用,远程协同和移动办公更是困难重重。

03 破局关键:实时云渲染让智慧能源“轻装上阵”

点量云流实时云渲染其核心原理是将海量三维模型的计算与渲染任务放在云端强大的服务器集群上完成,前端终端(无论是高性能工作站、普通笔记本,还是平板电脑)只需通过网页或轻量客户端,接收经过云端处理的视频流即可进行操作。

相较于传统网页3D效果受模型大小限制,点量云流实时云渲染能够在不消耗终端硬件性能的情况下,实现无需等待加载、即时打开与实时交互的体验。

这一转变带来了三个根本性改变:

  • 终端解放:运维人员不再受本地硬件性能束缚,用一台普通办公电脑或移动设备,就能流畅操控大型能源场(如风、电、煤等)的实景数字孪生模型。
  • 数据安全:所有核心模型与数据始终保存在云端服务器,前端只传输视频流,从根本上杜绝了三维数字资产通过终端泄露的风险。
  • 高效协同:不同地域的专家可以同时接入同一个三维场景,基于统一的、可视化的模型进行会诊、标注和决策,极大提升了跨团队协作效率。

04 实战图景:可视化如何重塑能源管理

技术的价值,最终需要体现在真实的场景中。当实时云渲染技术卸下了硬件的重担,一系列曾经难以落地的应用,正悄然成为智慧能源管理的日常。
1、运维:从“被动响应”到“主动预警”
基于高精度数字孪生模型,系统能深度融合实时数据与AI算法,提前洞察设备亚健康状态,精准预测潜在故障。运维策略由此从紧急抢修的“被动处置”,转向计划性干预的“主动预防”。而这一切得以实现的关键,在于实时云渲染技术让这套复杂的三维预警系统,得以在各级管理中心的普通电脑上便捷访问与联动,使预防性维护真正触手可及。

2、管理:从“分散孤岛”到“全域一张图”
传统管理中,物理设备、网络流量、业务数据往往分散于不同系统,形成信息壁垒。如今,通过实时云渲染技术,这些要素被整合进一个统一的动态三维界面,生成能源系统的“全景作战图”。结合云推流能力,无论是集控中心的大屏,还是巡检人员的移动终端,都能获得一致、流畅且可交互的全局视角,真正实现了“全域可视、全局可控”的集中化管控。

3、效率:从“人工跑腿”到“远程会诊”
对于地处偏远的风电场或水电站,专家亲赴现场耗时费力。通过点量云渲染平台,专家在千里之外即可指挥实景复刻的虚拟现场,通过三维模型远程指导一线人员排查故障,将响应时间从数小时大幅压缩至分钟级。这不仅是距离的缩短,更意味着高精度模型得以在PC、平板等多终端安全、流畅地访问,显著提升了跨地域协同、应急指挥与人员培训的效能。

随着实时云渲染技术与能源体系的深度融合,智慧能源的管理模式将迎来根本性变革。高精度的能源系统数字孪生将不再受限于本地硬件,而是通过云推流技术,成为在任何终端均可流畅访问与协同操作的“活地图”。从宏观调度到微观运维,决策都将基于一张全域同步、实时可视、深度交互的动态图谱。

这不仅是技术的叠加,更是从“经验驱动”到“全景数据驱动”的智慧跃迁。一个更高效、更透明、更坚韧的能源时代,正借由这条“云端高速路”,清晰地向我们驶来。

我是不相信第三方 API(主要是收费),所以自己找了一下
天气网: https://www.weather.com.cn/

image
打开网站,抓包就能看到怎么获取的,还可以白嫖 IP whois,这样就可以根据 IP 获取用户位置再查天气。
天气信息: https://d1.weather.com.cn/weather_index/101200101.html?_=1769051590198
这里怎么解析我就不赘述了,既然来这个论坛,解析这些数据都不是问题。
1、这个请求防盗链,需要加上 referer。
2、这个 101200101 怎么来的?

第一个问题看看就知道了,这里只说第二个问题。
101200101 这个不是随便编的 id,这个数字是城市代码
中国国家气象局天气预报信息接口: https://www.cnblogs.com/youlixishi/articles/3612035.html
中国天气网城市代码: https://blog.csdn.net/weixin_30492047/article/details/98287973

如果只是网上搜,那就是玄阶功法了,毕竟网上信息不一定及时,行政区划会有变化。
直接去爬
https://topic.alibabacloud.com/a/china-weather-net-interface-font-colorredreturnfont-font-colorredjsonfont-format-parsing-and-interface-xml-picture-interface-description_1_11_30654886.html
获取省级代码: http://www.weather.com.cn/data/list3/city.xml?level=1
比如湖北是 20
获取城市代码: https://www.weather.com.cn/data/list3/city20.xml?level=2
比如武汉市是 2001
获取区代码: https://www.weather.com.cn/data/list3/city2001.xml?level=3
这里城市天气代码,不是行政区划,所以有的没有(可能数据没更新,反正是顺序递增)
最终就是 101+20+01+01
打开报错是吧?
image
楼主,打开报错了。
那是因为显示的.xml 实际上不是 xml 文件,浏览器当 xml 渲染当然报错,直接查看源代码。
image
直接 view-source
image
简易信息

生成式 AI 的投资回报远超预期?Snowflake 调研全球 1900 位企业与 IT 专业人士后发现平均 ROI 高达 41%!点击下载完整报告

在当今竞争激烈的电商领域,为客户提供个性化体验已不再是奢侈选项,而是驱动成功的关键要素。运用人工智能驱动分析、数据科学与机器学习的企业正日益超越竞争对手。消费者越来越期待定制化推荐与动态购物体验——这正是 Snowflake ML 的用武之地。

 

通过 Snowflake ML,开发者和分析师可直接在 Snowflake 平台中使用标准 SQL 实现以下功能:

  • 加载与整合数据

  • 构建客户细分画像

  • 训练并部署机器学习模型

  • 生成个性化评分

  • 将结果输送到应用与实时工作流中

 

本文将深入探讨 Snowflake ML 如何为现代电商体验提供简洁、基于 SQL 的个性化解决方案。您将了解如何将客户数据接入 Snowflake,根据行为模式划分客群,并利用 Snowflake ML 构建预测高价值客户的智能模型。无论您是构建个性化工作流的开发者,还是提升营销效果的分析师,这些实践步骤都将助您快速入门。

 

请首先登录您的 Snowflake 账户(访问 Snowflake 网页控制台)。若尚未拥有账户或需测试环境进行学习,可在此免费注册体验。

步骤 1:加载并准备数据

我们将首先创建一个客户订单的小型模拟数据集。

请在 Snowflake SQL 工作表中完整运行以下代码块: 

-- Step 1.0: Create a database and schemaCREATE OR REPLACE DATABASE DATACLOUDDISPATCHSI;USE DATABASE DATACLOUDDISPATCHSI;CREATE OR REPLACE SCHEMA ECOMMERCE;USE SCHEMA ECOMMERCE;-- Step 1.1: Create a customer orders tableCREATE OR REPLACE TABLE CUSTOMER_ORDERS (  CUSTOMER_ID  NUMBER,  ORDER_ID     NUMBER,  ORDER_DATE   DATE,  ORDER_VALUE  NUMBER(10,2),  PRODUCT_ID   NUMBER);-- Step 1.2: Insert sample order dataINSERT INTO CUSTOMER_ORDERS (CUSTOMER_ID, ORDER_ID, ORDER_DATE, ORDER_VALUE, PRODUCT_ID) VALUES(1001,50001,'2023-01-15', 89.99,201),(1001,50022,'2023-03-02',120.49,305),(1002,50110,'2023-05-11', 45.00,110),(1003,50155,'2023-02-19',239.00,402),(1003,50190,'2023-05-22',130.00,233),(1003,50201,'2023-06-01', 99.99,110),(1004,50333,'2023-01-05', 19.99,502),(1001,50390,'2023-11-11',301.00,900),(1005,50400,'2023-12-12', 67.50,702);-- Step 1.3: Verify dataSELECT * FROM CUSTOMER_ORDERS ORDER BY ORDER_DATE;
复制代码

 

该数据集包含重复的客户购买记录、多样化的订单金额以及用于后续客户分群和机器学习建模的实用字段,足以支持基础建模需求。

使用 Snowflake Workspace

若您倾向于通过可视化界面而非 SQL 加载数据,Snowflake Workspace 支持将文件(包括 Excel 和 CSV 格式)直接拖放至环境中。

 

1. 在 Snowflake 左侧导航栏中进入 Projects。

2. 点击下拉菜单中的 Workspaces(如图所示)。

3. 创建并打开一个新的 Workspace。

4. 在 Workspace 内点击+ Worksheet 以新建 SQL 工作表。

5. 运行 SQL 代码前,请确保工作表已设置正确的角色、仓库、数据库与模式。

 

Article content

本教程步骤 1 至 3 中的所有 SQL 命令均需在此 SQL 工作表中粘贴并执行。Snowflake 虽提供 Workspace、Notebook 等多种项目工具,但本教程全程使用标准 SQL 工作表完成。

步骤二:使用 SQL 构建客户细分模型

Snowflake 支持集成机器学习模型,用于预测客户行为、推荐产品及定制促销策略。开发人员可通过 Python 或 R 语言,结合 Snowflake 的 Data Science Workspace 部署模型,该模型可输入客户数据并输出个性化推荐。

 

一种基础的个性化策略是基于客户历史行为进行识别,我们将计算以下指标:

  • 购买频率

  • 客单价(AOV)

USE DATABASE DATACLOUDDISPATCHSI;USE SCHEMA ECOMMERCE;
复制代码

-- Step 2.1: Create customer segmentsCREATE OR REPLACE TABLE CUSTOMER_SEGMENTS ASSELECT  CUSTOMER_ID,  COUNT(ORDER_ID)  AS PURCHASE_COUNT,  AVG(ORDER_VALUE) AS AVG_ORDER_VALUEFROM CUSTOMER_ORDERSWHERE ORDER_DATE BETWEEN '2023-01-01' AND '2023-12-31'GROUP BY CUSTOMER_ID;-- Step 2.2: Inspect customer segmentsSELECT * FROM CUSTOMER_SEGMENTS ORDER BY PURCHASE_COUNT DESC;
复制代码

 

由此构建的 CUSTOMER_SEGMENTS 表将成为机器学习模型的基础数据层。 

步骤三:训练与部署机器学习模型(基于 Snowflake ML 的纯 SQL 实现)

Snowflake ML 支持直接使用 SQL 训练模型,无需依赖 Python 或外部工具。

我们将完成以下任务:

1. 标记“高价值客户”(购买次数 ≥3 次)

2. 训练分类模型

3. 对全部客户进行评分

步骤 3.1:创建训练表

在 Snowflake 中训练机器学习模型前,需为模型提供学习样本。这意味着需要构建一个包含以下内容的表:

  • 特征(模型学习的输入变量)

  • 目标标签(模型需预测的结果)

 

本例中,我们的目标是识别高价值客户。因此,需要在历史数据中创建一列,明确标注哪些客户属于高价值客户。训练表的作用正在于此——它基于步骤二生成的客户分群,新增目标标签列。随后,Snowflake ML 将利用此标注表学习高价值客户的特征模式。

 

-- Step 3.1: Add a target label for modelingCREATE OR REPLACE TABLE CUSTOMER_SEGMENTS_TRAIN ASSELECT    CUSTOMER_ID,    PURCHASE_COUNT,    AVG_ORDER_VALUE,    IFF(PURCHASE_COUNT >= 3, 1, 0) AS TARGET_HIGH_VALUEFROM CUSTOMER_SEGMENTS;SELECT * FROM CUSTOMER_SEGMENTS_TRAIN ORDER BY PURCHASE_COUNT DESC;
复制代码

步骤 3.2:使用 Snowflake ML 训练分类模型

在获得已标注的训练表后,即可训练 Snowflake ML 识别高价值客户的潜在特征。通过训练分类模型,Snowflake 将学习:

  • 应从哪些输入特征中学习规律(如购买次数与平均订单金额)

  • 需要预测的目标结果(即高价值标签:0 或 1)

 

-- Step 3.2: Train the classification modelCREATE OR REPLACE SNOWFLAKE.ML.CLASSIFICATION HIGH_VALUE_MODEL (    INPUT_DATA     => SYSTEM$REFERENCE('TABLE', 'ECOMMERCE.CUSTOMER_SEGMENTS_TRAIN'),    TARGET_COLNAME => 'TARGET_HIGH_VALUE');Snowflake automatically trains and tunes the model based on your training table.(Optional) View metrics:CALL HIGH_VALUE_MODEL!SHOW_EVALUATION_METRICS();
复制代码

步骤 3.3:使用模型对客户进行评分(SQL)

模型训练完成后,即可用于预测。在此步骤中,模型将根据每位客户的购买行为(购买次数与平均订单金额)判断其是否为潜在高价值客户。

 

以下 SQL 命令将每位客户的特征输入模型,并返回预测结果:

-- Step 3.3: Score customersSELECT    s.CUSTOMER_ID,    s.PURCHASE_COUNT,    s.AVG_ORDER_VALUE,    HIGH_VALUE_MODEL!PREDICT(        INPUT_DATA => OBJECT_CONSTRUCT(            'PURCHASE_COUNT', s.PURCHASE_COUNT,            'AVG_ORDER_VALUE', s.AVG_ORDER_VALUE        )    ) AS MODEL_OUTPUTFROM CUSTOMER_SEGMENTS AS s;
复制代码
MODEL_OUTPUT 是什么?

 

Snowflake 将模型的预测结果以 VARIANT 类型(一种结构化对象)返回。您无需运行或执行它——它仅仅是 Snowflake 所展示的结果!

 

为了使预测结果更易于使用,您可以只提取预测类别(0 或 1)。

 

  • 1 表示模型将客户识别为高价值客户

  • 0 表示非高价值客户

 

提取预测类别的语句为:

SELECT    CUSTOMER_ID,    PURCHASE_COUNT,    AVG_ORDER_VALUE,    HIGH_VALUE_MODEL!PREDICT(        INPUT_DATA => OBJECT_CONSTRUCT(            'PURCHASE_COUNT', PURCHASE_COUNT,            'AVG_ORDER_VALUE', AVG_ORDER_VALUE        )    ):PREDICTION:"class"::NUMBER AS PREDICTED_HIGH_VALUEFROM CUSTOMER_SEGMENTS;
复制代码

 

这将为您提供一个清晰的 0/1 指标,用于判断客户是否被视为“高价值客户”。

 步骤 3.4:持久化个性化评分(可选)

至此,您已通过在查询中直接使用模型生成预测,这非常适合探索性分析——但在实际场景中,您通常需要将这些预测存储到表中,以便供仪表板、应用程序、营销活动等重复使用。

 

以下 SQL 语句创建一个名为 CUSTOMER_VALUE_SCORES 的新表,其中包含每位客户、其购买行为以及模型的预测结果。

CREATE OR REPLACE TABLE CUSTOMER_VALUE_SCORES ASSELECT    CUSTOMER_ID,    PURCHASE_COUNT,    AVG_ORDER_VALUE,    HIGH_VALUE_MODEL!PREDICT(        INPUT_DATA => OBJECT_CONSTRUCT(            'PURCHASE_COUNT', PURCHASE_COUNT,            'AVG_ORDER_VALUE', AVG_ORDER_VALUE        )    ):PREDICTION:"class"::NUMBER AS PREDICTED_HIGH_VALUEFROM CUSTOMER_SEGMENTS;SELECT * FROM CUSTOMER_VALUE_SCORES ORDER BY PREDICTED_HIGH_VALUE DESC;
复制代码

 

现在您已拥有一个可用于下游个性化流程的数据表。您可以持续引用这些评分来定位高价值客户、触发个性化优惠、提供推荐内容等。

步骤四:实时个性化

获得每位客户的预测评分后,即可结合实时行为数据提供更智能的个性化推荐。实时行为数据包括:

  • 最近浏览的商品

  • 购物车中新增或移除的商品

  • 浏览或会话事件

  • 实时库存更新

针对更高级的用例,Snowflake 支持在线特征存储,允许应用程序(如网站或推荐引擎)在毫秒级延迟内获取最新的客户特征——包括近期点击行为、会话历史或模型生成的评分。这对于需要在应用用户体验中实现实时个性化(而非依赖批量调度)的场景尤为理想。

 

Snowflake 可通过 Kafka、Kinesis 或 Event Hubs 等工具接收此类流式数据,从而根据客户行为变化持续更新推荐结果。

 

为保持个性化数据的时效性,您还可以通过 Snowflake 任务定期更新推荐表。以下示例展示了一个每小时运行并刷新热门商品推荐的简化任务:

 

--示例:定期更新推荐数据

 

CREATE OR REPLACE TASK PERSONALIZE_RECOMMENDATIONSWAREHOUSE = COMPUTE_WHSCHEDULE = 'USING CRON 0   UTC'ASMERGE INTO LATEST_RECOMMENDATIONS tgtUSING (    SELECT CUSTOMER_ID, PRODUCT_ID, SCORE    FROM ECOMMERCE.RECOMMENDATIONS_STREAM    WHERE SCORE > 0.8) srcON tgt.CUSTOMER_ID = src.CUSTOMER_ID AND tgt.PRODUCT_ID = src.PRODUCT_IDWHEN MATCHED THEN UPDATE SET SCORE = src.SCOREWHEN NOT MATCHED THEN INSERT VALUES (src.CUSTOMER_ID, src.PRODUCT_ID, src.SCORE);
复制代码

此方案使您的应用程序能够始终查询最新、最相关的推荐结果,从而实现完全动态的个性化购物体验。

总结

个性化推荐现已不再局限于手动规则或外部机器学习流水线。借助 Snowflake ML,您可以在 Snowflake 平台内直接驱动端到端的电商个性化推荐。本教程展示了如何:

  • 将全部电商数据整合至统一的单一平台

  • 完全使用 SQL 构建客户细分模型

  • 通过 Snowflake ML 训练机器学习模型——无需 Python 环境

  • 完成客户评分并生成个性化洞察

  • 利用实时数据流和任务机制保持推荐结果动态更新

最关键的是,所有操作均在 Snowflake 内完成——无需数据迁移、无需配置 Python 环境、无需依赖外部服务。这使得开发者、分析师和数据团队能够以前所未有的便捷度,提供高度个性化的购物体验。

 

注:本教程使用 SQL 和 Snowflake ML 进行演示,但 Snowflake 还提供更多人工智能与智能增强功能,可助力规模化扩展个性化应用场景。

想要一键复制代码以便跟随操作吗?

以下是您可以粘贴到 SQL workspace 中的分步最小可复现工作流程:

-- ============================================================-- E-COMMERCE PERSONALIZATION QUICKSTART (SQL-ONLY)-- End-to-end example:--  1. Create database & schema--  2. Load sample orders data--  3. Build customer segments--  4. Prepare training data for ML--  5. Train Snowflake ML classification model--  6. Score customers & optionally persist scores-- ============================================================------------------------------------------------------------------ (Optional) Step 0: Choose a warehouse------------------------------------------------------------------ Uncomment and replace <YOUR_WAREHOUSE> if needed:-- USE WAREHOUSE <YOUR_WAREHOUSE>;------------------------------------------------------------------ Step 1: Create database, schema, and sample CUSTOMER_ORDERS----------------------------------------------------------------CREATE OR REPLACE DATABASE DATACLOUDDISPATCHSI;USE DATABASE DATACLOUDDISPATCHSI;CREATE OR REPLACE SCHEMA ECOMMERCE;USE SCHEMA ECOMMERCE;-- Create the orders tableCREATE OR REPLACE TABLE CUSTOMER_ORDERS (  CUSTOMER_ID  NUMBER,  ORDER_ID     NUMBER,  ORDER_DATE   DATE,  ORDER_VALUE  NUMBER(10,2),  PRODUCT_ID   NUMBER);-- Insert sample e-commerce dataINSERT INTO CUSTOMER_ORDERS (CUSTOMER_ID, ORDER_ID, ORDER_DATE, ORDER_VALUE, PRODUCT_ID) VALUES(1001,50001,'2023-01-15', 89.99,201),(1001,50022,'2023-03-02',120.49,305),(1002,50110,'2023-05-11', 45.00,110),(1003,50155,'2023-02-19',239.00,402),(1003,50190,'2023-05-22',130.00,233),(1003,50201,'2023-06-01', 99.99,110),(1004,50333,'2023-01-05', 19.99,502),(1001,50390,'2023-11-11',301.00,900),(1005,50400,'2023-12-12', 67.50,702);-- Quick preview of raw ordersSELECT * FROM CUSTOMER_ORDERS ORDER BY ORDER_DATE;------------------------------------------------------------------ Step 2: Build customer segments (frequency & average order value)------------------------------------------------------------------ Aggregate behavior to create one row per customerCREATE OR REPLACE TABLE CUSTOMER_SEGMENTS ASSELECT    CUSTOMER_ID,    COUNT(ORDER_ID)  AS PURCHASE_COUNT,    AVG(ORDER_VALUE) AS AVG_ORDER_VALUEFROM CUSTOMER_ORDERSWHERE ORDER_DATE BETWEEN '2023-01-01' AND '2023-12-31'GROUP BY CUSTOMER_ID;-- Inspect segmentsSELECT * FROM CUSTOMER_SEGMENTS ORDER BY PURCHASE_COUNT DESC;------------------------------------------------------------------ Step 3: Prepare training data for Snowflake ML-- Add a label indicating whether a customer is “high-value”-- (in this example: 3 or more purchases)----------------------------------------------------------------CREATE OR REPLACE TABLE CUSTOMER_SEGMENTS_TRAIN ASSELECT    CUSTOMER_ID,    PURCHASE_COUNT,    AVG_ORDER_VALUE,    IFF(PURCHASE_COUNT >= 3, 1, 0) AS TARGET_HIGH_VALUEFROM CUSTOMER_SEGMENTS;-- View training data with targetSELECT * FROM CUSTOMER_SEGMENTS_TRAIN ORDER BY PURCHASE_COUNT DESC;------------------------------------------------------------------ Step 4: Train a classification model with Snowflake ML-- This learns to predict TARGET_HIGH_VALUE from the features-- PURCHASE_COUNT and AVG_ORDER_VALUE.----------------------------------------------------------------CREATE OR REPLACE SNOWFLAKE.ML.CLASSIFICATION HIGH_VALUE_MODEL (    INPUT_DATA     => SYSTEM$REFERENCE('TABLE','ECOMMERCE.CUSTOMER_SEGMENTS_TRAIN'),    TARGET_COLNAME => 'TARGET_HIGH_VALUE');-- (Optional) Inspect training metricsCALL HIGH_VALUE_MODEL!SHOW_EVALUATION_METRICS();------------------------------------------------------------------ Step 5: Score customers with the trained model-- This returns the predicted class (0 = not high-value, 1 = high-value).----------------------------------------------------------------SELECT    CUSTOMER_ID,    PURCHASE_COUNT,    AVG_ORDER_VALUE,    HIGH_VALUE_MODEL!PREDICT(        INPUT_DATA => OBJECT_CONSTRUCT(            'PURCHASE_COUNT', PURCHASE_COUNT,            'AVG_ORDER_VALUE', AVG_ORDER_VALUE        )    ):PREDICTION:"class"::NUMBER AS PREDICTED_HIGH_VALUEFROM CUSTOMER_SEGMENTSORDER BY PREDICTED_HIGH_VALUE DESC, PURCHASE_COUNT DESC;------------------------------------------------------------------ Step 6 (Optional): Persist personalized scores for downstream use-- This creates a reusable table that other teams, dashboards,-- and applications can query.----------------------------------------------------------------CREATE OR REPLACE TABLE CUSTOMER_VALUE_SCORES ASSELECT    CUSTOMER_ID,    PURCHASE_COUNT,    AVG_ORDER_VALUE,    HIGH_VALUE_MODEL!PREDICT(        INPUT_DATA => OBJECT_CONSTRUCT(            'PURCHASE_COUNT', PURCHASE_COUNT,            'AVG_ORDER_VALUE', AVG_ORDER_VALUE        )    ):PREDICTION:"class"::NUMBER AS PREDICTED_HIGH_VALUEFROM CUSTOMER_SEGMENTS;-- Final scored outputSELECT * FROM CUSTOMER_VALUE_SCORESORDER BY PREDICTED_HIGH_VALUE DESC, PURCHASE_COUNT DESC;
复制代码

原文地址:https://www.linkedin.com/pulse/how-leverage-snowflake-intelligence-e-commerce-personalization-60fhc/?trackingId=SamHZTb8T76gKESH2PP2SA%3D%3D

【Unity Shader Graph 使用与特效实现】专栏-直达

Billboard节点是UnityShaderGraph中一个功能强大的顶点变换工具,专门用于实现面向相机的渲染效果。在实时渲染中,Billboard技术被广泛应用于粒子系统、植被渲染、UI元素和特效制作等领域,能够确保特定物体始终面向摄像机,从而提供最佳的视觉效果。

Billboard技术概述

Billboard技术源于计算机图形学中的精灵渲染概念,其核心思想是通过动态调整物体的朝向,使其始终面对观察者。这种技术在游戏开发中具有重要价值:

  • 在粒子系统中用于渲染烟雾、火焰、魔法效果等动态元素
  • 在开放世界游戏中用于优化树木和植被的渲染性能
  • 在UI系统中确保界面元素始终以正确角度显示
  • 在特效制作中创建各种视觉欺骗效果

UnityShaderGraph中的Billboard节点封装了这一复杂技术,让开发者能够通过可视化方式轻松实现面向相机的渲染效果,无需编写复杂的着色器代码。

节点端口详解

Billboard节点包含多个输入和输出端口,每个端口都有特定的功能和用途。

输入端口

Position OS端口接收物体空间的顶点位置数据。这个端口是Billboard变换的基础,提供了需要进行旋转的原始顶点坐标信息。在实际应用中,这个端口通常直接连接到顶点着色器的位置输出,或者与其他位置变换节点相连。

Normal OS端口处理物体空间的法线向量。法线数据对于光照计算至关重要,Billboard节点会对法线进行相应的旋转,确保光照效果在物体旋转后仍然正确。如果忽略法线变换,可能会导致光照异常或材质表现不正确。

Tangent OS端口管理物体空间的切线向量。切线主要用于法线贴图和某些高级着色效果,Billboard节点会同步旋转切线数据,保持与顶点和法线的一致性。在需要复杂材质表现的场景中,正确的切线变换尤为重要。

输出端口

Position输出端口提供旋转后的物体空间顶点位置。这是Billboard节点的核心输出,包含了经过相机对齐变换后的顶点坐标。这个输出通常直接连接到主节点的顶点位置输入,完成最终的顶点变换。

Normal输出端口返回旋转后的物体空间法线向量。变换后的法线确保了光照计算与物体新朝向的一致性,对于保持材质视觉真实性至关重要。

Tangent输出端口提供旋转后的物体空间切线向量。这个输出确保了法线贴图和其他依赖切线空间的着色效果能够正确工作。

控件参数解析

Billboard Mode是Billboard节点最重要的控制参数,决定了物体的对齐方式和旋转行为。

All Axis模式

All Axis模式实现完全相机对齐,物体的所有坐标轴都会与相机坐标系对齐。在这种模式下,物体会完全面向相机,类似于始终正对观察者的广告牌。

这种模式的特点包括:

  • 物体完全面向相机,保持正面朝向观察者
  • 所有轴向都会根据相机方向进行旋转
  • 适用于需要完全正面展示的效果,如粒子特效、公告板文字等
  • 在VR和AR应用中特别有用,确保UI元素始终面向用户

All Axis模式的一个典型应用场景是粒子系统中的精灵渲染。当相机移动时,每个粒子都会自动调整方向,始终以最佳角度面向观察者,从而保证视觉效果的一致性。

Around Y Axis模式

Around Y Axis模式提供受限的对齐方式,物体仅围绕Y轴旋转,保持Y轴方向不变。这种模式在保持物体部分方向稳定的同时,实现基本的面向相机效果。

这种模式的特点包括:

  • 物体围绕世界空间或物体空间的Y轴旋转
  • X轴和Z轴与相机对齐,但Y轴保持原有方向
  • 适用于树木、路灯等需要保持垂直方向的物体
  • 在开放世界游戏中广泛用于植被渲染优化

Around Y Axis模式在大型场景的性能优化中特别有用。通过将3D树木替换为Billboard四边形,可以大幅减少渲染负载,同时通过限制Y轴旋转保持视觉上的自然感。

技术实现原理

理解Billboard节点的内部工作原理有助于更好地使用和调试相关效果。

顶点变换矩阵

Billboard节点的核心是基于视图矩阵的逆向变换。本质上,它计算相机的旋转矩阵,然后将这个旋转应用于输入的顶点数据。在All Axis模式下,节点会提取相机的完整旋转矩阵;而在Around Y Axis模式下,则会提取并修改旋转矩阵,将Y轴分量重置为单位矩阵的Y轴。

数学上,这个过程可以表示为:

旋转矩阵 = 提取相机旋转矩阵
如果模式为Around Y Axis:
    旋转矩阵[1] = [0, 1, 0] // 重置Y轴
变换后位置 = 旋转矩阵 × 原始位置

法线和切线变换

法线和切线的变换遵循与位置数据相同的旋转逻辑,但由于它们是方向向量而非位置点,变换时不考虑平移分量。正确的法线和切线变换确保了光照和材质效果在Billboard变换后仍然保持视觉一致性。

法线变换需要特别注意,由于法线是协变向量,其变换矩阵通常为顶点变换矩阵的逆转置矩阵。但在Billboard这种纯旋转的情况下,由于旋转矩阵是正交矩阵,逆转置矩阵等于原矩阵,因此可以直接使用相同的旋转矩阵。

实际应用案例

Billboard节点在游戏开发中有多种实际应用,以下是一些典型场景。

粒子系统效果

在粒子系统中,Billboard技术是创建各种视觉特效的基础。

火焰和烟雾效果可以通过Billboard四边形配合透明度渐变纹理实现。每个粒子都是一个面向相机的四边形,使用噪声纹理和颜色渐变创建动态的火焰和烟雾外观。通过All Axis模式确保无论相机如何移动,效果都能正确显示。

魔法和能量场效果利用Billboard节点创建环绕角色的魔法光环或能量屏障。结合扭曲效果和发光着色器,可以制作出视觉上吸引人的魔法特效。Billboard确保这些效果始终面向玩家,提供最佳的视觉体验。

环境装饰优化

在大型开放世界游戏中,Billboard技术是性能优化的重要手段。

树木和植被渲染使用Around Y Axis模式的Billboard技术,将复杂的3D树木模型替换为简单的四边形,大幅减少三角形数量。当玩家距离较远时,使用Billboard树木;当玩家靠近时,逐渐淡入完整的3D模型。这种LOD(层次细节)策略在保持视觉质量的同时显著提升性能。

远处山脉和云层可以通过Billboard技术创建。使用多层Billboard平面配合透明度混合,可以模拟出具有深度感的远景效果。这种方法比使用完整3D模型更加高效,特别适合移动平台或性能受限的场景。

UI和交互元素

在用户界面和交互设计中,Billboard技术确保重要信息始终可见。

世界空间UI元素使用Billboard技术创建始终面向玩家的对话框、任务提示或交互图标。这在3D游戏中特别有用,玩家可以从任何角度都能清晰看到UI内容。

AR和VR应用中的界面元素通过Billboard技术确保虚拟界面始终面向用户,提供自然的交互体验。无论是信息面板、控制菜单还是虚拟标签,Billboard都能保证最佳的可读性和可用性。

性能优化考虑

使用Billboard节点时需要考虑性能影响,特别是在大量使用的情况下。

渲染性能

Billboard技术通过减少几何复杂度来提升性能,但顶点着色器的计算负载会增加。在移动设备或低端硬件上,需要平衡视觉质量和性能消耗。

优化策略包括:

  • 控制Billboard物体的数量,避免在同一帧中渲染过多Billboard
  • 使用LOD系统,根据距离动态切换Billboard和完整模型
  • 合并多个Billboard物体,减少绘制调用
  • 在性能敏感的区域使用更简单的Billboard效果

内存和带宽

Billboard通常使用简单的四边形几何体,这有助于减少内存占用和顶点数据传输带宽。但在使用高质量纹理时,需要注意纹理内存的消耗。

优化建议:

  • 使用纹理图集将多个Billboard纹理合并为一张大图
  • 根据距离使用不同分辨率的纹理
  • 压缩纹理格式以减少内存占用
  • 合理管理纹理的加载和卸载,避免内存峰值

常见问题与解决方案

在使用Billboard节点时可能会遇到一些常见问题,以下是相应的解决方案。

光照异常

问题描述:Billboard物体上的光照显示不正确,高光或阴影位置异常。

解决方案:

  • 确保正确连接Normal OS端口,并提供准确的法线数据
  • 检查Billboard模式是否适合场景需求
  • 在复杂光照环境下,考虑使用自定义光照模型或简化光照计算
  • 验证法线贴图是否正确应用,确保切线数据正确变换

深度排序问题

问题描述:Billboard物体与其他物体的深度排序错误,出现穿透或遮挡异常。

解决方案:

  • 调整渲染队列顺序,确保Billboard物体在正确的渲染阶段绘制
  • 使用Alpha混合时,注意透明物体的渲染顺序问题
  • 在粒子系统中使用软粒子技术缓解深度冲突
  • 考虑使用自定义深度偏移解决特定的排序问题

运动模糊和抗锯齿

问题描述:快速移动的Billboard物体可能出现运动模糊异常或抗锯齿效果不佳。

解决方案:

  • 在运动剧烈的Billboard物体上禁用运动模糊,或使用自定义运动向量
  • 调整抗锯齿设置,确保Billboard边缘平滑
  • 对于特别敏感的视觉效果,考虑使用更高分辨率的纹理
  • 在后期处理中应用特定的抗锯齿技术,如TAA(时间性抗锯齿)

高级应用技巧

掌握了Billboard节点的基本用法后,可以探索一些高级应用技巧。

自定义Billboard效果

通过组合Billboard节点与其他ShaderGraph节点,可以创建独特的视觉效果。

倾斜Billboard效果通过修改旋转矩阵,使Billboard物体以特定角度倾斜,而不是完全面向相机。这种效果可以用于创建更有动态感的粒子特效或风格化的视觉元素。

动态朝向Billboard根据游戏逻辑或玩家输入动态调整Billboard的朝向,而不是始终面向主相机。这种技术可以用于创建始终面向特定目标的效果,如追踪导弹的尾焰或指向任务目标的导航标记。

与其他系统的集成

Billboard节点可以与Unity的其他系统集成,创建更复杂的效果。

与VFX Graph集成,在视觉特效图中使用Billboard技术创建高性能的粒子效果。VFX Graph提供了更强大的粒子系统功能,结合Billboard可以实现电影级的视觉效果。

与Shader Graph高级特性结合,如曲面细分、几何着色器或光线追踪,创建更复杂的Billboard效果。这些高级技术可以增强Billboard的视觉质量,提供更逼真或更风格化的外观。


【Unity Shader Graph 使用与特效实现】专栏-直达
(欢迎

点赞留言

探讨,更多人加入进来能更加完善这个探索的过程,🙏)

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!


一、前序

1. 介绍
Nanite是UE5中虚拟几何体(Virtualized Geometry System)的系统,主要用途是高效率渲染的高面数模型。Nanite会为模型自动生成LOD结构,与传统LOD不同,Nanite的LOD不再是每个模型的,而是精细到模型中的局部区域,艺术家不需再为制作或处理LOD烦恼。并且还能享有GPU Driven的高效剔除,单个绘制调用的好处。

2. 技术要点
Nanite技术结合了多种技术做到了高效渲染:

  1. Cluster Rendering:由Cluster组织三角形,可以享有更高效的剔除。
  2. Auto LOD:通过Graph Partitioning技术划分和简化模型构建LOD,并且把数据组织成BVH结构在Runtime时候可以高效地并行选择LOD,通过这种方式构建的LOD过渡非常丝滑。
  3. GPU Driven Pipeline:由GPU驱动的绘制,减少了CPU的性能开销。
  4. Occlusion Culling:更细颗粒的遮挡剔除,用于剔除不可见的三角形。
  5. Hardware/Software Rasterization:由于小三角形对于硬件光栅化非常不友好,所以针对这些三角形用Compute Shader执行软光栅提高效率。
  6. Visibility Buffer:利用Visibility Buffer减少Overdraw,进一步提高GPU效率。
  7. Streaming:加载只看到的相关数据,减少几何体对内存的压力。

3. 本文效果
由于Nanite系统非常庞大和有非常多的工程细节要处理,所以本文会简化和略过一些东西,仅实现核心部分,而且会与有UE5的版本有点出入。

下图是本文实现的效果,每个色块是一个三角形,可以看出LOD切换和相机剔除都非常丝滑。

色块表示三角面

色块表示Cluster

二、实现

1. Clusterize
第一步,在离线阶段处理,将复杂的超高精度网格模型高效且合理地分割成更小、更易于管理的簇(Cluster),每个Cluster最多128个三角形。这种划分不是简单的切割,而是旨在最小化簇与簇之间连接的边数(即切割大小),同时保持每个簇的大小大致均衡。

UE使用的Partition是Metis库:
https://github.com/KarypisLab/METIS

实现代码可以参考UE5的源码部分:
UnrealEngine-release\Engine\Source\Developer\NaniteBuilder\Private\NaniteBuilder.cpp

本文使用meshoptimizer实现Mesh的切分Cluster和Partition功能,这个库功能还有优化Over Draw,Shadow Depth Index等功能:
https://github.com/zeux/meshoptimizer

我们新建一个C++导出DLL的工程,封装几个主要函数让Unity可以使用。其实代码量不多,翻译成C#直接用也可以。

分别是:

  • meshopt_buildMeshlets(构建Cluster)
  • meshopt_partitionClusters(Cluster划分Partition)
  • meshopt_buildMeshletsBound(计算Cluster数量)
  • meshopt_computeSphereBounds(合并BoundsSphere)

在C#中引用这些函数:

unsafe static List<Cluster> clusterize(Vector3[] vertices, int[] indices)
    {
        constint max_vertices = 192; // TODO: depends on kClusterSize, also may want to dial down for mesh shaders
        constint max_triangles = kClusterSize; //128
        constint min_triangles = (kClusterSize / 3) & ~3;
        constfloat split_factor = 2.0f;
        constfloat fill_weight = 0.75f;
        int max_meshlets = BuildMeshletsBound(indices.Length, max_vertices, max_triangles);//meshopt_buildMeshletsBound 
        var meshlets = new Meshlet[max_meshlets * 2];
        var meshlet_vertices = newint[max_meshlets * max_vertices];
        var meshlet_triangles = newbyte[max_meshlets * max_triangles * 3];
        var meshlet_count = BuildMeshletFlex(meshlets, meshlet_vertices, meshlet_triangles, indices, indices.Length, vertices, vertices.Length, sizeof(float) * 3, max_vertices, min_triangles, max_triangles, 0.0f,
            split_factor);//meshopt_buildMeshlets 
        List<Cluster> clusters = new List<Cluster>(meshlet_count);
        for (int i = 0; i < meshlet_count; i++)
        {
            ref Meshlet meshlet = ref meshlets[i];
            fixed (int* ptr = &meshlet_vertices[meshlet.vertex_offset])
            {
                fixed (byte* ptr2 = &meshlet_triangles[meshlet.triangle_offset])
                {
                    OptimizeMeshlet(ptr, ptr2, (int)meshlet.triangle_count, (int)meshlet.vertex_count);
                }
            }

            Cluster cluster = new Cluster();
            cluster.indices = newint[meshlet.triangle_count * 3];
            for (int j = 0; j < meshlet.triangle_count * 3; ++j)
                cluster.indices[j] =
                    meshlet_vertices[meshlet.vertex_offset + meshlet_triangles[meshlet.triangle_offset + j]];

            cluster.parent.error = float.MaxValue;
            clusters.Add(cluster);
        }

        return clusters;
    }

然后可以直接通过meshopt_buildMeshlets函数,获得每个cluster的indexs。

2. Build DAG
有了这些Cluster,就可以构建“LOD”了,只需要循环这个操作:打组->合并->减面->clusterize。如下图:

这个过程感觉就像Mipmap一样,一层一层往上合并和简化,并记录一个Err误差值和Bounds用于运行时LOD选择用。而这些合并的的节点就叫做Cluster Group。最后得出一个DAG(有向无环图,Directed Acyclic Graph)的结构。

public struct ClusterGroup
    {
        public List<int> Children;
        public Vector3 Bounds;
        publicfloat radius;
        public Vector3 LODBounds;
        publicfloat MinLODError;
        publicfloat MaxParentLODError;
        publicint MipLevel;
    } 

publicclassNaniteSubMesh
    {
        public List<ClusterGroup> clusterGroupList;
        public List<Cluster> clusterList;
        publicint maxMipLevel;
    }

static NaniteSubMesh Nanite(Vector3[] vertices,Vector3[] normals, int[] indices)
    {
        NaniteSubMesh res = new NaniteSubMesh();
        List<ClusterGroup> clusterGroupList = new List<ClusterGroup>();
        var clusters = clusterize(vertices, indices);
        res.clusterList = clusters;
        res.clusterGroupList = clusterGroupList;
        res.maxMipLevel = 0;
        for (int i = 0; i < clusters.Count; ++i)
        {
            var c = clusters[i];
            c.self = Bounds(vertices, clusters[i].indices, 0f);
            c.mip = 0;
            clusters[i] = c;
        }

        List<int> pending = new List<int>(clusters.Count);
        int[] remap = newint[vertices.Length];
        for (int i = 0; i < remap.Length; ++i)
            remap[i] = i;
        for (int i = 0; i < clusters.Count; ++i)
            pending.Add(i);

        int curMip = 1;
        byte[] locks = newbyte[vertices.Length];
        while (pending.Count > 1)
        {
            List<List<int>> groups = partition(clusters, pending, remap, vertices);
            if (kUseLocks)
                lockBoundary(locks, groups, clusters, remap);
            pending.Clear();
            List<int> retry = new List<int>();
            int triangles = 0;
            int stuck_triangles = 0;
            for (int i = 0; i < groups.Count; ++i)
            {
                var curGroupClusters = groups[i];
                if (curGroupClusters.Count == 0)
                {
                    continue; // metis shortcut
                }

                List<int> merged = new List<int>(vertices.Length);
                for (int j = 0; j < curGroupClusters.Count; ++j)
                {
                    merged.AddRange(clusters[curGroupClusters[j]].indices);
                }
                LODBounds groupb = boundsMerge(clusters, curGroupClusters);
                ClusterGroup clusterGroup = new ClusterGroup();
                clusterGroup.Bounds = groupb.center;
                clusterGroup.MaxParentLODError = groupb.error;
                clusterGroup.radius = groupb.radius;
                clusterGroup.Children = new List<int>(merged.Count);
                clusterGroup.MipLevel = curMip - 1;
                for (int j = 0; j < curGroupClusters.Count; ++j)
                {
                    clusterGroup.Children.Add(curGroupClusters[j]);
                }
                clusterGroupList.Add(clusterGroup);

                // aim to reduce group size in half
                int target_size = (merged.Count / 3) / 2 * 3;
                float error = 0f;
                var simplified = simplify(vertices, normals, merged.ToArray(), kUseLocks ? locks : null, target_size,
                    ref error);
                if (simplified.Count > merged.Count * kSimplifyThreshold)
                {
                    stuck_triangles += merged.Count / 3;
                    for (int j = 0; j < curGroupClusters.Count; ++j)
                    {
                        retry.Add(curGroupClusters[j]);
                    }

                    continue; // simplification is stuck; abandon the merge
                }

                // enforce bounds and error monotonicity
                // note: it is incorrect to use the precise bounds of the merged or simplified mesh, because this may violate monotonicity

                var split = clusterize(vertices, simplified.ToArray());
                groupb.error += error; // this may overestimate the error, but we are starting from the simplified mesh so this is a little more correct
                // update parent bounds and error for all clusters in the group
                // note that all clusters in the group need to switch simultaneously so they have the same bounds
                for (int j = 0; j < curGroupClusters.Count; ++j)
                {
                    int clusterIndex = curGroupClusters[j];
                    var t = clusters[clusterIndex];
                    t.parent = groupb;
                    clusters[clusterIndex] = t;
                }

                for (int j = 0; j < split.Count; ++j)
                {
                    var sj = split[j];
                    sj.self = groupb;
                    sj.mip = curMip;
                    split[j] = sj;
                    clusters.Add(sj); // std::move
                    pending.Add(clusters.Count - 1);
                    triangles += sj.indices.Length / 3;
                }
            }

            curMip++;
        }

        if (pending.Count == 1)
        {
            var c = clusters[pending[0]];
            ClusterGroup clusterGroup = new ClusterGroup();
            clusterGroup.Bounds = c.self.center;
            clusterGroup.MaxParentLODError = c.self.error;
            clusterGroup.radius = c.self.radius;
            clusterGroup.Children = new List<int>(1);
            clusterGroup.MipLevel = curMip - 1;
            clusterGroup.Children.Add(pending[0]);
            clusterGroupList.Add(clusterGroup);
        }

        res.maxMipLevel = curMip - 1;
        return res;
    }

static void lockBoundary(byte[] locks, List<List<int>> groups, List<Cluster> clusters, int[] remap)
    {
        // for each remapped vertex, keep track of index of the group it's in (or -2 if it's in multiple groups)
        int[] groupmap = newint[locks.Length];
        for (int i = 0; i < groupmap.Length; ++i)
            groupmap[i] = -1;

        for (int i = 0; i < groups.Count; ++i)
        {
            var c = groups[i];
            for (int j = 0; j < c.Count; ++j)
            {
                var indices = clusters[c[j]].indices;
                for (int k = 0; k < indices.Length; ++k)
                {
                    var v = indices[k];
                    var r = remap[v];

                    if (groupmap[r] == -1 || groupmap[r] == i)
                        groupmap[r] = i;
                    else
                        groupmap[r] = -2;
                }
            }
        }

        // note: we need to consistently lock all vertices with the same position to avoid holes
        for (int i = 0; i < locks.Length; ++i)
        {
            var r = remap[i];
            locks[i] = (byte)((groupmap[r] == -2) ? 1 : 0);
        }
    }

这样我们得到各级Mip的一系列Clusters。

3. 加速结构
即使把三角形划分成Clusters数量也太多,使用Compute Shader来做并行结算效率也不高,于是Nanite就使用了BVH来作为ClusterGroup的加速结构,然后配合Persistent Threads做查找过滤。

Persistent Threads遍历BVH部分,有兴趣可以参考UE5源码:
Shaders\Private\Nanite\NaniteClusterCulling.usf

UE5中也有不使用Persistent Threads的流程,应该说一般默认就是不使用的。

UE5源码部分

个人认为Persistent Threads方案在GPU遍历这种BVH结构有点暴力和重度,所以简化了一下,把多个Cluster合并成一个剔除单元(Part),先并行对Part做剔除,再对Part里的Cluster去做并行剔除,两层结构来加速作为Persistent Threads的一个简单替代方案。

然后把多个Part组织成Page用于分块加载。材质处理细节也不同,UE5的材质是每个Cluster会记录MaterialRange,简单起见这里实现是每个SubMesh会去构建独立的Clusters。

代码如下:

 [Serializable]
    publicstruct NaniteCluster
    {
        publicint indiceIndex;
        publicint indiceCount;
        publicfloat selfErrer;
        publicfloat parentErrer;
        public Vector4 selfSphere;
        public Vector4 parentSphere;
        publicint subMeshID;
        publicint vertexOffset;
    };
    
    [Serializable]
    publicstruct NaniteClusterGroup
    {
        publicint ClusterStart;
        publicint ClusterCount;
        public Vector3 Bounds;
        publicfloat radius;
        public Vector3 LODBounds;
        publicfloat MinLODError;
        publicfloat MaxParentLODError;
        publicint MipLevel;
    }

    [Serializable]
    publicstruct NaniteMeshPart
    {
        publicint ClusterStart;
        publicint ClusterCount;
        public Vector4 selfSphere;
        publicfloat MaxParentLODError;
    }
public classNaniteSubMesh
    {
        public List<ClusterGroup> clusterGroupList;
        public List<Cluster> clusterList;
        publicint maxMipLevel;
    }
publicclassBuildPart
    {
        public List<int> clusterList;
        publicint mip;
        publicint subMesh;

    }
public static void BuildNaniteMesh(Mesh mesh)
    {
          var vertices = mesh.vertices;
        var normals = mesh.normals;
        var uvs = mesh.uv;

        int subMeshCount = mesh.subMeshCount;
        int totalClusterCount = 0;
        int totalIndexCount = 0;
        List<NaniteSubMesh> subMeshList = new List<NaniteSubMesh>();
        for (int i = 0; i < subMeshCount; i++)
        {
            var triangles = mesh.GetTriangles(i);
            var subMesh = Nanite(vertices,normals,triangles);
            subMeshList.Add(subMesh);
            totalClusterCount += subMesh.clusterList.Count;
        }

        List<BuildPart> buildPartsList = new List<BuildPart>(totalClusterCount);
        int MAX_PART_PERPAGE = 128;
        int MAX_CLUSTER_PERPART = 8;

        for (int subMeshIndex = 0; subMeshIndex < subMeshList.Count; subMeshIndex++)
        {
            var subMesh = subMeshList[subMeshIndex];
            List<Cluster> clusters = subMesh.clusterList;
            var groupsList = subMesh.clusterGroupList;
            BuildPart buildPart = null;
            for (int i = 0; i < groupsList.Count; i++)
            {
                var gIndex = i; // sortGroups[i].OldIndex;
                var g = groupsList[gIndex];
                var childs = g.Children;
                for (int c = 0; c < childs.Count; c++)
                {
                    int cIndex = childs[c];
                    int cMip = clusters[cIndex].mip;
                    totalIndexCount += clusters[cIndex].indices.Length;
                    //new Part
                    if (buildPart == null || buildPart.clusterList.Count >= MAX_CLUSTER_PERPART ||
                        buildPart.mip != cMip)
                    {
                        buildPart = new BuildPart();
                        buildPart.clusterList = new List<int>(MAX_CLUSTER_PERPART);
                        buildPart.mip = cMip;
                        buildPart.subMesh = subMeshIndex;
                        buildPartsList.Add(buildPart);
                    }

                    buildPart.clusterList.Add(cIndex);
                }
            }
        }

        int buildPartCount = buildPartsList.Count;
        NaniteMeshPage[] pageArray = new NaniteMeshPage[(buildPartCount+(MAX_PART_PERPAGE-1))/MAX_PART_PERPAGE];//ceil
        List<int> tempIndiceList = new List<int>(totalIndexCount);
        List<int> mipLists = new List<int>(totalClusterCount);
        int partIndex = 0;
        for (int i = 0; i < pageArray.Length; i++)
        {
            //create new page
            var p = ScriptableObject.CreateInstance<NaniteMeshPage>();
            pageArray[i] = p;
            tempIndiceList.Clear();
            int partCount =  (i == (pageArray.Length -1)) ? (buildPartCount % MAX_PART_PERPAGE) : MAX_PART_PERPAGE;
            p.parts = new NaniteScene.NaniteMeshPart[partCount];
            List<NaniteScene.NaniteCluster> pageClusters = new List<NaniteScene.NaniteCluster>(partCount * MAX_CLUSTER_PERPART);
            for (int j = 0; j < partCount; j++)
            {
                var buildPart = buildPartsList[partIndex];
                var buildPartCluster = buildPart.clusterList;
                //create part
                var part = new NaniteScene.NaniteMeshPart();
                part.ClusterStart = pageClusters.Count; //local index
                part.ClusterCount = buildPartCluster.Count;
                int subMeshID = buildPart.subMesh;
                float maxParentErr = 0f;
                var clusters = subMeshList[subMeshID].clusterList;
                for (int c = 0; c < buildPartCluster.Count; c++)
                {
                    var cluster = clusters[buildPartCluster[c]];
                    mipLists.Add(cluster.mip); 
                    //create Cluster
                    NaniteScene.NaniteCluster naniteCluster = new NaniteScene.NaniteCluster();
                    naniteCluster.indiceIndex = tempIndiceList.Count;
                    naniteCluster.indiceCount = cluster.indices.Length;
                    naniteCluster.parentErrer = cluster.parent.error;
                    naniteCluster.parentSphere = new Vector4(cluster.parent.center.x,cluster.parent.center.y,cluster.parent.center.z, cluster.parent.radius);
                    naniteCluster.selfErrer = cluster.self.error;
                    naniteCluster.selfSphere = new Vector4(cluster.self.center.x,cluster.self.center.y,cluster.self.center.z, cluster.self.radius);
                    naniteCluster.subMeshID = subMeshID;
                    tempIndiceList.AddRange(cluster.indices);
                    maxParentErr = Mathf.Max(naniteCluster.parentErrer, maxParentErr);
                    pageClusters.Add(naniteCluster);
                }

                LODBounds partBounds =  boundsMerge(clusters, buildPartCluster,true);
                part.selfSphere = new Vector4(partBounds.center.x,partBounds.center.y,partBounds.center.z,partBounds.radius);
                part.MaxParentLODError = maxParentErr;
                p.parts[j] = part;
                partIndex++;
            }
            p.clusterArray = pageClusters.ToArray();
            p.indiceArray = tempIndiceList.ToArray();
            p.clusterMip = mipLists.ToArray();
        }

        string fileName = AssetDatabase.GetAssetPath(mesh);
        string extension = Path.GetExtension(fileName);
        fileName = fileName.Replace(extension, "");
        //Build page
        int totalVerts = 0;
        for (int i = 0; i < pageArray.Length; i++)
        {
            var page = pageArray[i];
            var clusterArray = page.clusterArray;
            var indiceArray = page.indiceArray;
            Dictionary<int,int> indicesMap = new Dictionary<int,int>();
            List<Vector3> tempVerts = new List<Vector3>(vertices.Length);
            List<Vector3> tempNormals = new List<Vector3>(vertices.Length);
            List<Vector2> tempUVs = new List<Vector2>(vertices.Length);
            List<int> newIndices = new List<int>(totalIndexCount);
            for (int c = 0; c < clusterArray.Length; c++)
            {
                refvar cluster = ref clusterArray[c];
                var indexStart = cluster.indiceIndex;
                var indexEnd = indexStart+cluster.indiceCount;
                for (int index = indexStart; index < indexEnd; index++)
                {
                    int vertIndex = indiceArray[index];
                    int newIndex;
                    if (!indicesMap.TryGetValue(vertIndex,out newIndex))
                    {
                        newIndex = newIndices.Count;
                        indicesMap.Add(vertIndex, newIndex);
                        tempVerts.Add(vertices[vertIndex]);
                        tempNormals.Add(normals[vertIndex]);
                        if (uvs.Length == 0)
                        {
                            tempUVs.Add(Vector2.zero);
                        }
                        else
                        {
                            tempUVs.Add(uvs[vertIndex]);
                        }

                        newIndices.Add(newIndex);
                    }

                    indiceArray[index] = newIndex;
                }
            }

            page.vertexStride = 5;//pos3 + uv2
            page.vertexData = newfloat[tempVerts.Count * page.vertexStride];
            page.vertexCount = tempVerts.Count;
            for (int v = 0; v < tempVerts.Count; v++)
            {
                int vertexIndex = v * page.vertexStride;
                page.vertexData[vertexIndex + 0] = tempVerts[v].x;
                page.vertexData[vertexIndex + 1] = tempVerts[v].y;
                page.vertexData[vertexIndex + 2] = tempVerts[v].z;
                page.vertexData[vertexIndex + 3] = tempUVs[v].x;
                page.vertexData[vertexIndex + 4] = tempUVs[v].y;
            }
            totalVerts +=tempVerts.Count;
            string newPath = fileName + "_p"+i +".asset";
            AssetDatabase.CreateAsset(page, newPath);
        }
        AssetDatabase.Refresh();

        Debug.Log("mesh Vertx:"+vertices.Length +" mesh Nanite:"+ totalVerts + " cluster:"+totalClusterCount + "part:"+ buildPartCount +" page:"+pageArray.Length);
        NaniteMesh naniteMesh = ScriptableObject.CreateInstance<NaniteMesh>();
        {
            naniteMesh.subMeshCount = subMeshCount;
            naniteMesh.pageArray = new NaniteMeshPage[pageArray.Length];
            for (int i = 0; i < pageArray.Length; i++)
            {
                string newPath = fileName + "_p" + i + ".asset";
                naniteMesh.pageArray[i] = AssetDatabase.LoadAssetAtPath<NaniteMeshPage>(newPath);
            }
        }

        var meshBound = mesh.bounds;
        naniteMesh.boundingSphere = meshBound.center;
        naniteMesh.boundingSphere.w = meshBound.extents.magnitude;
        string meshExt = "_mesh.asset";
        AssetDatabase.CreateAsset(naniteMesh, fileName + meshExt);
        AssetDatabase.Refresh();
    }

到这里离线部分基本结束,可以得到一个Nanite的资源。当然UE5原文还做了很多操作,如BVH、Encode、编码、压缩、Page的划分、顶点属性优化等,个人认为这些都属于工程细节。

4. 运行时资源
来到Runtime部分,我们需要把这个Nanite Mesh加载上来,方便起见,这里直接引用一下资源在脚本上,偷懒省略加载部分。

把资源、Object、材质信息整合起来,传到GPU的Buffer中。这里做法很不正式还是偷懒来处理。当然也可以用Compute Shader来更新Page数据到GPUBuffer中。

    public static List<NaniteRenderer> renderers = new List<NaniteRenderer>();
    privatestatic SceneObject[] gpuObjects = new SceneObject[2048];
    //cluster -> part -> page
    publicstruct SceneObject
    {
        publicint naniteMeshID;
        public Matrix4x4 localToWorldMatrix;
        publicint materialIDOffset;
    }
    publicstruct NaniteRes
    {
        public Vector4 boundingSphere;
        publicint partIndex;
        publicint partCount;
    }

unsafe static void UpdateRenderList()
    {
         if(renderers.Count == 0)
            return;
        //object update
        if (renderers.Count > gpuObjects.Length)
        {
            gpuObjects = new SceneObject[Mathf.NextPowerOfTwo(renderers.Count)];
        }

        objectCount = 0;
        maxPartCount = 0;
        naniteMeshes.Clear();
        materialList.Clear();
        List<int> materialIndices = new List<int>();
        for (int i = 0; i < renderers.Count; i++)
        {
           var renderer = renderers[i];
           var nMesh = renderer.naniteMesh;
            foreach (var p in nMesh.pageArray)
           {
               maxPartCount += p.parts.Length;
               maxClusterCount += p.clusterArray.Length;
           }

           SceneObject obj = new SceneObject();
           obj.localToWorldMatrix = renderer.transform.localToWorldMatrix;
            //mesh index
           int index = naniteMeshes.IndexOf(nMesh);
           if (index < 0)
           {
               index = naniteMeshes.Count;
               naniteMeshes.Add(nMesh);
           }
           obj.naniteMeshID = index;
           //mat indexs
           obj.materialIDOffset = materialIndices.Count;
           for (int m = 0; m < renderer.materials.Length; m++)
           {
               var mat = renderer.materials[m];
               int matIndex = materialList.IndexOf(mat);
               if (matIndex < 0)
               {
                   matIndex = materialList.Count;
                   materialList.Add(mat);
               }
               materialIndices.Add(matIndex);
           }
           gpuObjects[i] = obj;
           renderer.transformChanged = false;
           objectCount++;
        }

        if(candidateClusterBuffer!=null)
            candidateClusterBuffer.Dispose();
        candidateClusterBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, maxClusterCount *2, sizeof(int));

        if(visibleClusterBuffer != null)
            visibleClusterBuffer.Dispose();
        visibleClusterBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured,maxClusterCount *2, sizeof(int));

        if (objectsBuffer != null)
            objectsBuffer.Dispose();
        objectsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, objectCount, sizeof(SceneObject));
        objectsBuffer.SetData(gpuObjects,0,0,objectCount);

        if(visObjectsBuffer !=null)
            visObjectsBuffer.Dispose();
        visObjectsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured,objectCount, sizeof(int));

        int vertCount = 0;
        List<NaniteCluster> tempClusters = new List<NaniteCluster>(2048);
        List<NaniteMeshPart> tempParts = new List<NaniteMeshPart>(2048);
        List<NaniteRes> naniteRes = new List<NaniteRes>(2048);
        List<int> tempIndices = new List<int>(2048 * 100);
        List<float> vertexDataList = new List<float>();
        //load page
        for (int nID = 0; nID < naniteMeshes.Count; nID++)
        {
            NaniteRes res = new NaniteRes();
            var nMesh = naniteMeshes[nID];
            //填充到GPU
            var pages = nMesh.pageArray;
            res.partIndex = tempParts.Count;
            res.partCount = 0;
            res.boundingSphere = nMesh.boundingSphere;
            for (int p = 0; p < pages.Length; p++)
            {
                var page = pages[p];
                var parts = page.parts;
                int vertOffset = vertCount;
                int indicesOffset = tempIndices.Count;
                int clusterOffset = tempClusters.Count;

                //add all cluster
                var clusters = page.clusterArray;
                for (int c = 0; c < clusters.Length; c++)
                {
                    var cluster = clusters[c];
                    cluster.indiceIndex += indicesOffset;
                    cluster.vertexOffset = vertOffset;
                    tempClusters.Add(cluster);
                }

                //add all part
                for (int partIndex = 0; partIndex < parts.Length; partIndex++)
                {
                    var part = parts[partIndex];
                    part.ClusterStart += clusterOffset;
                    tempParts.Add(part);
                    res.partCount++;
                }

                //add page data
                tempIndices.AddRange( page.indiceArray);
                vertexDataList.AddRange(page.vertexData);
                vertCount += page.vertexCount;
            }
            naniteRes.Add(res);
        }

        //TODO GPU Update Buffer
        if (naniteResBuffer != null)
            naniteResBuffer.Dispose();
        naniteResBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, naniteRes.Count, sizeof(NaniteRes));
        naniteResBuffer.SetData(naniteRes);

        if (partsBuffer != null)
            partsBuffer.Dispose();
        partsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured,tempParts.Count, sizeof(NaniteMeshPart));
        partsBuffer.SetData(tempParts);

        if (clusterBuffer != null)
            clusterBuffer.Dispose();
        clusterBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, tempClusters.Count, sizeof(NaniteCluster));
        clusterBuffer.SetData(tempClusters);


        if (indiceseBuffer != null)
            indiceseBuffer.Dispose();
        indiceseBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Raw, tempIndices.Count, sizeof(int));
        indiceseBuffer.SetData(tempIndices);

        if(materialIndexBuffer!=null)
            materialIndexBuffer.Dispose();
        materialIndexBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured,materialIndices.Count, sizeof(int));
        materialIndexBuffer.SetData(materialIndices);

        if(vertexDataBuffer!=null)
            vertexDataBuffer.Dispose();
        vertexDataBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Raw, vertexDataList.Count,sizeof(float));
        vertexDataBuffer.SetData(vertexDataList);
    }

    //input object ID => 
    public unsafe static void UpdateNaniteScene()
    {
        if (renderListDirty)
        {
            UpdateRenderList();
           // UpdateRenderListGPU();
            renderListDirty = false;
        }

       for (int i = 0; i < renderers.Count; i++)
       {
           var renderer = renderers[i];
           if (renderer.transformChanged)
           {
               gpuObjects[i].localToWorldMatrix = renderer.transform.localToWorldMatrix;
               renderer.transformChanged = false;
               transformDirty = true;
           }
       }

       if (objectsBuffer != null && transformDirty)
           objectsBuffer.SetData(gpuObjects, 0, 0, objectCount);
    }

5. 剔除
这时离线时候已经把Clusters扁平化到数组中了,这些Clusters是可以并行进行剔除的,巧妙之处是他记录了父级的误差和自己的误差,当我们传入误差系数时候就可以独立地判断自己是否被剔除,而和上下级无关。

先从CPU发起剔除Compute Shader的Dispatch。这里因为组织数据时候就知道了所有Object最大的Parts/Cluster数量,所以直接用这个数去Dispatch了。

Objects剔除:

根据Object找到NaniteMesh的Parts进行Culling:

ClustersCulling:

6. 软光栅
略。

7. VisibilityBuffer
VBuffer主要用来减少Overdraw,着色器直接输出InstanceID、ClusterID、材质ID。然后用这个VBuffer来计算顶点数据来着色。

这个得益于GPUDriven的好处,一个DrawProceduralIndirect就可以绘制所有物体了:
一次DrawProceduralIndirect绘制多个物体

VBuffer存哪些属性,多少位,都是工程细节这里就不考究了。

8. 着色
有了VBuffer就需要逐材质进行绘制,原文是材质ID分Tile组合IndirectDraw画Quad的思想。

需要注意一下这里VBuffer通过三角重心插值求出的UV是不能直接采样贴图的,因为DDXY不对,所以需求重新计算,计算的代码放下面。并且利用SampleGrad(samplerName, coord2, dpdx, dpdy)来采样。

uint MurmurMix(uint Hash)
{
    Hash ^= Hash >> 16;
    Hash *= 0x85ebca6b;
    Hash ^= Hash >> 13;
    Hash *= 0xc2b2ae35;
    Hash ^= Hash >> 16;
    return Hash;
}
float3 IntToColor(uint Index)
{
    uint Hash = MurmurMix(Index);

    float3 Color = float3
    (
        (Hash >> 0) & 255,
        (Hash >> 8) & 255,
        (Hash >> 16) & 255
    );

    return Color * (1.0f / 255.0f);
}

struct FBarycentrics
{
    float3 Value;
    float3 Value_dx;
    float3 Value_dy;
};

float2 Lerp(float2 Value0, float2 Value1, float2 Value2, FBarycentrics Barycentrics, out float2 dxy)
{
    float2 Value = Value0 * Barycentrics.Value.x + Value1 * Barycentrics.Value.y + Value2 * Barycentrics.Value.z;
    dxy.x = Value0 * Barycentrics.Value_dx.x + Value1 * Barycentrics.Value_dx.y + Value2 * Barycentrics.Value_dx.z;
    dxy.y = Value0 * Barycentrics.Value_dy.x + Value1 * Barycentrics.Value_dy.y + Value2 * Barycentrics.Value_dy.z;

    return Value;
}

/** Calculates perspective correct barycentric coordinates and partial derivatives using screen derivatives. */
FBarycentrics CalculateTriangleBarycentrics(float2 PixelClip, float4 PointClip0, float4 PointClip1,
                                            float4 PointClip2, float2 ViewInvSize)
{
    FBarycentrics Barycentrics;
    PixelClip.y = 1 - PixelClip.y;
    PixelClip.xy = PixelClip.xy * 2 - 1;
    const float3 RcpW = rcp(float3(PointClip0.w, PointClip1.w, PointClip2.w));
    const float3 Pos0 = PointClip0.xyz * RcpW.x;
    const float3 Pos1 = PointClip1.xyz * RcpW.y;
    const float3 Pos2 = PointClip2.xyz * RcpW.z;

    const float3 Pos120X = float3(Pos1.x, Pos2.x, Pos0.x);
    const float3 Pos120Y = float3(Pos1.y, Pos2.y, Pos0.y);
    const float3 Pos201X = float3(Pos2.x, Pos0.x, Pos1.x);
    const float3 Pos201Y = float3(Pos2.y, Pos0.y, Pos1.y);

    const float3 C_dx = Pos201Y - Pos120Y;
    const float3 C_dy = Pos120X - Pos201X;

    const float3 C = C_dx * (PixelClip.x - Pos120X) + C_dy * (PixelClip.y - Pos120Y);
    // Evaluate the 3 edge functions
    const float3 G = C * RcpW;

    constfloat H = dot(C, RcpW);
    constfloat RcpH = rcp(H);

    // UVW = C * RcpW / dot(C, RcpW)
    Barycentrics.Value = G * RcpH;

    // Texture coordinate derivatives:
    // UVW = G / H where G = C * RcpW and H = dot(C, RcpW)
    // UVW' = (G' * H - G * H') / H^2
    // float2 TexCoordDX = UVW_dx.y * TexCoord10 + UVW_dx.z * TexCoord20;
    // float2 TexCoordDY = UVW_dy.y * TexCoord10 + UVW_dy.z * TexCoord20;
    const float3 G_dx = C_dx * RcpW;
    const float3 G_dy = C_dy * RcpW;

    constfloat H_dx = dot(C_dx, RcpW);
    constfloat H_dy = dot(C_dy, RcpW);

    Barycentrics.Value_dx = (G_dx * H - G * H_dx) * (RcpH * RcpH) * (2.0f * ViewInvSize.x);
    Barycentrics.Value_dy = (G_dy * H - G * H_dy) * (RcpH * RcpH) * (-2.0f * ViewInvSize.y);

    return Barycentrics;
}

到这里其实基本完成了,利用IntToColor函数,可以对ClustersID或者IndexID对三角形或Cluster进行可视化。

三、总结

不得不说Nanite技术真是太强大了,但是也有很多工程细节需要处理,本文只是实现了其中一小部分。整体像是处理图片的Mipmap过程。

参考

22.GPU驱动的几何管线-nanite (Part 2) | GAMES104-现代游戏引擎:从入门到实践

[UnrealCircle]Nanite技术简介 | Epic Games China 王祢

Karis_Nanite_SIGGRAPH_Advances_2021_final.pdf

Nanite-GPU-Driven

UE5 Nanite源码入口:
Engine\Source\Runtime\Renderer\Private\Nanite\NaniteCullRaster.cpp (渲染流程入口)
Engine\Shaders\Private\Nanite\ (GPU的Shader入口)
Engine\Source\Developer\NaniteBuilder\Private\ (离线生成Nanite资源入口)


这是侑虎科技第1939篇文章,感谢作者傻头傻脑亚古兽供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/tian-cai-ya-gu-shou

再次感谢傻头傻脑亚古兽的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

Uber 构建了HiveSync,这是一个分片式批量复制系统,能够使 Hive 和 HDFS 数据在多个区域之间保持同步,它每天处理数百万个 Hive 事件。HiveSync 确保了跨区域数据的一致性,实现了 Uber 的灾难恢复策略,并消除了由次要区域闲置而导致的低效问题——此前次要区域需承担与主区域一样的硬件成本,而 HiveSync 在维持高可用性的同时彻底解决了这一问题

 

HiveSync 基于开源项目 AirbnbReAir构建并做了一些扩展,包括实现了分片、基于DAG的编排以及控制平面和数据平面的分离。ETL作业现在只在主数据中心执行,而 HiveSync 处理跨区域复制,实现了近乎实时的一致性,保持了灾难应对能力和分析访问权限。分片功能允许将表和分区划分为独立的单元,从而实现并行复制和细粒度容错。

 

HiveSync 将控制平面(负责编排作业和管理关系元数据存储中的状态)与数据平面(执行HDFSHive文件操作)分离。Hive Metastore 事件监听器负责捕获 DDL 和 DML 变更,将它们记录到MySQL中,并触发复制工作流。任务以有限状态机的形式呈现,支持任务重启与健壮的故障恢复机制。

HiveSync 架构:控制平面和数据平面分离(来源:Uber博文

 

HiveSync 有两个主要组件:HiveSync 复制服务和数据修复服务。复制服务使用 Hive Metastore 事件监听器实时捕获表和分区变更,将它们异步记录到 MySQL 中。这些审计条目被转换为异步复制作业,以有限状态机的形式执行,为确保可靠性,状态会被持久化。Uber 使用了混合策略:规模比较小的作业使用RPC以提高效率,而规模比较大的作业则利用YARN上的 DistCp。DAG 管理器强制执行分片级的排序和锁定,而静态和动态分片技术则实现了水平扩展,确保复制过程一致且无冲突。

HiveSync 复制服务(来源:Uber博文

 

数据修复是一个持续检测异常的服务,如缺失的分区或非预期的 HDFS 更新,恢复数据中心 1(DC1)和数据中心 2(DC2)之间的一致性,从而保证数据的正确性。HiveSync 保证了每四小时一次的复制 SLA,99百分位的延迟大约为 20 分钟,并支持一次性复制,用于在切换到增量复制之前,一次性地将历史数据集导入新区域或集群。Uber 的数据修复服务会扫描 DC1 和 DC2,检测异常(如缺失或多余的分区),并修复任何不匹配的情况,从而确保跨区域的一致性,目标是准确性超过 99.99%。

数据修复服务分析和解决数据中心之间的不一致性(来源:Uber博文

 

HiveSync 的规模很大,管理着 80 万个 Hive 表,总计约 300PB 的数据,单表数据量从几 GB 到数十 PB 不等,单表分区数从几百到一百万多不等。每天,HiveSync 处理超过 500 万个 Hive DDL 和 DML 事件,跨区域复制约 8PB 的数据。

 

展望未来,随着批量分析和 ML 管道迁移到谷歌云平台,Uber 计划将 HiveSync 扩展到云端复制场景,进一步利用分片、编排和数据一致性技术来高效地维护其 PB 级数据的完整性。

 

原文链接:

https://www.infoq.com/news/2026/01/uber-hivesync-data-lake/

供应商来对账,数据对不上,耽误好几天;采购价格不透明,成本居高不下;供应商绩效全凭印象,合作质量参差不齐...如果你也正被这些问题困扰,是时候了解一下供应商管理系统了。

今天我们就来一次深度测评,聊聊市面上主流的几款供应商管理解决方案,帮你找到最适合自家业务的那一款。

一、选型要点:好的系统,到底该看什么?

在直接推荐产品前,先明确几个核心选型标准,这是避开坑的关键。

第一,要看它能不能解决你真实的痛点。很多企业痛点很具体:比如采购流程不规范、线上线下数据对不上、供应商质量不稳定、对账周期漫长等。系统功能是否直击这些要害,是首要考量。

第二,灵活性和扩展性至关重要。特别是成长型企业,业务变化快,今天用的功能明天可能就要调整。如果系统僵硬,改个流程都要找原厂花大价钱二开,那用起来会很痛苦。所以,是否支持一定程度的自定义或低代码调整,是个加分项。

第三,性价比和长期投入成本。这不单指软件本身的购买费用,还包括实施费用、每年的维护费、未来需求变化的二次开发成本,甚至数据迁移的成本。一个“买得起但用不起”的系统,不如一开始就放弃。

第四,厂商的服务与可持续性。软件即服务,后续的响应速度、问题解决能力、版本迭代计划,都直接影响你的使用体验。选择有成熟服务团队、产品持续迭代的厂商,更稳妥。

基于以上几点,结合市场主流选择,我筛选出 8款值得深入考察的供应商管理系统,并对其核心特点、适用场景进行分析。

二、测评盘点:8款主流供应商管理系统

1. 支道

https://www.zdsztech.com

核心特点:基于无代码平台构建,高度可定制

如果要用一个词形容支道的供应商管理方案,那就是 “灵活”。它并非一个功能固化的标准产品,而是基于其强大的无代码开发平台,能够快速搭建出贴合企业实际采购业务流程的系统。

从测评角度看,它的优势很明显:可视化搭建,改起来方便

企业的采购审批流程、供应商准入标准、询比价模板,都可以通过拖拉拽的方式配置和修改,业务人员经过培训也能参与调整。这解决了很多企业“需求说不清、软件改不动”的痛点。

具体到SRM功能上,它覆盖了供应商全生命周期管理:供应商电子档案、在线准入申请、询价/招标/比价流程、采购订单协同、送货与验收协同、对账付款、以及供应商绩效评估。

亮点在于流程的在线化和自动化,比如报价自动汇总比价、订单状态自动同步给供应商、绩效数据自动采集计算等。

适合谁用:业务独特、流程经常优化、或者未来可能将SRM与内部CRM、项目管理系统打通的成长型企业。它的无代码特性让长期迭代成本更低。

需要注意:高度灵活也意味着初期需要更多的业务梳理和配置投入,更适合愿意在管理梳理上花时间、追求长期适配性的企业。

2. 用友

核心特点:与ERP、财务系统天然集成,业财一体化能力强

用友作为国内企业管理软件的老牌厂商,其YonSuite中的SRM模块最大优势在于 “集成”。如果你的企业已经在使用或用友的ERP、财务系统,那么选择它的SRM模块,在数据打通上会非常顺畅。

采购订单直接生成应付、入库信息实时同步、成本数据自动归集,真正实现业务流、信息流、资金流合一。

功能层面,它提供标准的供应商管理、寻源管理、采购协同、库存协同等功能。在供应商绩效方面,支持多维度指标(如质量、交期、价格、服务)的量化评估。

适合谁用:尤其是那些已经使用用友体系产品的中大型企业,或者对财务业务一体化要求极高、希望杜绝数据孤岛的企业。

需要注意:作为标准化程度较高的产品,在面对一些非常规的、行业特有的采购流程时,可能需要通过二次开发来实现,成本和周期需提前评估。

3. 金蝶

核心特点:强调供应链协同,尤其在生产制造领域有深度方案

金蝶的云星空SRM,在制造业企业中口碑不错。它的设计思路强调 “供应链协同” ,不止管理供应商,更注重与供应商之间的高效协作。比如,支持供应商门户,让供应商自助查看订单、确认交期、填报送货单;支持与生产计划的联动,实现采购需求的精准触发。

其功能亮点在于对 VMI库存管理、JIT准时化采购、寄售业务 等复杂场景的支持,这些都是制造企业的核心痛点。在供应商风险方面,也提供了诸如资质预警、交期预警等管理功能。

适合谁用:生产制造型企业,特别是对原材料采购协同、精益生产有要求的企业。也适合金蝶ERP的老用户,保障系统连贯性。

需要注意:方案相对偏向中大型制造企业,对于贸易类、项目服务类企业的贴合度可能需要详细验证。

4. SAP

核心特点:全球化、战略寻源、网络化协同

SAP Ariba 是全球领先的采购云平台,它的定位更高,更像一个 “采购网络”。其核心优势在于 全球寻源和战略采购。如果你的企业采购范围遍布全球,需要管理跨国供应商、进行复杂的招标和合同管理,Ariba 提供了强大的支持。它拥有庞大的供应商网络,方便发现新供应商。

功能极其全面,从支出分析、寻源招标、合同管理、到供应商协同、发票与付款,覆盖整个直接和间接采购流程。其数据分析能力强大,能帮助企业深度洞察采购支出,优化采购策略。

适合谁用:大型集团企业、跨国公司,或者采购品类复杂、将采购视为战略职能的企业。预算充足是前提。

需要注意:实施和运维成本非常高,系统复杂,对内部管理规范性和团队能力要求极高。对于中小型企业来说,可能“杀鸡用牛刀”。

5. 甄云

核心特点:产品化程度高,开箱即用,聚焦采购全流程数字化

甄云是国内较早专注于采购数字化SRM的厂商之一。其产品特点是 “全流程、产品化” ,功能模块成熟,设计理念清晰。它围绕企业采购业务,提供从供应商管理、寻源管理、采购协同、到财务协同的完整闭环。用户体验和界面设计比较现代化,易于上手。

在供应商风险管控方面,它整合了外部大数据,可以提供供应商的工商、司法、舆情等多维度风险监控和预警,这是个很实用的亮点。

适合谁用:希望快速部署一套成熟、完整SRM系统的中大型企业,特别是对供应商风险有主动管理需求的企业。它降低了从零自研的风险和成本。

需要注意:作为标准化SaaS产品,在应对极端个性化的业务流程时,灵活性可能不如低代码/无代码平台。

6. 携客云

核心特点:SaaS模式,轻量化,以“协同”为核心,实施快

携客云主打 “轻量化、易实施” 的SaaS SRM。它的核心价值在于快速解决制造企业与供应商之间的 “协同效率” 问题,比如订单确认、交货、对账等高频场景。

它的供应商门户做得很轻便,供应商上手门槛低。通过它,企业可以快速实现采购订单发布、送货预约、质量反馈、对账确认等业务的在线化,显著减少打电话、发邮件的低效沟通。

适合谁用:广大中小制造企业,作为ERP的延伸,首要解决与供应商的日常业务协同问题。需求明确、预算有限、希望快速上线看到效果的企业可以重点关注。

需要注意:在战略寻源、深度供应商绩效分析、复杂业务流程管控等更深层的管理需求上,功能可能不如前面几款全面。

7. 企企通

核心特点:平台化思路,强调连接与生态

企企通的SRM平台同样强调协同,但其特色在于 “平台化” 和 “连接能力” 。它致力于成为连接采购方和供应商的协作平台。除了常规的SRM功能外,它在 非生产性物料采购、电商化采购 方面有特色方案,支持企业搭建内部采购商城。它也具备较强的集成能力,可以与企业内部ERP、OA等系统对接,实现流程和数据贯通。

适合谁用:注重与供应商建立在线化协作生态,特别是间接物料采购(MRO)需求旺盛的大中型企业。也适合希望整合分散采购渠道的企业。

需要注意:平台的综合性强,企业需要明确自身核心需求是“管理”还是“连接协同”,以便判断是否匹配。

8. 浪潮云

核心特点:贴合大型集团管控需求,尤其在高安全要求行业有积累

浪潮的云ERP中包含SRM解决方案,其优势在于服务 大型集团企业、国有企业 的经验。在供应商集中管控、分级管理、采购合规性、审计追溯等方面有较深的设计。对于有严格内控和合规性要求的行业,如国资、军工等,是重点考察对象。

功能上,支持集中采购、分散采购等多种模式,与浪潮的财务、预算系统也能深度集成。

适合谁用:大型集团、国有企业、对采购合规性和集中管控有刚性要求的组织。

需要注意:产品和实施风格相对“稳重”,在用户体验和敏捷性上可能不是其首要追求。

三、总结与建议:如何选择?

看了一圈,你可能更纠结了。别急,最后给你一些落地的建议:

如果业务灵活多变支道这类无代码平台的长远适配性更好。预算不仅要看首次投入,更要评估3-5年的总拥有成本。并且一定要看演示、做试点,功能列表都是美好的,真实体验才能暴露问题。要求厂商用你的真实数据(脱敏后)或模拟场景进行演示。条件允许的话,选择一个非核心采购品类或一个分子公司进行试点,这是最有效的试金石。

供应商管理系统的选型,没有“最好”,只有“最适合”。它不仅是采购工具,更是企业供应链竞争力的数字化体现。

花时间厘清自身需求,结合以上测评信息,相信你能找到最适合自己提升管理效率、降低运营成本的优秀系统。