Files
codex-market-app/CONTRACT.md
T
2026-07-15 18:01:56 +08:00

104 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.*`;**不得**在前端出现市场地址或密钥。
- 空状态、加载中、错误提示都要有像样的样式。