Files

104 lines
7.5 KiB
Markdown
Raw Permalink Normal View History

2026-07-15 18:01:56 +08:00
# Codex 插件市场客户端 —— 开发契约(唯一事实来源)
Electron + Node。三名开发者按本文件分工,**接口/数据结构/文件归属必须严格遵守**,避免联调对不上。
## 分工与文件归属(互不重叠)
- **负责人(已完成)**:`package.json`、`electron/preload.js`、本契约、`build/` 图标。
- **开发 A(Node 后端逻辑)**:`electron/main.js` + `electron/lib/*.js`。
- **开发 B(前端 UI)**:`renderer/index.html`、`renderer/styles.css`、`renderer/app.js`。
- **开发 C(打包/跨平台)**:`electron-builder.yml`、`README.md`、`scripts/`(如需)。
> 任何人不得修改不属于自己的文件。preload.js 是契约边界,已冻结。
## 常量(写死在 electron/main.js 或 lib,前端不可见)
```
MARKET_URL = "https://sc.tokendun.cc"
MARKET_KEY = "2e8e96d10ea0c223cc5cc6439df00a97bb4543ca58a1e5be"
```
市场地址与密钥**绝不能出现在 renderer 前端代码里**,只能在主进程。
## 数据结构
```ts
// 本机/仓库里的一个可备份/可还原项
type Item = {
category: "插件 Plugin" | "技能 Skill",
source: string, // 如 openai-primary-runtime;技能为 "skills"
name: string,
rel: string, // 相对 ~/.codex,如 plugins/cache/openai-primary-runtime/presentations
size: number // 字节
}
// 市场条目
type Entry = {
id?: number, name: string, source: string, category: string,
rel: string, repo: string, branch: string,
web_url: string | null, size: number,
description: string | null, published_at?: string
}
```
## 前端可调用的 API(preload 已暴露为 window.api)
全部返回 Promise。出错时 reject(前端用 try/catch 显示错误)。
| 方法 | 入参 | 返回 | 说明 |
|---|---|---|---|
| `api.scanLocal()` | — | `Item[]` | 扫描本机 ~/.codex 的插件+技能 |
| `api.pickDir()` | — | `string \| null` | 弹系统选目录框 |
| `api.packLocal(rels, destDir)` | `string[], string` | `{path:string}` | 打包选中项到本地目录 |
| `api.upload(rels, repo, token, isPrivate)` | `string[],string,string,boolean` | `{owner:string, published:number}` | 打包→推 GitHub→自动发布市场 |
| `api.marketList()` | — | `Entry[]` | 拉官方市场列表 |
| `api.marketInstall(entry)` | `Entry` | `{ok:true}` | 从市场下载并装进 ~/.codex+启用 |
| `api.ghPull(repoInput, token)` | `string,string` | `Item[]` | clone 指定仓库并列出可还原项 |
| `api.ghRestore(rels, enable)` | `string[],boolean` | `{ok:true}` | 把上次 pull 的选中项还原到 ~/.codex |
| `api.getSecret(name)` | `"github"\|"registry"` | `string` | 取已存密钥(无则空串) |
| `api.setSecret(name, value)` | `string,string` | `{ok:true}` | 存密钥(safeStorage 加密) |
| `api.openExternal(url)` | `string` | — | 系统浏览器打开链接 |
| `api.onLog(cb)` | `(msg:string)=>void` | — | 订阅主进程日志流(安装/上传进度) |
## IPC 通道名(开发 A 在 main.js 用 ipcMain.handle 实现;开发 B 不直接用,只用 window.api)
`scan-local` `pick-dir` `pack-local` `upload` `market-list` `market-install`
`gh-pull` `gh-restore` `get-secret` `set-secret` `open-external`
日志:主进程 `webContents.send("log", msg)` → preload 转成 `api.onLog`。
## 关键逻辑要点(开发 A 必读,从现有 Python 版移植)
1. **扫描**:`~/.codex/plugins/cache/<source>/<plugin>/` 每个是插件;`~/.codex/skills/<skill>/` 每个是技能。跳过 `.` 开头目录。size 递归求和。
2. **打包排除**:拷贝时跳过隐私文件 `auth.json`、`config.toml`、`history.jsonl`、`*.sqlite*`、`*.log`、`*.bak`、`.DS_Store`、`installation_id`。
3. **打包产物**:staging 内保持相对结构;额外生成 `.gitignore`、`restore.sh`、`README.md`、`enabled-plugins.toml`(记录选中插件在本机 config.toml 里的启用状态)。
4. **上传**:用 isomorphic-git。先 `GET https://api.github.com/user`(Bearer token)拿 owner,并读响应头 `x-oauth-scopes` 校验含 `repo`/`public_repo`(缺失给明确错误:让用户用 classic token 勾 repo);`POST /user/repos` 建仓(422=已存在则忽略,403/404 继续尝试 push);git init/add/commit/push(force)。push 认证 `onAuth: () => ({ username: token })`。
5. **发布市场**:上传成功后,对选中的**插件**项(技能无 config 登记)构造 Entry:`repo=owner/reponame, branch=main, web_url=https://github.com/owner/repo/tree/main/<rel>`,`POST {MARKET_URL}/api/plugins`,头 `X-API-Key: MARKET_KEY`,体 `{plugins:[...]}`。
6. **克隆/安装**:isomorphic-git clone(私有仓库 onAuth 用 token)到临时目录 → 扫描 → 按 `entry.rel` 精确匹配 → 拷进 `~/.codex/<rel>` → 若是插件则在 `config.toml` 追加 `[plugins."name@source"]\nenabled = true`(已存在则不重复;改动前备份 config.toml)。
7. **仓库地址解析**:接受 `owner/repo`、`https://github.com/owner/repo(.git)`。
8. **密钥存储**:`safeStorage.encryptString` 加密后写入 `app.getPath("userData")/secrets.json`;读时解密。name 取 `github` / `registry`。
9. **市场 API_KEY**:安装(读)不需要 key;仅发布需要,用写死的 MARKET_KEY。
## 会话记忆(新增模块:electron/lib/memory.js)
自动把 Codex 会话浓缩成交接摘要:监视 `~/.codex/sessions`,会话静默 idleMinutes 后摘要
(优先走路由中转可用分组的 LLM,失败退化本地提取)→ 存 `userData/memory.db`(sql.js)
→ 写项目根目录 `HANDOFF.md` + 在 `AGENTS.md` 加带标记的引导段(`<!-- codex-memory:start/end -->`)。
配置在 `userData/memory.json`(enabled / disabledProjects / idleMinutes / backfillDays / handoffSessions)。
前端 API(preload 已暴露):
| 方法 | 入参 | 返回 | 说明 |
|---|---|---|---|
| `api.memoryStatus()` | — | `{enabled, engine, total, pending, idleMinutes, projects[]}` | 状态 + 项目列表 |
| `api.memoryList(opts)` | `{project?, q?, limit?}` | 摘要行数组 | 检索记忆库 |
| `api.memorySetEnabled(b)` | `boolean` | status | 总开关 |
| `api.memoryToggleProject(cwd, b)` | `string, boolean` | status | 项目级写入开关 |
| `api.memoryResummarize(path)` | rollout 路径 | `{ok}` | 手动重摘要一条 |
| `api.memoryDelete(id)` | 会话 id | `{ok}` | 删一条摘要 |
| `api.memoryOpenHandoff(cwd)` | 项目路径 | — | Finder 中显示 HANDOFF.md |
| `api.onMemoryChanged(cb)` | 回调 | 取消函数 | 摘要更新推送 |
IPC 通道:`memory-status` `memory-list` `memory-set-enabled` `memory-toggle-project`
`memory-resummarize` `memory-delete` `memory-open-handoff`;事件 `memory-changed`。
router.js 新增导出 `llmGroup()`(供内部功能取可调用的中转分组)。
## UI 设计方向(开发 B)
- 现代、精致、可直接对外发布的观感;深色为主、支持浅色;圆角卡片、柔和阴影、清晰层级。
- 主色建议靛蓝→紫渐变(与图标一致 #5B6CFF→#A855F7),强调按钮用该渐变。
- 两大区域:**① 插件市场**(卡片列表 + 搜索框 + 分类筛选 + 每卡「安装」按钮 + 安装态/进度);**② 备份与还原**(子区:从本机导出上传 / 从 GitHub 拉取还原)。可用侧边栏或顶部标签切换。
- 顶部品牌区(logo + 名称「Codex 插件市场」)。底部或抽屉式「运行日志」。
- 无外部 CDN(离线可用),字体用系统字体栈。图标可用内联 SVG。
- 所有后端调用走 `window.api.*`;**不得**在前端出现市场地址或密钥。
- 空状态、加载中、错误提示都要有像样的样式。