矩阵云开发手册

小白讲解 · 专家实现 · 动手练习 · 排障与上线 | 支持离线阅读,可用浏览器打印为 PDF

矩阵云开发手册

从零理解软件开发,用自然语言与 AI 协作,并逐步学会独立判断、修改、测试和维护项目。

适用读者:第一次接触开发的项目负责人,以及接手矩阵云的开发者。阅读方式:先读第一至第九章的小白讲解并完成练习,再阅读后面的实现细节。专家读者可以直接从第十章开始。

本手册以本仓库 9b9b12f 的代码结构为讲解依据,整理于 2026 年 10 月 8 日。代码示例分为“项目实际实现”和“教学示例”。教学示例不能直接替代生产代码。历史截图中的报错是排查线索,不等于已经证实的根因;文中的修复方法也不代表这些线上问题已经修复。

你最终要学会的事情是:把想法变成可验证的需求,理解每个模块承担什么责任,让 AI 在明确范围内修改代码,并用证据判断是否能交给客户使用。学会这套方法后,即使换电脑、换开发者、换 AI 工具,你也能继续管理项目。

阅读导航

章节 解决的问题 阅读后的产出
1 项目怎样成为软件 自然语言怎样变成能运行的系统 画出系统的五个部分
2 基础词汇与代码 文件、代码、程序、接口到底是什么 读懂简单函数和 JSON
3 准备学习环境 在 Windows 或 Mac 上从哪里开始 一个独立练习目录
4 第一个完整小系统 前端、后端、任务如何连起来 能运行的模拟同步页面
5 读懂矩阵云目录 项目文件分别有什么用 一张代码定位地图
6 从需求到实现 怎样让 AI 正确理解你的需求 一份带验收条件的需求单
7 页面与交互 为什么按钮、列表和状态经常出错 能解释状态更新与错误提示
8 数据与数据库 数据为什么会丢、重复或串账号 理解 ID、事务与数据归属
9 日常开发流程 每次迭代应该先后做什么 一次完整的小改动记录
10 真实系统架构 登录、同步、草稿如何跨模块运行 按代码追踪一条请求
11 任务与一致性 重复点击、断网、取消如何处理 一张状态转换和异常处理表
12 系统性排障 怎样找根因而不是改报错文字 可复现、可证伪的问题记录
13 测试与验收 什么证据才能证明修复有效 分层测试与平台验收矩阵
14 跨平台开发 Windows、Mac、浏览器差异如何处理 可重复的环境检查步骤
15 部署与迁移 本地正常为何线上不正常 发布、迁移与回滚清单
16 维护与性能 怎样减少混乱、开发费用和运行故障 模块化维护与性能证据
17 学习计划与模板 学完之后如何持续练习 六周路线与可复用提示词

第一章 项目怎样成为软件

1.1 小白理解

你说“我要一个能管理多个小红书账号、生成内容并保存草稿的系统”,这是一项业务目标。电脑不能直接理解这句话中的全部细节:谁可以操作、内容保存在哪里、用户连点两次怎么办、断网后怎么办,都需要明确。

开发过程就是把这个目标逐层展开:

业务目标
  ↓ 拆成具体用户动作
页面、按钮、输入和结果
  ↓ 定义每个动作需要的数据
接口、任务、权限、数据库
  ↓ 编写规则
程序代码
  ↓ 检查规则能否正确工作
测试、实际操作、发布
  ↓ 客户使用后形成新证据
记录问题、定位原因、继续迭代

AI 可以替你完成大量写代码、查文件和运行检查的工作。你的核心职责是说清业务、确定优先级,并检查交付证据。自然语言开发依然遵循软件工程规律:没有准确需求,AI 会补全它认为合理的细节;没有验收,它可能把“程序能启动”当作“客户能完成工作”。

矩阵云有五个主要部分:

部分 类比 实际职责
工作台页面 店铺前台 显示账号、内容、按钮和任务进度
云端后端 业务办事人员 识别用户、检查权限、创建任务和保存结果
数据库及媒体目录 账本与文件仓库 保存用户、内容、任务、图片及关联信息
客户电脑连接器 本地执行人员 在客户电脑打开浏览器、执行同步和草稿动作
部署与维护系统 维修和管理工具 安装新版本、备份、检查、回滚和诊断

这五部分必须协同,某一部分成功并不意味着全部成功。例如连接器在线,只能说明电脑与云端有联系;它不能证明小红书登录成功,更不能证明内容已经进入数据库。

1.2 一次内容同步发生了什么

客户点击开始同步
  → 页面向云端发送请求
  → 后端识别客户和目标账号
  → 后端检查电脑、账号状态及已有任务
  → 创建任务并发送到指定连接器
  → 连接器在本机浏览器读取平台内容
  → 检查读取到的身份是否为目标账号
  → 把结果传回云端
  → 后端校验结果并写入数据库
  → 页面重新读取数据并显示结果

如果最后页面仍为空,可能是浏览器没有读到内容、结果没有传回、后端拒绝数据、写进了其他工作区,或者页面没有更新。不能只看到页面空白,就断定是“前端问题”。

1.3 从零开发时应怎样安排顺序

先完成一个最小闭环:一个用户登录、一个账号连接、同步一篇内容、在页面查看。再逐步增加多账号、多用户、素材、生成、草稿、权限和维护。

每扩展一个能力,都要保留之前闭环的测试。这样你增加“批量同步”时,会知道是否破坏了“单账号同步”。先堆完所有功能再集中试用,会让故障来源难以区分。

这是一套适合重建理解的开发顺序,不是对本项目早期每一次开发操作的历史还原。仓库较早的基线已经包含大量代码,不能仅凭现有提交反推所有早期决策。

1.4 本章练习

用自己的话解释三个问题:点击按钮是谁处理的?账号内容存在哪里?浏览器为什么需要在客户电脑运行?能分别说出页面、云端和连接器的责任,就可以继续。

第二章 看懂文件和最基本的代码

2.1 文件不是都可以直接双击运行

后缀或名称 意义 如何理解
.md Markdown 文档 用编辑器或支持 Markdown 的阅读器打开
.html 网页结构 浏览器可以显示
.css 外观样式 控制颜色、间距、布局
.js、.mjs JavaScript 程序 浏览器或 Node 运行,取决于内容
.ts TypeScript 程序 增加类型检查,通常需要工具处理
.tsx 含界面代码的 TypeScript 本项目 React 组件使用
.json 结构化数据 配置、接口数据常用
.yaml 配置描述 部署、依赖锁定等使用
.env 环境配置 可能包含秘密,不能当普通资料公开
package.json 项目工具清单 描述依赖、启动和检查入口
pnpm-lock.yaml 依赖版本账本 记录实际安装的依赖版本关系

源码相当于制作配方。把源码复制到另一台电脑,不等于那台电脑已经装好了运行工具、数据库和配置。“文件打开了”“程序启动了”“业务可用了”是三个不同结果。

2.2 变量、对象、数组和函数

下面是教学示例:

const account = { id: 'demo-1', name: '练习账号', connected: true };
const accounts = [account];

function canSync(item) {
  return item.connected === true;
}

console.log(canSync(accounts[0])); // 输出 true

逐句理解:const 给一个值起名字;花括号组成对象,对象里面有字段;方括号组成数组,数组是一组值;函数是一段可以反复使用的规则;return 把结果交给调用者。数组从 0 开始编号,accounts[0] 是第一个元素。

JavaScript 中 = 是赋值,=== 是严格比较。account.connected = true 会修改状态,account.connected === true 只是检查状态。把两者混淆,就可能把未连接账号错误地当作已经连接。

2.3 JSON 是数据,不是让客户阅读的报错页面

{
  "accountId": "demo-1",
  "status": "running",
  "message": "正在同步内容"
}

JSON 的字段名和字符串使用双引号;最后一个字段后没有多余逗号;不能写 JavaScript 注释。接口常用 JSON 交换数据。客户界面应把 message 显示为清楚的中文,把开发诊断信息留给日志。

Unexpected end of JSON input 表示解析器没有得到完整合法的 JSON。它不直接说明数据库损坏,也不直接说明客户网络差;空响应、被中断的响应、错误接口地址、代理返回内容都需要检查。

2.4 同步和异步

程序中的“异步”是指某件事需要等待,程序不必一直占住当前执行过程。它和业务里的“同步账号内容”不是同一个概念。

async function loadAccounts() {
  const response = await fetch('/api/accounts');
  if (!response.ok) throw new Error('读取账号失败');
  return await response.json();
}

fetch 发起网络请求;await 等待结果;response.ok 检查 HTTP 是否为成功状态;response.json() 读取并解析响应。即便 HTTP 成功,也还要检查业务状态是否成功。服务器可能成功返回“任务已接受”,但任务本身尚未完成。

2.5 TypeScript 的作用

type Account = {
  id: string;
  name: string;
  connected: boolean;
};

类型告诉工具字段应该是什么。它能提前发现“把数字当字符串使用”等错误,但不会自动验证外部网络数据。浏览器传来的数据可能不符合声明,后端仍需要运行时校验,本项目使用 Zod 等工具完成相关检查。

2.6 终端不是黑盒

终端是在指定目录里启动工具的窗口。执行前先知道当前目录,执行后看退出码和完整结果。0 一般代表程序成功结束,非零一般代表失败。

Windows PowerShell 使用 Get-Location 查看目录,Mac 或 WSL 使用 pwd。两边都可以用 cd 进入目录。路径包含空格时要加引号,例如 PowerShell 的 cd "C:\dev\矩阵云学习"。

不要把文档中的 $、PS> 等提示符复制成命令。本文代码块不添加这些提示符。也不要把多套环境的命令混着执行:PowerShell、WSL 和 Mac 的终端是不同环境。

