<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>李美旺</title>
        <link>https://limw.top</link>
        <description>李美旺's blog</description>
        <lastBuildDate>Sun, 24 May 2026 22:55:06 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>李美旺</title>
            <url>https://limw.top/favicon.ico</url>
            <link>https://limw.top</link>
        </image>
        <copyright>All rights reserved 李美旺 2026</copyright>
        <item>
            <title><![CDATA[从 0 到 1 开发一个 AI 视频提示词生成工具：我的「导演工具」项目复盘]]></title>
            <link>https://limw.top/blogs/ai-video-storyboard-tool</link>
            <guid>https://limw.top/blogs/ai-video-storyboard-tool</guid>
            <pubDate>Fri, 08 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[从需求洞察、技术架构、核心功能实现到部署优化，完整复盘一个 AI 电商视频分镜生成工具——用户上传商品图，自动输出 9 镜头专业分镜脚本与可直接使用的英文视频提示词。]]></description>
            <content:encoded><![CDATA[
> 产品地址：story.limw.top | 技术栈：Next.js 16 + PostgreSQL + 多模态视觉模型

## 一、为什么要做这个工具？——需求洞察

做电商的朋友都经历过：拍一条 15 秒的产品视频，从写脚本、画分镜、到找素材、请剪辑，动辄几天起步。而 AI 视频生成工具（Runway、Pika、即梦、可灵）已经能产出相当质量的视频，但**最大的门槛不再是生成能力，而是"提示词"本身**——大多数人根本不知道该怎么写一段能让 AI 生成高质量视频的 prompt。

这个工具要解决的就是这个痛点：**用户上传商品图片，自动输出 9 镜头专业分镜头脚本，包括每个镜头的景别、运镜、画面内容、卖点方向，以及直接可用的英文 AI 视频提示词**。

目标用户很清晰：跨境电商卖家、国内电商运营、短视频内容创作者——他们需要大量产品视频素材，但缺乏视频导演的专业能力。

## 二、产品设计思路

核心交互极简：**上传图片 → 选择参数 → 生成分镜**，三步完成。

表单区只有四个选择：商品类型（箱包/服装/数码/美妆）、视频风格（高级感/日常/户外/商务/轻奢）、投放平台（TikTok/抖音/Amazon）、时长（15s/30s）。选择这些参数不是为了让操作变复杂，而是为了让 AI 生成的内容更精准——不同品类的分镜套路完全不同，箱包卖的是材质和容量，美妆卖的是质地和妆效。

商业模式上采用了"后付费"机制：用户提交任务时不扣费，拿到满意结果后才扣，避免 AI 抽风导致用户白花钱。新用户注册即送 3 次免费试用，4 档付费套餐覆盖测试到重度使用。

## 三、技术架构

```text
┌─────────────────────────────────────────────┐
│                 Next.js 16 App Router        │
│  ┌──────────────────────────────────────┐   │
│  │        page.tsx (单页 SPA)            │   │
│  │  form → polling → storyboard display │   │
│  └──────────────┬───────────────────────┘   │
│                 │ HTTP                       │
│  ┌──────────────▼───────────────────────┐   │
│  │           7 个 API Routes             │   │
│  │  auth/sms | generate | tasks | pay   │   │
│  └──────────────┬───────────────────────┘   │
│                 │                            │
│  ┌──────────────▼───────────────────────┐   │
│  │          Service Layer                │   │
│  │  storyboard-engine | ai-service      │   │
│  │  prompt-templates | billing-service  │   │
│  └──────────────┬───────────────────────┘   │
│                 │                            │
│  ┌──────────────▼───────────────────────┐   │
│  │    PostgreSQL (Supabase) + Prisma     │   │
│  └──────────────────────────────────────┘   │
└─────────────────────────────────────────────┘
```

**前端**：Next.js 16 App Router，单页面应用（`"use client"`）。放弃多页路由，所有状态在一个页面内流转：`idle → loading → success/error`，配合登录弹窗、支付弹窗的按需弹出。UI 全部 Tailwind CSS v4 + Lucide 图标，深色主题。

**后端**：7 个 API Route，承载认证、生成、任务轮询、支付回调四块功能。生成链路是异步的——提交即返回 `202 + taskId`，前端每 3 秒轮询任务状态（最长 120 秒超时）。

**数据库**：PostgreSQL (Supabase 托管)，Prisma ORM，5 张表：User、VerificationCode、Usage、Order、Task。

**AI 层**：OpenAI 兼容接口抽象，一套代码同时支持豆包（doubao-seed-1-6-vision）、智谱（GLM-4V-Plus）、GPT-4o 三个视觉模型，环境变量切换即可，零代码改动。

## 四、核心功能实现：从图片到专业分镜脚本

这是整个项目最有技术含量的部分，拆成四个环节：

### 4.1 图片解析

浏览器端用 `FileReader` 读文件并校验尺寸（最大 4096px、10MB、JPG/PNG/WebP/HEIC），服务端用 `Buffer` 转 base64。两套代码路径严格分离，因为 `FileReader` 和 `Image` 对象只在浏览器环境可用。

```typescript
// 客户端：FileReader + Image (获取尺寸)
const reader = new FileReader();
reader.readAsDataURL(file);
const img = new Image();
img.src = url; // 获取 naturalWidth / naturalHeight

// 服务端：Buffer 直转
const buffer = Buffer.from(await file.arrayBuffer());
const base64 = buffer.toString("base64");
```

转换后的 base64 图片直接嵌入 OpenAI 兼容 API 的 `image_url` 块中发送给视觉模型，不走任何中间存储，节省了对象存储的成本。

### 4.2 Prompt 工程

System Prompt 将 AI 角色设定为"专业电商视频导演"，强制 JSON-only 输出，并给出 7 个字段的精确 schema：

```json
{
  "id": 1,
  "duration": 1.5,
  "shot": "特写",
  "camera": "推进",
  "content": "中文画面描述（50-100字）",
  "selling_point": "卖点方向（10-20字）",
  "prompt_en": "英文 AI 视频提示词（≤200字符，纯英文）"
}
```

User Prompt 部分则根据商品类型注入不同的"分镜套路"。以箱包为例，9 个镜头分别对应：

| 镜头 | 目的 |
|------|------|
| 1 | Logo/材质特写 → 建立品质感 |
| 2 | 360度旋转 → 展示包型轮廓 |
| 3 | 通勤场景佩戴 → 场景代入 |
| 4 | 打开内部 → 展示容量 |
| 5 | 五金/缝线特写 → 做工信任 |
| 6 | 多角度快速切换 → 全面展示 |
| 7 | 不同搭配 → 百搭属性 |
| 8 | 核心卖点强化 → 购买理由 |
| 9 | 品牌收尾定格 → 转化引导 |

这套模板来自实际电商视频的套路总结，四种品类（箱包/服装/数码/美妆）各有一套，确保了生成内容的专业性和一致性。

### 4.3 AI 调用与容错

**Provider 抽象**：`OpenAICompatibleProvider` 封装了所有 OpenAI 兼容接口的视觉模型。豆包走火山方舟（`ark.cn-beijing.volces.com`），智谱走 `open.bigmodel.cn`，GPT-4o 走默认地址。三个 provider 共享完全相同的调用代码，切换只需改环境变量。

**JSON 提取**：视觉模型偶尔会在 JSON 外包一层 markdown code block，或者在前面加几句"Here is the storyboard:"的废话。解析器做了两层防御：

```typescript
// 1. 正则剥离 markdown code block
const match = text.match(/```(?:json)?\s*([\s\S]*?)```/);
// 2. 查找 JSON 起止大括号位置
const start = text.indexOf("{");
const end = text.lastIndexOf("}");
cleaned = text.slice(start, end + 1);
```

**自动重试**：JSON 解析失败时自动重试一次（不是无限重试，避免消耗过多 token）。实测下来，首次成功率约 85%，二次重试后成功率达到 98%+。

### 4.4 prompt_en 的硬约束

`prompt_en` 是最关键也是最有挑战的字段——它必须 ≤200 字符、≤50 个英文单词、纯英文无中文。这是为下游 AI 视频生成工具（Runway/Pika/即梦）适配的硬性限制。System Prompt 里反复强调了这个约束，并在解析阶段做了长度校验，超长的会被要求重试。

### 4.5 30 秒视频的分段处理

15 秒视频直接用 9 个镜头。30 秒视频则在生成 9 个镜头后，在第 5 个镜头处拆分：`part1`（1-5 镜头）和 `part2`（6-9 镜头），各带独立的 `full_prompt`，方便用户在视频工具中分段生成再拼接。

## 五、后付费计费系统

"先出结果，后扣费"的异步扣费模型：

1. 提交时只校验余额 ≥ 1，**不扣费**
2. 后台异步生成，完成后结果写入 Task 表
3. 前端轮询到 `status === "completed"` 时，触发首次扣费
4. 扣费成功后标记 `isCharged = true`，返回结果

并发安全靠 PostgreSQL 的原子更新保证：

```sql
UPDATE User SET credits = credits - 1
WHERE id = $userId AND credits >= 1
```

Prisma 的 `updateMany` 配合 `where: { credits: { gte: 1 } }` 实现了 TOCTOU 防护——两人同时拿同一个结果时，只有一个人能扣成功。

## 六、部署与成本控制

部署在单台 2C4G 的轻量云服务器上，每月成本不到 100 元。用到的几个低成本策略：

- **利旧 PostgreSQL**：复用 Supabase 免费额度（500MB 数据库），不用额外部署数据库
- **无对象存储**：图片 base64 直传 AI API，不落盘，无需 OSS
- **纯 API Route**：没有独立的 Node.js 后端服务，全部用 Next.js API Route，减少运维复杂度
- **PM2 进程守护**：`next start` 由 PM2 管理，自动重启
- **一键部署脚本**：本地打包 → `scp` 上传 → 解压安装依赖 → `pm2 reload`，一行命令完成

遇到的坑：Next.js 16 生产模式下编译需要较多内存，1G 内存的机器 `npm run build` 会 OOM。解决方案是在本地 build 后把 `.next` 目录一起打包上传，服务器只跑 `next start`。

## 七、下一步优化方向

1. **更多商品品类**：目前支持 4 种，扩展到 10+ 种需要补充对应的分镜规则模板
2. **生成结果评分/反馈**：收集用户对生成结果的满意度，用于优化 prompt 模板
3. **视频预览**：生成分镜后如果能直接调用视频生成 API 产出一段预览，闭环体验会大幅提升
4. **边端函数加速**：AI 调用耗时 15-30 秒，如果改成 SSE 流式返回生成过程，用户等待体验会更好

## 写在最后

这个项目从 idea 到上线大约花了两周时间。最大的感受是：**AI 时代的产品开发，核心竞争力不再是"能不能调用 AI"，而是"你能把 AI 封装得多好用"**。prompt 工程的质量、领域知识的注入、交互流程的打磨，这三件事决定了用户是否愿意为你的产品付费。

如果你也在做类似的 AI 工具产品，欢迎交流。
]]></content:encoded>
            <author>19322932086@163.com (李美旺)</author>
        </item>
        <item>
            <title><![CDATA[Claude Code 用 DeepSeek 的人，都有一个 HUD 空白要填]]></title>
            <link>https://limw.top/blogs/claude-hud-deepseek-balance</link>
            <guid>https://limw.top/blogs/claude-hud-deepseek-balance</guid>
            <pubDate>Fri, 22 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[官方 claude-hud 只支持 Claude API 展示用量，DeepSeek 用户那块一直是空的。我 fork 了一份，加上了余额显示。]]></description>
            <content:encoded><![CDATA[
> 项目在这 → [github.com/limeiwang/claude-hud](https://github.com/limeiwang/claude-hud) `feat/deepseek-balance` 分支

Claude HUD 这个插件挺好用的——上下文用量、工具调用、Agent 状态都在底下那条状态栏上。但我用不了，因为我模型接的是 DeepSeek：

```
ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
ANTHROPIC_AUTH_TOKEN=sk-xxxx
```

协议兼容没问题，跑 Claude Code 一切正常。问题是 HUD 的用量数据从 stdin 的 JSON 里读，DeepSeek 不回传 `rate_limits` 和 `usage`，那块就一直空着。

官方 claude-hud 不可能去对接 DeepSeek 的余额接口，跟 Anthropic 又没关系。理解归理解，但每天看着空一块还是有点烦。

## 怎么搞的

翻了下源码，加个 fallback 就行。DeepSeek 有个 `/user/balance` 接口，传 API Key 返回余额。在 HUD 拿不到 stdin 数据时，去拉余额显示出来。

先判断是不是 DeepSeek——看 `ANTHROPIC_BASE_URL` 里有没有 "deepseek"，没有就跳过：

```typescript
function isDeepSeekEndpoint(): boolean {
  const baseUrl = process.env.ANTHROPIC_BASE_URL?.trim()
  if (!baseUrl) return false
  return /deepseek/i.test(baseUrl)
}
```

然后调接口。一开始还担心 DeepSeek 的 API 要不要额外鉴权，试了一下发现就是标准 Bearer token，直接调就行：

```typescript
async function fetchBalanceLabel(apiKey: string): Promise<string | null> {
  const response = await fetch('https://api.deepseek.com/user/balance', {
    headers: { Authorization: `Bearer ${apiKey}` },
    signal: AbortSignal.timeout(5000),
  })
  if (!response.ok) return null
  const data = await response.json()
  // balance_infos[0].total_balance
  // 返回 "Balance ¥xxx.xx" 或 "$xxx.xx"
}
```

5 秒超时，挂了就算了，不影响 HUD 其他东西。

缓存倒是想了一下。HUD 每 300ms 刷新一次，每次都调接口肯定不行。写了个文件缓存扔到 `~/.claude/plugins/claude-hud/.deepseek-cache.json`，5 分钟内不重复请求。

最后在原有用量的获取链里加了一步，就一行代码：

```typescript
usageData = deps.getUsageFromStdin(stdin)
if (!usageData) {
  usageData = (await deps.getDeepSeekUsage()) ?? null
}
if (!usageData) {
  usageData = deps.getUsageFromExternalSnapshot(config, deps.now())
}
```

拿到余额填进 `UsageData.balanceLabel`，HUD 自己会展示。最终效果就是状态栏上多了个 `Balance ¥99.99`。

整个文件 130 行，大部分是类型定义和缓存读写。

## 安装步骤

DeepSeek 用户想试试的话：

```bash
# 1. 先装官方版完成注册
/plugin marketplace add jarrodwatts/claude-hud
/plugin install claude-hud
/claude-hud:setup

# 2. 替换成我的 fork
cd ~/.claude/plugins
git clone -b feat/deepseek-balance https://github.com/limeiwang/claude-hud.git
cd claude-hud && npm install

# 3. 改 statusLine 路径
# ~/.claude/settings.json 里找到 statusLine.command
# 从 cache/ 目录改成 plugins/ 目录
```

有个坑要注意：装完检查一下 `statusLine.command` 指向的是 `plugins/claude-hud/dist/index.js`，不是 `cache/` 下那个官方版。我第一次发给朋友就栽在这——他说装完没效果，查了半天发现 HUD 加载的还是原版。路径这个事不看源码根本想不到。

## 一点感想

130 行代码解决了一个不算刚需、但每天在眼前晃的问题。现在瞟一眼 HUD 就能看到余额，还挺安心。

如果还有下个版本，想加个余额告警——用 DeepSeek 最怕的就是扣完了没发现，会话断到一半。
]]></content:encoded>
            <author>19322932086@163.com (李美旺)</author>
        </item>
        <item>
            <title><![CDATA[个人服务器架构改造：从多端口混乱到统一 443 + 子域名]]></title>
            <link>https://limw.top/blogs/nginx-443-subdomain</link>
            <guid>https://limw.top/blogs/nginx-443-subdomain</guid>
            <pubDate>Tue, 05 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[一台腾讯云轻量服务器运行 5 个项目，从每个服务一个 HTTPS 端口的混乱局面，到统一 443 入口、通配符 SSL、一键部署的现代化方案。]]></description>
            <content:encoded><![CDATA[
## 背景

我有一台腾讯云轻量服务器，上面陆续跑了 5 个项目。最初为了方便，每个服务分配了一个独立端口，久而久之变成了这样：

```
https://www.limw.top:443    → 电商分镜
https://www.limw.top:3333   → Python 前端
https://www.limw.top:1667   → Python API
https://www.limw.top:8088   → POS 收银
https://www.limw.top:39002  → AI 医疗
http://www.limw.top:1234    → Vue 项目
```

存在的问题很明显：非标准端口浏览器会提示不安全，SEO 被降权，前端跨端口请求各种混乱，每次部署都要 SSH 手动操作。

花了两天时间彻底重构，下面是完整的改造过程。

---

## 改造后架构

```
https://limw.top / www.limw.top     → 博客（Next.js + MDX）
https://story.limw.top              → 电商视频分镜AI（Next.js + AI）
https://pos.limw.top                → POS 收银系统（Vue + Express）
https://py.limw.top                 → Python 服务
https://doctor.limw.top             → AI 医疗（React）
HTTP（80端口）                      → 301 跳转 HTTPS
```

核心思路：**所有服务统一走 443 端口，用子域名区分，一张通配符 SSL 证书覆盖全部**。

---

## 一、SSL 通配符证书

这是最关键的一步。用 acme.sh + Let's Encrypt 免费签发 `*.limw.top` 通配符证书：

```bash
# 首次申请（手动 DNS 验证）
export DP_Id='你的SecretId'
export DP_Key='你的SecretKey'
/root/.acme.sh/acme.sh --issue -d limw.top -d '*.limw.top' \
  --dns dns_dp --server letsencrypt

# 安装到 Nginx
/root/.acme.sh/acme.sh --install-cert -d limw.top -d '*.limw.top' \
  --key-file /path/to/ssl/your-domain.key \
  --fullchain-file /path/to/ssl/your-domain_bundle.pem \
  --reloadcmd "nginx -s reload"
```

配置了 DNSPod API 自动续期，crontab 每天检查，到期前 30 天自动续签，完全不用管。

---

## 二、Nginx 子域名分发

每个服务一个独立的 Nginx server block，通过 `server_name` 匹配：

```nginx
# 博客（默认站）
server {
    listen 443 ssl;
    server_name limw.top www.limw.top;
    location / { proxy_pass http://127.0.0.1:3002; }
}

# 电商分镜
server {
    listen 443 ssl;
    server_name story.limw.top;
    location / { proxy_pass http://127.0.0.1:3000; }
}

# POS 收银（静态前端 + API）
server {
    listen 443 ssl;
    server_name pos.limw.top;
    location / { root /var/www/pos; try_files $uri $uri/ /index.html; }
    location /api { proxy_pass http://127.0.0.1:3001; }
}

# HTTP → HTTPS 强制跳转
server {
    listen 80;
    server_name _;
    return 301 https://$host$request_uri;
}
```

关键点：`proxy_pass` 统一用 `127.0.0.1` 而不是公网 IP，避免了流量绕出公网又回来的路由问题。

---

## 三、一键部署

每个项目都配了 `deploy.sh`，修改代码后一个命令搞定：

```bash
bash deploy.sh
```

脚本自动完成：打包 → SCP 上传 → 解压 → npm install → npm run build → 重启 PM2 → 清除 Nginx 缓存。

重点修复了两个坑：

1. **tar 目录嵌套**：`--strip-components=1` 解决解压后路径不对的问题
2. **Nginx proxy_cache**：每次部署清除 Nginx 缓存目录，不然页面永远是旧的

---

## 四、POS 系统接口地址迁移

POS 前端是 Vue 构建产物，JS 里硬编码了旧地址 `https://www.limw.top:8088`。两个方案：

**临时方案** — Nginx sub_filter 自动替换：
```nginx
sub_filter_once off;
sub_filter_types application/javascript;
sub_filter 'https://www.limw.top:8088' 'https://pos.limw.top';
```

**根治方案** — 修改 `.env.production` 源码重建：
```
VITE_API_BASE_URL='https://pos.limw.top'
```

---

## 踩坑记录

| 问题 | 原因 | 解决 |
|------|------|------|
| 部署后页面不变 | Nginx proxy_cache | 每次构建后清缓存 |
| 域名访问旧版本 | proxy_pass 走公网 IP 回源 | 改用 127.0.0.1 |
| 子域名无 HTTPS | 需要通配符证书 | acme.sh + Let's Encrypt |
| GitHub 克隆超时 | 国内服务器 | ZIP 打包 + SCP 上传 |
| doctor 域名显示博客 | 端口监听错误 | 39002 → 443 |

---

## 总结

改造前后对比：

- **改造前**：6 个不同端口，手动部署，证书分散，维护痛苦
- **改造后**：统一 443 入口，子域名清晰，自动续期，一键部署

核心经验：**别在端口上做文章，用子域名做服务隔离，一张通配符证书解决所有 SSL 问题**。

如果你也有类似的"祖传服务器"，强烈建议花一个周末统一一下架构，后续开发体验会好很多。
]]></content:encoded>
            <author>19322932086@163.com (李美旺)</author>
        </item>
        <item>
            <title><![CDATA[欢迎访问我的博客]]></title>
            <link>https://limw.top/blogs/test-blog</link>
            <guid>https://limw.top/blogs/test-blog</guid>
            <pubDate>Mon, 04 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[这是我的第一篇博客文章，后续会在这里分享 AI 应用实践、架构设计和产品落地方面的思考。]]></description>
            <content:encoded><![CDATA[
## 关于这个博客

这里会记录我在 AI 应用开发、前端架构设计和产品商业化方面的一些思考与实践。

### 计划写作的方向

- **AI 应用实践** — 大模型接入、AI 产品开发经验
- **架构设计** — 微前端、全栈架构、系统设计
- **产品落地** — 从技术到商业化产品的思考

敬请期待更多内容。
]]></content:encoded>
            <author>19322932086@163.com (李美旺)</author>
        </item>
        <item>
            <title><![CDATA[让 AI 文本更像真人——Unbot 开发实录]]></title>
            <link>https://limw.top/blogs/unbot-ai-text-humanizer</link>
            <guid>https://limw.top/blogs/unbot-ai-text-humanizer</guid>
            <pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[纯正则实现的中英文 AI 文本"去套话"工具。覆盖规则引擎设计、Chrome 扩展划词即用、CLI 批量处理，以及中文 NLP 规则工程里那些没想到的坑。]]></description>
            <content:encoded><![CDATA[
> 产品定位：AI 文本后处理工具 | 技术栈：Next.js 15 + React 19 + TailwindCSS 4 + Chrome Extension MV3 + esbuild | npm：`@unbot/core` · `@unbot/cli`

## 一、为什么要做这个工具？

不管是 ChatGPT、Claude 还是文心一言，AI 生成的文本都有一个共同的毛病——**一看就是 AI 写的**。

中文尤其明显。「值得注意的是」、「综上所述」、「从技术角度来看」——这些短语单独看没问题，但 AI 每段话都来一遍，读起来就像在念论文摘要。英文也一样，`It is worth noting that`、`First of all`、`In conclusion` 几乎成了 AI 的 signature。

**大模型被训练得"太有礼貌了"**。它在回答里加了一大堆过渡词、免责声明和总结套话，写论文没问题，但发微信只会让人读着生硬和啰嗦。

我的需求很简单：股票监控系统每天通过微信推送选股报告和买卖信号，AI 生成的分析文本不能直接发。手动一条条删不现实——所以需要一个自动化工具。

## 二、产品设计

**把 AI 文本过一道"去套话"管道，保留实质内容，去掉废话**。

三个入口，覆盖不同场景：

- **Web 编辑器**：粘贴文本 → 可视化调参 → 即时预览 Before/After
- **Chrome 扩展**：选中文本 → 右键菜单 → 一键优化 → 自动替换剪贴板
- **CLI 工具**：`echo "text" | unbot`，设计为可嵌入其他项目

用户可调节的参数有两个：
1. **套话类型**：10 类（中文 6 类 + 英文 4 类），可单独开关——比如"过渡词可以去掉，但免责声明我想保留"
2. **短句参数**：每段最多字数、最多行数，超出则截断

## 三、技术架构

```
输入文本 → Parser（段落分割） → Humanizer Pipeline → Renderer（微信格式） → 输出
                                    │
                          ┌─────────┼─────────┐
                     CN Categories  │  EN Categories
                   (hedging/summary │ (hedging/conclusion
                    /transitions/   │  /transitions/
                    perspective/    │  perspective/filler)
                    filler/ending)  │
                                    │
                              Shortener
                          (断句 + 缩句 + 去重)
```

三层管道：

- **Parser**：按空行分割段落区块，保留空行的语义分隔
- **Humanizer**：核心引擎。每个段落依次过 N 道规则，支持分类开关。纯正则，零依赖
- **Renderer**：将优化结果重新渲染为微信友好的格式——空行间距、段落缩进、Bullet List 对齐

任何一层都可以被替换或关闭。要适配钉钉/Lark，只需要加一个 Renderer。

## 四、Humanizer 规则引擎——最复杂的部分

### 4.1 纯正则，零依赖

MVP 阶段没用 NLP 模型，选了纯正则。**确定性比杀鸡用牛刀更有用**——套话无非是模式匹配，AI 翻来覆去就那么几十个短语，正则够用、可调试、可预测。

### 4.2 中文 6 类规则

按处理顺序排列（顺序决定了正确性）：

| 类型 | 示例 | 策略 |
|------|------|------|
| 免责套话 | 值得注意的是、不可否认的是 | 直接删除（可出现在句中任何位置） |
| 冗余总结 | 基于以上分析、综上所述 | 句首匹配，删除整句引导 |
| 过渡套话 | 首先、其次、综合来看 | 句首匹配，保留原句连接 |
| 视角套话 | 从技术角度来看、在宏观层面上 | 句首匹配，删除视角设定 |
| 填充套话 | 在一定程度上、换言之 | 句中任意位置，直接删除 |
| 模板结尾 | 建议投资者保持谨慎乐观 | 整句删除 |

### 4.3 英文 5 类规则

同样按顺序处理：

| 类型 | 示例 | 策略 |
|------|------|------|
| Hedging | It is worth noting that | 句首/句中，直接删除 |
| Conclusion | In conclusion, To sum up | 句首匹配 |
| Transitions | First of all, Secondly | 句首匹配 |
| Perspective | From a technical perspective | 句首匹配 |
| Filler | Generally speaking | 句首/句中，直接删除 |

### 4.4 中文边界匹配的坑

中文不像英文有天然的空格分词。正则匹配句首时，英文用 `(^|[.!?]\s*)` 就够了，但中文还要考虑句号、问号、感叹号。

一开始只考虑了 `。(句号)` 做边界，结果"今天天气不错。首先，从技术角度来看..."——优化后变成了"今天天气不错。从技术角度来看..."——"首先"去掉了，但"从技术角度来看"还在。

**解决办法**：规则按正确顺序排列。冗余总结和视角类规则必须在过渡规则之前处理。如果"基于以上分析"和"首先"同时出现，要先去掉"基于以上分析"，再去掉"首先"。顺序不对，"首先"就匹配不到（因为它前面不是句号，而是"分析"这个词）。

### 4.5 句首大写保护

英文句子去掉句首短语后，第一个字母可能变成小写。比如 `First of all, the market is strong.` → 去掉 `First of all, ` 后得到 `the market is strong.`。

修复方案：末尾过一道 `capitalizeSentences()`，把每个句子开头的字母大写。

```typescript
function capitalizeSentences(text: string): string {
  return text
    .replace(/(^|[.!?]\s*)([a-z])/g, (_, pre, char) => pre + char.toUpperCase())
    .trim();
}
```

### 4.6 数据效果

70 个 E2E 测试覆盖了全部规则的正确性和边界情况：

- 中文内容：通常缩减 15-35%
- 英文内容：通常缩减 10-25%
- 报告类文本（如选股结果）：40-50% 缩减 —— 因为 AI 生成的总结套话最多

## 五、Chrome 扩展：划词即用

选中网页上的 AI 文本，右键 → "优化文本" → 结果自动复制到剪贴板。这是最常用的使用方式。

几个值得说的技术细节：

**Content Script 动态注入**
Chrome MV3 的 content script 并不是在所有页面都自动加载。如果页面加载后才安装扩展，context menu 点击时会收不到响应。

解决方案：background.js 在点击菜单时先 `chrome.tabs.sendMessage` 发 ping，如果超时则用 `chrome.scripting.executeScript` 动态注入。

**Tooltip 定位**
优化后的 tooltip 用 `position: fixed` 相对于视口定位。`getBoundingClientRect()` 返回的坐标已经是视口相对坐标，不需要加 `window.scrollY/scrollX`——这是一个踩过的坑，加了反而会让 tooltip 偏移。

**CSP 兼容**
有些网站会通过 Content-Security-Policy 禁止 `<style>` 标签注入。解决方案：改用内联 `style` 属性设置样式，完全绕过 CSP。

## 六、CLI 工具：让优化器可嵌入

Web 和扩展给"人"用，但我真正要的是**嵌入到其他项目中**。

Stock-monitor 是 Python 项目，每天定时跑选股、买卖信号和收盘复盘，通过 Hermes gateway 发微信。改造方式很简单：在 `send_bridge.py` 里发微信前，先把文本管道给 `unbot`：

```python
result = subprocess.run(
    ["unbot", "--json"],
    input=text, capture_output=True, text=True, timeout=10,
)
data = json.loads(result.stdout)
text = data["optimized"]
```

不侵入原有逻辑，三份 Python 脚本零改动。

为了跨项目分发，我用 esbuild 把 CLI 打包成单文件 CJS bundle，发布到 npm：

```bash
npm install -g @unbot/cli
echo "值得注意的是..." | unbot
```

零外部依赖，复制到任意项目即可运行。

## 七、遇到的坑

| 问题 | 原因 | 解决 |
|------|------|------|
| 句首过渡词删不掉 | 前一类规则残留文本打乱了句首边界 | 调整 6 类规则的执行顺序 |
| 英文句首小写 | 去掉短语后首字母恢复原文大小写 | 后处理 capitalizeSentences() |
| Chrome tooltip 位置偏移 | 给 position:fixed 加了 scrollY | 去掉 scroll 修正 |
| Tooltip 样式被 CSP 拦截 | 网站禁止 `<style>` 注入 | 改用内联 style 属性 |
| esbuild bundle 太大 | 输出 ESM 格式 + 依赖未 external | 改 CJS 格式，external Node builtins |
| 中英混排的标点处理 | 中文句号不影响英文正则边界 | 在所有英文 pattern 的边界组里加上中文句号 |

## 八、总结

**不要低估规则工程的复杂度**。

20 条正则看起来简单，但要覆盖中英混排、嵌套套话、标点差异、顺序依赖——每一步"小聪明"最终都变成了 bug。70 个 E2E 测试才兜住。

纯正则还是 LLM 做后处理？零成本、可预测、可调试是正则的好处；代价是手工维护规则库，AI 在变，套话也在变。长期看可能需要一个轻量分类模型来辅助。

下一步计划：
1. **更多 Renderer 适配**：Lark、钉钉、Telegram 各有不同的消息格式约束
2. **规则在线市场**：用户可以提交自定义规则，形成社区规则库
3. **更多语言支持**：日语和韩语的敬语也是常见的 AI 文本标记
]]></content:encoded>
            <author>19322932086@163.com (李美旺)</author>
        </item>
        <item>
            <title><![CDATA[让 AI 文本更像真人——微信文本人性化引擎开发实录]]></title>
            <link>https://limw.top/blogs/wechat-ai-text-humanizer</link>
            <guid>https://limw.top/blogs/wechat-ai-text-humanizer</guid>
            <pubDate>Wed, 20 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[纯正则实现的中英文 AI 文本"去套话"工具。覆盖规则引擎设计、Chrome 扩展划词即用、CLI 批量处理，以及中文 NLP 规则工程里那些没想到的坑。]]></description>
            <content:encoded><![CDATA[
> 产品定位：AI 文本后处理工具 | 技术栈：Next.js 15 + React 19 + TailwindCSS 4 + Chrome Extension MV3 + esbuild | 源码：私有项目

## 一、为什么要做这个工具？

不管是 ChatGPT、Claude 还是文心一言，AI 生成的文本都有一个共同的毛病——**一看就是 AI 写的**。

中文尤其明显。「值得注意的是」、「综上所述」、「从技术角度来看」——这些短语单独看没问题，但 AI 每段话都来一遍，读起来就像在念论文摘要。英文也一样，`It is worth noting that`、`First of all`、`In conclusion` 几乎成了 AI 的 signature。

**大模型被训练得"太有礼貌"了**。它在回答里加了一大堆过渡词、免责声明和总结套话，写论文没问题，但发微信只会让人读着生硬和啰嗦。

我的需求很简单：股票监控系统每天通过微信推送选股报告和买卖信号，AI 生成的分析文本不能直接发。手动一条条删不现实——所以需要一个自动化工具。

## 二、产品设计

**把 AI 文本过一道"去套话"管道，保留实质内容，去掉废话**。

三个入口，覆盖不同场景：

- **Web 编辑器**：粘贴文本 → 可视化调参 → 即时预览 Before/After
- **Chrome 扩展**：选中文本 → 右键菜单 → 一键优化 → 自动替换剪贴板
- **CLI 工具**：`echo "text" | npx tsx cli/index.ts`，设计为可嵌入其他项目

用户可调节的参数有两个：
1. **套话类型**：10 类（中文 6 类 + 英文 4 类），可单独开关——比如"过渡词可以去掉，但免责声明我想保留"
2. **短句参数**：每段最多字数、最多行数，超出则截断

## 三、技术架构

```
输入文本 → Parser（段落分割） → Humanizer Pipeline → Renderer（微信格式） → 输出
                                    │
                          ┌─────────┼─────────┐
                     CN Categories  │  EN Categories
                   (hedging/summary │ (hedging/conclusion
                    /transitions/   │  /transitions/
                    perspective/    │  perspective/filler)
                    filler/ending)  │
                                    │
                              Shortener
                          (断句 + 缩句 + 去重)
```

三层管道：

- **Parser**：按空行分割段落区块，保留空行的语义分隔
- **Humanizer**：核心引擎。每个段落依次过 N 道规则，支持分类开关。纯正则，零依赖
- **Renderer**：将优化结果重新渲染为微信友好的格式——空行间距、段落缩进、Bullet List 对齐

任何一层都可以被替换或关闭。要适配钉钉/Lark，只需要加一个 Renderer。

## 四、Humanizer 规则引擎——最复杂的部分

### 4.1 纯正则，零依赖

MVP 阶段没用 NLP 模型，选了纯正则。**确定性比杀鸡用牛刀更有用**——套话无非是模式匹配，AI 翻来覆去就那么几十个短语，正则够用、可调试、可预测。

### 4.2 中文 6 类规则

按处理顺序排列（顺序决定了正确性）：

| 类型 | 示例 | 策略 |
|------|------|------|
| 免责套话 | 值得注意的是、不可否认的是 | 直接删除（可出现在句中任何位置） |
| 冗余总结 | 基于以上分析、综上所述 | 句首匹配，删除整句引导 |
| 过渡套话 | 首先、其次、综合来看 | 句首匹配，保留原句连接 |
| 视角套话 | 从技术角度来看、在宏观层面上 | 句首匹配，删除视角设定 |
| 填充套话 | 在一定程度上、换言之 | 句中任意位置，直接删除 |
| 模板结尾 | 建议投资者保持谨慎乐观 | 整句删除 |

### 4.3 英文 5 类规则

同样按顺序处理：

| 类型 | 示例 | 策略 |
|------|------|------|
| Hedging | It is worth noting that | 句首/句中，直接删除 |
| Conclusion | In conclusion, To sum up | 句首匹配 |
| Transitions | First of all, Secondly | 句首匹配 |
| Perspective | From a technical perspective | 句首匹配 |
| Filler | Generally speaking | 句首/句中，直接删除 |

### 4.4 中文边界匹配的坑

中文不像英文有天然的空格分词。正则匹配句首时，英文用 `(^|[.!?]\s*)` 就够了，但中文还要考虑句号、问号、感叹号。

一开始只考虑了 `。(句号)` 做边界，结果"今天天气不错。首先，从技术角度来看..."——优化后变成了"今天天气不错。从技术角度来看..."——"首先"去掉了，但"从技术角度来看"还在。

**解决办法**：规则按正确顺序排列。冗余总结和视角类规则必须在过渡规则之前处理。如果"基于以上分析"和"首先"同时出现，要先去掉"基于以上分析"，再去掉"首先"。顺序不对，"首先"就匹配不到（因为它前面不是句号，而是"分析"这个词）。

### 4.5 句首大写保护

英文句子去掉句首短语后，第一个字母可能变成小写。比如 `First of all, the market is strong.` → 去掉 `First of all, ` 后得到 `the market is strong.`。

修复方案：末尾过一道 `capitalizeSentences()`，把每个句子开头的字母大写。

```typescript
function capitalizeSentences(text: string): string {
  return text
    .replace(/(^|[.!?]\s*)([a-z])/g, (_, pre, char) => pre + char.toUpperCase())
    .trim();
}
```

### 4.6 数据效果

70 个 E2E 测试覆盖了全部规则的正确性和边界情况：

- 中文内容：通常缩减 15-35%
- 英文内容：通常缩减 10-25%
- 报告类文本（如选股结果）：40-50% 缩减 —— 因为 AI 生成的总结套话最多

## 五、Chrome 扩展：划词即用

选中网页上的 AI 文本，右键 → "优化文本" → 结果自动复制到剪贴板。这是最常用的使用方式。

几个值得说的技术细节：

**Content Script 动态注入**
Chrome MV3 的 content script 并不是在所有页面都自动加载。如果页面加载后才安装扩展，context menu 点击时会收不到响应。

解决方案：background.js 在点击菜单时先 `chrome.tabs.sendMessage` 发 ping，如果超时则用 `chrome.scripting.executeScript` 动态注入。

**Tooltip 定位**
优化后的 tooltip 用 `position: fixed` 相对于视口定位。`getBoundingClientRect()` 返回的坐标已经是视口相对坐标，不需要加 `window.scrollY/scrollX`——这是一个踩过的坑，加了反而会让 tooltip 偏移。

**CSP 兼容**
有些网站会通过 Content-Security-Policy 禁止 `<style>` 标签注入。解决方案：改用内联 `style` 属性设置样式，完全绕过 CSP。

## 六、CLI 工具：让优化器可嵌入

Web 和扩展给"人"用，但我真正要的是**嵌入到其他项目中**。

Stock-monitor 是 Python 项目，每天定时跑选股、买卖信号和收盘复盘，通过 Hermes gateway 发微信。改造方式很简单：在 `send_bridge.py` 里发微信前，先把文本管道给 `wechat-optimize.js`：

```python
result = subprocess.run(
    ["node", "wechat-optimize.js", "--json"],
    input=text, capture_output=True, text=True, timeout=10,
)
data = json.loads(result.stdout)
text = data["optimized"]
```

不侵入原有逻辑，三份 Python 脚本零改动。

为了跨项目分发，我用 esbuild 把 CLI 打包成单文件 CJS bundle：

```bash
node scripts/build-cli.js
# 输出: cli/dist/wechat-optimize.js (15KB)
```

零外部依赖，复制到任意项目即可运行。

## 七、遇到的坑

| 问题 | 原因 | 解决 |
|------|------|------|
| 句首过渡词删不掉 | 前一类规则残留文本打乱了句首边界 | 调整 6 类规则的执行顺序 |
| 英文句首小写 | 去掉短语后首字母恢复原文大小写 | 后处理 capitalizeSentences() |
| Chrome tooltip 位置偏移 | 给 position:fixed 加了 scrollY | 去掉 scroll 修正 |
| Tooltip 样式被 CSP 拦截 | 网站禁止 `<style>` 注入 | 改用内联 style 属性 |
| esbuild bundle 太大 | 输出 ESM 格式 + 依赖未 external | 改 CJS 格式，external Node builtins |
| 中英混排的标点处理 | 中文句号不影响英文正则边界 | 在所有英文 pattern 的边界组里加上中文句号 |

## 八、总结

**不要低估规则工程的复杂度**。

20 条正则看起来简单，但要覆盖中英混排、嵌套套话、标点差异、顺序依赖——每一步"小聪明"最终都变成了 bug。70 个 E2E 测试才兜住。

纯正则还是 LLM 做后处理？零成本、可预测、可调试是正则的好处；代价是手工维护规则库，AI 在变，套话也在变。长期看可能需要一个轻量分类模型来辅助。

下一步计划：
1. **更多 Renderer 适配**：Lark、钉钉、Telegram 各有不同的消息格式约束
2. **规则在线市场**：用户可以提交自定义规则，形成社区规则库
3. **更多语言支持**：日语和韩语的敬语也是常见的 AI 文本标记
]]></content:encoded>
            <author>19322932086@163.com (李美旺)</author>
        </item>
    </channel>
</rss>