北海的鱼 - AI 2026-10-10T15:23:25+08:00 Typecho https://blog.qsuu.cn/feed/atom/tag/AI/ <![CDATA[想让 AI 代发文章?Typecho 后台自动化对接踩坑实录]]> https://blog.qsuu.cn/archives/AI-Typecho.html 2026-10-10T15:23:25+08:00 2026-10-10T15:23:25+08:00 北海的鱼 https://blog.qsuu.cn 最近在折腾一件事:让 AI 助手直接登录博客后台,帮我整理并发布文章。想法很朴素——内容采集、改写、排版都交给它,我只负责最后确认。

真动手才发现,这条路比想象中绕。前后踩了三个坑,记录一下,省得你重走。

第一个坑:curl 登录,看起来成功其实是假的

最直觉的做法是命令行模拟表单提交。Typecho 的登录表单长这样:

<form action="https://example.com/index.php/action/login?_=xxxx" method="post">
  <input type="text" name="name" />
  <input type="password" name="password" />
  <input type="hidden" name="referer" value="https://example.com/admin/" />
</form>

于是:

curl -c cookie.txt -L "https://example.com/admin/" -o login.html
TOKEN=$(grep -oE 'login\?_=[a-zA-Z0-9]+' login.html | head -1 | cut -d= -f2)
curl -b cookie.txt -c cookie.txt -L \
  --data-urlencode "name=你的账号" \
  --data-urlencode "password=你的密码" \
  --data-urlencode "referer=https://example.com/admin/" \
  "https://example.com/index.php/action/login?_=$TOKEN"

诡异的是,返回 302 且 Location 指向后台——看着像成功了。但一访问后台,还是登录页。

关键判断点在这里:

curl ... -D - -o /dev/null "..." | grep -i "set-cookie"

如果响应头里没有 Set-Cookie,那就是没登录成功,不管 Location 跳去哪。Typecho 登录成功一定下发 __typecho_authCode。

为什么失败?看完整响应头就能发现线索:

EO-Cache-Status: MISS
EO-LOG-UUID: 17057971157060924413

站点在腾讯云 EdgeOne 这类 CDN 后面,非浏览器请求在会话层就被处理掉了,压根到不了 PHP。结论:curl 这条路对这类站点不通,别浪费时间调参数。

第二个坑:浏览器工具下载 Chromium 会超时

既然要真实浏览器,那就装自动化工具。agent-browser 这类 CLI 有个 install 子命令负责拉 Chromium,约 198MB。

实际结果:

40/198 MB (20%)
Retrying download (attempt 2/3)
Retrying download (attempt 3/3)
✗ Download error: operation timed out

国内网络拉 Google 的 chrome-for-testing 源,超时是常态。不要在这步死等。

第三个坑:headless 模式也会被拦

绕过下载的办法:你机器上大概率已经有 Chrome 或 Edge,直接拿来用。

npm install playwright-core

playwright-core 不含浏览器,只有驱动,几 MB,秒装。然后指向本机 Chrome:

const browser = await chromium.launch({
  executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
  headless: true
});

结果访问后台直接 30 秒超时。改成 headless: false 立刻正常——同样的 cookie、同样的 URL。CDN 对无头浏览器同样有识别。

最终能跑通的方案

const { chromium } = require('playwright-core');

(async () => {
  const browser = await chromium.launch({
    executablePath: 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
    headless: false,
    args: ['--disable-blink-features=AutomationControlled']
  });
  const ctx = await browser.newContext({
    viewport: { width: 1280, height: 860 },
    userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
  });
  const page = await ctx.newPage();

  await page.goto('https://example.com/admin/', { waitUntil: 'domcontentloaded' });
  await page.fill('#name', '你的账号');
  await page.fill('#password', '你的密码');
  await page.check('#remember');          // 勾选记住登录,cookie 有效期更长
  await page.click('button[type="submit"]');
  await page.waitForTimeout(5000);

  console.log(await page.title());        // 输出「网站概要」即为成功
  await ctx.storageState({ path: 'state.json' });   // 保存登录态
  await browser.close();
})();

跑完看到标题变成「网站概要 - ...」,就算进去了。

storageState 这一步很值钱:登录态存成 state.json 后,后续所有脚本都这样复用,不用再输一次密码:

const ctx = await browser.newContext({ storageState: 'state.json' });

发布文章:先摸清表单字段

不同主题的写作页字段差异不小(特别是加了自定义字段的主题)。先探测再填,别猜:

document.querySelectorAll('input[name], select[name], textarea[name]').forEach(el => {
  console.log(el.tagName, el.name, el.type, el.id);
});

Typecho 原生字段一般是这些:

  • title 标题、text 正文(Markdown)、slug 别名
  • category[] 分类(checkbox,值是分类 ID)
  • tags 标签、date 日期
  • visibility 可见性、allowComment 允许评论

带自定义字段的主题还会有 fields[excerpt](摘要)、fields[imgst](封面图)、fields[catalog](目录开关)这类,按需填。

填完点「发布文章」提交:

await page.fill('#title', '文章标题');
await page.evaluate(t => { document.querySelector('#text').value = t; }, 正文内容);
await page.check('#category-1');        // 勾选分类,注意是 ID 不是名字
await page.fill('#tags', '标签1,标签2');
await page.click('button:has-text("发布文章")');
await page.waitForTimeout(5000);

摘要里那句「注意是 ID 不是名字」是血泪:分类复选框的 id 是 category-1 这种,对应的是分类 ID,得先遍历一遍把 ID 和名称对应上:

document.querySelectorAll('input[name="category[]"]').forEach(el => {
  const lab = document.querySelector(`label[for="${el.id}"]`);
  console.log(el.value, lab ? lab.textContent.trim() : '?');
});

几条安全提醒

折腾完回看,这几条比技术细节更重要:

  1. 密码不要写进任何笔记或脚本。登录一次拿到 state.json 就够了,后续全靠它
  2. state.json 等同于账号权限,别提交到公开仓库,也别随手发给别人
  3. 发文章前先看一眼草稿。自动化发错分类、发错时间,删起来很烦
  4. 采集来的内容要重写再发,直接搬运既有版权风险,也不利于收录
  5. 文章里涉及自己站点的后台路径、账号名,一律用 example.com 之类替代

小结

三个坑其实指向同一件事:现代站点前面挡着 CDN,自动化得按真实浏览器的路子走。curl 走不通,无头浏览器走不通,用本机浏览器 + 有头模式反而最省事。

真正跑通之后,体验是值得的:内容整理、排版、配图、发布一气呵成,人只做最后的判断。这条路走顺了,后面就是批量化的事了。

]]>
<![CDATA[如何快速部署一个属于自己的GPT网站]]> https://blog.qsuu.cn/archives/56.html 2024-06-14T11:33:00+08:00 2024-06-14T11:33:00+08:00 北海的鱼 https://blog.qsuu.cn 如何快速部署一个属于自己的GPT网站?之前写过一篇,但是教程图片没了,重新写一下吧!
ChatGPT-Next-Web应该很多人听说过这个,开源作品,免费更新,目前71.6k stars,主要是不需要服务器,不需要域名,开袋即食!
Test
首先进入github源码地址: https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web ,进去往下滑,点击Deploy,使用github账号快捷登录,当然你有其他账号也可以用。
Test
Test
登录好以后就是以下界面,然后点击Create,如果名称不对,就改成纯数字也行。
Test
Test
然后下面1是你的GPT的apikey,可填可不填,如果你填上那么后面网站部署好就能直接用,如果没有设置密码,任何人打开都能直接用,如果不填后面部署好,可以再设置里面填写即可。
2就是设置使用密码,假如上面第一步你填了你的api,又不想让别人用,那么可以设置这个密码,即便网站泄露也得需要密码才能使用,第一步没填api就无所谓了。
第一第二又是必填,不然不能部署,所以不填api就随便填就行,然后点击下面部署,等待部署。
Test
Test
部署完以后,点击左上角你的账号那里来到首页,点击你刚才创建好的项目,箭头所指Domains下面就是你的临时域名,这个域名需要用上魔法才能打开,不然打不开。
Test
Test
如果想要不用魔法就能打开网页,就得需要自己注册域名,腾讯,阿里等都行。我这里就是用自己的域名演示。点击右上角Domains---输入域名---点击Add---选第三个,然后点击Add。
Test
Test
Test
完事以后要把你的域名解析到cname.vercel-dns.com.地址,不会的可以百度或者看我下一篇教程吧。
Test
解析完以后就是这个状态,点击域名就能访问,到此网站就部署成功了。
Test
Test
成功以后点击右下角设置,找到自定义接口,默认接口是官方接口,只需要填写自己的key就行,下面是选择想使用的模型。
如果不想使用官方接口,就把接口换成第三方的填上,key也是第三方的key,适合使用转发的api!
Test
Test
以上就是ChatGPT-Next-Web基本部署教程,其他的自己摸索吧!

]]>