第三章 准备一个与生产分开的学习环境

3.1 先区分三类电脑

客户电脑只负责使用系统和运行连接器;开发电脑负责修改源码、构建和测试;服务器负责持续提供线上服务。同一台物理电脑可以承担多个角色,但对应的配置和数据仍应分开。

Windows 客户不用为了使用连接器安装开发工具。你要在 Windows 上二次开发,则需要 Git、代码编辑器、Node 和 pnpm;需要贴近服务器测试 PostgreSQL 和 Linux 部署时,再使用 WSL2 与 Docker Desktop。

3.2 推荐的学习目录

在桌面之外建立专门目录,例如 Windows 的 C:\dev\matrix-learning,或 Mac 的 ~/Developer/matrix-learning。后面的第一份练习只写到这个目录,不读取任何生产凭证,也不会操作真实小红书。

先安装项目指定的 Node 运行环境。对照项目 .node-version、package.json 和 Dockerfile:当前目标为 Node 22.14.0,pnpm 11.25.0。版本号来自仓库约定,不是要求永远使用这些版本;以后升级应作为独立变更验证。

在终端检查:

node --version
pnpm --version
git --version

如果提示找不到命令,说明工具尚未安装、未进入 PATH,或安装后终端尚未重新打开。不要立刻修改项目代码。第四章练习只需要 Node,不需要 pnpm、数据库或 Docker。

3.3 编辑器怎样使用

用编辑器的“打开文件夹”选择学习目录,创建文件,把示例完整粘贴进去并保存。Windows 需要确认实际扩展名是 .mjs,不是 .mjs.txt。文件编码选 UTF-8。

编辑器负责读写文件;终端负责运行程序;浏览器负责查看页面。保存源码后,已经运行的普通 Node 进程不一定自动更新,需要停止并重启;开发工具的热更新只在对应工具已启动时生效。

3.4 密钥、密码与环境变量

环境变量是在启动程序时提供的配置。例如 PORT 决定监听端口,DATA_DIR 决定写数据的位置。它们不是写在每个业务文件里的固定值。

不要将线上密码复制进教学代码。开发练习使用虚构账号。SSH 公钥可以给服务器管理员配置授权,私钥应保留在受控位置;只有公钥不能替代私钥完成登录。

第四章 亲手运行第一个完整系统

这份练习模拟“点击同步 → 后端创建任务 → 稍后完成 → 页面显示内容”。它不连接小红书、不调用模型、不收费、不访问生产数据。内容暂存在内存,重启后清空,这是本练习刻意保留的简化。

4.1 创建 server.mjs

在学习目录创建 server.mjs,写入以下完整代码:

import http from 'node:http';
import { randomUUID } from 'node:crypto';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const page = `<!doctype html>
<html lang="zh-CN"><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>我的第一个同步系统</title>
<style>
body{font:18px/1.7 system-ui;max-width:720px;margin:60px auto;padding:20px}
button{font:inherit;padding:10px 20px;cursor:pointer}li{margin:10px 0}
</style>
<h1>练习账号</h1><p>这是本地模拟数据,不会操作真实账号。</p>
<button id="sync">开始同步</button><p id="status">等待操作</p><ul id="notes"></ul>
<script>
const button = document.querySelector('#sync');
const status = document.querySelector('#status');
const list = document.querySelector('#notes');
async function request(url, options) {
  const response = await fetch(url, options);
  const data = await response.json();
  if (!response.ok) throw new Error(data.message || '请求失败');
  return data;
}
button.onclick = async () => {
  button.disabled = true;
  status.textContent = '正在提交任务';
  try {
    let job = await request('/api/sync', {method: 'POST'});
    while (job.status === 'running') {
      status.textContent = '正在同步,请稍候';
      await new Promise(resolve => setTimeout(resolve, 300));
      job = await request('/api/jobs/' + encodeURIComponent(job.id));
    }
    if (job.status !== 'succeeded') throw new Error('同步未完成');
    const notes = await request('/api/notes');
    list.replaceChildren();
    for (const note of notes) {
      const item = document.createElement('li');
      item.textContent = note.title;
      list.append(item);
    }
    status.textContent = '同步完成,共 ' + notes.length + ' 篇内容';
  } catch (error) {
    status.textContent = error.message;
  } finally {
    button.disabled = false;
  }
};
</script></html>`;

export function createDemoServer({ delayMs = 1000 } = {}) {
  const jobs = new Map();
  let activeJobId = null;
  let notes = [];
  function json(response, code, value) {
    response.writeHead(code, {'Content-Type': 'application/json; charset=utf-8'});
    response.end(JSON.stringify(value));
  }
  return http.createServer((request, response) => {
    const path = new URL(request.url, 'http://localhost').pathname;
    if (request.method === 'GET' && path === '/') {
      response.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
      response.end(page);
    } else if (request.method === 'POST' && path === '/api/sync') {
      const active = jobs.get(activeJobId);
      if (active?.status === 'running') return json(response, 202, active);
      const job = {id: randomUUID(), status: 'running'};
      jobs.set(job.id, job);
      activeJobId = job.id;
      setTimeout(() => {
        notes = [{id: 'note-1', title: '我已经跑通前端到后端的完整流程'}];
        job.status = 'succeeded';
        activeJobId = null;
      }, delayMs);
      json(response, 202, job);
    } else if (request.method === 'GET' && path.startsWith('/api/jobs/')) {
      const job = jobs.get(path.slice('/api/jobs/'.length));
      json(response, job ? 200 : 404, job ?? {message: '任务不存在'});
    } else if (request.method === 'GET' && path === '/api/notes') {
      json(response, 200, notes);
    } else {
      json(response, 404, {message: '页面或接口不存在'});
    }
  });
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  createDemoServer().listen(4300, '127.0.0.1', () => {
    console.log('学习页面:http://127.0.0.1:4300');
  });
}

4.2 启动并观察

在学习目录执行:

node server.mjs

保持终端窗口打开,在浏览器输入 http://127.0.0.1:4300。点击“开始同步”,应先显示等待,再显示一篇内容。终端按 Ctrl+C 停止程序。127.0.0.1 指当前电脑,所以其他电脑打开同一个地址,不会访问你的这份练习。

打开浏览器开发者工具中的 Network/网络面板,再点击按钮。你会看到:POST /api/sync 创建任务;GET /api/jobs/某个编号 查询状态;GET /api/notes 读取最终内容。浏览器接收的是数据,再由页面决定怎么展示。

4.3 逐层解释

http.createServer 是后端入口,每个请求都会进入回调;路径决定执行哪项业务。POST 用于发起动作,GET 用于读取。202 表示任务已接受,并不表示业务已经完成。

Map 暂存任务;randomUUID() 创建任务编号;setTimeout 模拟等待外部操作。后端检查 activeJobId,即便请求重复到达,也会返回同一个运行中的任务。页面禁用按钮只是改善体验,后端检查才是在接口层阻止重复任务。

页面用 textContent 写入文字,避免把内容当 HTML 执行。try/catch/finally 分别负责正常过程、错误显示和按钮恢复。没有 finally 时,一次失败就可能让按钮一直不可点击。

4.4 给练习增加自动测试

在同一目录创建 server.test.mjs:

import test from 'node:test';
import assert from 'node:assert/strict';
import { once } from 'node:events';
import { createDemoServer } from './server.mjs';

test('同步流程、重复请求和不存在的任务', async () => {
  const server = createDemoServer({delayMs: 300});
  server.listen(0, '127.0.0.1');
  await once(server, 'listening');
  const origin = 'http://127.0.0.1:' + server.address().port;
  try {
    const initial = await fetch(origin + '/api/notes');
    assert.deepEqual(await initial.json(), []);
    const first = await fetch(origin + '/api/sync', {method: 'POST'});
    assert.equal(first.status, 202);
    const job = await first.json();
    const duplicate = await fetch(origin + '/api/sync', {method: 'POST'});
    assert.equal((await duplicate.json()).id, job.id);
    let current;
    const deadline = Date.now() + 3000;
    do {
      await new Promise(resolve => setTimeout(resolve, 30));
      current = await (await fetch(origin + '/api/jobs/' + job.id)).json();
    } while (current.status === 'running' && Date.now() < deadline);
    assert.equal(current.status, 'succeeded');
    const notes = await (await fetch(origin + '/api/notes')).json();
    assert.equal(notes.length, 1);
    assert.equal(notes[0].id, 'note-1');
    const missing = await fetch(origin + '/api/jobs/missing');
    assert.equal(missing.status, 404);
    assert.equal((await missing.json()).message, '任务不存在');
  } finally {
    await new Promise((resolve, reject) => {
      server.close(error => error ? reject(error) : resolve());
      server.closeAllConnections();
    });
  }
});

执行 node --test server.test.mjs。看到一项测试通过、失败数为零,表示这些断言成立。测试没有验证真实平台、权限、断网、持久化或多台电脑,因此不能把它当成矩阵云的全流程验收。

随本手册整理时,这两个代码块已提取到独立临时目录,在本机 Node 24.19.0 中实际执行测试并通过。它验证的是教学示例的接口流程;项目目标 Node 22.14.0、Windows 实机及页面视觉效果不包含在这次练习测试结果中。

4.5 三个递进练习

第一步,把返回内容的标题改成你自己的文字,重启程序,确认页面改变。这帮助你理解源码和运行中程序的关系。

第二步,把 delayMs 默认值改成 3000,观察按钮等待状态和网络轮询。再恢复到 1000。这帮助你理解任务耗时不应堵住页面。

第三步,请 AI 增加“停止模拟任务”,先要求它定义停止后的状态、定时器如何取消、是否还会写入结果,再写测试和实现。这帮助你发现:只把按钮文字改成“已停止”,不代表后台真正停止。

