Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/ci-cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CLI

# meebox CLI(cli/,独立 Go module)的门禁,与 Node/Nx 的 CI 分开:
# 路径过滤只能加在 workflow 的 on 层(不能按 job 过滤),故独立成一条流水线——仅当 cli/ 变更时才跑,
# 既隔离 Go 工具链、又省 CI 分钟。发布期的交叉编译 / 出包见 release.yml 的 cli job。
on:
push:
branches: [master]
paths:
- 'cli/**'
- '.github/workflows/ci-cli.yml'
pull_request:
paths:
- 'cli/**'
- '.github/workflows/ci-cli.yml'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
cli:
name: Vet + Test + Build
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: cli
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: cli/go.mod
cache-dependency-path: cli/go.sum

- name: go vet
run: go vet ./...

- name: go test
run: go test ./...

- name: go build
run: go build ./...
66 changes: 65 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ concurrency:
cancel-in-progress: false

jobs:
build:
gui:
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -137,3 +137,67 @@ jobs:
make_latest: ${{ !contains(github.ref_name, '-') }}
# 正文 = RELEASE_NOTES(安装 / 首次打开 / 校验和)+ 注入的本版 CHANGELOG 段
body_path: RELEASE_BODY.md

# meebox CLI(cli/,独立 Go module):纯 Go、无 CGO → 单 runner 交叉编译全平台。出四平台压缩包 +
# 校验和,随桌面安装包挂到同一个 GitHub Release(不打进安装包,是独立可分发物)。仅 tag 触发上传;
# workflow_dispatch 仍构建做编译冒烟、不上传。本 job 不设 Release 正文(由 build job 注入),仅追加产物。
cli:
name: CLI (${{ matrix.goos }}/${{ matrix.goarch }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { goos: windows, goarch: amd64, ext: '.exe', archive: zip }
- { goos: darwin, goarch: arm64, ext: '', archive: zip }
- { goos: linux, goarch: amd64, ext: '', archive: tar.gz }
- { goos: linux, goarch: arm64, ext: '', archive: tar.gz }
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version-file: cli/go.mod
cache-dependency-path: cli/go.sum

- name: 交叉编译 meebox CLI
shell: bash
working-directory: cli
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
CGO_ENABLED: '0'
run: |
VERSION="${GITHUB_REF_NAME#v}" # tag 去 v 前缀;非 tag(dispatch)取 ref 名,仅用于编译冒烟
mkdir -p dist
go build -trimpath \
-ldflags "-s -w -X github.com/huhamhire/code-meeseeks/cli/cmd.version=${VERSION}" \
-o "dist/meebox${{ matrix.ext }}" .

- name: 打包压缩包 + 校验和
if: startsWith(github.ref, 'refs/tags/')
shell: bash
working-directory: cli/dist
run: |
VERSION="${GITHUB_REF_NAME#v}"
BIN="meebox${{ matrix.ext }}"
ARCHIVE="meebox-cli-${VERSION}-${{ matrix.goos }}-${{ matrix.goarch }}"
cp ../../LICENSE .
if [ "${{ matrix.archive }}" = "zip" ]; then
zip -q "${ARCHIVE}.zip" "${BIN}" LICENSE
else
tar -czf "${ARCHIVE}.tar.gz" "${BIN}" LICENSE
fi
for f in "${ARCHIVE}".zip "${ARCHIVE}".tar.gz; do
[ -e "$f" ] && sha256sum "$f" > "$f.sha256"
done

- name: 上传到 Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
# 本 matrix 项仅产出自身的压缩包 + .sha256(fresh runner);不设 body_path 以免覆盖 build job 注入的正文
files: cli/dist/meebox-cli-*
fail_on_unmatched_files: true
prerelease: ${{ contains(github.ref_name, '-') }}
make_latest: ${{ !contains(github.ref_name, '-') }}
57 changes: 31 additions & 26 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,26 @@

## 仓库结构

Electron 桌面应用 + npm workspaces + Nx 单仓多包。关键路径(`apps/desktop` 是主战场):
Electron 桌面应用npm workspaces + Nx 单仓多包)外加一个独立 Go CLI 子工程。关键路径(`apps/desktop` 是主战场):

```
apps/desktop/
apps/desktop/ # Electron 应用(唯一有 build/dist 的项目)
├── src/
│ ├── main/ # 主进程:index.ts(启动/单例锁) · ipc.ts(IPC handlers) · adapters.ts · utils/
│ ├── main/ # 主进程:业务与 IO 唯一所在(启动/单例锁 · IPC handlers · 平台适配 · 服务层:pr · agent 编排 · 本地 API)
│ ├── preload/ # contextBridge 暴露泛型 invoke()
│ └── renderer/src/ # React 渲染层(components/ 等)
├── scripts/ # assemble-pragent-runtime.mjs · pragent-shim/(shim) · pragent-runtime.json
├── build-resources/ # after-pack.cjs(ad-hoc 签名) · entitlements.mac.plist
├── electron-builder.yml
└── vendor/pragent/ # 嵌入式运行时(gitignored,由 prepare:pragent 生成)
│ └── renderer/src/ # React 渲染层(UI / 交互)
├── scripts/ # 嵌入式 pr-agent 运行时组装 + monkeypatch shim
├── build-resources/ # electron-builder 打包资源(签名钩子 · entitlements)
└── vendor/pragent/ # 嵌入式运行时(gitignored,prepare:pragent 生成)

packages/<name>/src/index.ts # 各库入口;shared 还含 ipc.ts(IPC 契约) · config.ts · poller-contract.ts
packages/<name>/ # 内部库 @meebox/*(各 src/index.ts 为入口);shared 含共享类型 + IPC 契约
cli/ # 独立 Go module:跨平台 CLI meebox(命令树 + HTTP client;经本地 API 集成,不入 npm/Nx)
```

- `apps/desktop` —— Electron 应用(main + preload + renderer/React)。唯一有 `build`/`dist` 的项目。
- `packages/*` —— 内部库(`@meebox/*`),按职责拆分;其中 `shared` 含共享类型与 IPC 契约。
- `docs/arch/` 各模块设计文档(首选入口);`docs/ROADMAP.md` 路线图;`tools/` 杂项脚本。
- `cli/` —— 独立分发的 Go 命令行工具 `meebox`(外部集成用,不属 npm/Nx;详见下「CLI 工程(cli/)」段)。
- `docs/arch/` 各模块设计文档(首选入口);`docs/guide/` 使用说明;`docs/ROADMAP.md` 路线图;`tools/` 杂项脚本。

**命名约定**:代码内部统一用中性代号 `meebox`(npm 作用域 `@meebox/*`);对外品牌名 `Code Meeseeks`;用户数据目录 `~/.code-meeseeks/`。`pr-agent` 为第三方依赖,不在重命名范围内。

Expand Down Expand Up @@ -67,17 +68,19 @@ npm --prefix apps/desktop run prepare:pragent # 对齐嵌入式 pr-agent 运

## 发布流程

发版从 `dev` 汇入 `master` 后,在 `master` 打 `v*` tag 触发 [release.yml](.github/workflows/release.yml)(出 Windows / macOS 安装包 + GitHub Release)。**打 tag 前必须在同一批改动里完成三步前置,且随发版改动一并经 `dev` → `master`**——漏任一步 CI 不报错(仅 `::warning::`)但会产出错误的 Release
发版:`dev` 汇入 `master` 在 `master` 打 `v*` tag 触发 [release.yml](.github/workflows/release.yml)(出 Windows / macOS 安装包 + CLI 二进制 + GitHub Release)。

1. **版本号** —— 把 [apps/desktop/package.json](apps/desktop/package.json) 的 `version` 改成目标版本(去 `v` 前缀,预发布带后缀,如 `0.5.0-alpha.1`)。electron-builder 的 `artifactName: code-meeseeks-${version}-...` 直接取此值——不改则安装包文件名缺 `-alpha.N`、与 tag 不符。改完跑一次 `npm install` 同步 lockfile。
2. **CHANGELOG** —— 把 [CHANGELOG.md](CHANGELOG.md) 的 `## [Unreleased]` 改名为 `## [<版本>] - <YYYY-MM-DD>`,并在文件底部补 `[<版本>]: …/compare/…` 链接引用(仿现有行)。**发布即移除 Unreleased、不再另起空段**——`## [Unreleased]` 仅**开发期**存在以累积变更,发布时被改名消费掉;下一笔开发期 changelog 改动时再新建一个 `## [Unreleased]`(见「版本号规则」的 `-dev` 开发态)。release.yml 按 `## [<版本>]` 字面抽段注入 Release 正文的「版本变更」区——缺这段则正文回退、无任何变更说明。**若本次是正式版(无 `-` 后缀)、且其内容来自此前的 alpha/预发布**:此时开发期通常无独立 Unreleased(内容已在预发布段),直接把该预发布段改名为正式版段、删去对应的 `[<x>-alpha.N]:` 链接引用即可(内容并入正式版段,不再保留空壳 stub);尚无对应正式版的其它预发布段保留。
3. **校对** —— 确认 `## [<版本>]` 段已覆盖自上版本以来合入 `dev` 的全部要点(新增 / 变更 / 修复)。
⚠️ **打 tag 前必须在同一批改动里完成三步前置(版本号 / CHANGELOG / 校对),随发版一并经 `dev` → `master`**——漏任一步 CI 不报错(仅 `::warning::`)但会产出错误的 Release。**完整前置清单、`-dev` 版本号规则、CHANGELOG 撰写风格见 [打包与发布](docs/development/packaging-release.md)**。tag 名须等于 package.json 版本(`v<版本>`);名含 `-` 的预发布 tag 自动标 prerelease、不抢占 Latest。

tag 名与 package.json 版本必须一致(`v<版本>`)。预发布 tag(名含 `-`,如 `-alpha.N`)由 release.yml 自动标 prerelease 且不抢占 Latest。
## CLI 工程(cli/)

**版本号规则**:每次正式发版后,`dev` 立即把 [apps/desktop/package.json](apps/desktop/package.json) 切到**下一版的 `-dev` 预发布号**(如发完 `0.6.0` 即切 `0.7.0-dev`,并 `npm install` 同步 lockfile),标记开发态、与正式版区分。`-dev` 仅作开发标记——**不打 tag、不发版**;发版时按上面三步前置把它改成目标号(预发布 `0.7.0-alpha.N` 或正式 `0.7.0`)。`-dev` 是合法 semver(`0.6.0` < `0.7.0-dev` < `0.7.0`),不影响更新检测([update-check.ts](apps/desktop/src/main/utils/update-check.ts) 用 `semver.gt` 比对、不用 range,故无「预发布不满足范围」陷阱)与构建
`cli/` 是独立分发的跨平台命令行客户端 `meebox`(供外部 agent / 脚本经[本地 API 服务](docs/arch/04-integration/01-service-api.md)集成)。设计见 [docs/arch/04-integration/02-cli.md](docs/arch/04-integration/02-cli.md),用法见 [docs/guide/06-cli.md](docs/guide/06-cli.md)

**CHANGELOG 撰写风格**(面向用户、求简):① 版本引言 `>` 区直接进入「本版重点」、要点用**无序列表**排版,不堆成长句,**不写「首个 / 第 N 个正式版」之类的版本序数引言**;② 新增 按**功能场景**分类、用缩进的二级列表表达,每个小点一句话点到即止;③ 重构类任务**前后端合并**为一条总结、不展开实现细节;④ 修复 **不写「怎么修的」机制**,每条一句话只述修复的现象/影响;⑤ 通篇不写 IPC 通道名、函数名、文件路径、字段名等实现细节,优先突出新增特性与改良;⑥ **安装 / 升级注意事项**(版本引言里的 ⚠️ 警示,如先卸载旧版、per-machine 提权等)属安全关键信息,**保留完整、不参与精简**——这些会随 release.yml 注入 GitHub Release 正文,删减会让用户漏看升级风险;⑦ **分段标题用中文 + emoji**:`### ✨ 新增 / ♻️ 变更 / 🔧 修复 / 🗑️ 移除 / 🔒 安全`(对应 Keep a Changelog 的 Added / Changed / Deprecated / Removed / Fixed / Security)。外部贡献者的 PR 习惯性致谢(仿 `(#65,感谢 @user)`)。
- **独立 Go module,不入 npm/Nx**:自带 `cli/go.mod`(纯 Go、无 CGO),非 workspace 成员、不进 Nx——根 `lint/typecheck/test/build` 不覆盖它,CLI 自成一套。
- **本地命令**(在 `cli/`):`go vet ./...` → `go test ./...` → `go build ./...`,改完 CLI 三步过了再收尾。`go.sum` 入库(锁校验和);构建产物(`bin/` / `meebox` 等)已 gitignore(见 `cli/.gitignore`)。
- **CI 分两条**:PR 门禁 [ci-cli.yml](.github/workflows/ci-cli.yml)(路径过滤 `cli/**`,跑 vet/test/build,与 Node 的 ci.yml 分开);发布产出在 [release.yml](.github/workflows/release.yml) 的 `cli` job(`v*` tag 触发,交叉编译 Windows / macOS / Linux×2,出压缩包挂同一 Release;Windows / macOS 用 `.zip`、Linux 用 `.tar.gz`)。版本经 `-ldflags -X …/cmd.version` 注入、与应用同 tag。
- **只读边界**:CLI 只做浏览与评审操作,**不提供评论发送等写操作**;写工具(approve/needswork/publish)在 CLI 与服务端双重硬拒绝。新增命令先确认对应 API 端点已存在且只读——CLI 不得绕过 API 直连应用内部。
- **契约同步**:CLI 与服务端唯一耦合是 HTTP/JSON 线协议。当前手写 Go 结构对齐契约,契约增长后转 OpenAPI / Schema 代码生成。默认输出 YAML(人类向),`--output json` 供机器;配置走环境变量 / flag / `~/.code-meeseeks/cli.yaml`(与 GUI 的 `config.yaml` 隔离),代理遵循标准 `HTTP(S)_PROXY` / `NO_PROXY`。

## 约定

Expand All @@ -96,7 +99,11 @@ tag 名与 package.json 版本必须一致(`v<版本>`)。预发布 tag(

## 国际化 (i18n)

GUI 文本走 **react-i18next**(key 为中立标识符,`zh-CN` / `en-US` / `ja-JP` / `de-DE` 为**对等译文集**,无源/译层级;UI 语言由 `config.language` 经 `resolveLanguage` 决定,空则按 OS 回落英语)。**默认 / 兜底语言取 `en-US`**(国际化标准:缺 key 回退英文而非中文):渲染层 en-US 静态打包进入口 + 其余懒加载、`fallbackLng: 'en-US'`,主进程各持一份 locale、同样兜底 en-US。设计、key 命名、翻译规范见 [docs/arch/03-gui/04-i18n](docs/arch/03-gui/04-i18n.md)。三条易踩的:①新增文本须在**各语言 locale 都加**并保持**递归字典序**(日语复数同中文仅 `_other`、德语同英语需 `_one`/`_other`);②i18next **只有 `count`** 触发复数,普通计数插值要换别的变量名;③**不要开 `nonExplicitSupportedLngs`**——它把 `zh-CN` 按基码 `zh` 查找、与按 `zh-CN` 注册的 bundle 错位 → 整页裸 key。
GUI 文本走 **react-i18next**(key 为中立标识符,`zh-CN` / `en-US` / `ja-JP` / `de-DE` 为**对等译文集**;**默认 / 兜底 `en-US`**,缺 key 回退英文)。设计、key 命名、翻译规范见 [docs/arch/03-gui/04-i18n](docs/arch/03-gui/04-i18n.md)。三条易踩(详见该篇):

1. 新增文本各语言 locale 都加且保持**递归字典序**;
2. 复数只认 `count`(普通计数插值换别名);
3. 勿开 `nonExplicitSupportedLngs`(按基码 `zh` 错位 → 整页裸 key)。

## 文档约定

Expand All @@ -107,14 +114,12 @@ GUI 文本走 **react-i18next**(key 为中立标识符,`zh-CN` / `en-US` / `

## 工程维护坑

- **新增内部 `@meebox/*` 包必做两步登记**(漏则报 `Cannot find module …/src/<x>.js`):内部包源码是 `.ts`、相对 import 带 `.js` 扩展(NodeNext 约定),Node 运行期不能直接读。新建一个被 desktop 主/preload 引用的内部包后,除 `npm install`(建 workspace 软链)外,**必须**:① 在 `apps/desktop/package.json` 依赖加 `"@meebox/<name>": "*"`;② 在 [apps/desktop/electron.vite.config.ts](apps/desktop/electron.vite.config.ts) 的 `internalPackages` 数组加该名——让 electron-vite 把它 **bundle**(转译 TS、解析 `.js`→`.ts`)而非 externalize。漏 ② 时 Node 把它当外部包按 `main: src/index.ts` 加载,撞到 `export … from './x.js'` 而文件是 `.ts` → 运行期崩。
- **嵌入式 pr-agent 运行时**(见 [docs/arch/02-agent/03-pragent-runtime](docs/arch/02-agent/05-pragent-runtime.md)):`apps/desktop/scripts/assemble-pragent-runtime.mjs` 按 `pragent-runtime.json` 把可重定位 CPython + pinned pr-agent 装到 `apps/desktop/vendor/pragent/`(gitignored)。
- **monkeypatch shim** `apps/desktop/scripts/pragent-shim/`(薄加载器 `sitecustomize.py` + 领域拆分包 `meebox_pragent_shim/`:`patches/` 各 patch + `cli/` 本地 CLI provider + `runtime.py`/`usage.py`。对 pr-agent 的无侵入补丁):
- 改了它,跑一次 `npm --prefix apps/desktop run prepare:pragent` 即重新同步进 vendor(幂等跳过分支也会同步 shim),**无需 `--force` 全量重建**。
- 受版本守卫:`meebox_pragent_shim/runtime.py` 的 `_EXPECTED_PRAGENT_VERSION` 必须等于 `pragent-runtime.json` 的 `prAgent.version`(assemble 构建期强校验,运行期不符则跳过补丁 + stderr WARNING)。升级 pr-agent 要同步两处并重新验证。
- **拆分铁律**:各 patch 对 `pr_agent` 的 import 一律放在 patch 函数体内(惰性);模块顶层只 import 同包内的 runtime/usage 等,**绝不在顶层 import pr_agent**(否则 sitecustomize 阶段即 eager 加载,拖慢每次 python 启动)。
- 调试:`MEEBOX_SHIM_DEBUG=1` 让 shim 打 stderr 调试。
- **新增内部 `@meebox/*` 包必做两步登记**(漏则报 `Cannot find module …/src/<x>.js`):内部包源码是 `.ts`、相对 import 带 `.js` 扩展(NodeNext 约定),Node 运行期不能直接读。新建一个被 desktop 主/preload 引用的内部包后,除 `npm install`(建 workspace 软链)外**必须**:
1. 在 `apps/desktop/package.json` 依赖加 `"@meebox/<name>": "*"`;
2. 在 [apps/desktop/electron.vite.config.ts](apps/desktop/electron.vite.config.ts) 的 `internalPackages` 数组加该名——让 electron-vite 把它 **bundle**(转译 TS、解析 `.js`→`.ts`)而非 externalize。

漏第 2 步时 Node 把它当外部包按 `main: src/index.ts` 加载,撞到 `export … from './x.js'` 而文件是 `.ts` → 运行期崩。
- **pr-agent 运行时 / shim**:嵌入式 CPython + pinned pr-agent 由 `assemble-pragent-runtime.mjs` 装到 `vendor/pragent`(gitignored);对 pr-agent 的无侵入补丁在 `scripts/pragent-shim/`。机制与铁律(惰性 import 拆分 · 版本守卫 · 改后跑 `prepare:pragent` 同步 · 调试 `MEEBOX_SHIM_DEBUG=1`)见 [02-agent/05-pragent-runtime](docs/arch/02-agent/05-pragent-runtime.md)。弱网 pip 超时加 `PIP_DEFAULT_TIMEOUT=120`。
- **二进制资源走 Git LFS**(`*.png/.ico/.icns` 等):本地没装 git-lfs 时拿到的是指针文件,electron-builder 转图标会崩 → `brew install git-lfs && git lfs pull`。
- **dev 起不来**:若 `npm run dev` 报 `electron does not provide an export named …`,是环境里有 `ELECTRON_RUN_AS_NODE=1`(VSCode 扩展宿主会注入)→ `unset ELECTRON_RUN_AS_NODE` 再跑。
- **grep 个别文件无输出**:如 `repo-mirror-manager.ts` 被 `file` 判为 `data`(含非 UTF-8 字节),普通 grep 静默 → 用 `grep -a`。
- **prepare:pragent 网络**:pip 默认 15s 超时,弱网下加 `PIP_DEFAULT_TIMEOUT=120` 或配国内镜像(`~/.config/pip/pip.conf`)。
Loading
Loading