diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b97e5b2..5d43296 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to Klear-Team-Brain -Tuser2s for your interest! Issues and pull requests are welcome. +Thanks for your interest! Issues and pull requests are welcome. ## How this repo works diff --git a/DEPLOY.md b/DEPLOY.md index 658073b..40e8b6b 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -59,6 +59,8 @@ node server/admin.mjs add alice --name "Alice" --server-url https://brain.yourdo This appends Alice to `team.yaml` + `tokens.yaml`, restarts the service, and prints a `BRAIN-…` **invite code** plus a ready-to-send onboarding message. That invite code is exactly what `brain join` expects in [section 2](#2-clients-each-dev-machine). +> **From your own machine:** once your laptop's `client.config.yaml` has an `admin: { ssh, dir }` block (pointing at the server's SSH target + code dir — see `client.config.example.yaml`), you can run the same thing locally: `brain admin add alice --name "Alice"`. It SSHes in and runs `server/admin.mjs` for you, so you don't need to log into the box. Same for `brain admin who` / `rm` / `org` / `repo` / `gitlab` / `gitea`. + **Manual alternative.** Create the files by hand — copy `team.example.yaml` → `team.yaml`, and `tokens.example.yaml` → `tokens.yaml` with one `openssl rand -hex 24` per member. Members set up this way use the **manual client path** in §2 (raw token in `client.config.yaml`) — `brain join` takes an invite code, not a raw token. ### 1.4 Pick a durable, backed-up path for the truth store @@ -67,6 +69,15 @@ This appends Alice to `team.yaml` + `tokens.yaml`, restarts the service, and pri export TRUTH_DIR=/var/lib/team-brain/truth # the authoritative git truth store — back it up ``` +The truth store is a plain git repo — it's the whole value of the system and **can't be rebuilt**, so back it up. A simple cron-able snapshot: + +```bash +git -C "$TRUTH_DIR" bundle create /backups/truth-$(date +%F).bundle --all # single-file, restore with: git clone +# or just: tar czf /backups/truth-$(date +%F).tgz -C "$TRUTH_DIR" . +``` + +> On a low-RAM VPS, avoid `git gc --aggressive` on the truth store — it can OOM. Plain `git gc` (or letting git auto-gc) is fine. + ### 1.5 HTTPS (easiest: Caddy auto-certs) `/etc/caddy/Caddyfile`: @@ -102,22 +113,64 @@ WantedBy=multi-user.target ```bash sudo systemctl enable --now team-brain -curl https://brain.yourdomain.com/health # {"ok":true} +curl https://brain.yourdomain.com/health # {"ok":true,"version":"…","truth":{"dir":true,"git":true,"writable":true}} ``` +> `/health` does a cheap readiness check of the truth store (exists / is a git repo / writable) and returns **HTTP 503** with the failing field if any check fails — point your uptime monitor at it and alert on non-2xx. + > The server binds `127.0.0.1` by default, so port 8787 is **not** reachable on the public IP — only the Caddy/HTTPS front door is. Keep it that way: if your reverse proxy runs on a different host (e.g. Docker), set `Environment=HOST=0.0.0.0` **and** firewall 8787 so only the proxy can reach it. Never expose 8787 to the internet directly (that bypasses TLS → tokens travel in plaintext). > Raw sessions are stored unredacted on disk (the redacted `.md` is what the query layer surfaces). Security rests on HTTPS + keeping this host open **only to your circle** + token auth. Don't push the truth store to any public remote. -### 1.7 Optional: GitHub code state +### 1.7 Optional: code state (GitHub / GitLab / Gitea) + +Create `registry.yaml` (copy from `registry.example.yaml`, **secret → gitignored**) to register the orgs/repos that should become first-class spaces, each with a read-only PAT. A global `GITHUB_TOKEN` (env or `GITHUB_TOKEN_FILE`) is the fallback. With either configured, `read_github` + the 4h code-state poll are enabled. + +Beyond GitHub, **self-hosted GitLab and Gitea** are first-class too — `registry.example.yaml` shows the multi-instance shape (host + base URL + token, with group/project or org/repo entries), and you can maintain them from your own machine with `brain admin gitlab …` / `brain admin gitea …`. + +> Code-state only includes branches pushed within the last **30 days**. Long-lived release branches that go quiet drop out of view — raise the window with `Environment=CODESTATE_ACTIVE_DAYS=365` to keep them. If `GITHUB_TOKEN_FILE` is set but unreadable, the server logs a warning at startup and runs as if no token were configured (so `read_github` would otherwise report a misleading "no permission"). + +### 1.8 Optional: doc mirror (Feishu/Lark · Notion · Google Docs) + +Mirror human-written team docs into the truth store (one-way) so the asking agent can `grep`/`read` them alongside sessions and code. Each source is gated by its own secret yaml (leave it out → that layer stays quietly off), and all follow the same "share with the bot, then it mirrors" model: + +- **Feishu / Lark** — copy `feishu.example.yaml` → `feishu.yaml`, fill in your custom-app `app_id`/`app_secret`, restart. The full walkthrough (creating the app, scopes, and the **non-obvious whole-wiki authorization step** — you can't add the app directly; you add a *group containing the app's bot* as a wiki admin) is in the README under *Optional: mirror team docs (Lark / Feishu)*. +- **Notion** — copy `notion.example.yaml` → `notion.yaml`, fill `api_token`, **share** the pages/databases with your integration, restart. +- **Google Docs** — copy `google.example.yaml` → `google.yaml`, point it at a service-account JSON key, **share** the docs/folders with the service account's email, restart. + +All are **secret → gitignored**. Each polls every `poll_hours` (default 4). + +### 1.9 Optional: in-dashboard Q&A (`ASK_ENABLED`) + +The web dashboard can host a natural-language "ask one question" box that answers over the truth store. It's **off by default** and spawns a server-side `codex` process per question, so it has cost/latency implications and needs `codex` installed on the server. Enable + tune via env: -Create `registry.yaml` (copy from `registry.example.yaml`, **secret → gitignored**) to register the GitHub orgs/repos that should become first-class spaces, each with a read-only PAT. A global `GITHUB_TOKEN` (env or `GITHUB_TOKEN_FILE`) is the fallback. With either configured, `read_github` + the 4h code-state poll are enabled. +| Var | Default | What it does | +|---|---|---| +| `ASK_ENABLED` | (off) | Set `1` to enable the `/ask` endpoint + the dashboard Q&A box. | +| `ASK_CODEX_BIN` | `codex` | Path to the codex binary the server spawns. | +| `ASK_CODEX_ARGS` | `exec --skip-git-repo-check` | Args passed to codex. | +| `ASK_CWD` | `TRUTH_DIR` | Working directory (defaults to the truth store, so codex can `grep`/`read` the `.md` directly). | +| `ASK_TIMEOUT_MS` | `120000` | Per-question timeout. | +| `ASK_MAX_CONCURRENT` | `2` | Max concurrent questions before returning `429`. | -### 1.8 Optional: doc mirror (Lark / Feishu) +### 1.10 Environment variables (reference) -Mirror a Lark/Feishu **wiki** into the truth store (one-way) so the asking agent can `grep`/`read` your team docs alongside sessions and code. Copy `feishu.example.yaml` → `feishu.yaml` (**secret → gitignored**), fill in your custom-app credentials, and restart. Leave it out and the doc layer stays quietly off. +Everything the server honors (all optional; sensible defaults shown): -The full walkthrough — creating the app, which scopes to enable, and the **non-obvious whole-wiki authorization step** (you can't add the app directly; you add a *group containing the app's bot* as a wiki admin) — is in the README under *Setting up the doc mirror (Lark / Feishu)*. +| Var | Default | Purpose | +|---|---|---| +| `TRUTH_DIR` | `/truth-server` | The authoritative git truth store. **Set this to a durable, backed-up path** (§1.4). | +| `PORT` | `8787` | Listen port. | +| `HOST` | `127.0.0.1` | Bind address. Keep loopback behind a proxy; only set `0.0.0.0` with a firewall (§1.6). | +| `TEAM_FILE` | `/team.yaml` | Roster path. | +| `TOKENS_FILE` | `/tokens.yaml` | Member tokens (secret). | +| `REGISTRY_FILE` | `/registry.yaml` | Registered orgs/repos + PATs (secret). | +| `GITHUB_TOKEN` / `GITHUB_TOKEN_FILE` | — | Global fallback GitHub token (§1.7). | +| `CODESTATE_ACTIVE_DAYS` | `30` | Active-branch window for code-state (§1.7). | +| `FEISHU_FILE` / `NOTION_FILE` / `GOOGLE_FILE` | `/.yaml` | Doc-source credentials (§1.8). | +| `NO_POLL` | (off) | `1` disables all background polling (code-state + doc sources) — useful for static/dev. Note: the Docker image defaults this to `1`. | +| `PUBLIC_URL` | request `Host` | The base URL baked into the `curl …/get \| bash` install script. **Set it when behind a proxy** so members get the right install URL. | +| `ASK_*` | — | In-dashboard Q&A (§1.9). | --- @@ -161,9 +214,14 @@ upload_folders: # the collection allowlist — the field you'll touc - /Users/you/Code/team-stuff # a session uploads only if its cwd is under one of these exclude: # subdirectories to keep private - /Users/you/Code/team-stuff/secret +# collect_all: true # ignore upload_folders and collect ALL sessions on this machine + # (what the installer sets when you pick no workspaces — don't use on machines with secrets/client data) codex: true # also collect ~/.codex/sessions +session_history_md: true # also collect session_history/**/*.md under upload_folders +trae_memory: true # also collect Trae's native session memory under upload_folders interval_sec: 60 # how often to scan, in seconds debounce_sec: 60 # seconds a session must be idle before it's "stable" enough to upload +# auto_update: false # default on: the resident self-updates from the server daily; set false to pin ``` Day to day only two fields change: add a project to `upload_folders` when you pick it up (not in the list = never uploaded), or add a subdir to `exclude` to keep it private — `brain service restart` after either. `token` and `me.id` are your identity: changing `me.id` breaks per-person lookups, a wrong `token` means `401`. The file holds a secret and is gitignored — never commit it. @@ -175,11 +233,13 @@ Start with 2 people, run a few days, and check: are searches accurate, is the up ## Daily ops ```bash -brain status # resident status + last sync +brain status # resident status + last sync + collection footprint (uploaded/skipped) +brain viewer # open the local footprint console (127.0.0.1): see/exclude/retract what this machine uploads brain logs -f # collector logs brain update # pull latest client from the server + restart resident brain service restart # restart the resident after config/code changes brain uninstall # stop resident + remove MCP + delete token config +brain admin add|who|rm # (admins only, needs the admin: block) manage the roster from your machine over SSH ``` ## Troubleshooting diff --git a/README.md b/README.md index d849e4c..1768de8 100644 --- a/README.md +++ b/README.md @@ -95,11 +95,13 @@ With MCP wired up, ask in plain language inside CC / Codex (*"where did the auth | `sessions` | Find sessions by person + work time (who did what, in a given window). | | `stats` | Aggregate token usage / session counts / turns by day/week/person/repo/tool. | | `log` | Activity timeline (the git history; narrow by space/author/since). | -| `read_github` | Reach out to GitHub for live code state or a file's current contents (code itself isn't stored). | +| `read_github` | Reach out to GitHub / GitLab / Gitea (incl. self-hosted) for live code state or a file's current contents (code itself isn't stored). | Server-side queries run via `git grep` / `git ls-files` / `git log` / `fs` — **execFile, no shell, locked inside `TRUTH_DIR`, read-only** — with **zero server-side LLM calls**. > **Wiring other editors:** the MCP server is a stdio server; the command is always ` /mcp/server.mjs` (`brain mcp` prints your exact path). Add that as a stdio MCP server in Claude Code, Codex, or any MCP-capable client (Gemini CLI / Cursor / Cline / opencode…). +> +> **Remote / cloud agents (HTTP transport):** an agent that can't run the local stdio binary can mount the memory over HTTP instead — point it at `https://your-server/mcp` with your member token as a `Bearer` header. Same tools, no local install. ### ② Browse the web UI @@ -123,7 +125,7 @@ each machine (client) your server (self-hosted) │ ① collector client/sync │ gzip+token │ ② server/server.mjs (HTTP, HTTPS │ │ resident, watches AI │ ──────────▶ │ via a proxy like Caddy) │ │ sessions; gated to the │ │ /ingest → git truth store │ -│ allowlisted circle │ │ TRUTH_DIR + 4h GitHub poll → │ +│ allowlisted circle │ │ TRUTH_DIR + 4h repo poll → │ │ │ │ code-state │ │ query A: ask in editor │ search+fetch│ /grep /find /read /ls /log │ │ query B: open the web UI │ ◀────────── │ + static dashboard hosted at / │ @@ -135,8 +137,8 @@ The memory is a single **git repository** fusing three sources — **each captur | What you want to know | Lives mainly in | How it enters the memory | |---|---|---| | **Progress · reasoning** | CC/Codex sessions | distilled + redacted, stored as full-text transcript | -| **Code state** | GitHub | not stored; fetched on demand + 4h poll into `code-state` | -| **Goals · decisions (human-written)** | Team docs (Lark/Feishu wiki) | one-way mirror of doc bodies; grep/read it, edit at the source | +| **Code state** | GitHub / GitLab / Gitea (incl. self-hosted) | not stored; fetched on demand + 4h poll into `code-state` | +| **Goals · decisions (human-written)** | Team docs (Feishu/Lark wiki · Notion · Google Docs) | one-way mirror of doc bodies; grep/read it, edit at the source | > **Design trade-off:** only the "truth" (raw material + metadata) is kept clean and complete — because it's expensive and can't be rebuilt; "views" (queries / indexes / the dashboard) are swappable and throwaway. Sessions enter **distilled** (inline images stripped, giant tool outputs truncated — signal, not bytes); the byte-exact original stays on the producer's machine (`~/.codex` / `~/.claude`). @@ -145,7 +147,8 @@ The memory is a single **git repository** fusing three sources — **each captur ## Privacy & security - **Scope gate:** a session is uploaded only if its working directory is under your machine's `upload_folders` allowlist — inside the circle is shared by default, outside is private by default. -- **Redaction:** secrets/tokens + home-directory paths are stripped **client-side before upload**, with second passes when the server projects the `.md` and at the `/read` exit. +- **Redaction:** secrets/tokens + home-directory paths are stripped **client-side before upload**, with second passes when the server projects the `.md` and at the `/read` exit. Each producer can also keep a **personal redaction wordlist** (client names, code names…) that's stripped before anything leaves their machine. +- **Producer transparency & control:** run `brain viewer` for a local console (127.0.0.1, only you can see it) that shows, per session, exactly what was uploaded vs skipped — and lets you exclude individual sessions, **retract** ones already in the shared store, or add personal redaction terms. - **Credentials never stored:** member tokens, GitHub PATs, and doc-source credentials all live server-side and are gitignored; the roster (no secrets) can be committed. - **The memory is the whole value — never push it to a public remote, and back it up.** Self-host on infrastructure open only to your circle. @@ -166,7 +169,7 @@ If your team keeps human-written docs (goals, decisions, notes) in a Lark/Feishu The app then has read access to the *entire* wiki (propagation takes ~1–2 minutes). 4. **Drop credentials on the server:** copy `feishu.example.yaml` → `feishu.yaml` (secret → gitignored), fill in `app_id` / `app_secret`, and restart. Leave it out entirely and the doc layer stays quietly off. -5. **Verify:** after a poll cycle, the docs appear under `feishu//…` in the memory, searchable via `grep`. +5. **Verify:** after a poll cycle, the docs appear under `feishu/__/…` in the memory, searchable via `grep`. > China Feishu (`open.feishu.cn`) and international Lark (`open.larksuite.com`) are isolated platforms — create the app on whichever one your team uses. Create a **new** wiki later? Repeat step 3 for it, or the brain won't see it. diff --git a/README.zh-CN.md b/README.zh-CN.md index 2a4b053..53a5eff 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -96,11 +96,13 @@ npm run sync -- --once # 收一次(或 `npm run sync` 让它在后台持续 | `sessions` | 按人 + 工作时间检索 session(某人某段时间干了啥)。 | | `stats` | 按 天/周/人/repo/工具 聚合 token 用量 / session 数 / 对话轮次。 | | `log` | 活动时间线(git 历史;可按 space/author/since 收窄)。 | -| `read_github` | 出网现拉某仓代码状态或文件最新内容(代码本体不入库)。 | +| `read_github` | 出网现拉 GitHub/GitLab/Gitea(含自建)某仓代码状态或文件最新内容(代码本体不入库)。 | 服务器侧查询全走 `git grep` / `git ls-files` / `git log` / `fs`——**execFile 无 shell、锁死在 `TRUTH_DIR` 内、只读**——**零服务器端 LLM 调用**。 > **接别的编辑器:** MCP 是个 stdio server,命令固定为 ` <安装目录>/mcp/server.mjs`(`brain mcp` 会打印你这台的实际路径)。任何支持 MCP 的客户端(Claude Code / Codex / Gemini CLI / Cursor / Cline / opencode…)加一个 stdio MCP server 即可。 +> +> **远程/云端 agent(HTTP 传输):** 跑不了本地 stdio 二进制的 agent,可改用 HTTP 挂载——指向 `https://你的服务器/mcp`,把个人 token 作为 `Bearer` 头带上即可。工具一样,无需本地安装。 ### ② Web GUI 浏览 @@ -124,7 +126,7 @@ npm run sync -- --once # 收一次(或 `npm run sync` 让它在后台持续 │ ① 采集器 client/sync │ gzip+token│ ② server/server.mjs(HTTP,前面 │ │ 常驻,盯 AI session │ ─────────▶ │ 可套 Caddy 等做 HTTPS) │ │ 按白名单闸门只传圈内 │ │ /ingest → git 真相库 TRUTH_DIR │ -│ │ │ + 每 4h 拉 GitHub 出 code-state │ +│ │ │ + 每 4h 拉代码仓出 code-state │ │ 查询 A: 编辑器里问(MCP) │ 搜+拉,综合 │ /grep /find /read /ls /log │ │ 查询 B: 浏览器开 Web GUI │ ◀──────────│ + 在 / 托管纯静态看板 │ └────────────────────────┘ └──────────────────────────────────┘ @@ -135,8 +137,8 @@ npm run sync -- --once # 收一次(或 `npm run sync` 让它在后台持续 | 你想知道 | 主要长在 | 怎么进记忆库 | | --------------- | ------------------ | -------------------------------- | | **进展 · 思考过程** | CC/Codex session | 蒸馏 + 脱敏后存全文 transcript | -| **代码现状** | GitHub | 不存本体,查询时现拉 + 4h 轮询出 `code-state` | -| **目标 · 决策(人写)** | 团队文档(Lark/飞书 wiki) | 正文单向镜像,可搜可读;改去源文档 | +| **代码现状** | GitHub/GitLab/Gitea(含自建) | 不存本体,查询时现拉 + 4h 轮询出 `code-state` | +| **目标 · 决策(人写)** | 团队文档(飞书/Lark wiki · Notion · Google Docs) | 正文单向镜像,可搜可读;改去源文档 | > **设计取舍:** 只有“真相”(原料 + 元数据)被认真存、保持干净——因为它贵且不可重建;“视图”(查询/索引/看板)随时可换可丢。session 以**蒸馏**形态入库(剥内联图片、截巨型 tool 输出,存信号不存字节),字节精确的原文留在产出者本机(`~/.codex` / `~/.claude`)。 @@ -145,7 +147,8 @@ npm run sync -- --once # 收一次(或 `npm run sync` 让它在后台持续 ## 隐私与安全 - **范围闸门:** 只有 session 的 cwd 在你本机 `upload_folders` 白名单内才上传——圈内默认共享、圈外默认私有。 -- **脱敏:** 上传前在客户端就抹掉密钥/token + 家目录路径,服务器投影 `.md` 和 `/read` 出口再各兜底一次。 +- **脱敏:** 上传前在客户端就抹掉密钥/token + 家目录路径,服务器投影 `.md` 和 `/read` 出口再各兜底一次。每个生产者还能维护一份**个人脱敏词表**(客户名、代号…),上传前先抹掉。 +- **生产者透明可控:** 跑 `brain viewer` 打开本机控制台(127.0.0.1,仅你可见),逐条看每个 session 到底传了什么、跳过了什么——可逐条排除、把已进共享库的**撤回**、或加个人脱敏词。 - **凭证不入库:** 成员 token、GitHub PAT、文档源凭证都住服务器、都 gitignore;花名册(不含密钥)可提交。 - **记忆库就是全部价值——别 push 到任何公开 remote,并定期备份。** 自托管在只对圈内开放的基础设施上。 @@ -171,7 +174,7 @@ npm run sync -- --once # 收一次(或 `npm run sync` 让它在后台持续 4. **凭证放服务器:** 把 `feishu.example.yaml` 复制成 `feishu.yaml`(含密钥 → 已 gitignore),填好 `app_id` / `app_secret`,重启服务。不配则文档层安静关闭。 -5. **验证:** 一个轮询周期后,文档会出现在记忆库 `feishu/<知识库>/…` 下,可用 `grep` 搜到。 +5. **验证:** 一个轮询周期后,文档会出现在记忆库 `feishu/<知识库>__/…` 下,可用 `grep` 搜到。 > 国内飞书(`open.feishu.cn`)与国际版 Lark(`open.larksuite.com`)数据隔离——在你团队实际所在的平台建应用。以后每**新建**一个知识库,都要对它重复第 3 步,否则大脑看不见。 diff --git a/SECURITY.md b/SECURITY.md index ccfafa2..a1d17dd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,7 +9,7 @@ Klear-Team-Brain handles sensitive data: it aggregates redacted full-text transc Instead, report privately via one of: - GitHub's [private vulnerability reporting](https://github.com/Asklear/Klear-Team-Brain/security/advisories/new) (preferred), or -- email the maintainers at **neiyouser2@gmail.com**. +- email the maintainers at **neiyohank@gmail.com**. Please include: a description of the issue, steps to reproduce or a proof of concept, affected versions/components, and any suggested mitigation. diff --git a/cli/brain.mjs b/cli/brain.mjs index ba88faa..37a17ca 100644 --- a/cli/brain.mjs +++ b/cli/brain.mjs @@ -121,7 +121,9 @@ function writeConfig({ server, token, id, name, folders = [], consumer = false, ? `# 未指定工作空间 → 采集本机所有 session(含全部项目)。要收窄改 upload_folders。\ncollect_all: true\nupload_folders: []\n` : `upload_folders:\n${folders.map((f) => ` - ${f}`).join("\n")}\n`) + `exclude: []\n\n` + - `docs: true\ncodex: true\nagentdocs: true\ninterval_sec: 60\ndebounce_sec: 60\n`); + // 采集开关:codex / session_history / Trae 原生记忆默认都开(与 client.config.example.yaml 对齐)。 + // 别再写 docs/agentdocs —— M3 起这两个键没有任何代码读,是历史残留。 + `codex: true\nsession_history_md: true\ntrae_memory: true\ninterval_sec: 60\ndebounce_sec: 60\n`); } // 从本机 CC session 推测"你常在哪些 git 仓干活",作为 upload_folders 建议 @@ -256,7 +258,8 @@ async function joinCmd(code) { const consume = !!inv.consumer || process.argv.includes("--consume-only"); // 1. 可达 + 鉴权(分清三种失败:连不上服务器 / token 无效 / 网络抖动 —— 别一律说成「邀请码过期」误导排查) - const reach = await fetch(srv + "/health").then((r) => r.ok).catch(() => false); + // 任意 HTTP 响应都算「连得上」(含 /health 因真相库降级回的 503)—— 只有网络层抛错才是真不可达。 + const reach = await fetch(srv + "/health").then(() => true).catch(() => false); if (!reach) die(`连不上 ${srv}(服务器没起 / 网络 / 代理?先确认地址能在浏览器打开)`); let who = null, whoErr = ""; try { @@ -299,7 +302,7 @@ async function joinCmd(code) { writeConfig({ server: srv, token, id, name, folders, consumer: consume, collectAll }); console.log(c.ok(`✓ 配置写好 ${CFG}`)); const mcpAttached = addMcp(); - console.log(c.dim("→ 首次回填历史 session(跨境可能要一会,别关)…")); + console.log(c.dim("→ 首次回填历史 session(跨境可能要几分钟,别关)。下面会滚动日志,结束有一句「本机足迹」小结…")); spawnSync(NODE, [join(ROOT, "client", "sync.mjs"), "--once"], { cwd: ROOT, stdio: "inherit" }); serviceInstall({ soft: true }); // 装常驻失败不致命:配置/MCP/回填都已成 —— 再起服务(sync 启动时已能读到完整 config) if (mcpAttached.length) @@ -463,18 +466,31 @@ function status() { } let running = false; if (IS_MAC) { + // print 退出码先分清「没装/没加载」与「装了」:失败=没加载(别再被正则兜成误导的状态字)。 const r = sh("launchctl", ["print", `${gui()}/${LABEL}`]); - const st = (r.stdout.match(/state = (\w+)/) || [, "未装"])[1]; - const pid = (r.stdout.match(/pid = (\d+)/) || [, "-"])[1]; - running = st === "running"; - console.log(`常驻: ${running ? c.ok(`running (pid ${pid})`) : c.warn(st)}`); + if (r.status !== 0) { + console.log(`常驻: ${c.warn("未装 / 未加载")}`); + } else { + const st = (r.stdout.match(/state = (\S+)/) || [, "unknown"])[1]; + const pid = (r.stdout.match(/pid = (\d+)/) || [, null])[1]; + running = st === "running"; + console.log(`常驻: ${running ? c.ok(`running (pid ${pid})`) : c.warn(`已加载但没在跑(state=${st})—— 看 brain logs 找原因`)}`); + } } else { const a = sh("systemctl", ["--user", "is-active", "team-brain-sync"]).stdout.trim(); running = a === "active"; - console.log(`常驻: ${running ? c.ok(a) : c.warn(a || "未装")}`); + console.log(`常驻: ${running ? c.ok(a) : c.warn(a || "未装 / 未加载")}`); } + // 采集足迹(从结果账本):让用户一眼看到「传了多少 / 跳过多少」,不必去 viewer + try { + const led = JSON.parse(readFileSync(join(ROOT, ".brain-ledger.json"), "utf8")); + const ss = led.sessions || []; + const up = ss.filter((s) => s.status === "uploaded").length; + const sk = ss.filter((s) => s.status === "skipped").length; + if (up || sk) console.log(`采集: ${c.ok(`${up} 已传`)}${sk ? c.dim(` · ${sk} 跳过(brain viewer 看逐条原因)`) : ""}`); + } catch {} if (existsSync(LOG)) { - // 取最近一条 tick(logfmt:` INFO tick cc_up=… …`),只显示时间 + 计数体,别再匹配老格式的「·」 + // 一次读,既取最近一条 tick,也扫尾巴有没有失败/报错 —— 别让「同步坏了」时 status 还显得一切正常 const lines = readFileSync(LOG, "utf8").trimEnd().split("\n"); const last = [...lines].reverse().find((l) => / tick /.test(l)); if (last) { @@ -482,6 +498,8 @@ function status() { const body = last.split(" tick ")[1] || last; console.log(`最近同步: ${c.dim((ts.slice(11, 19) + " " + body).trim())}`); } + const fails = lines.slice(-300).filter((l) => /上传失败|tick 异常|出错|ERROR/.test(l)).length; + if (fails) console.log(c.warn(`⚠ 最近日志里有 ${fails} 条失败/报错 —— brain logs -f 看详情`)); } try { const vi = JSON.parse(readFileSync(join(ROOT, ".brain-viewer.json"), "utf8")); if (vi.url) console.log(`查看器: ${c.ok(vi.url)} ${c.dim("(brain viewer 打开)")}`); } catch {} if (!running && existsSync(CFG)) console.log(c.dim("\n起常驻:brain service install")); diff --git a/client.config.example.yaml b/client.config.example.yaml index 493537c..9cee487 100644 --- a/client.config.example.yaml +++ b/client.config.example.yaml @@ -23,6 +23,7 @@ trae_memory: true # 是否采集 Trae 原生 ~/.trae-cn/memory/projects/<工 # - -Users-you-Documents-Code-your-team-stuff # 也可直接填 slug;必须能对应某个 upload_folders interval_sec: 60 # 多久扫一次 debounce_sec: 60 # session 空闲多少秒才算"稳定"、才上传 +# auto_update: false # 默认开:常驻每天自检 /version,比本机高就拉新客户端代码并以新代码重启。设 false 关掉自动更新。 # 注:M3 起只采集 session。文档迁飞书(不再有 docs/agentdocs/vault 配置); # space 身份由服务器按 registry 定(github__owner__repo 命中登记的 / 否则 local__ + folder 标签)。 diff --git a/docs/CHANGELOG.en.md b/docs/CHANGELOG.en.md index a8ba9ba..25b8a16 100644 --- a/docs/CHANGELOG.en.md +++ b/docs/CHANGELOG.en.md @@ -4,10 +4,42 @@ English | [中文](./CHANGELOG.zh-CN.md) This project follows [Semantic Versioning](https://semver.org/), formatted after [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Check your local version with `brain version`. -> Still pre-1.0 and early, incrementing by patch. Formal versioning began at `v0.1.11`; earlier versions are a retroactive split of the development history by milestone. +> Still pre-1.0 and early, incrementing by patch. Formal versioning began at `v0.1.11`; earlier versions are a retroactive split of the development history by milestone. `v0.1.12`–`v0.1.13` were internal-only and never released publicly. --- +## [0.1.22] - 2026-06-25 · Hardened read-only queries + observable status + +### Added +- **`brain status` shows your capture footprint and warns on failures**: it reads the result ledger to show what's been uploaded vs skipped, and scans the log tail for errors, so a broken sync no longer looks healthy. It also tells "not installed / not loaded" apart from "loaded but not running" via launchctl exit codes instead of guessing from a regex. +- **`/health` is now a real readiness check**: verifies the truth store exists, is a git repo, and is writable; degrades to `503` + version instead of a blind `200`. +- Configurable active-branch window for code-state via `CODESTATE_ACTIVE_DAYS` (default 30). + +### Changed +- `brain join` first-run backfill now streams a rolling log and ends with a local-footprint summary. +- Overview / repo / code-state wording for un-pushed progress is now the neutral "activity since last push". + +### Fixed +- **Invalid regex in `grep` returns a clear error** instead of being swallowed as "no matches". +- **`GITHUB_TOKEN_FILE` set but unreadable now warns at startup** instead of silently running token-less. +- **Large-file guard on `/read`**: oversized full reads are refused; reads without a limit default to a 5000-line truncation with a pagination hint. +- `brain join` reachability probe decoupled from `/health` `503`: any HTTP response now counts as "reachable". + +## [0.1.21] - 2026-06-25 · Footprint viewer polish + English activity log + +### Fixed +- Local footprint viewer: sidebar footer is now pinned full-height (previously only on the overview page), and the Activity log is fully in English. + +## [0.1.20] - 2026-06-25 · Login-free viewer + device token + +### Changed +- **The local footprint viewer no longer asks for a token to log in** (accessing your own machine from your own machine needs no password); it shows your device token directly instead, and the hosted console gained a "Local console" entry that jumps straight to your on-machine viewer. + +## [0.1.19] - 2026-06-25 · Producer footprint viewer + +### Added +- **Producer footprint viewer**: every producer can open a local web page to see exactly which of their sessions were captured and what was uploaded, exclude or retract individual items already in the memory, and maintain a personal redaction wordlist (custom sensitive terms masked before upload) — self-service transparency over what leaves your machine. The installer now bundles `web/viewer.*` (which previously 404'd on real installs), with zero new client dependencies. + ## [0.1.18] - 2026-06-25 · Mount the memory from any agent (HTTP-transport MCP) ### Added diff --git a/docs/CHANGELOG.zh-CN.md b/docs/CHANGELOG.zh-CN.md index 9571424..59ad4dd 100644 --- a/docs/CHANGELOG.zh-CN.md +++ b/docs/CHANGELOG.zh-CN.md @@ -4,10 +4,42 @@ 本项目版本号遵循[语义化版本](https://semver.org/lang/zh-CN/),格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。本机版本可用 `brain version` 查看。 -> 项目仍处于 1.0 前的早期阶段,按 patch 递增。版本管理在 `v0.1.11` 正式引入,更早的版本是按里程碑对历史开发的回溯切分。 +> 项目仍处于 1.0 前的早期阶段,按 patch 递增。版本管理在 `v0.1.11` 正式引入,更早的版本是按里程碑对历史开发的回溯切分。`v0.1.12`–`v0.1.13` 仅内部使用,未公开发布。 --- +## [0.1.22] - 2026-06-25 · 只读查询加固 + status 看得见采集足迹 + +### 新增 +- **`brain status` 显示采集足迹、同步坏了会告警**:从结果账本展示「已传/跳过」足迹,并扫日志尾巴对失败/报错告警,同步坏了不再显得一切正常;用 launchctl 退出码区分「未装/未加载」与「已加载但没跑」,不再被正则兜成误导状态。 +- **`/health` 改为真·就绪检查**:校验真相库存在、是 git 仓、可写;不就绪降级回 `503` + version,不再盲目 `200`。 +- code-state 活跃分支窗口可配 `CODESTATE_ACTIVE_DAYS`(默认 30)。 + +### 改进 +- `brain join` 首次回填改为滚动日志、结尾给「本机足迹」小结。 +- 概览 / 仓库 / code-state 的「未推进度」措辞改为中性的「push 后有活动」。 + +### 修复 +- **`grep` 非法正则回清晰报错**,不再吞成「无命中」。 +- **`GITHUB_TOKEN_FILE` 配了却读不到 → 启动告警**,不再静默以 token-less 跑。 +- **`/read` 大文件护栏**:超大整读拒绝;无 limit 时默认 5000 行截断带分页提示。 +- `brain join` 可达探测与 `/health` `503` 解耦:任意 HTTP 响应即「连得上」。 + +## [0.1.21] - 2026-06-25 · 查看器打磨 + 日志英文化 + +### 修复 +- 本机足迹查看器:侧边栏页脚固定满高(不再只在 overview 页显示),Activity 日志全部英文化。 + +## [0.1.20] - 2026-06-25 · 查看器免登录 + 设备 token + +### 改进 +- **本机足迹查看器不再要求 token 登录**(本机访问本机无需口令),改为直接展示设备 token;线上控制台新增「本机控制台」入口,一键跳到本机查看器。 + +## [0.1.19] - 2026-06-25 · 生产者本机足迹查看器 + +### 新增 +- **生产者本机足迹查看器**:每个产出者都能打开本机网页,查看自己究竟有哪些 session 被采集、上传了哪些足迹,逐条排除或撤回已进记忆库的内容,并维护个人脱敏词表(自定义敏感词在上传前抹掉)——对「什么离开了你的机器」做自助式透明管控。装机包现已补带 `web/viewer.*`(此前真实安装会 404),且客户端零新增依赖。 + ## [0.1.18] - 2026-06-25 · 任意 agent 远程挂载(HTTP 传输 MCP) ### 新增 diff --git a/package.json b/package.json index 286edf4..02874ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "klear-team-brain", - "version": "0.1.21", + "version": "0.1.22", "type": "module", "description": "Self-hosted team memory for AI coding agents — fuses Claude Code/Codex sessions, GitHub code state, and team docs into one git truth store, queryable from your editor over MCP.", "license": "Apache-2.0", diff --git a/registry.example.yaml b/registry.example.yaml index 3b5a8de..bb0b728 100644 --- a/registry.example.yaml +++ b/registry.example.yaml @@ -1,4 +1,4 @@ -# registry —— 登记的代码托管 org/repo + 各自的 PAT/token,决定"有哪些团队 space"(REDESIGN §3.4)。 +# registry —— 登记的代码托管 org/repo + 各自的 PAT/token,决定"有哪些团队 space"(space 模型见 README)。 # 支持三家:github(顶层,单实例 github.com)/ gitlab / gitea(自建可多实例,见下)。 # 复制成 registry.yaml 填真实值。registry.yaml 含 PAT → 已 gitignore,别提交(住服务器,跟 tokens.yaml 一样)。 # 服务器启动加载、改完 systemctl restart 生效。可用 `brain admin org/repo add ... --pat ` 维护。 diff --git a/server/codestate.mjs b/server/codestate.mjs index 7b001e8..794c6e6 100644 --- a/server/codestate.mjs +++ b/server/codestate.mjs @@ -12,6 +12,10 @@ import { writeSpaceMeta, readSpaceMeta } from "./space.mjs"; const REPO_PREFIXES = ["github__", "gitlab__", "gitea__"]; const isRepoKey = (k) => REPO_PREFIXES.some((p) => k.startsWith(p)); +// 「活跃分支」窗口:最后 push 超过这么多天的分支不进 code-state。默认 30 天,长命名 release 分支会被滤掉, +// 用 CODESTATE_ACTIVE_DAYS 调大(如 365)即可纳入。 +const ACTIVE_DAYS = Number(process.env.CODESTATE_ACTIVE_DAYS) || 30; + // 读不到该仓时,写一张显式告警 code-state,让管理员在 read_github / 搜索里一眼看到该补 token/权限。 function renderNoAccess(spaceKey, provider, ref, status) { const why = status === 404 ? "仓库不存在,或该 token 无权访问它(私有仓未纳入授权)" @@ -52,7 +56,7 @@ function render(spaceKey, ref, rows, pulls, prNote = "") { const lines = [`# code-state · ${spaceKey}`, `repo: ${ref}`, ``, `## 活跃分支`]; for (const r of rows) { lines.push(`- **${r.name}** — \`${r.sha}\` ${r.msg} (${(r.when || "").slice(0, 16)})` + - (r.leads ? ` ⚠️ **有未推进度**(session ${r.sess.slice(0, 16)} 晚于最后 push,见 sessions/` + (r.leads ? ` · **push 后有活动**(session ${r.sess.slice(0, 16)} 晚于最后 push,见 sessions/` + `${r.name.replace(/\//g, "-")}/)` : "")); } lines.push(``, `## Open PR/MR`); @@ -64,7 +68,7 @@ function render(spaceKey, ref, rows, pulls, prNote = "") { } // 刷新单个团队仓 space:从 space.yaml 取 provider/owner/repo + registry 取 token/baseUrl,按 provider 分发客户端。 -export async function refreshSpace(truthDir, spaceKey, registry, fallbackToken, { activeDays = 30 } = {}) { +export async function refreshSpace(truthDir, spaceKey, registry, fallbackToken, { activeDays = ACTIVE_DAYS } = {}) { const spaceDir = join(truthDir, "spaces", spaceKey); const meta = readSpaceMeta(truthDir, spaceKey); const { provider, owner, repo, ref } = meta; diff --git a/server/query.mjs b/server/query.mjs index 0da9093..435793e 100644 --- a/server/query.mjs +++ b/server/query.mjs @@ -26,7 +26,10 @@ async function runGrep(cmd, args, strip) { return { matches: lines.slice(0, MAX_LINES).join("\n").trimEnd(), truncated: lines.length > MAX_LINES }; } catch (e) { if (e.code === 1) return { matches: "", truncated: false }; // 退出码 1 = 无命中(非错误) - throw e; + // 退出码 ≥2 = grep 自己报错(最常见:正则非法,如未配对括号/悬挂反斜杠)。别把它吞成「无命中」 + // 也别让 raw execa 错冒成 500 —— 把 stderr 首行清洗后抛出,端点据此回 400,agent 才知道是正则写错了。 + const detail = (e.stderr || e.message || "").split("\n").map((l) => l.trim()).find(Boolean) || "grep failed"; + throw new Error(`grep 失败(多半是正则非法,检查模式):${detail.replace(/^fatal:\s*/i, "")}`); } } diff --git a/server/server.mjs b/server/server.mjs index a1e3b96..212e7b9 100644 --- a/server/server.mjs +++ b/server/server.mjs @@ -4,7 +4,7 @@ import http from "node:http"; import { fileURLToPath } from "node:url"; import { dirname, join, extname } from "node:path"; -import { readFileSync, existsSync, createReadStream, statSync, writeFileSync, mkdtempSync, rmSync } from "node:fs"; +import { readFileSync, existsSync, createReadStream, statSync, writeFileSync, mkdtempSync, rmSync, accessSync, constants } from "node:fs"; import { tmpdir } from "node:os"; import { execFileSync, execFile } from "node:child_process"; import { promisify } from "node:util"; @@ -39,6 +39,8 @@ const PORT = Number(process.env.PORT) || 8787; // 容器/反代在别的网段时用 HOST=0.0.0.0 显式放开(此时务必靠防火墙/安全组封住该端口)。 const HOST = process.env.HOST || "127.0.0.1"; const MAX_BODY = 64 * 1024 * 1024; +const READ_HARD_MAX = 24 * 1024 * 1024; // /read 整读护栏:超此值又没带 offset/limit 就拒,免得把巨文件整个读进内存 +const READ_MAX_LINES = 5000; // /read 没带 limit 时的默认行上限:超了截断并告知总行数,免撑爆 agent 上下文 // 安全响应头:挂在所有响应上。CSP 锁死外部加载面(脚本只许自身、无 inline)——给渲染不可信内容的 GUI 兜底。 // style 仍放行 inline(GUI 大量用 style="");img 放行 data:(favicon 是内联 svg)。 const SEC_HEADERS = { @@ -61,9 +63,15 @@ const WEB_TYPES = { const roster = loadRoster(process.env.TEAM_FILE || join(ROOT, "team.yaml")); // 默认 ROOT/team.yaml;docker 用 TEAM_FILE 指到持久卷 const tokens = tokenIndex(roster, loadTokens(process.env.TOKENS_FILE || join(ROOT, "tokens.yaml"))); const registry = loadRegistry(process.env.REGISTRY_FILE || join(ROOT, "registry.yaml")); // 登记的 github org/repo(启动加载、restart 生效) -const GITHUB_TOKEN = process.env.GITHUB_TOKEN - || (process.env.GITHUB_TOKEN_FILE && existsSync(process.env.GITHUB_TOKEN_FILE) - ? readFileSync(process.env.GITHUB_TOKEN_FILE, "utf8").trim() : ""); +// GITHUB_TOKEN:直给优先;否则读 GITHUB_TOKEN_FILE。文件配了却读不到不能静默吞成空—— +// 否则后面 read_github / code-state 会以「无 token」跑、报「无权限」误导排查,启动就警一声。 +const GITHUB_TOKEN = (() => { + if (process.env.GITHUB_TOKEN) return process.env.GITHUB_TOKEN; + const f = process.env.GITHUB_TOKEN_FILE; + if (!f) return ""; + try { return readFileSync(f, "utf8").trim(); } + catch (e) { log.warn("GITHUB_TOKEN_FILE 配了但读不到 → 将以「无 GitHub token」运行(read_github/code-state 受影响)", { file: f, err: e.message }); return ""; } +})(); const FEISHU = loadFeishu(process.env.FEISHU_FILE || join(ROOT, "feishu.yaml")); // 文档层(飞书)凭证:缺则不启用 const NOTION = loadNotion(process.env.NOTION_FILE || join(ROOT, "notion.yaml")); // 文档层(Notion)凭证:缺则不启用 const GOOGLE = loadGoogle(process.env.GOOGLE_FILE || join(ROOT, "google.yaml")); // 文档层(Google Docs)凭证:缺则不启用 @@ -258,7 +266,16 @@ const server = http.createServer((req, res) => { async function handle(req, res, u) { // --- 健康检查 --- - if (req.method === "GET" && u.pathname === "/health") return json(res, 200, { ok: true }); + // 深一点:光回 200 没意义——真相库不存在 / 不是 git 仓 / 不可写,都该让监控(和 brain join 的可达探测)看见。 + // 全是 statSync/accessSync 级廉价检查,不扫库,安全频繁打。 + if (req.method === "GET" && u.pathname === "/health") { + const truth = { dir: false, git: false, writable: false }; + try { truth.dir = existsSync(TRUTH) && statSync(TRUTH).isDirectory(); } catch {} + try { truth.git = existsSync(join(TRUTH, ".git")); } catch {} + try { accessSync(TRUTH, constants.W_OK); truth.writable = true; } catch {} + const ok = truth.dir && truth.git && truth.writable; + return json(res, ok ? 200 : 503, { ok, version: CLIENT_VERSION, truth }); + } // --- 最新客户端版本(无需鉴权,版本号非机密):采集器每天自检,比本机高就自动更新 --- if (req.method === "GET" && u.pathname === "/version") return json(res, 200, { version: CLIENT_VERSION }); @@ -355,11 +372,19 @@ async function handle(req, res, u) { let abs; try { abs = safeRelPath(TRUTH, path, "path"); } catch { return json(res, 400, { error: "bad path" }); } if (!existsSync(abs)) return json(res, 404, { error: "not found" }); - if (statSync(abs).isDirectory()) return json(res, 400, { error: "是目录,请用 ls" }); - let text = redactReadable(readFileSync(abs, "utf8")); + const st = statSync(abs); + if (st.isDirectory()) return json(res, 400, { error: "是目录,请用 ls" }); const offset = Math.max(0, Number(u.searchParams.get("offset")) || 0); const limit = Number(u.searchParams.get("limit")) || 0; - if (offset || limit) text = text.split("\n").slice(offset, limit ? offset + limit : undefined).join("\n"); + // 大文件护栏:没带 offset/limit 又超硬上限 → 别把整文件读进内存,回提示让 agent 分页或先 grep/find 定位。 + if (!offset && !limit && st.size > READ_HARD_MAX) + return json(res, 413, { error: `文件太大(${(st.size / 1048576).toFixed(1)}MB):带 offset/limit 分页读,或先用 grep/find 定位再 read 具体段。`, size: st.size }); + let text = redactReadable(readFileSync(abs, "utf8")); + if (offset || limit) { text = text.split("\n").slice(offset, limit ? offset + limit : undefined).join("\n"); return json(res, 200, { path, text }); } + // 没带 limit 但行数巨多 → 默认截断并告知总行数,免得一次回几万行撑爆 agent 上下文(带 offset 接着读)。 + const lines = text.split("\n"); + if (lines.length > READ_MAX_LINES) + return json(res, 200, { path, text: lines.slice(0, READ_MAX_LINES).join("\n"), truncated: true, total_lines: lines.length, hint: `仅回前 ${READ_MAX_LINES} 行;要后面的加 offset=${READ_MAX_LINES}(可继续递增)` }); return json(res, 200, { path, text }); } diff --git a/team.example.yaml b/team.example.yaml new file mode 100644 index 0000000..4c9a489 --- /dev/null +++ b/team.example.yaml @@ -0,0 +1,15 @@ +# 团队花名册(公开可提交,不含密钥)。复制成 team.yaml 填真实成员。 +# 身份归一:把每个人本机 git user.name 的各种写法都列进 git_names, +# 大脑里就能把 session 归到同一个 canonical 成员(作者显示、按人检索靠它)。 +# 每人的接入 token 另放 tokens.yaml(机密、已 gitignore),key = 这里的 id。 +# 加人也可不手写:管理员机器上 `brain admin add --name <显示名> [--email][--git-name]`(经 SSH 落到服务器)。 +members: + - id: alice # 唯一短 id(= tokens.yaml 的 key、邀请码里的 id) + name: Alice Chen # 显示名 + email: alice@example.com # 可选 + git_names: # 本机 git user.name 的各种写法(可留空) + - alice + - Alice Chen + - id: bob + name: Bob + git_names: [bob] diff --git a/web/app.js b/web/app.js index 67599b5..cdb5bfa 100644 --- a/web/app.js +++ b/web/app.js @@ -68,7 +68,7 @@ const I18N = { "stat.attention": "Needs attention", "ov.recentActivity": "Recent activity", "ov.allSessions": "All sessions →", - "ov.attentionHead": "⚠️ Needs attention · unpushed", + "ov.attentionHead": "Active after last push", "ov.allRepos": "All repos →", "ov.recentDocs": "Recent docs", "ov.feishuDocs": "Docs →", @@ -77,8 +77,8 @@ const I18N = { "load.docs": "Loading docs", "ov.noActivity": "No activity yet", "ov.noActiveRepos": "No active repos.", - "ov.branchesUnpushed": (n) => `${n} branches unpushed`, - "ov.allPushed": "All pushed ✓", + "ov.branchesUnpushed": (n) => `${n} active after push`, + "ov.allPushed": "Nothing newer than last push ✓", "ov.noMirror": "No doc mirror yet.", // 仓库 "load.repos": "Loading repos", @@ -87,7 +87,7 @@ const I18N = { "repos.active": (n) => `Active · ${n}`, "repos.noActive": "No active repos", "repos.registered": (n) => `Registered only · ${n}`, - "repos.unpushedBadge": (n) => `${n} unpushed`, + "repos.unpushedBadge": (n) => `${n} after push`, "card.registeredOnly": "Registered only", "card.meta": (n, agoStr, people) => `Active · ${n} sessions · ${agoStr}${people > 1 ? ` · ${people} people` : ""}`, // code-state @@ -95,8 +95,8 @@ const I18N = { "cs.noPR": "No open PRs.", "cs.activeBranches": "Active branches", "cs.openPR": "Open PRs", - "cs.unpushed": "unpushed", - "cs.unpushedTitle": "Local session is newer than the last push", + "cs.unpushed": "after push", + "cs.unpushedTitle": "A session on this branch is newer than its last push — post-push activity, not necessarily unpushed commits", "cs.none": "No code-state for this repo yet (registered only, no sessions yet, or awaiting the first 4h poll).", // 单仓 "load.repo": "Loading repo", @@ -271,7 +271,7 @@ const I18N = { "stat.attention": "待关注", "ov.recentActivity": "最近活动", "ov.allSessions": "全部会话 →", - "ov.attentionHead": "⚠️ 待关注 · 未推进度", + "ov.attentionHead": "push 后有活动", "ov.allRepos": "全部仓库 →", "ov.recentDocs": "最近文档", "ov.feishuDocs": "文档 →", @@ -280,8 +280,8 @@ const I18N = { "load.docs": "加载文档", "ov.noActivity": "暂无活动", "ov.noActiveRepos": "没有活跃仓库。", - "ov.branchesUnpushed": (n) => `${n} 分支未推`, - "ov.allPushed": "全部已推送 ✓", + "ov.branchesUnpushed": (n) => `${n} 个分支 push 后有活动`, + "ov.allPushed": "无晚于 push 的活动 ✓", "ov.noMirror": "还没有文档镜像。", "load.repos": "加载仓库", "repos.title": "GitHub 仓库", @@ -289,15 +289,15 @@ const I18N = { "repos.active": (n) => `活跃 · ${n}`, "repos.noActive": "暂无活跃仓库", "repos.registered": (n) => `仅登记 · ${n}`, - "repos.unpushedBadge": (n) => `${n} 未推`, + "repos.unpushedBadge": (n) => `${n} 晚于推送`, "card.registeredOnly": "仅登记", "card.meta": (n, agoStr, people) => `活跃 · ${n} 会话 · ${agoStr}${people > 1 ? ` · ${people} 人` : ""}`, "cs.noBranches": "无活跃分支(30 天内)。", "cs.noPR": "无 Open PR。", "cs.activeBranches": "活跃分支", "cs.openPR": "Open PR", - "cs.unpushed": "未推进度", - "cs.unpushedTitle": "本地 session 比最后一次 push 新", + "cs.unpushed": "晚于推送", + "cs.unpushedTitle": "本分支有比最后一次 push 更晚的 session —— push 后的活动,不一定有未推 commit", "cs.none": "该仓尚无 code-state(仅登记、还没 session,或等首次 4h 轮询)。", "load.repo": "加载仓库", "repo.defaultBranch": "默认分支", diff --git a/web/viewer.js b/web/viewer.js index f8dcb05..5cfdb0a 100644 --- a/web/viewer.js +++ b/web/viewer.js @@ -18,7 +18,7 @@ const T = { "foot.sync": ["synced {t}", "同步 {t}"], "foot.noSync": ["no sync yet", "尚无同步"], "who": ["{name}'s local footprint", "{name} 的本机足迹"], "ov.title": ["Overview", "概览"], - "ov.sub": ["What this machine has published to the team's shared library — at a glance, retractable anytime.", "team-brain 从这台机器往全队共享库发布了什么 —— 一眼看清,随时可撤。"], + "ov.sub": ["What this machine has published to the team's shared library — at a glance, retractable anytime.", "Klear-Team-Brain 从这台机器往全队共享库发布了什么 —— 一眼看清,随时可撤。"], "ov.what": ["This page lives only on your machine (127.0.0.1) and only you can see it. It governs what the collector uploads from this computer to the team's shared library. Preview and exclude happen locally; only Retract reaches the shared library to delete an entry.", "这个页面只在你本机(127.0.0.1)、只有你能看。它管的是「采集常驻从这台电脑上传到全队共享库的内容」。预览、排除都在本地发生;点「撤回」才会去线上库删掉对应那条。"], "ov.recent": ["Recent activity", "最近活动"],