4.6 常见问题

现象 先检查 处理
找不到 server.mjs 当前目录和扩展名 进入保存文件的目录;显示真实文件扩展名
EADDRINUSE 4300 是否被另一个练习进程占用 停止自己之前启动的进程,再启动
页面无法连接 终端程序是否还在运行 保持运行窗口;确认完整地址含端口
改代码没变化 是否保存并重启 普通 Node 启动不会自动重载
测试找不到导出函数 两个文件是否同目录、代码是否完整 对照 export function createDemoServer

第五章 找到矩阵云的真实代码

5.1 目录地图

以下路径均相对于你打开的仓库根目录,而不是相对于某个人的 Mac 用户目录:

路径 内容 什么时候读
src/ 工作台页面、交互和样式 按钮、列表、布局、客户提示问题
server/ API、权限、数据库、队列及维护服务 请求失败、数据保存、任务状态问题
shared/ 前后端共用定义 两端字段、类型或协议不一致
local-connector/ 客户电脑连接器 连接、配对、任务领取、浏览器启动
scripts/ 浏览器执行及维护脚本 实际平台动作、构建、迁移和发布
deploy/ 部署与代理配置 端口、容器和公网入口问题
docs/ 开发、运维、历史验收资料 理解约定、确认历史证据
work/ 运行状态和本地数据 诊断前先判断是否含真实数据
outputs/ 构建及验证产物 对照发布清单;不能仅看文件名判断最新

5.2 如何确认读的是正确版本

本项目存在主目录和 .worktrees/reliability 等不同工作目录。Git worktree 是同一个仓库的另一份独立检出,可以有不同分支和修改。修改主目录,不会自动修改另一个工作目录里的文件。

在准备修改的目录执行:

git rev-parse --show-toplevel
git branch --show-current
git rev-parse --short HEAD
git status --short

把四个结果记录到本次任务中。git status --short 没有输出表示没有未提交修改,不代表线上版本相同。线上容器可能来自另一个提交,必须用发布记录、镜像摘要及运行配置核对。

5.3 从页面问题找到代码

假设问题是“发布中心进入后显示素材库”。先搜索页面上的文字或组件名,再读相关组件,而不是把整个仓库发给 AI。

rg -n "图文创作|模板素材库" src
rg -n "PublishCenter" src

项目实际 src/PublishCenter.tsx 中,标签状态初始化为 create。素材库和创作区域通过 hidden 切换显示,这会保留已经挂载的组件状态。选素材后会设置素材选择并切回创作页。

但“保留组件状态”不等于“刷新浏览器后仍保留草稿”。刷新后是否恢复,需要独立检查持久化机制。不要从一个正确的局部实现推断另一项能力已完成。

5.4 文档、代码、生产以什么为准

代码说明当前检出版本怎样实现;测试说明哪些输入输出已被检查;生产记录说明某一时刻部署过什么;实时诊断说明当前实际上运行什么。四者各自回答不同问题。

旧方案里的“计划采用 better-sqlite3”不能覆盖当前 server/sqlite-client.ts 使用 node:sqlite 的事实。旧验收里的某次单账号成功,也不能替代今天新版本、多账号、跨设备的验收。

第六章 把想法写成能实现的需求

6.1 一条需求至少写清六件事

角色是谁;在什么页面;做什么动作;期望什么结果;失败时怎么办;哪些已有行为必须保留。这六项越清楚,AI 自行猜测的空间越小。

例如“修好删除账号”太笼统,应该改为:

团队管理员在账号矩阵删除本团队某个账号后,账号从当前列表消失;该账号以后仍能重新登录并同步。删除前若存在运行任务,必须按约定停止并确认;迟到结果不能重新创建刚删除的账号。其他账号、其他团队和素材保持不变。请同时验证删除、重新添加、断网和旧任务回传。

其中“账号历史内容删除还是保留”仍是需要做决定的业务语义。不要让开发者仅凭“删除”两个字自行决定全部关联数据的命运。

6.2 使用 Given When Then 写验收条件

它只是把“前提、动作、结果”固定下来。例如:

前提:账号 A 存在,没有运行任务;账号 B 也存在。
动作:删除账号 A,然后重新登录同一个平台账号。
结果:A 可以重新建立关联并同步;B 的数据不变。

前提:账号 A 的任务已经停止,但旧电脑稍后发来结果。
动作:服务器收到旧任务回传。
结果:拒绝旧结果,不重新创建被删除的账号,不覆盖新绑定。

需求里有这些条件,测试才知道应该断言什么。否则测试容易只验证“接口返回 200”,漏掉真正的业务问题。

6.3 限定修改范围

把“必须改”和“保持不变”同时写清。矩阵云目前的重要边界包括:保留生成供应商及费用设置、素材库和已有素材、洗图二创,以及单任务 1–12 张图片能力;开发费用优化不等于改变客户业务费用。

范围不是阻碍修复。它让开发者把改动集中在真正相关的模块,并在需要扩大范围时说明因果关系。例如登录身份识别会影响同步与草稿,属于关联模块;为了修复登录而更换整个生成计费方案则没有对应因果关系。

6.4 给 AI 的任务结构

目标:说明客户最终要完成的动作。
现象:实际结果、期望结果、出现时间、平台、版本。
复现:从干净状态开始,按顺序列出操作。
边界:哪些数据、功能和配置必须保留。
调查要求:先指出调用链与证据,再提出最小完整修复。
验证要求:列出正常流程、异常流程、关联回归和未验证项。
交付要求:修改文件、根因、检查结果、上线状态分别说明。

不要要求 AI 用“绝对无问题”代替测试证据。更有效的是要求它逐项说明“在哪个版本、什么环境、执行了什么动作、看到什么结果”。

第七章 页面为什么会出现各种不一致

7.1 小白理解 React

React 把页面拆成组件。组件可以理解为带逻辑的界面零件,例如账号卡片、任务列表、发布表单。一个组件接收输入数据,根据状态决定显示什么。

props 是父组件传进来的数据;state 是组件自己维护、改变后会推动界面更新的数据。状态最好有明确来源。如果页面同时保存三份“当前账号”,却没有同步规则,就可能出现顶部选 A、按钮操作 B、列表显示 C。

7.2 三种状态分开管理

状态 示例 应由谁负责
临时界面状态 打开的标签、正在编辑的文字 前端组件或页面状态
持久业务状态 账号、内容、草稿任务结果 后端和数据库
执行状态 本机是否在线、浏览器任务进度 执行器报告并由云端核对

前端不应该单凭点击按钮就永久认定服务器已完成删除,也不应该单凭连接器心跳就认定平台已登录。

7.3 删除后卡片不消失的排查思路

先看请求有没有发出,再看返回是否成功,然后看数据库是否改变,最后看前端列表有没有重新读取或正确更新。如果数据已删除而页面仍显示,重点检查查询缓存、列表过滤和刷新时序;如果数据根本没删除,先修复后端,而不是在页面上强行隐藏卡片。

对于可撤销操作,可以设计乐观更新:先让界面变化,失败后恢复并提示。但需要真的实现恢复逻辑。对于删除设备、取消任务等可能影响执行的操作,界面应清楚区分“请求已提交”和“已确认完成”。

7.4 客户提示与开发诊断

客户看到的是“内容同步暂未完成,已保存内容保留,请查看连接电脑的状态”;开发日志记录任务编号、步骤、错误类型、耗时和脱敏上下文。两者通过错误编号关联。

不要把 Zod 校验数组、正则表达式、堆栈或数据库路径直接拼进页面。本项目的 customer-message.ts 负责相关转换,但仍需检查所有错误入口是否经过它,包括登录轮询、同步轮询、批量操作和历史任务显示。

7.5 统一发布入口的真正含义

所有历史任务使用同一套动作决策:哪些状态能保存草稿,哪些需要核对结果,哪些正在运行必须先停止,哪些只读。按钮颜色统一只是外观统一;真正的统一是相同状态调用相同规则,不能让旧任务继续走另一套危险重试路径。

可以把状态到动作的映射写成纯函数并测试,界面只渲染函数结果。这样增加新状态时,不必在多个页面复制判断。是否采用这一重构,需看当前代码重复程度和对应测试,不要为了一条按钮文字修改就重写全部页面。

第八章 数据如何被正确保存

8.1 ID 与显示名称

账号昵称可能改变或重复,ID 用来稳定识别记录。显示名称是给人看的,关联 ID 是给程序使用的。把昵称当作唯一主键,后续改名就可能导致内容串联错误。

矩阵云同时存在工作区/租户、系统用户、平台账号、电脑设备、任务等不同 ID。排障时必须记录是哪一种,不能把“用户 ID”和“小红书账号 ID”混用。

8.2 表、记录、字段和关联

把数据库表理解为有规则的电子表格:一行是一条记录,一列是一个字段。区别是数据库能强制约束、事务处理、并发访问与权限。

对象 示例信息 关系
租户 某个客户团队 拥有用户、账号和内容
用户 登录工作台的人 属于租户,有角色
平台账号 某个小红书身份 属于租户,关联内容
内容 标题、正文、平台内容编号 属于平台账号及租户
设备 某台电脑 受租户授权并报告在线状态
任务 同步或草稿动作 关联租户、账号、设备或执行信息

8.3 事务为什么重要

假设需要同时创建任务和执行记录。如果第一步成功、第二步失败,系统可能看到一项没有执行信息的任务。事务让一组数据库操作一起成功或一起回滚。

教学 SQL:

BEGIN;
-- 在同一事务中写入互相依赖的记录。
-- 参数由程序绑定,不直接拼接用户输入。
COMMIT;

数据库事务不能自动回滚已经发生在小红书上的上传。外部平台动作和数据库写入之间需要额外的任务状态、凭据核对和补偿流程。把整个业务包进一个数据库事务,不会自动变成跨平台原子操作。

8.4 时间与格式

2026-10-08T09:30:00.000Z 是带 UTC 信息的 ISO 时间。2026-10-08 17:30:00 缺少明确时区,浏览器或校验器可能作出不同解释。

项目曾出现 lastSyncedAt 格式校验失败的截图。正确排查是找到这个字段从哪里生成、经过哪些序列化步骤、接口允许哪些格式,再在边界统一格式并测试。不能把任意解析失败都替换成当前时间,否则失败同步会被显示为刚刚成功。

8.5 图片文件与数据库的关系

数据库一般保存素材信息及文件引用,媒体目录保存实际文件。只备份数据库不一定包含图片;只复制图片也没有账号、内容和权限关系。完整迁移要保证两者匹配,并保留模型配置所需的加密密钥。

判断文件是否无用,不能只看修改时间。先核对内容、素材、模板、生成任务和活动上传的引用。仍被引用的旧图片依然是有效数据。

8.6 本章练习

画出“团队 A → 账号 A1 → 内容 N1 → 图片 F1”的关系,再画团队 B。说明为什么在查询内容时只检查账号 ID 还不够,以及删除 A1 时必须考虑哪些关联对象。

第九章 完成一次真实的小改动

9.1 选择适合第一轮的任务

第一次实践可以修改一个不改变业务行为的客户提示,例如把过于技术化的按钮说明改得清楚。先不要从数据库迁移或平台自动化驱动开始。

完整流程是:确定版本 → 记录原状 → 找到组件 → 明确验收 → 修改 → 看差异 → 做相关检查 → 实际打开页面 → 保存提交。你应当知道每一步产生什么证据,而不是只记住命令顺序。

9.2 先检查已有修改

git status --short
git diff --stat
git diff

如果已有别人或上一轮的未提交修改,先弄清用途。不要执行 reset --hard 或 clean 来制造“干净环境”,它们可能丢掉未保存的工作。已有合适工作区就继续使用,确有隔离需求时才另建分支或 worktree。

在已确认没有需要处理的未提交修改时,可以创建自己的分支:

git switch -c codex/learn-customer-copy

分支名称已经存在时,先查看它是什么,不要强行覆盖。分支是工作版本的指针,不是数据库备份。

9.3 安装依赖、构建和启动是三件事

在仓库根目录、指定 Node 和 pnpm 环境下执行:

pnpm install --frozen-lockfile
pnpm build

第一条按锁文件安装工具和库;第二条执行 TypeScript 检查及 Vite 前端构建。安装失败先读首个实际错误,可能是网络、运行时版本或平台依赖问题。不要为了让安装通过就删除锁文件,因为这会改变依赖组合。

启动开发模式使用 pnpm dev,它同时启动 Vite 和 server/grey.ts。Vite 代理 /api 到本机 4174。看到前端地址后,使用与配置一致的地址访问;如果默认端口被占用,工具可能选择其他端口,同源校验配置也需要相应一致。

9.4 首次练习必须使用隔离数据

以下是本地 SQLite 学习模式的启动配置,不是生产 PostgreSQL 恢复流程。执行前使用一个新的终端,不加载生产配置;关闭可能影响本次练习的旧开发进程。

Windows PowerShell,仓库根目录:

Remove-Item Env:DATABASE_URL -ErrorAction SilentlyContinue
Remove-Item Env:DATABASE_URL_FILE -ErrorAction SilentlyContinue
Remove-Item Env:SYNC_DATA_DIR -ErrorAction SilentlyContinue
Remove-Item Env:MCP_SERVICE_TOKEN -ErrorAction SilentlyContinue
Remove-Item Env:STUDIO_TOKEN -ErrorAction SilentlyContinue
Remove-Item Env:CUSTOMER_PUBLIC_ORIGIN -ErrorAction SilentlyContinue
Remove-Item Env:TLS_CERT -ErrorAction SilentlyContinue
Remove-Item Env:TLS_KEY -ErrorAction SilentlyContinue
$env:DATA_DIR = Join-Path $env:TEMP 'matrix-learning-data'
$env:HOST = '127.0.0.1'
$env:PORT = '4174'
$env:PUBLIC_ORIGIN = 'http://localhost:5173'
pnpm dev

Mac 或 WSL 的终端,仓库根目录:

unset DATABASE_URL DATABASE_URL_FILE SYNC_DATA_DIR MCP_SERVICE_TOKEN STUDIO_TOKEN
unset CUSTOMER_PUBLIC_ORIGIN TLS_CERT TLS_KEY
export DATA_DIR="${TMPDIR:-/tmp}/matrix-learning-data"
export HOST=127.0.0.1
export PORT=4174
export PUBLIC_ORIGIN=http://localhost:5173
pnpm dev

这组配置把学习数据放在临时目录,系统清理临时文件后数据可能消失;它不适合存重要内容。同一个目录再次启动会复用前次学习数据。需要重新开始时使用新的学习目录名称,避免误删其他目录。

初次访问要经过项目现有的初始化或账号流程。若页面显示“不允许初始化”,检查请求是否真正来自回环地址、Host 和 Origin 是否一致。容器代理可能让服务器看到网桥地址,不能只凭浏览器显示 localhost 就认定后台看到的也是回环地址。不要为跳过这一步关闭线上权限。

上述配置解释了当前入口的启动方式,整套项目首次启动仍需在目标电脑验证。本手册第四章的独立练习才是不依赖项目数据库初始化的第一课。

9.5 检查变化并保存

修改后先运行相关测试,再检查差异:

pnpm exec vitest run src/__tests__/publish-center-flow.test.tsx
git diff --check
git diff --stat
git diff

如果改动涉及类型或组件结构,再运行 pnpm build。在浏览器检查实际页面、键盘焦点和常见宽度。改一个文档链接,不需要为了显得认真重跑整个平台自动化测试。

提交时明确列出文件,例如:

git add src/PublishCenter.tsx
git commit -m "fix: clarify publish center guidance"

路径和提交说明必须对应真实改动。不要习惯性 git add .,否则可能把日志、临时数据或不相关修改一起提交。提交只是保存源码版本,不会自动部署到网站。

9.6 每轮交付记录

问题:客户在什么条件下无法完成什么动作。
根因:哪个模块中的哪条规则导致错误。
修改:变更文件以及新行为。
验证:测试、环境、操作步骤、实际结果。
影响范围:关联功能是否检查。
未验证:缺少的设备、账号或外部条件。
发布:尚未发布,或明确记录已发布的提交和镜像。

第十章 专家视角下的真实系统架构

10.1 先画部署边界

工作台浏览器
    │ HTTPS:页面、API、用户会话
    ▼
公网入口与反向代理
    ├── 业务服务 Express
    │      ├── PostgreSQL:租户业务数据
    │      ├── 媒体文件目录:图片、视频等
    │      ├── 本地执行状态:部分 SQLite/文件
    │      └── 连接器通道
    │              ▲ 客户电脑主动建立连接
    │              │
    │        本机连接器 Node
    │              └── 本机 Chrome/Edge 与账号独立目录
    │                       └── 小红书平台
    └── 维护 MCP 服务
           └── 授权检查、工作区修改、验证、发布和回滚

工作台浏览器和执行浏览器不是同一个角色。用户可以用 Safari 打开工作台,但本机平台执行依赖连接器支持的 Chrome/Edge。宣称“支持 Safari”必须说明支持的是工作台访问,不能写成连接器控制 Safari 已经通过。

10.2 启动入口与配置优先级

server/grey.ts 负责启动业务服务:确定数据目录,读取或生成访问令牌,选择 PostgreSQL 或本地存储,建立应用及连接器通道,再监听端口。

server/runtime-paths.ts 通过模块位置计算项目根目录,避免完全依赖启动时的当前目录。SYNC_DATA_DIR 优先于 DATA_DIR,两者未设置时业务数据使用默认目录;CUSTOMER_PUBLIC_ORIGIN 优先于 PUBLIC_ORIGIN。排查配置时要看实际优先级,不能只检查一个变量。

server/postgres.ts 中 DATABASE_URL_FILE 的有效值优先于 DATABASE_URL。只修改环境里的数据库 URL,而忘记仍有凭证文件配置,可能让程序继续连接旧数据库。

启动时若配置 PostgreSQL,应用会查询 schema_migration 的版本 2。它不会因为你启动了服务就自动完成所有 schema 迁移。当前基线里版本号是明确条件,将来升级 schema 必须同时调整迁移与兼容检查。

10.3 登录其实有四种不同过程

过程 证明了什么 没有证明什么
工作台账号登录 系统用户身份有效 平台账号已经登录
电脑配对 设备得到该团队授权 浏览器执行正常
小红书登录 当前浏览器具备平台身份 所有页面均可读写或草稿已保存
维护 MCP 授权 AI 工具获得指定维护权限 自动获得生产发布确认

把这些状态统称为“已连接”,会让用户与开发者都误判。页面可以简洁,内部状态必须分别保留。

工作台登录实现主要在 src/AccountLogin.tsx 和 server/tenant-auth.ts。当前会话使用 HttpOnly、SameSite 严格策略的 Cookie;随机会话令牌在数据库中保存摘要。用户、租户停用等条件仍需逐请求检查,不能只在登录时检查一次。

Cookie 随访问域名或 IP 变化而变化。迁移后访问新地址通常需要重新建立工作台会话;这不等于账号内容已经丢失。平台浏览器登录状态则保存在客户电脑相应目录中,和云端工作台 Cookie 是不同数据。

10.4 租户隔离不是一个查询条件就结束

server/postgres.ts 的事务设置 app.tenant_id,schema 中的 RLS 策略使用该上下文;许多查询同时显式带 tenant_id。这形成两层约束,但超级用户等特权角色可能绕过 RLS,因此应用账号不应借用迁移管理员的全部权限。

隔离还必须覆盖文件路径、媒体下载、模型配置、设备配对、后台任务及日志查询。测试时准备两个租户,让租户 B 尝试读取、更新、删除或领取 A 的对象。只验证各自首页显示不同内容,不能证明对象接口不会越权。

10.5 同步调用链的阅读顺序

先从 src/GreyApp.tsx 的请求封装和同步动作入手,再读 server/grey-app.ts 的路由,然后追踪 server/xhs-sync.ts、设备通道和本机执行脚本。不要一开始就阅读全部浏览器脚本。

关键接口包括:

接口 职责
GET /api/operations 读取工作台业务视图
POST /api/xhs/qr-login 发起平台登录流程
GET /api/xhs/qr-login/:id 读取登录任务状态
POST /api/accounts/:id/sync 发起指定账号同步
GET /api/accounts/:id/sync/:jobId 查询指定同步任务
POST /api/accounts/:id/sync/:jobId/cancel 请求停止同步
POST /api/accounts/:id/delete 删除账号关联及相关处理

接口名包含 qr-login 是内部兼容命名,不表示必须把二维码复制到工作台显示。当前客户流程要求在连接器打开的浏览器中完成扫码。

阅读每一层时记录五个字段:租户、账号、设备、任务编号、状态。若这些字段在中途改变或缺失,先解释它为什么改变,再决定是否是故障。

10.6 内容和草稿是不同的数据流

内容同步主要从平台读取信息并导入云端。保存草稿会向平台执行写入,包括文案、图片和浏览器中的草稿保存。两者共享身份识别与调度,但错误恢复策略不同。

读取失败通常可以在核对任务状态后重新读取;草稿写入超时则可能已经在平台成功。重复上传可能生成重复草稿,所以必须先确认原任务结果。数据库里写上 succeeded 不能代替真实草稿凭据及内容匹配验证。

相关模块集中在 server/studio-routes.ts、server/studio-execution.ts、local-connector/studio-jobs.mjs 和 scripts/studio-executor.mjs。修复共享登录逻辑后,应同时回归同步与草稿,避免只修通读取路径。

10.7 当前 SQLite 的真实定位

生产业务权威数据是 PostgreSQL,但当前代码仍存在本地 SQLite,用于部分执行锁、状态及本地开发。server/sqlite-client.ts 使用 Node 内置 node:sqlite,设置 5 秒 busy timeout,执行完成后关闭数据库。

这不是外部 sqlite3.exe,也不是旧方案里的 better-sqlite3。Node 版本与实验性 API 的具体能力会影响适配层,运行时升级需要针对实际方法做验证。

“生产业务已迁到 PostgreSQL”不能作为删除所有 .sqlite 的依据。应逐个列出谁还在读写、是否权威、能否重建、恢复时是否需要,再决定隔离或清理。

第十一章 任务状态和跨设备一致性

11.1 小白理解为什么任务不能只有成功或失败

网络中断时,服务器可能不知道客户电脑有没有完成动作。因此除了成功、失败,还需要运行中、等待停止确认、受阻或结果未知等状态。真实代码中各任务类型的枚举不完全相同,不能把下面的教学状态名不加转换就写入数据库。

情况 允许的行为 不能直接做的事
排队尚未执行 移出队列并标记取消 让稍后的队列领取继续执行
正在执行 发停止请求并等待确认 立即显示“已停止”而不管执行器
写入结果未知 核对原电脑和平台凭据 自动换电脑重复写入
平台要求验证 停止后续动作并展示下一步 自动重复探测或绕过验证
已经完成 展示真实结果 被迟到的旧回调改回运行中

11.2 重复请求与幂等

幂等的目标是:同一意图重试,不重复产生业务副作用。前端要复用请求编号;后端要把编号和结果持久保存,并用唯一约束或事务协调并发请求。只在内存 Map 中记住编号,服务重启后就忘了。

必须区分“用户再次明确创建新版本”和“网络错误后的同一请求重试”。前者可以产生新任务,后者应复用原任务或返回可核对的状态。不能为了节约开发成本擅自把新生成需求变成缓存命中。

11.3 本项目实际的同步任务实现

server/postgres-jobs.ts 在创建同步任务时使用租户与账号组成的事务 advisory lock,检查是否已有运行任务;已有任务时返回原状态,并标记 fresh: false。这是服务端防止并发重复创建的重要一层。

新任务写入 job 和 job_attempt,租约在当前实现中设为 11 分钟。恢复逻辑会把租约过期的运行中同步任务标记为 blocked。更新任务时使用行锁;已有终态不会被普通运行中补丁重新改写。

这些实现不代表所有任务都具有相同机制,也不代表租约自动延长。若要调整长任务行为,必须核对领取、心跳、执行器自停、终态更新和页面提示之间的关系。

11.4 数据库任务锁与浏览器账号锁不同

server/browser-runs.ts 使用本地 SQLite 保存账号散列到运行编号的映射,用于本机服务范围内的跨租户、跨设备账号互斥。它与 PostgreSQL 的 job 租约不是同一张表,也没有同一套自动到期规则。

因此一个任务已被标记过期,不代表浏览器锁一定释放;反过来删除锁,也不代表客户电脑已经停止。应把“任务终态、执行确认、设备绑定、浏览器互斥”当作一组关联状态检查。

如果以后扩展多个业务服务实例,单机 SQLite 锁不能天然提供全实例全局互斥。必须先重新设计共享协调和归属,再横向扩容。加第二个容器不是无条件的性能优化。

11.5 删除后不能重新登录的正确推理

可能相关的对象有:账号记录、设备绑定、运行任务、浏览器锁、删除标记和旧回传。逐个检查,不要只删除账号表中的一行。

当前 browser-runs.ts 提供按账号释放孤立锁的能力,目的是避免删除后永久阻塞重新添加。它必须与删除、停止确认和旧结果拒绝机制配合使用。不能把“每次登录前清空全部锁”作为通用修复,那会允许同账号并发执行。

最低回归序列是:添加 → 同步 → 删除 → 重加 → 同步;再分别加入运行中删除、断网删除、旧结果迟到、跨设备重加和其他租户同名账号的情况。

11.6 设备切换与迟到结果

任务应绑定执行时的设备和运行编号。用户切换电脑后,新任务使用新绑定,旧设备的迟到响应不能覆盖新绑定或新任务结果。

判定有效响应不能只看账号 ID;至少核对租户、设备、运行编号和当前任务状态。对于已经发生外部写入但响应丢失的旧任务,应保留待核对信息,而不是为了让按钮可点就当作从未发生。

11.7 不要轻易宣称恰好一次

在外部平台不提供幂等写入接口时,网络中断后很难同时保证绝不重复和自动无限重试。系统应通过持久任务编号、原设备核对、阶段记录和明确的未知结果来减少重复并控制恢复。

向客户解释时可以说“保存结果待核对,请查看原执行电脑”;向开发者解释时要明确哪个步骤已确认、哪个步骤未知以及允许怎样恢复。

第十二章 系统性定位故障根因

12.1 先冻结证据,再改变系统

一次有效的问题记录至少包含:发生时间和时区、工作台版本、连接器版本、系统和浏览器、租户范围、目标账号、任务编号、期望结果、实际结果、可重复步骤。

记录错误前后的短日志即可,不必复制全部日志。不要附带 Cookie、私钥、令牌或完整客户内容。截图能说明界面现象,网络响应和任务日志才能继续说明哪一层失败。

12.2 按边界定位

依次回答以下问题,遇到第一个不符合预期的边界就深入检查:

  1. 页面有没有发出预期请求?请求路径、方法、参数是什么?
  2. 公网入口有没有把请求交给正确服务?有没有跳到旧地址?
  3. 后端有没有识别到正确用户、租户和对象?
  4. 有没有创建任务?拒绝原因是权限、已有任务、版本还是数据格式?
  5. 正确电脑有没有收到并确认任务?
  6. 浏览器是否打开、身份是否匹配、平台是否返回预期内容?
  7. 结果是否完整回传并通过校验?
  8. 数据库有没有提交到正确租户?
  9. 页面查询是否重新读取并展示新结果?

不要同时改三层来试运气。每一项假设都配一个能区分真假的检查,再依据结果修改。

12.3 案例一 连接器在线但无法登录和同步

可能的假设:电脑只完成配对但浏览器没启动;登录的是创作者页而读取的是另一个页面;身份读取字段发生变化;账号锁残留;设备绑定错误;登录完成回传被后端拒绝。

检查方法:使用同一运行编号,核对云端创建、连接器领取、浏览器启动、身份识别、回传接受五个阶段。如果设备已领取而身份始终未知,就应调查浏览器页面与识别器,不应重复修改防火墙。

修复要求:正常登录、已登录启动、登录过期、页面暂未就绪、身份不匹配都要有对应结果。把所有异常统一当“未登录”会制造无休止扫码;把所有不确定情况当“已登录”则可能同步错误账号。

12.4 案例二 原始 JSON 解析错误直接显示给客户

先查看请求的状态码、Content-Type、响应长度和脱敏摘要。响应可能为空、HTML 错误页、完整业务 JSON 或中途截断;这些分支对应不同根因。

后端应在可控错误路径返回一致的错误结构;代理配置要避免把 API 错误替换成无法识别的页面;客户端对不完整响应给出客户可读提示,并记录诊断编号。不能只在前端吞掉错误返回空数组,否则“同步失败”会被伪装成“账号没有内容”。

回归至少覆盖:正常 JSON、空响应、非 JSON、网络断开、服务端错误和旧错误记录展示。写操作遇到响应未知时,先查原任务,避免盲目重复。

12.5 案例三 迁移后旧内容不见了

先核对新旧环境的数据源、数据库名、租户 ID、账号 ID、媒体挂载目录和读取路径。页面为零有可能是进入了新工作区或读到空目录,不一定是数据物理丢失。

按每个租户统计记录数,再抽查稳定 ID、内容字段、媒体引用和文件哈希。数据库有记录但图片不显示,进一步检查文件是否迁移、路径是否正确、下载权限和公网地址是否匹配。

不能把旧 SQLite 再次全量导入来“补数据”,否则可能复活已删除账号和撤销设备、覆盖迁移后的新内容。当前迁移脚本有已迁移租户与非空目标检查,正是为避免这类覆盖;仍需结合备份时间与业务停写窗口判断完整性。

12.6 案例四 Mac 提示 node 无法打开

需要区分文件损坏、架构不匹配、执行权限不足、签名或公证问题,以及启动脚本目录错误。检查包哈希、内置 Node 架构、执行位、签名验证和实际启动退出信息。

同一个包在开发者电脑可运行,不代表新客户从浏览器下载后可运行。下载隔离属性和系统安全检查可能只在首次启动出现。正式验收应使用真实下载解压路径和干净用户环境,不能依靠开发机已经放行的状态。

“签名成功”和“完成公证”是不同结论。历史记录说明 Mac 包签名过但公证未完成,不能在教程或交付说明中把它们合并成“全部通过”。

12.7 案例五 发布按钮没反应

先检查点击事件是否执行、按钮是否因状态被禁用、请求是否返回、任务是否创建。若有任务,再检查设备是否领取、是否因已有未保存页面而停止、图片下载是否成功、保存凭据是否返回。

不能把按钮强行解禁作为完整修复。按钮被禁用可能是正确地保护未知写入结果,也可能是旧任务状态没有正确结束。根因需要通过任务状态与实际执行证据区分。

12.8 根因报告的写法

现象:同步后页面仍显示尚未同步。
证据:任务 X 已回传数据,但 API 因字段 Y 格式不符拒绝;数据库未提交。
根因:连接器的序列化格式与云端接口契约不一致。
影响:所有经过此回传路径且包含该格式的任务。
修复:在约定边界统一格式,保留原始失败诊断,客户页面展示可读提示。
验证:合法值、旧格式、空值、时区、错误值及真实回传分别验证。
排除:不是浏览器未打开,也不是页面仅缺少刷新。

这是报告格式示例,不是对当前生产故障的实测结论。优秀的根因报告解释为什么发生、影响谁、为什么修复能解决,并能由别人重现。

第十三章 什么才算验证通过

13.1 测试分层

层级 适合验证 无法单独证明
类型和构建 字段类型、模块引用、前端能否打包 客户业务能完成
单元测试 纯函数、状态转换、格式转换 数据库和浏览器真实配合
接口集成测试 权限、校验、数据库行为 客户电脑能启动浏览器
前端交互测试 点击、禁用、错误提示、列表更新 真实平台页面结构仍然一致
故障注入测试 超时、取消、重复请求、迟到结果 所有网络和平台变化
实机端到端 指定系统与浏览器的真实业务闭环 没测过的所有机型

模拟服务是让异常可以重复出现的重要工具。真实平台不适合为了测试而反复制造异常登录或限流;这些分支优先在模拟层验证,再用正常授权账号完成有限的真实闭环。

13.2 本项目相关测试怎么找

rg --files src/__tests__ server/__tests__
rg -n "重新|删除|cancel|timeout|duplicate" src/__tests__ server/__tests__

可从以下实际文件开始阅读:

文件 阅读重点
src/__tests__/publish-center-flow.test.tsx 发布中心操作流程
src/__tests__/connector-pairing-panel.test.tsx 配对界面
server/__tests__/connector-channel.test.ts 云端与设备通道
server/__tests__/sqlite-client.test.ts 本地数据库适配
server/__tests__/postgres-integration.test.ts PostgreSQL 集成边界
server/__tests__/maintenance-auth.test.ts 维护授权
server/__tests__/sync-path.test.ts 同步路径

测试文件名只能帮助定位,不能替代阅读断言。测试可能跳过外部依赖,也可能只覆盖部分分支;交付时要同时记录通过、失败、跳过和未运行。

13.3 每次发现真实故障如何补测试

先找最小可复现输入,例如一个带时区的时间字段、一个已删除的账号绑定、一个迟到任务结果。写测试使旧代码表现出原问题,再修复实现,让该测试通过,最后检查共享模块的关联流程。

不要写只重复实现本身的测试。例如实现把字段赋为 true,测试只检查它等于 true,却不验证用户能完成任务,就容易得到没有实际保护作用的高通过率。

13.4 端到端验收表

每个格子填“环境、版本、日期、实际结果、证据”,不要只填一个勾。

场景 必须观察到的结果
首次连接电脑 云端出现正确设备、租户和版本
重复启动连接器 复用或明确提示已有实例,不重复绑定
撤销并重新配对 旧授权失效,新授权可用,界面及时更新
新账号登录 打开正确电脑浏览器,身份识别一致
登录后自动同步 有任务、内容写入正确租户、页面展示
删除后重新添加 可重新登录,旧结果不复活旧绑定
全部账号同步 不漏账号,不跨租户,不并发冲突
保存草稿 平台草稿实际存在且内容、图片、账号匹配
重复点击 不重复创建同一意图的付费调用或草稿
网络中断及恢复 状态可解释,未知结果不盲目重复
取消执行 后续动作停止;未确认时显示未确认
素材与 12 张生成 原规则保持,素材引用完整

平台至少分别列 Windows 10/11 x64、Mac Apple 芯片、Mac Intel;工作台浏览器与执行浏览器分别记录。未取得的设备不能标成已通过。不同 CPU 机型不是仅改变网页宽度就能模拟出来的。

13.5 发布前检查怎样安排才不浪费

局部变更先运行相关测试。跨模块变更和准备上线时集中运行完整检查:

pnpm build
pnpm exec vitest run --maxWorkers=2 --minWorkers=1
git diff --check

上面是当前项目可用的检查入口,不能证明 PostgreSQL、Docker 和三种实机都已执行。相关测试有外部前提时,还要准备独立环境并记录是否跳过。已经通过且代码、依赖、配置没变化的检查,不必为了重复汇报再运行一次。

第十四章 Windows 和 Mac 开发的差异

14.1 先区分原生和 WSL 环境

Windows PowerShell 使用 Windows 路径和程序;WSL 使用 Linux 路径和程序;Docker 容器又有自己的文件系统。C:\dev\project、/mnt/c/dev/project、/app 可能通过挂载关联,但不是同一个路径字符串。

选择 WSL2 开发时,把项目、Node、pnpm 和依赖放在同一套 Linux 环境里。不要用 Windows 安装 node_modules,再让 Linux 容器直接复用,因为其中可能含平台相关二进制。

客户连接器仍运行在 Windows 宿主系统,才能操作客户本机浏览器。不能因为后端使用 WSL2,就要求小白客户也在 WSL 内启动本机浏览器任务。

14.2 环境检查步骤

Windows PowerShell:

wsl --status
wsl --list --verbose
docker version
docker compose version
git --version

wsl --list --verbose 用于确认发行版和版本;docker version 需要看服务端信息,只有客户端版本不代表 Docker 引擎运行。docker compose version 检查插件入口,不能因为 docker 命令存在就假定 Compose 可用。

启用或安装这些组件的步骤取决于 Windows 版本、权限和现有设置,按工具官方安装界面完成。项目开发脚本应在检查失败时给出明确提示,不应打印成功后让用户到空页面排查。

14.3 当前 compose.dev.yaml 的实际前提

当前文件定义 PostgreSQL 和业务服务,并为开发数据使用命名卷;它不是完整生产恢复,也没有独立定义维护 MCP 服务。它的依赖顺序不能替代数据库 readiness 与 schema 初始化。

这份基线还存在需要处理的首次启动问题:配置的 MCP_SERVICE_TOKEN 开发值短于应用要求的 32 位;业务入口要求已有 schema 版本 2,而 compose 文件没有迁移步骤。因此不能把当前 bootstrap-windows.ps1 的“开发环境已准备”输出当作成功验收。

准备使用这条路径时,应由开发者先在独立开发配置中设置足够长度的随机令牌,使用管理员开发凭证初始化 schema 和业务账号权限,等待数据库就绪,再启动业务服务并检查健康接口。这些是需要实施与验证的开发环境工作,不能靠关闭校验完成。

14.4 路径写法

Node 代码应使用路径 API:

import { join, delimiter } from 'node:path';
import { tmpdir } from 'node:os';

const temporaryFile = join(tmpdir(), 'matrix-example.json');
const searchPaths = (process.env.PATH ?? '').split(delimiter);

Windows 的 PATH 分隔符通常是分号,类 Unix 系统是冒号;本地文件路径和网页 URL 也不能混用。URL 应使用 new URL() 等 URL 工具,不能用 Windows 的反斜杠拼接下载地址。

Mac .command 和 Windows .bat 应先定位脚本自身目录,再寻找运行时。Windows 使用 %~dp0,路径参数加引号;Mac 使用脚本所在目录。中文路径和空格路径必须真实测试。

14.5 编码、换行和权限

文本统一使用 UTF-8;脚本换行由 .gitattributes 约束。PowerShell 5.1 对某些无 BOM 的非 ASCII 脚本读取行为与 PowerShell 7 不同,应在目标版本验证;BAT 的终端编码与脚本编码也要一起检查。不能只看编辑器里中文正常就宣布客户终端无乱码。

Mac 和 Linux 的执行权限是文件属性,ZIP 解压后必须核对;Windows 主要依赖程序格式、扩展名和系统策略。跨平台打包程序必须保留正确权限和文件名编码。

14.6 浏览器适配的两条测试线

工作台适配关注布局、Cookie、跨源请求、下载、焦点、网络错误与登录回调。执行浏览器适配关注可执行文件探测、启动参数、独立用户目录、页面身份读取和平台动作。

Edge-only 的 Windows 电脑、Chrome 自定义安装路径、Mac 下载后首次打开,都应有独立场景。CHROME_BIN 作为自定义路径配置时,需要检查文件存在、可执行及架构,而不是直接把任意字符串交给 shell。

14.7 跨平台验收记录模板

操作系统及版本:
CPU 架构:
工作台浏览器及版本:
连接器版本及包哈希:
执行浏览器及版本:
安装路径是否含中文和空格:
是否依赖系统 Node:
首次启动/重启/配对/撤销结果:
平台登录/同步/草稿/停止结果:
未通过项及关联日志编号:

第十五章 构建 上线 迁移与回滚

15.1 四种产物各有什么用

源码描述程序;前端构建产物供浏览器加载;容器镜像描述服务器运行环境;客户连接器包含客户电脑需要的代码和运行时。数据库与媒体是运行数据,不属于这些代码产物本身。

因此“改了源码”“构建完成”“镜像已创建”“网站在运行新版本”“客户下载到新连接器”必须分别核对。很多迁移问题来自只更新了其中一项。

15.2 环境配置清单

配置类别 需要明确的内容 常见故障
运行时 Node、包管理器、依赖锁 开发可用,服务器 API 不兼容
数据库 地址、库名、角色、schema 连接到旧库或空库
文件 数据目录、媒体目录、挂载权限 数据库有内容而图片不存在
公网入口 外部 origin、代理、证书 登录、回调或下载地址错位
连接器 协议、平台、架构、服务地址 只建立通道但不能执行新任务
加密 密钥标识及受控存放位置 配置密文存在却不能解密
维护 MCP 服务、可信脚本、授权 可读取但无法正确检查或回滚

清单记录位置和标识,不把秘密原文写进普通日志。不同环境使用各自的配置,部署时生成,不在多处手写 IP。

15.3 一次发布的正确顺序

  1. 确认当前运行版本、候选提交和未提交修改。
  2. 完成相关测试、构建、依赖和配置检查。
  3. 准备可恢复的数据库与文件备份,并验证恢复方法。
  4. 检查迁移是否兼容保留的回滚版本。
  5. 构建候选镜像,记录摘要与提交号。
  6. 在隔离环境启动,检查 readiness、关键接口和业务流程。
  7. 根据需要安排短暂停写窗口,执行最终数据处理。
  8. 切换服务并检查公网、认证、设备通道及关键业务。
  9. 记录发布结果,保留回滚版本,观察实际任务。
  10. 验收后才清理可重建产物,保留清单。

上线确认应绑定具体提交、镜像和检查结果。确认之后若继续修改代码,应重新检查对应变化,不能复用过期确认。

15.4 健康检查分三层

存活检查回答进程是否响应;就绪检查回答数据库、schema 和必要依赖是否足以接受工作;业务验收回答真实用户是否能完成任务。

GET /api/health 返回成功只能证明该接口检查的条件成立。要知道它检查了什么,应读实现。不要把一个健康接口的 200 作为真实扫码、内容同步和草稿成功的证据。

15.5 迁移前必须形成一致快照

数据库备份与媒体文件应属于同一个一致性窗口。初次复制后,还要考虑期间新上传的素材和新任务。通常先预复制,在短暂停写后做最终数据库备份及文件增量核对,再切换。

迁移清单至少包含用户、租户、账号、内容、素材、任务的分租户数量;关键记录的稳定 ID;媒体文件数和哈希;模型密文的可解密验证;版本、配置和备份时间。

哈希用于确认文件字节一致,不能证明业务关系正确;数量相同也不能证明记录内容相同。因此数量、ID、引用关系、内容抽查和哈希应组合使用。

15.6 回滚不只是换回旧容器

如果新代码改了 schema 或写入了旧代码不理解的数据,换回旧镜像可能启动失败或破坏新数据。发布前就需要明确兼容策略、是否可回退、回退时哪些新写入需要保留。

数据库恢复到旧备份会丢失备份之后的新写入,不能作为每次失败的默认动作。优先使用向后兼容的 schema 变更;确需恢复时,先停写并保存当前故障现场,清楚记录恢复时间点和影响。

15.7 Nginx TLS 和端口

公网通常通过 443 提供 HTTPS;80 可以用于跳转和证书验证。数据库、本机浏览器控制端口和内部服务不应因为迁移方便而直接全部开放公网。

证书要核对适用域名或 IP、到期时间、自动续期任务及新证书加载机制。曾经续期成功不等于下次一定成功;应查看实际最近执行结果。目录浏览告警要定位具体路径和代理规则,不能仅凭告警标题判断必须关闭整个网站。

15.8 维护 MCP 的原理

MCP 把固定维护能力提供给支持该协议的 AI 客户端。它不是另一个自动收费模型,也不是给 AI 一个无限权限的 shell。项目维护服务与业务服务分离,包含授权、源码读取、变更、检查、发布准备和回滚等模块。

OAuth 用于授权客户端;PKCE 绑定授权过程;scope 限定只读、修改、发布和清理能力;撤销凭证使旧授权失效。项目发布确认机制还绑定检查与具体版本,历史文档约定确认 15 分钟过期、一次使用。

客户端支持哪些能力要单独验收。能添加 MCP 地址不等于授权完成;能读取不等于能修改;能修改也不等于可以自动上线。接入细节以项目维护文档、当前服务配置和实际客户端能力为准。

第十六章 怎样把系统越维护越清楚

16.1 清理代码之前先证明它没有职责

文件数量多不必然导致不稳定。重复实现、过期配置、没有边界的共享状态和不一致的数据源才会使维护困难。

判断一个文件可删除,至少检查引用、动态加载、脚本入口、构建复制、部署依赖和恢复用途。没有普通 import 的脚本,也可能由定时任务或维护 MCP 调用。先画依赖,再删;删除后验证真正受影响的流程。

源码用 Git 保存可恢复历史;数据清理使用引用清单、隔离和保留期。Git 不会自动备份未跟踪的客户媒体、数据库卷和密钥,不能用“都有 Git”作为删除这些数据的理由。

16.2 重构与修复怎样安排

修复改变错误行为;重构改善结构但保持外部行为。共享模块过于混乱时,可以先补行为测试,再提取小模块,最后修改错误规则。不要一次同时更换数据库、页面框架、任务协议和部署方式,再希望测试定位是哪项造成回归。

好的模块边界围绕业务责任,例如身份识别、任务调度、结果导入和客户错误映射。单纯把一个大文件机械拆成十个文件,却保留大量隐式共享变量,不会自动得到清晰架构。

16.3 性能优化先测量

把一次同步耗时拆成:排队、浏览器启动、登录确认、页面读取、媒体下载、回传、数据库写入、页面刷新。占比最大的环节才是优先调查对象。

数据库慢,先查看查询次数、执行计划、索引和返回量;页面慢,检查重复请求、渲染次数和大列表;媒体慢,检查文件体积、网络与缓存;任务慢,检查是否排队或等待外部平台。

不要为了“看起来快”移除身份检查,也不要直接提升并发而不核对浏览器互斥。性能改动应同时报告耗时变化、正确性、错误率和资源使用。

16.4 当前实现需要知道的容量边界

PostgreSQL 封装当前连接池上限为 8、连接超时 5 秒、语句超时 10 秒。它们是当前配置,不是通用最佳值。增加连接池可能加重数据库压力;提高超时可能只是把失败推迟。

node:sqlite 的同步调用会占用 Node 当前执行线程,长 SQL 或锁等待可能影响服务响应。应通过测量判断是否需要减少同步工作、缩小事务或调整职责,不能只看到“用了 SQLite”就断定必须全删。

16.5 开发费用怎样压缩

你的“降本”指 AI 帮你开发和维护消耗的费用,而不是修改客户生图或文案价格。有效方法是减少重复调查和无效迭代:

  1. 每次开工先记录分支、提交和当前问题范围。
  2. 保留简短模块地图和最新验证记录。
  3. 搜索定位后只读相关文件,不反复发送整个项目和历史对话。
  4. 一个根因形成一组测试,之后复用测试避免重复排查。
  5. 使用确定性脚本完成哈希、清单、统计和格式检查。
  6. 只在代码、依赖或配置变化影响到结果时重跑检查。
  7. 每轮结尾留下精简的已完成、未完成、证据和下一步。

这些方法节省的是工作量,不能预先承诺具体百分比。可以按每轮工具读取量、重复执行次数、任务耗时与实际计费记录测量改善。

16.6 日志应该记录什么

推荐把一条任务日志写成结构化事件:时间、提交或连接器版本、租户标识、任务编号、阶段、耗时、状态、错误编号。诊断界面默认展示摘要,需要时再读取对应任务附近的上下文。

日志不应包含完整会话令牌、私钥、密码和客户浏览器 Cookie。账号和内容字段按排障必要程度脱敏。发生故障时,保留可定位的信息比收集所有数据更有用。

16.7 维护记录示例

日期与提交:
修改目的:
涉及模块:
固定业务边界:
本轮证据:
相关测试与结果:
尚未验证的环境:
是否上线及镜像摘要:
下一次从哪里继续:

保持一份当前事实入口,历史材料按日期归档。清理重复文档时先更新所有引用,避免新开发者误读旧方案为当前实现。

第十七章 六周学习路线与协作模板

17.1 第一周 认识程序

目标:完成第四章练习,理解前端、后端和接口。

每天用 30–60 分钟:第一天准备 Node 和目录;第二天运行页面;第三天看网络请求;第四天修改文字;第五天读函数与对象;第六天运行自动测试;第七天不用看书画出完整链路。

验收:你能解释为什么服务器返回 202 后页面还需要查询任务,为什么重启后内容消失,以及为什么禁用按钮不是后端防重复的全部实现。

17.2 第二周 学会读项目

目标:从页面文字定位一个真实组件,再找到调用的 API。

先读 package.json 和目录地图,再读发布中心、登录组件和请求封装。每次只记录一个流程的输入、处理、输出、失败。不要尝试一天读懂整个项目。

验收:给你一张页面截图,你可以说出先搜索哪个文字、查看哪个文件、在网络面板观察哪个请求。

17.3 第三周 学会小改动与 Git

目标:独立完成一个客户提示修改,不改变业务行为。

记录起始提交,检查已有修改,创建学习分支,修改一处文字,检查差异,运行相关检查并提交。尝试查看提交前后的差异,理解恢复源码与恢复运行数据的区别。

验收:你可以清楚说明改了什么、没改什么、怎样验证,以及该提交是否已经部署。

17.4 第四周 理解数据和权限

目标:能解释租户、账号、内容、任务和设备的关系。

用教学数据画关系图,阅读 PostgreSQL schema 和事务封装,再读一个查询。练习解释为什么 ID、租户过滤、唯一约束和事务各解决不同问题。

验收:能写出两个客户互不可见的测试场景,以及迁移图片为什么不能只复制数据库。

17.5 第五周 理解任务和故障

目标:能系统排查一次失败,并区分症状与根因。

在第四章模拟系统中增加停止、失败或延迟场景;先写验收,再让 AI 实现。对照真实项目任务状态阅读,解释未知结果为什么不能直接重试写入。

验收:提交一份包含复现、证据、假设、验证、修复和回归的故障报告,而不是只有“已经修好了”。

17.6 第六周 理解发布与维护

目标:能审查一次发布方案并判断证据是否充分。

在独立测试环境演练构建、启动、检查版本、备份和恢复。填写平台验收表,练习识别“构建通过但真实流程未测”的交付缺口。

验收:能说明候选提交、镜像、数据库版本和连接器版本的关系,并解释失败时怎样回滚、会不会丢失新数据。

17.7 让 AI 讲解代码的模板

我从零学习,请解释这个模块。
先用日常语言说它负责什么,再用技术语言解释输入、输出和依赖。
挑一个真实用户动作,逐步追踪代码,不要一次讲整个仓库。
每个新术语第一次出现时解释含义。
区分当前代码事实、教学简化和建议改进。
最后给我一个不触碰生产数据的小练习,并写出预期结果。

17.8 让 AI 实现功能的模板

角色与场景:
希望完成的动作:
正常结果:
失败、取消、重复点击和断网时的结果:
必须保留的既有功能与数据:

先检查当前目录、版本和相关模块,指出已有实现。
将需求转为验收条件,再修改最小完整范围。
完成后提供差异、相关检查、实测证据和未验证项。
不要把源码提交、构建通过或接口返回成功当作生产业务已通过。

17.9 让 AI 修复问题的模板

实际现象:
期望结果:
出现时间和时区:
系统、浏览器、连接器版本:
复现步骤:
任务编号或脱敏日志:

请从请求到数据库和执行器追踪同一次操作。
提出可验证的根因假设,先找第一个失败边界。
修复后检查所有共享该模块的流程。
报告根因、影响范围、修改、证据、未验证项。
保留原有生成配置、素材和多图能力。

17.10 一轮工作结束时你要问的五句话

“你验证的是哪一个版本?”“真实用户动作完成到了哪一步?”“有没有只是模拟通过的部分?”“这次修改会影响哪些已有流程?”“如果失败,怎样恢复?”

这五句话能帮助你判断工程质量,不要求你立刻能写所有代码。逐渐熟悉之后,再主动阅读差异和测试,减少完全依赖 AI 口头说明。

附录一 常用词汇对照

术语 小白解释 在项目中的作用
Runtime 运行时 执行程序的环境 Node 执行后端与连接器
Dependency 依赖 程序借用的工具库 React、pg、Playwright 等
Build 构建 把源码整理成可交付形式 前端打包、类型检查
API 接口 程序之间约定的办事窗口 创建同步、读取任务
HTTP 状态码 网络请求处理结果类别 200 成功响应、202 已接受、401 身份、403 权限、500 服务异常
JSON 一种固定格式的数据文本 请求与响应
Schema 数据结构规则 数据库表与接口校验
Tenant 租户 一个客户的数据与权限范围 客户之间隔离
Session 会话 一段已识别用户的访问状态 工作台登录
Cookie 浏览器随请求带的少量信息 保存会话标识
Token 令牌 访问授权凭据 会话、设备、MCP
Hash 哈希 数据的固定长度指纹 文件校验、令牌摘要
Encryption 加密 用密钥保护原文 模型配置与迁移备份
Transaction 事务 一组操作一起提交或回滚 数据一致性
Idempotency 幂等 同一意图重试不重复副作用 防重复任务或付费请求
Lease 租约 有有效时间的执行权 检测中断或失联任务
Mutex 互斥 同时只允许一个占用者 同账号浏览器操作协调
Migration 迁移 改结构或移动数据 SQLite 到 PostgreSQL、服务器更换
Rollback 回滚 恢复到可工作的兼容版本 发布失败恢复
Readiness 就绪 已准备接受实际工作 部署健康判断
Regression 回归 确认旧功能没有被新改动破坏 每轮迭代检查
Mock 模拟 用可控替身代替外部系统 稳定复现异常
E2E 端到端 从用户操作到最终业务结果 真实登录、同步、草稿
Git commit 提交 一份可追踪的源码变更记录 版本恢复与审查
Docker image 镜像 程序及环境的版本产物 服务器部署
Volume 卷 容器外保留的数据空间 数据库和媒体持久化
MCP AI 客户端调用工具的协议 受控项目维护

附录二 命令使用速查

命令 执行位置 作用与注意事项
node --version 任意终端目录 查看当前终端实际使用的 Node
git status --short 仓库内 查看未提交修改,不会修改文件
git diff 仓库内 查看尚未暂存的差异
git diff --cached 仓库内 查看已暂存准备提交的差异
git log -5 --oneline 仓库内 看最近五个提交
pnpm install --frozen-lockfile 仓库根目录 安装依赖,可能联网,不应改变锁定方案
pnpm dev 隔离开发环境的仓库根目录 启动开发服务,会写本地开发数据
pnpm build 仓库根目录 类型检查与前端构建,会生成构建产物
pnpm exec vitest run 文件路径 仓库根目录 执行指定测试,先读外部依赖前提
node scripts/maintenance-context.mjs 仓库根目录 读取项目维护摘要
docker compose version 已安装 Docker 的环境 检查 Compose 插件

preflight 是项目检查入口,但当前实现只覆盖部分版本、文件、差异及特定目录扫描条件。它没有自动证明 Docker、全部环境变量、所有秘密排除和真实业务流程都通过。以脚本真正执行的检查为准,不以成功输出的名字推断能力。

附录三 面向接手开发者的阅读顺序

先读 AGENTS.md 和维护摘要,再读 package.json、运行时路径、服务入口、鉴权和数据源。随后选择一条业务链路,配套阅读接口、调度、连接器、执行脚本和测试。最后读部署与维护模块。

主题 首要源码
工作台结构 src/GreyApp.tsx
发布中心 src/PublishCenter.tsx、src/OneClickPublish.tsx
工作台登录 src/AccountLogin.tsx、server/tenant-auth.ts
业务入口 server/grey.ts、server/grey-app.ts
路径与配置 server/runtime-paths.ts、server/postgres.ts
数据模型 server/domain.ts、server/postgres-schema.sql
内容存储 server/postgres-store.ts
同步任务 server/xhs-sync.ts、server/postgres-jobs.ts
浏览器互斥 server/browser-runs.ts
设备通道 server/connector-channel.ts、server/postgres-pairing.ts
草稿执行 server/studio-execution.ts、local-connector/studio-jobs.mjs
本机执行 local-connector/connector.mjs、scripts/xhs-playwright.mjs
本地数据库 server/sqlite-client.ts
迁移 scripts/migrate-postgres.ts
维护服务 server/maintenance-service.ts、server/maintenance-runner.ts

配套文档:开发环境、架构说明、运维说明、发布说明、Windows 客户连接器、维护 MCP。这些文档包含历史约定,涉及实际操作时需与当前代码和环境核对。

附录四 检查自己是否真的理解

  1. 为什么网页能打开,账号同步仍然可能失败?
  2. 为什么复制源码不能自动复制数据库和登录状态?
  3. 为什么前端按钮禁用不能完全防止重复任务?
  4. 为什么数据库事务不能撤销平台已经保存的草稿?
  5. 为什么删除账号后还需要考虑任务、绑定和迟到结果?
  6. 为什么一个任务过期不等于所有互斥锁都已释放?
  7. 为什么 Safari 工作台适配不等于 Safari 自动化执行?
  8. 为什么 TypeScript 检查不能取代接口运行时校验?
  9. 为什么恢复旧数据库可能丢失迁移后的新内容?
  10. 为什么测试通过必须说明运行环境和未覆盖范围?

参考思路分别对应系统边界、产物与数据分离、服务端幂等、外部副作用、生命周期一致性、不同状态来源、浏览器角色、编译期与运行期、恢复时间点以及证据适用范围。能够举出自己项目里的例子,比背诵术语更重要。

学习结束时,你应能拿着一个客户问题,定位到一条真实流程,提出可验证的假设,约束修改范围,再判断交付证据是否足以支持上线。这就是自然语言开发逐步走向可靠工程管理的过程。