wezard 1.4.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +12 -7
- package/README.md +121 -67
- package/dist/daemon/approval.js +9 -3
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/cc-bridge.js.map +1 -1
- package/dist/daemon/chat-name.js +8 -18
- package/dist/daemon/chat-name.js.map +1 -1
- package/dist/daemon/detail.js +53 -12
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/graph.js +5 -7
- package/dist/daemon/graph.js.map +1 -1
- package/dist/daemon/inbound.js +135 -158
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +381 -192
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +2 -2
- package/dist/daemon/mirror-bridge.js +221 -134
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/mirror-store.js.map +1 -1
- package/dist/daemon/model-select.js +74 -0
- package/dist/daemon/model-select.js.map +1 -0
- package/dist/daemon/peers.js +10 -16
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +27 -4
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/task-gate.js +97 -0
- package/dist/daemon/task-gate.js.map +1 -0
- package/dist/daemon/task-registry.js +132 -0
- package/dist/daemon/task-registry.js.map +1 -0
- package/dist/daemon/tasks.js +126 -54
- package/dist/daemon/tasks.js.map +1 -1
- package/dist/daemon/wizard-memory.js +57 -0
- package/dist/daemon/wizard-memory.js.map +1 -0
- package/dist/daemon/wizard.js +84 -38
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +138 -76
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/chat-http.js +185 -108
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +26 -34
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/chat-view.js +14 -2
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/config.js +23 -4
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/detail-render.js +12 -6
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/role-render.js +52 -0
- package/dist/shared/role-render.js.map +1 -0
- package/dist/shared/role-view.js +210 -0
- package/dist/shared/role-view.js.map +1 -0
- package/dist/shared/schedule-spec.js +92 -116
- package/dist/shared/schedule-spec.js.map +1 -1
- package/dist/shared/session-label.js +103 -72
- package/dist/shared/session-label.js.map +1 -1
- package/dist/shared/std.js +3 -0
- package/dist/shared/std.js.map +1 -1
- package/dist/shared/task-file.js +120 -0
- package/dist/shared/task-file.js.map +1 -0
- package/dist/shared/trigger.js +232 -0
- package/dist/shared/trigger.js.map +1 -0
- package/dist/shared/world.js +7 -5
- package/dist/shared/world.js.map +1 -1
- package/dist/svr/index.js +67 -2
- package/dist/svr/index.js.map +1 -1
- package/package.json +1 -1
- package/web/chat.css +407 -212
- package/web/chat.js +932 -321
package/CLAUDE.md
CHANGED
|
@@ -54,15 +54,20 @@ Three processes, all coordinating via 127.0.0.1:17890 and `~/.wezard/`:
|
|
|
54
54
|
- `approval.ts` builds the button card, parks an HTTP request via `pending.ts`, and resolves it on `event.template_card_event`. Three click outcomes: `allow` / `allow_window` (auto-allow N min for that session) / `deny`. `session-cache.ts` short-circuits repeats.
|
|
55
55
|
- `inbound.ts` routes `message.text` / `message.image` / `message.mixed` → either headless `cc-bridge.ts` or `mirror-bridge.ts`. Gates on `allowFrom`; one-shot exception is the bootstrap claim (see below).
|
|
56
56
|
- `cc-bridge.ts` (mode=headless) spawns `claude -p --output-format stream-json --verbose`, parses `assistant`/`stream_event` lines, and pumps text via `client.replyStream`. Per-principal queue serializes turns; session id is captured from the `system/init` line and persisted to `~/.wezard/sessions.json` for `--resume`.
|
|
57
|
-
- `mirror-bridge.ts` (mode=mirror) tails `<projectsDir>/<encoded(cwd)>/<sid>.jsonl` and pushes new `assistant` content to a chat — but only for turns the chat itself started: a turn opened by a human's CLI keystroke is recorded to the turn store (
|
|
57
|
+
- `mirror-bridge.ts` (mode=mirror) tails `<projectsDir>/<encoded(cwd)>/<sid>.jsonl` and pushes new `assistant` content to a chat — but only for turns the chat itself started: a turn opened by a human's CLI keystroke is recorded to the turn store (rolepage) and kept out of the chat (`wrc.mirror.chatOriginOnly`, brief mode only); approval/askq cards and `[mirror]` notices bypass that gate. Inbound injection is either `claude --resume <sid> -p` (writes a turn into the same jsonl) or `tmux paste-buffer` against a live TTY (only path that makes WeCom messages visible in the user's CLI window). `recentInjects` ring buffer suppresses the WeCom→CLI echo when tailing (which is why an injected line's text is stamped as the turn's `userQuery` at inject time — the tail never sees it). **Every turn has a channel** (`TurnDetailRecord.channel`): the base of the public chat it was spoken in and where its reply goes — a human can `.name` a wizard homed in another chat and the reply (text and cards) returns to the chat that asked, not the wizard's home; `""` = a private wizard↔wizard turn whose reply is pushed nowhere and lives only on the rolepage.
|
|
58
58
|
- `claim.ts` is a single-shot bootstrap: an exact-match magic phrase from any sender bypasses `allowFrom`, writes that principal as `defaultChat`, and appends to `allowFrom`. This is the **only** legitimate ingress for first-time setup.
|
|
59
|
-
- `peers.ts` is the read side of multi-agent awareness — "who else is working in this chat, and are they busy".
|
|
60
|
-
- `wizard.ts` is the identity layer over those sessions: a target key plus a name (the chat's name
|
|
59
|
+
- `peers.ts` is the read side of multi-agent awareness — "who else is working in this chat, and are they busy". A wizard's HOME chat is its target key's base principal (`baseOfKey`): the untagged default plus every slot `chat:xxx#slot`. Pure parsing only — bounded transcript-tail reads (`tailTurns` / `lastAssistantText` / `summarizeTail`, shared with `session-scan.ts`) and pane classification (`paneIsBusy`). Identity (name / job / memory / lineage) lives in `wizard.ts`, not here. **Busy detection matches the TUI spinner footer** (`… (12m 31s · …)` on current versions, `esc to interrupt` on older ones) in the last 8 non-blank rows — transcript mtime is useless here, it goes quiet during long tool calls.
|
|
60
|
+
- `wizard.ts` is the identity layer over those sessions: a target key plus a **globally unique name** — the wizard's only address, written `.name` everywhere a human or model sees it (a chat's default session takes the chat's name, a slot takes its slot id; collisions get `-N` via `pickName`; `settleName` claims lazily, `settleAll` migrates at boot, default sessions first). The slot id inside the key is NOT the name: a rename never rekeys a session. Plus a one-line job description, a long-term `memory` list, and a `parent` link that makes a session a CLONE of another. The registry is a write-through json (`~/.wezard/wizards.json`, `wrc.mirror.wizardsFile`) bound process-wide via `bindWizardStore` so the IM side can read it too; everything else in the module is a pure renderer. **Identity travels as a system prompt, never as a message**: `renderCharter` builds the wizard's charter (who it is, its workspace, its clones, its memory, the chat's and the workspace's shared memory (`wizard-memory.ts`: `~/.wezard/memory/{chats,workspaces}/*.md`, human-editable, appended by `wizard_remember({scope})`), the tool roster, the orchestration playbook, the public-vs-private etiquette) and `spawn-tmux` presses it in with `--append-system-prompt "$(cat <file>)"` — so it survives `/clear`, costs no turn, and never shows in chat. Every spawn path (`/new`, a dead-pane respawn, a clone) pulls it from `MirrorBridge.setCharterProvider`, installed in `index.ts`. Backends with no such flag (codebuddy) spawn without a charter rather than with a rejected flag.
|
|
61
61
|
- **Cloning is a context fork**: `MirrorBridge.cloneSession` spawns `--resume <parent sid> --fork-session`, so the child starts holding everything the parent had read while the parent is untouched — that is what makes "load the shared material once, then fan out" work. Two invariants: `--fork-session` is mandatory (without it a second pane appends to the parent's jsonl and two chats cross-wire), and the forked transcript only appears when that pane takes its FIRST message — so the clone's opening line is the trigger, and the attach waits for the fork; if it never appears the pane is killed rather than bound to the parent's sid.
|
|
62
|
+
- **Chats are public channels, wizard↔wizard is private by default.** `send_peer` injects with `channel:""` and posts no bubble; `public:true` (the model decides) posts `emoji .a → .b` into the sender's current channel and the reply follows there. No spawn/relay bubbles remain — only job open/close and `notify`.
|
|
62
63
|
- `jobs.ts` is the ledger for one fan-out: which wizards belong to a job, which of them were spawned *for* it (only those get recycled on `close_job` — a long-lived wizard pulled in to help is not), and the two bubbles the chat sees. It holds no control flow on purpose: that stays in the initiating wizard's context, so a reload costs one retry instead of a resurrected state machine. Closed rows older than 24h are dropped on the next write.
|
|
63
64
|
- `notices.ts` is the other half of awareness. A charter's roster is a spawn-time snapshot and only gets staler; a variation (a birth, a `stop_wizard`, a changed job description, a chat getting named) posts one line into each affected wizard's in-memory box, and the next text injected into that pane — from a human, a peer or a scheduled task — carries it along as a `<system-reminder>`. No extra turn, no bubble, no transcript. Delivery clears the box: a notice is a hint, the truth is always `wizard_roster`.
|
|
64
|
-
- `chat-name.ts` turns a chat's unreadable `chat:wr…` id into a
|
|
65
|
-
- `tasks.ts` fires a scheduled prompt into a target wizard's session — exactly equivalent to someone saying it in the chat at that moment. Lives in the daemon, not in a session, so a CLI restart / `/clear` / session end can't carry it away
|
|
65
|
+
- `chat-name.ts` turns a chat's unreadable `chat:wr…` id into a readable name (which its default wizard inherits), and auto-fills one from the workspace basename when nobody named it. The fill happens wherever a name is about to be handed to a model — including `renderCharter`, since a system prompt outlives the process that read it. Fill-only; a human-chosen name is never overwritten.
|
|
66
|
+
- `tasks.ts` fires a scheduled prompt into a target wizard's session — exactly equivalent to someone saying it in the chat at that moment. Lives in the daemon, not in a session, so a CLI restart / `/clear` / session end can't carry it away. It runs one fixed skeleton per shot — `trigger → gate → render → dispatch` — and nothing else: the *specifics* of each stage live in the task's own file, because a schedule is "run this logic at this time" and logic does not fit in json.
|
|
67
|
+
- A task is `~/.wezard/tasks/<id>.task.mjs`, an ES module exporting `when` / `gate` / `prompt` / `target`+`fresh` / `owner` (the wizard that scheduled it — its rolepage shows the task under 日程) — **the config is source the wizard edits directly** (hot-reloaded by mtime; a file that fails to load keeps its previous version running and surfaces the reason in `list_tasks`). Source stays source: `lastFired` and the gate's cross-round memo live in `~/.wezard/task-state.json`, so the daemon never writes back over a file a wizard just edited.
|
|
68
|
+
- `shared/trigger.ts` is the trigger AST (Specification + Interpreter). **Clock vs Guard is the whole point**: `every`/`daily`/`at` produce instants and answer "when next", `between`/`onDays`/`not` only veto. `and(clock, …guards)` is their intersection — that's what makes "白天每小时" expressible, and why `nextFire` enumerates clock candidates and picks the first one every guard passes. An AST, not a closure, because it must be renderable (`每 1 小时 · 08:00-21:00`), storable and drawable. `shared/schedule-spec.ts` under it is pure lexer (weekday / time-of-day / interval / window) — it grows with how people speak, the AST grows with how things are computed.
|
|
69
|
+
- `gate` (`task-gate.ts`) runs **in a child process with a timeout**: it is user/wizard-written code that shells out, and a wedged gate must not stall every other schedule — timeout, throw and crash are all the same verdict, *hold fire this round*. Returning `false` costs nothing and says nothing; that is how "only if there's work" stops spending a pane per empty round. A gate **error** posts one bubble (only on the good→bad transition) — a broken gate's silence is indistinguishable from "nothing to do today".
|
|
70
|
+
- **rolepage** (`shared/role-view.ts` pure view model, `shared/role-render.ts`, served by `shared/chat-http.ts` as `/role?id=…&role=…`, legacy `/chat` = same page; frontend `web/chat.js`): one role's IM — every group chat and single chat (human or wizard DM) it took part in. Each turn folds into two messages (in: speaker/peer/task → wizard; out: wizard → them); own messages right, others left, clicking the blank opposite a bubble switches role. Any valid `?id=` ticket may view every role (single-user deployment); the standalone svr has no registry and names roles from the pushed `WorldFacts`.
|
|
66
71
|
- `graph.ts` is the loop-graph runner: `nodes` = tagged sessions (own cli/model/cwd), `steps` = an ordered pipeline walked `rounds` times, each step injecting a prompt, waiting for that agent to go idle, then feeding its reply forward as `{{last}}` / `{{<tag>}}`. Stops early on the `until` sentinel. All effects go through injected deps backed by `mirror-bridge`, so a step is exactly what a human typing `#tag …` would do. Runs are **in-memory only** — a reload cancels them (panes survive).
|
|
67
72
|
|
|
68
73
|
**Configuration** (`shared/config.ts`):
|
|
@@ -75,13 +80,13 @@ Three processes, all coordinating via 127.0.0.1:17890 and `~/.wezard/`:
|
|
|
75
80
|
- `shared/json-map-store.ts` (`loadJsonMap`) is the write-through keyed-JSON file behind `sessions.ts`, `mirror-store.ts`, `wizard.ts` and `jobs.ts`. Tolerant at both ends by design: a corrupt file reads as empty, a failed write is swallowed — everything it holds is recovery state, so losing it must cost a re-attach, never a live session. `gc` runs just before each write (jobs expires closed rows that way).
|
|
76
81
|
- `shared/exec-path.ts` (`augmentedPath`) — PATH widening for every daemon-side spawn.
|
|
77
82
|
- `shared/std.ts` — `sleep`, `truncate`, `truncateWithCount` (`…(+N)`), `clipLine` (collapse whitespace, then cut).
|
|
78
|
-
- `shared/paths.ts` (`expandHome`, `sanitizeId`), `shared/session-label.ts` (`tagOfKey` / `baseOfKey` / `withTagHeader` — use these names, don't re-alias them locally).
|
|
83
|
+
- `shared/paths.ts` (`expandHome`, `sanitizeId`), `shared/session-label.ts` (`tagOfKey` / `baseOfKey` / `withTagHeader` / `nameTokenRe` / `displayName` — use these names, don't re-alias them locally; names reach it through the injected `TagLinker`, since `shared/` can't import the daemon registry).
|
|
79
84
|
|
|
80
85
|
**Hook + sync**:
|
|
81
86
|
- `hooks/hooks.json` is registered automatically by Claude Code when the plugin is installed (uses `${CLAUDE_PLUGIN_ROOT:-${CODEBUDDY_PLUGIN_ROOT}}` so the same file works under CodeBuddy's plugin loader if ever published there).
|
|
82
87
|
- For non-plugin installs (`claude-internal` wrapper), `cli/sync.ts` writes the MCP server entry + `WEZARD_DAEMON_BASE` env into target `settings.json`s listed in `config.sync.targets`. **It deliberately strips legacy hook entries** for `kind:"claude"` targets — registering both the plugin's `hooks.json` AND a settings.json hook fires the hook twice per tool call (= duplicate cards). For `kind:"codebuddy"` targets, since the wezard plugin is not published to CodeBuddy's marketplace, sync **writes the hook directly into `~/.codebuddy/settings.json`** with an absolute path to `hooks/pre-tool-use.sh` — this is the only registration path for CodeBuddy. `~/.wezard/sync.lock.json` tracks what was written so `--remove` is reversible.
|
|
83
88
|
|
|
84
|
-
**MCP server** (`mcp/server.ts`): stdio transport, completely stateless w.r.t. WeCom. Every tool just `POST`s to the daemon. Add a tool here AND a route handler in `daemon/`. The collaboration tools (`wizard_whoami` / `wizard_identity` / `wizard_roster` / `spawn_clone` / `stop_wizard` / `wizard_remember` / `wizard_handoff_self`, plus the older `list_peers` / `peek_peer` / `send_peer` / `wait_peer` / `run_agent_graph` / `graph_status` / `stop_graph`) all send `selfRef()` = `{sessionId, tmuxPane}` so the daemon can resolve *which* session is asking — env sessionId goes stale after a `/clear`, pane id doesn't. Their descriptions are the whole discovery mechanism for wizard↔wizard collaboration: a wizard only realizes it can clone itself, or drive
|
|
89
|
+
**MCP server** (`mcp/server.ts`): stdio transport, completely stateless w.r.t. WeCom. Every tool just `POST`s to the daemon. Add a tool here AND a route handler in `daemon/`. The collaboration tools (`wizard_whoami` / `wizard_identity` / `wizard_roster` / `spawn_clone` / `stop_wizard` / `wizard_remember` / `wizard_handoff_self`, plus the older `list_peers` / `peek_peer` / `send_peer` / `wait_peer` / `run_agent_graph` / `graph_status` / `stop_graph`) all send `selfRef()` = `{sessionId, tmuxPane}` so the daemon can resolve *which* session is asking — env sessionId goes stale after a `/clear`, pane id doesn't. Their descriptions are the whole discovery mechanism for wizard↔wizard collaboration: a wizard only realizes it can clone itself, or drive `.fix`, because the tool descriptions say so. Their address parameter is `name` (`fix` / `.fix`); the daemon still accepts the legacy `tag`/`tags` fields, because an already-running wizard's MCP process is old code. Tool NAMES stayed on the older `peer` vocabulary on purpose — a name is an address, and renaming would break every hint, doc and habit pointing at it; the descriptions carry the wizard/clone vocabulary.
|
|
85
90
|
|
|
86
91
|
## Important patterns / pitfalls
|
|
87
92
|
|
package/README.md
CHANGED
|
@@ -38,11 +38,11 @@ flowchart LR
|
|
|
38
38
|
| 📋 **计划审批** | Agent 在 plan mode 结束(`ExitPlanMode`)时,把计划摘要 + 审批卡推到 IM:点 ✅同意 退出 plan mode 开始执行,或 ✏️继续改 留在 plan mode 继续完善。`AskUserQuestion` 多选题也镜像为投票卡。 |
|
|
39
39
|
| 🪞 **会话镜像** | 你电脑上跑的 Agent 流式打字、tool_use、思考过程,实时同步到企业微信;IM 里发消息原样落进 CLI 输入框。 |
|
|
40
40
|
| 🖼 **图片直贴** | 企业微信发图,自动走 macOS 剪贴板 + tmux 粘贴,Agent 当贴图处理(不走 Read,不耗 token)。 |
|
|
41
|
-
| 🔍
|
|
41
|
+
| 🔍 **细节页 / rolepage** | 工具调用 / 审批请求都生成本地 HTML 详情页,IM 里点链接看完整 input / result / git diff;点气泡头的 `.name` 进那个 wizard 的 rolepage——它视角下的全部群聊与单聊。 |
|
|
42
42
|
| 📡 **MCP 主动推送** | Agent 通过 `wecom__send_markdown` / `wecom__send_card` / `wecom__ask_user` 主动汇报或问询。 |
|
|
43
43
|
| 📄 **文档读写** | Agent 通过 `wecom_doc_list_tools` / `wecom_doc_call` 直接调企业微信智能机器人的 doc / smartsheet / smartpage MCP,新建在线文档、写 Markdown、读链接、操作智能表格——全程在内网,不需要 corp access_token。 |
|
|
44
44
|
| 🗂 **多会话发现/切换** | Agent 通过 `list_claude_sessions` / `switch_claude_session` / `new_claude_session` 列出本机 tmux 内所有在跑的会话(带摘要 + 稳定动物 emoji 标签)、切换 IM 镜像、或在指定路径新开会话。审批卡标题也带同一枚 emoji,多会话兜底到同一 IM 时一眼区分。 |
|
|
45
|
-
| 🧙 **多 wizard 协同** |
|
|
45
|
+
| 🧙 **多 wizard 协同** | 每个 wizard 有一个**全局唯一**的名字,写作 `.name`,在任何群里都叫得到;有职责、有记忆、能私聊。`spawn_clone` fork 上下文出分身,`open_job` / `wait_peer({names})` / `close_job` 把一次 fan-out 收成群里两条气泡,干完整批回收。 |
|
|
46
46
|
| 🔄 **重启即续** | 电脑重启 / tmux 全没了 / daemon 崩了都不掉档:IM ↔ 会话绑定持久化在 `~/.wezard/mirror-attachments.json`,下一条 IM 消息自动 `claude --resume` 拉起新 tmux pane,历史完整继承;`tmux attach -t wezard` 接管即可。 |
|
|
47
47
|
|
|
48
48
|
<details>
|
|
@@ -53,7 +53,8 @@ flowchart LR
|
|
|
53
53
|
- [体验是什么样](#体验是什么样)
|
|
54
54
|
- [文档 / 智能表格 / 智能文档](#文档--智能表格--智能文档)
|
|
55
55
|
- [定时任务 / 跨群通知](#定时任务--跨群通知)
|
|
56
|
-
- [
|
|
56
|
+
- [wizard:全局唯一的 `.name`](#wizard全局唯一的-name)
|
|
57
|
+
- [rolepage:一个 wizard 视角下的 IM](#rolepage一个-wizard-视角下的-im)
|
|
57
58
|
- [编排:读完材料才知道要分几路](#编排读完材料才知道要分几路)
|
|
58
59
|
- [跨聊天:给聊天命名](#跨聊天给聊天命名)
|
|
59
60
|
- [多 CLI 后端](#多-cli-后端claude--claude-internal--codebuddy)
|
|
@@ -108,7 +109,7 @@ flowchart TD
|
|
|
108
109
|
1. **发首条消息**:在企微里随便说句话(比如 `hi`)。它既是绑定信号也是第一句 prompt——daemon 自动拉起 tmux 窗口 + Agent 会话,回复逐字流式推回 IM。
|
|
109
110
|
2. **切到你的项目**:新会话默认落在 `~/.wezard/workspace`,直接对 AI 说「切到 /path/to/proj」——它调 `set_workspace` MCP 一步换目录重开会话,收到 📂 项目回执即切换完成,`/pwd` 随时确认。
|
|
110
111
|
3. **第一次审批**:Agent 要跑 `Bash` / `Edit` 时 IM 弹按钮卡,点 `❌` / `⏱10h自动过` / `✅总是` / `✅`;点卡片里的链接看完整 input / result / git diff。
|
|
111
|
-
4. **`/h` 拉出命令表**:`/new` 开新会话、`/clear` 清上下文、`/sessions`
|
|
112
|
+
4. **`/h` 拉出命令表**:`/new` 开新会话、`/clear` 清上下文、`/sessions` 切换、`.name` 叫任意 wizard、`/usage` `/cost` 查额度——全部命令一屏可查。
|
|
112
113
|
|
|
113
114
|
---
|
|
114
115
|
|
|
@@ -160,100 +161,154 @@ IM 里发 `/new` 开新 tmux 窗口 + 新 Agent 会话,`/clear` 清当前上
|
|
|
160
161
|
|
|
161
162
|
## 定时任务 / 跨群通知
|
|
162
163
|
|
|
163
|
-
**定时任务**:到点把一句话说给某个 wizard 听——等价于那一刻有人在群里对它说了这句话,所以它**真的会去干活**,产出照常落在群里。它活在守护进程里,跨 CLI 重启、`/clear
|
|
164
|
+
**定时任务**:到点把一句话说给某个 wizard 听——等价于那一刻有人在群里对它说了这句话,所以它**真的会去干活**,产出照常落在群里。它活在守护进程里,跨 CLI 重启、`/clear`、会话结束都还在。每条任务是一份 `~/.wezard/tasks/<id>.task.mjs`,热加载。
|
|
165
|
+
|
|
166
|
+
**日程跟着 wizard 走**:每条任务记着排它的那个 wizard(`owner`),它的 rolepage 顶栏「日程」里就列着它名下的任务;默认到点新起一个白板 wizard 去干,名字是 `<owner>-task-xxxx`、家谱挂在 owner 名下,干完收掉。
|
|
164
167
|
|
|
165
168
|
`when` 用人话说就行,不用翻译成 cron:
|
|
166
169
|
|
|
167
170
|
| 说 | 工具 | 干什么 |
|
|
168
171
|
| --- | --- | --- |
|
|
169
|
-
| 「每个工作日晚上 9:30 跑一遍回归」 | `schedule_task(when, prompt,
|
|
170
|
-
| 「我设了什么定时」 | `list_tasks()` | 列出 id / 人话回显的 when / 下次触发时刻 /
|
|
172
|
+
| 「每个工作日晚上 9:30 跑一遍回归」 | `schedule_task(when, prompt, name?)` | 归在自己名下;省略 `name` = 到点新起白板 wizard,点了 `name` = 投进那个已有 wizard |
|
|
173
|
+
| 「我设了什么定时」 | `list_tasks(mine?)` | 列出 id / 人话回显的 when / 下次触发时刻 / owner / 目标;`mine:true` 只看自己名下的 |
|
|
171
174
|
| 「取消那个定时」 | `cancel_task(id)` | 按 id 删 |
|
|
172
175
|
|
|
173
176
|
认得的说法:`每天 8:00`、`每个工作日晚上9:30`、`每周三下午3点`、`每隔两小时`、`每 30 分钟`、`20 分钟后`、`明早 9 点`。
|
|
174
177
|
|
|
175
|
-
**跨群通知**:`notify(to?, markdown)` 把一段 markdown 贴进指定聊天**给人看**——和 `send_peer` 正好相反,它不驱动任何 agent、不触发一轮对话。`to` 写聊天名(见[给聊天命名](#跨聊天给聊天命名)
|
|
178
|
+
**跨群通知**:`notify(to?, markdown)` 把一段 markdown 贴进指定聊天**给人看**——和 `send_peer` 正好相反,它不驱动任何 agent、不触发一轮对话。`to` 写聊天名(见[给聊天命名](#跨聊天给聊天命名)),省略就是自己这一轮所在的群(人从哪个群叫的它,就回哪个群);气泡头是它的 `emoji .name`,挂着它的 rolepage。
|
|
176
179
|
|
|
177
180
|
典型用法:一个长活在 `build` 群跑完,`notify(["ops"], "🔴 回归挂了 3 例…")` 把结论送到该看的人那里。
|
|
178
181
|
|
|
179
|
-
##
|
|
182
|
+
## wizard:全局唯一的 `.name`
|
|
180
183
|
|
|
181
|
-
一个绑定了聊天的会话,在 wezard 里叫一个 **wizard
|
|
184
|
+
一个绑定了聊天的会话,在 wezard 里叫一个 **wizard**:有自己的终端、工作区、名字和职责,知道还有谁在,也叫得动它们。**名字全局唯一**,写作 `.name`——它就是地址,不管那个 wizard 住在哪个群,在任何群里都叫得到它。
|
|
182
185
|
|
|
183
186
|

|
|
184
187
|
|
|
185
|
-
|
|
188
|
+
**名字从哪来**
|
|
189
|
+
|
|
190
|
+
| wizard | 名字 |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| 聊天的默认 wizard | 聊天的名字(`/name` 起的,或按工作区自动补的,见[给聊天命名](#跨聊天给聊天命名)) |
|
|
193
|
+
| `.docs /new` 叫出来的 / 分身 | 你给的那个名字 |
|
|
194
|
+
| 撞名 | 自动加后缀:`fix` → `fix-2` → `fix-3`(大小写不敏感) |
|
|
195
|
+
|
|
196
|
+
wizard 自己也能改名(对它说「以后你就叫 sanitizer」→ `wizard_identity`),撞名同样加后缀并告诉它。名字只是名册 `~/.wezard/wizards.json` 里的一个字段:内部的会话 key(`chat:xxx#slot`)不随改名变,改名不会动到它的 pane、jsonl 和历史。
|
|
197
|
+
|
|
198
|
+
**创建**
|
|
186
199
|
|
|
187
200
|
```
|
|
188
|
-
/new
|
|
189
|
-
|
|
190
|
-
/new
|
|
201
|
+
.docs /new 让一个叫 docs 的 wizard 在这个群里就位(tmux 窗口名也叫 docs)
|
|
202
|
+
.api 看下这个报错 开头写一个还不存在的 .api = 就地叫出一个新的 api,并把这句话发给它
|
|
203
|
+
/new 本聊天默认那个,老玩法
|
|
191
204
|
```
|
|
192
205
|
|
|
193
206
|
**消息路由**
|
|
194
207
|
|
|
195
|
-
|
|
208
|
+
消息里任意位置带一个**已知**的 `.name`(空白/句首/句尾分隔),就路由到那个 wizard——它可以住在任何一个聊天里:
|
|
196
209
|
|
|
197
210
|
```mermaid
|
|
198
211
|
flowchart LR
|
|
199
|
-
msg["IM
|
|
200
|
-
scan -- "无
|
|
201
|
-
scan -- "
|
|
202
|
-
scan -- "
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
scan -. "
|
|
212
|
+
msg["IM 里的一条消息<br/>(群 B)"] --> scan["扫 .name<br/>空白或首尾分隔"]
|
|
213
|
+
scan -- "无 / 不认识" --> dflt["群 B 的默认 wizard"]
|
|
214
|
+
scan -- ".docs (住在群 B)" --> docs[".docs"]
|
|
215
|
+
scan -- ".fix (住在群 A)" --> fix[".fix"]
|
|
216
|
+
scan -- "开头的 .x (没人叫这个)" --> newx["在群 B 叫出新的 .x"]
|
|
217
|
+
docs --> back["回复带 🦊 .docs<br/>回到群 B"]
|
|
218
|
+
fix --> back2["回复带 🐬 .fix<br/>回到群 B (不是它的 home 群 A)"]
|
|
219
|
+
scan -. "其余已知的 .name<br/>原样透传给模型" .-> hint["尾部挂一条不可见提示:<br/>.b 是活着的同类, 去叫它"]
|
|
207
220
|
```
|
|
208
221
|
|
|
209
|
-
|
|
222
|
+
**回复回到发话的那个群**:人在群 B 里叫住在群 A 的 `.fix`,它这一轮的回复(连同审批卡、提问卡)推回群 B,而不是它的 home 群 A——在哪儿问,就在哪儿答。
|
|
210
223
|
|
|
211
224
|
```
|
|
212
|
-
|
|
213
|
-
帮我看下这个报错
|
|
214
|
-
/pwd
|
|
215
|
-
/stop
|
|
216
|
-
/clear
|
|
225
|
+
.docs 帮我把 README 的目录补一下
|
|
226
|
+
帮我看下这个报错 .api
|
|
227
|
+
/pwd .docs → 只看 docs 的工作区
|
|
228
|
+
/stop .api → 只打断 api
|
|
229
|
+
/clear .docs → 只清 docs 的上下文
|
|
217
230
|
```
|
|
218
231
|
|
|
219
|
-
不带
|
|
232
|
+
不带 `.name` 的消息始终落到本聊天默认那个。句中写一个没人叫的 `.x`(`a.ts`、`.gitignore` 这类)原样透传,不会凭空造出 wizard——只有**开头**那个才会。
|
|
220
233
|
|
|
221
234
|
**回复标识**
|
|
222
235
|
|
|
223
|
-
|
|
236
|
+
每个 wizard 的每条回复都带 `emoji .name` 头(emoji 由名字 hash 决定,稳定),默认 wizard 也不例外;头挂着它的 [rolepage](#rolepage一个-wizard-视角下的-im) 链接,正文里提到的已知 `.name` 同样可点。在群里引用它的气泡,就等于跟它说话:
|
|
224
237
|
|
|
225
238
|
```
|
|
226
|
-
🦊
|
|
227
|
-
|
|
228
|
-
(这里是 docs 这个 wizard 的回复……)
|
|
239
|
+
🦊 .docs (这里是 docs 这个 wizard 的回复……)
|
|
229
240
|
```
|
|
230
241
|
|
|
231
|
-
|
|
242
|
+
改名之前发出去的老 `#tag` 气泡照样能引用——名字就是从 tag 迁移过来的,指得还是那个 wizard。
|
|
232
243
|
|
|
233
|
-
|
|
244
|
+
**名字语法**:`[\p{L}\p{N}_-]{1,32}`,支持中英文数字与 `_`、`-`;代码块与行内代码里的 `.x` 不算提及。
|
|
234
245
|
|
|
235
246
|
**身份、记忆与分身**
|
|
236
247
|
|
|
237
|
-
每个 wizard 的身份(名字、工作区、职责、记忆、家谱)在启动时以**系统提示**压进进程——不占一轮对话、群里看不见、`/clear`
|
|
248
|
+
每个 wizard 的身份(名字、工作区、职责、记忆、家谱)在启动时以**系统提示**压进进程——不占一轮对话、群里看不见、`/clear` 也抹不掉。它一睁眼就知道自己是谁、还有谁在、自己能做什么:
|
|
238
249
|
|
|
239
250
|
```
|
|
240
251
|
你叫什么 / 你负责什么 → 它调 wizard_whoami
|
|
241
|
-
以后你就叫 sanitizer → wizard_identity
|
|
252
|
+
以后你就叫 sanitizer → wizard_identity(全局唯一,撞名加后缀)
|
|
242
253
|
记住:发版前必须更新 CHANGELOG → wizard_remember,跨 /clear 活着,每次重开重新入场
|
|
254
|
+
记到这个群 / 这个仓库的记忆里 → wizard_remember({scope:"chat" | "workspace"}),见下
|
|
243
255
|
还有谁在跑 / 谁在弄那个项目 → wizard_roster(名字、职责、忙闲、谁是谁的分身)
|
|
244
256
|
分个身去把这三个目录都扫一遍 → spawn_clone ×3,干完 stop_wizard 收掉
|
|
245
257
|
上下文快满了 → 它自己 wizard_handoff_self:写简报、原地重开、把简报贴回去
|
|
246
258
|
```
|
|
247
259
|
|
|
260
|
+
**三份记忆**:`wizard_remember` 的 `scope` 决定记在哪——
|
|
261
|
+
|
|
262
|
+
| scope | 落在 | 谁读得到 |
|
|
263
|
+
| --- | --- | --- |
|
|
264
|
+
| `self`(默认) | 它自己的名册记录 | 只有它自己 |
|
|
265
|
+
| `chat` | `~/.wezard/memory/chats/<聊天>.md` | 以这个群为 home 的每个 wizard |
|
|
266
|
+
| `workspace` | `~/.wezard/memory/workspaces/<工作区>.md` | 在这个目录下干活的每个 wizard |
|
|
267
|
+
|
|
268
|
+
每个 wizard 出生时,群记忆与工作区记忆都注进它的系统提示——「这个群习惯先出方案再动手」「这个仓库没有测试、reload 要先 build」不必再教一遍新来的。两份都是 markdown,你也可以直接打开改。
|
|
269
|
+
|
|
248
270
|
**分身(clone)默认继承上下文**:`spawn_clone({inherit:true})` 用 `--resume <父> --fork-session` 起 pane,CLI 把父亲的 transcript 复制一份再往下写——分身开局就带着父亲读过的材料,父亲毫发无损。于是「先把公共文档读进一个基座,再分出 N 个干活的」成立:材料只读一遍,却进了 N 份上下文。要白纸一张传 `inherit:false`(那时才能顺便换工作区)。分身自己也能再分身,层级不限;名下同时活着的分身有上限(`wrc.mirror.cloneMax`,默认 8)——一次跑飞的递归编排足以把 fd 吃光。
|
|
249
271
|
|
|
250
272
|
**模型是 wizard 的属性**,不是 spawn 那一瞬的开关:`/new [cli] [model]`、`new_claude_session({model})`、`spawn_clone({model})`、`run_agent_graph` 的节点都能挑,挑了就写进绑定记录。pane 死了自愈重生仍在那个模型上,名册每一行也看得见谁跑在什么模型上——要判断的给 opus、跑腿的给 haiku,这句话得看得见才执行得了。
|
|
251
273
|
|
|
252
|
-
|
|
274
|
+
**群是公开频道,wizard 之间默认私聊**:`send_peer` 派的活、分身之间的来回,默认只落进双方的 rolepage,群里不出气泡。wizard 自己判断「这件事该当着人说」时传 `send_peer({public:true})`:群里出一条 `🦊 .a → 🐬 .b` 的气泡,对方这一轮的回复也发进这个群。分身出生、派活、回程结论都不再各刷一条;群里只留**人发起的那一轮的回复**、`notify`、工单的**开工 / 收工**两条,以及它们主动公开的讨论。
|
|
275
|
+
|
|
276
|
+
**名册会自己更新,不必去问**:charter 里那份「出生时还有谁」只是快照,只会越来越假。谁出生、谁收工、谁改了职责,会以一行 `<system-reminder>` 挂在**下一次**进到它的任何文本尾巴上——不占一轮、不进气泡、不进 transcript。感知 = spawn 时的快照 + turn 时的增量。要当下完整的名册仍然是 `wizard_roster`,它按 `query` / `cwd` / `chat` / `alive` 过滤(「谁在这个目录里干活」问得出来)。
|
|
253
277
|
|
|
254
|
-
|
|
278
|
+
**cwd 是聊天级的,不是 session 级**:同一聊天里默认 wizard 与在这里叫出来的 wizard **共用**一个 cwd,`.foo /new` 在当前聊天绑定的 cwd 下起 pane。换项目直接对 AI 说「切到 /path/to/proj」,它调 `set_workspace` 一步到位:杀掉当前 pane、在新 cwd 重开新会话,以新会话的 📂 项目回执为准,上下文不延续。多个 wizard 因此天然对齐到同一个项目根,不用重新指路径。
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## rolepage:一个 wizard 视角下的 IM
|
|
283
|
+
|
|
284
|
+
点气泡头的 `emoji .name`,打开的是那个 wizard 的 **rolepage**:不是「一个群里有哪些会话」,而是**它**的 IM——它参与的每个群聊与单聊、在每一处说了什么、听到了什么。
|
|
285
|
+
|
|
286
|
+
```mermaid
|
|
287
|
+
flowchart LR
|
|
288
|
+
subgraph side["侧栏"]
|
|
289
|
+
g["群聊<br/>(公开频道)"] --> subs["子项: 群里其他 role<br/>点开 = 只看我与它在这个群里的往来"]
|
|
290
|
+
dm["单聊"] --> h["与人<br/>(企业微信单聊)"]
|
|
291
|
+
dm --> w["与 wizard<br/>(私聊)"]
|
|
292
|
+
end
|
|
293
|
+
subgraph main["消息窗"]
|
|
294
|
+
mright["我发的 → 靠右"]
|
|
295
|
+
mleft["我收到的 → 靠左"]
|
|
296
|
+
end
|
|
297
|
+
subgraph topbar["顶栏"]
|
|
298
|
+
ident[".name · 职责 · 家谱<br/>cwd · 创建时间 · 模型"]
|
|
299
|
+
ses["session 切换<br/>(/clear /new 轮换)"]
|
|
300
|
+
rel["关系图"]
|
|
301
|
+
sch["日程"]
|
|
302
|
+
end
|
|
303
|
+
side --> main
|
|
304
|
+
mleft -. "点它右边的空白 / 头像" .-> flip["切到对端的视角"]
|
|
305
|
+
mright -. "点它左边的空白" .-> flip
|
|
306
|
+
```
|
|
255
307
|
|
|
256
|
-
|
|
308
|
+
- **消息从 turn 记录折出来**:一轮 = 一进一出——发话方(人 / 同伴 wizard / 定时任务)→ 它,它 → 发话方。落在哪个会话由这一轮的**频道**决定:公开频道(群 / 与人的单聊)按聊天归并,wizard 间私聊按对端归并。工具调用、用量细节照旧折叠在回复里。
|
|
309
|
+
- **切视角**:点靠左消息右边的空白(或靠右消息左边的空白),rolepage 切到对端——能留在同一个群就留在同一个群。
|
|
310
|
+
- **顶栏**:身份、cwd、创建时间;同一个名字下有多个 session(`/clear`、`/new`、交接重开都会轮换)时出一个切换器;与别的 role 有关系(家谱、派活)时出「关系图」入口,聚焦在它身上;「日程」列出它名下的定时任务与它开过 / 参与的工单。
|
|
311
|
+
- **链接**:`/role?id=<票据>&role=<target>`,老的 `/chat` 链接打开同一个页面。页面的核心动作就是顺着消息走到对端,而对端可能住在任何一个群——所以**任一有效票据可以看全部 role**。要把正文严格关在一个群里的部署,不该对外暴露这些路由。
|
|
257
312
|
|
|
258
313
|
---
|
|
259
314
|
|
|
@@ -274,8 +329,8 @@ sequenceDiagram
|
|
|
274
329
|
W->>K: spawn_clone({inherit:true, task, job})
|
|
275
330
|
Note over W,K: fork 父上下文,材料不必重读;派活时要求一行 RESULT: 收口
|
|
276
331
|
end
|
|
277
|
-
Note over W,K:
|
|
278
|
-
W->>K: wait_peer({
|
|
332
|
+
Note over W,K: 五个分身并行干活,往来是私聊,过程落在各自的 rolepage
|
|
333
|
+
W->>K: wait_peer({names:[…], need})
|
|
279
334
|
K-->>W: 各自的 RESULT: 行
|
|
280
335
|
W->>U: 📋 收工 close_job(汇总)
|
|
281
336
|
Note over W,K: 为这个工单生出来的分身整批回收,被拉来帮忙的长期 wizard 不在其列
|
|
@@ -285,8 +340,8 @@ sequenceDiagram
|
|
|
285
340
|
|
|
286
341
|
| 原语 | 它解决的问题 |
|
|
287
342
|
| --- | --- |
|
|
288
|
-
| **工单** `open_job` / `close_job` / `list_jobs` | 五路 fan-out 原本要刷十条交叉气泡,人读不出结构。带 `job` 的派活只在群里留「开工 /
|
|
289
|
-
| **一次等一组** `wait_peer({
|
|
343
|
+
| **工单** `open_job` / `close_job` / `list_jobs` | 五路 fan-out 原本要刷十条交叉气泡,人读不出结构。带 `job` 的派活只在群里留「开工 / 收工」两条,过程照旧在各自的 rolepage;收工那条把成员与各自那段活一并交代。`close_job` 还把**为这个工单生出来的**分身整批回收——忘记收是常态,而每个分身都占一个 pane 和一份上下文。 |
|
|
344
|
+
| **一次等一组** `wait_peer({names, need})` | 分身本来就在并行干活。一个一个等,墙钟是所有人之和;一起等只花最慢那一个的时间。`need` 决定满几个就返回(`1` = 谁先完事就先处理谁),法定人数一满剩下的等待立刻撤掉,它们照常继续干。 |
|
|
290
345
|
| **收口行** `RESULT: …` | join 的载荷本来是「对方最后一条 assistant 文本」——可能是「好的我开始了」,也可能是八百字散文,两种都没法直接汇总。改不了 CLI 的输出格式,就在派活的提示里要求收口:`wait_peer` 摘最后一个 `RESULT:`(或「结论:」)行放进 `result`,摘不到就是空串,照旧读 `lastText`。 |
|
|
291
346
|
| **投递时机** `send_peer({when:"idle"})` | 两个 wizard 同时找第三个时,两段文本会挤进同一个输入框被当成一轮读掉——这在协同网络里是常态而非边角。`idle` 先等对方闲下来再投;「回答它的提问」「打断它」仍用默认的 `now`。返回一律带 `wasBusy`。 |
|
|
292
347
|
|
|
@@ -296,7 +351,7 @@ sequenceDiagram
|
|
|
296
351
|
|
|
297
352
|
## 跨聊天:给聊天命名
|
|
298
353
|
|
|
299
|
-
|
|
354
|
+
wizard 的名字全局唯一,叫它本来就不分群;但**聊天本身**的身份仍是 `chat:wrkS…` 这种既读不出也打不进去的 id。给聊天起个名字,它就有了能写进消息、也能传给工具的地址——`notify` 往哪个群发、新 wizard 生在哪个群,都写这个名字。它同时也是这个群**默认 wizard 的名字**。
|
|
300
355
|
|
|
301
356
|
```
|
|
302
357
|
/name daily 给本聊天起名为 daily
|
|
@@ -305,26 +360,26 @@ sequenceDiagram
|
|
|
305
360
|
/chats 列出所有已知聊天、各自跑着哪些 wizard(名字 / 职责 / 工作区)
|
|
306
361
|
```
|
|
307
362
|
|
|
308
|
-
**名字规则**:1–32 个字符,字母 / 数字 / `_` / `-`(不能有空格、`#`、`/`、`:`)。全机唯一、大小写不敏感,重名会被拒;改名即覆盖,一个聊天只留一个名字。名字写在 `~/.wezard/config.jsonc` 的 `chats`
|
|
363
|
+
**名字规则**:1–32 个字符,字母 / 数字 / `_` / `-`(不能有空格、`#`、`/`、`:`)。全机唯一、大小写不敏感,重名会被拒;改名即覆盖,一个聊天只留一个名字。名字写在 `~/.wezard/config.jsonc` 的 `chats` 里,手改也行。默认 wizard 的名字在它第一次被用到时取自聊天名,此后就是它自己的名字;与别的 wizard 撞上时它加后缀(`daily-2`),聊天名不变。让 AI 调 `name_chat` 改聊天名时,默认 wizard 若还叫旧聊天名会跟着改。
|
|
309
364
|
|
|
310
|
-
|
|
365
|
+
**寻址**只剩一种写法——wizard 的名字:
|
|
311
366
|
|
|
312
367
|
| 写法 | 指向 |
|
|
313
368
|
|---|---|
|
|
314
|
-
| `fix` |
|
|
315
|
-
| `daily
|
|
316
|
-
| `
|
|
317
|
-
| `chat:wr…#
|
|
369
|
+
| `fix` / `.fix` | 名册里叫 fix 的那个 wizard,不管它住在哪个群 |
|
|
370
|
+
| `daily` | 名字叫 daily 的 wizard——也就是 daily 这个聊天的默认 wizard |
|
|
371
|
+
| `''` | 调用方自己这个聊天的默认 wizard |
|
|
372
|
+
| `chat:wr…#slot` | 全量 key(调试用);改名前的老写法 `daily#fix` 也还认得,但别再写 |
|
|
318
373
|
|
|
319
|
-
|
|
374
|
+
在**任何群**里直接说人话即可:
|
|
320
375
|
|
|
321
376
|
```
|
|
322
|
-
让
|
|
323
|
-
在 daily 里开个
|
|
324
|
-
别的群还有谁在跑
|
|
377
|
+
让 .fix 看一眼这个报错 → AI 调 send_peer("fix", …)(默认私聊;要当着人说就 public:true)
|
|
378
|
+
在 daily 里开个 .ingest 跑 ~/repo → AI 调 new_claude_session({ chat: "daily", name: "ingest", cwd })
|
|
379
|
+
别的群还有谁在跑 → AI 调 list_chats
|
|
325
380
|
```
|
|
326
381
|
|
|
327
|
-
|
|
382
|
+
`new_claude_session` 的 `chat` 参数决定新 wizard 的 **home** 群(它的 cwd、默认频道、群记忆都跟着这个群),不必拉个人去那边手打 `/new`。叫的时候反正不分群:它的回复回到叫它的那个频道。
|
|
328
383
|
|
|
329
384
|
**没名字的聊天会自动补一个**。名字就是地址,而「等人想起来去 `/name`」是等不到的。每个聊天本来就带着一个可读的标识——它在干哪个项目:`~/develop/Guxi11/weclaude` → `weclaude`,撞名加序号(`lisct` / `lisct-2`),非法字符折成 `-`。补名发生在「**要把名字交给模型**」的那一刻:各 MCP 的收件解析,以及渲染 charter 时——后者最要紧,系统提示随进程终身,一个在聊天还没名字时出生的 wizard 会一辈子以为自己住在「(未命名)」的地方。只填空,**永不覆盖你起过的名字**;工作区为空推不出名字的原样留着——宁可没名字,也不造一个同样不可读的 `chat-wr4`。
|
|
330
385
|
|
|
@@ -344,20 +399,19 @@ daemon 同时挂载所有已安装的 CLI,不是二选一:一个 tmux 窗口
|
|
|
344
399
|
|
|
345
400
|
默认后端由 `wrc.defaultCli` 决定(缺省 `claude`),二进制路径可用 `wrc.cliBackends.<name>.bin` 覆盖。
|
|
346
401
|
|
|
347
|
-
**和
|
|
402
|
+
**和 `.name` 完全正交**,两者可以任意组合:
|
|
348
403
|
|
|
349
404
|
```
|
|
350
|
-
/new codebuddy
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
/clear #docs → 只清它,且仍留在 codebuddy 上
|
|
405
|
+
.docs /new codebuddy 用 codebuddy 叫出一个叫 docs 的 wizard
|
|
406
|
+
.docs 帮我改 README → 路由到那个 codebuddy 会话
|
|
407
|
+
/clear .docs → 只清它,且仍留在 codebuddy 上
|
|
354
408
|
```
|
|
355
409
|
|
|
356
|
-
切换 CLI
|
|
410
|
+
切换 CLI 后按名字路由的所有行为都保持不变:
|
|
357
411
|
|
|
358
|
-
- `/clear
|
|
412
|
+
- `/clear .name` rotate 出的新 jsonl 仍落在该 CLI 的 projects 目录,watcher 按该后端的 dialect 迁移绑定;
|
|
359
413
|
- pane 挂了自愈 `--resume` 用的是**该会话所属**的二进制,不会串到 `defaultCli`;
|
|
360
|
-
-
|
|
414
|
+
- 新叫出的 `.name` 还没有自己的历史时,**继承本聊天基础会话的 CLI**(同 cwd 的聊天级继承规则),不会悄悄退回默认后端;
|
|
361
415
|
- `/sessions` 列表在混用多个 CLI 时,每行自动标注 `(codebuddy)` 之类的来源。
|
|
362
416
|
|
|
363
417
|
---
|
|
@@ -399,7 +453,7 @@ IM 里发 `/help` 可随时拉出完整命令表;每次 `/new`、`/clear` 之
|
|
|
399
453
|
```
|
|
400
454
|
/new · /clear · /stop · /n · /kill 会话控制(/kill 连 pane 一起收掉)
|
|
401
455
|
/sessions [emoji|id] 列出 / 切换 live 会话
|
|
402
|
-
/new <cli> [model] [
|
|
456
|
+
[.name] /new <cli> [model] [第一句] 切换 CLI 后端 / 挑模型 / 叫出新 wizard
|
|
403
457
|
/reveal 把终端的 tmux 窗口切到本会话
|
|
404
458
|
/peers · /wizards 本聊天的 wizard:名字、职责、忙闲、家谱
|
|
405
459
|
/name [名字|-] · /chats 给本聊天起名 / 跨聊天目录
|
|
@@ -450,7 +504,7 @@ wezard uninstall # 完整卸载(先于 npm uninstall)
|
|
|
450
504
|
|
|
451
505
|
## 深入了解
|
|
452
506
|
|
|
453
|
-
- [技术说明](技术说明.md) —
|
|
507
|
+
- [技术说明](技术说明.md) — 架构、消息双向同步、`.name` 路由与频道、wizard 网络(身份 / 分身 / 感知 / 编排)、rolepage、保活判定、文档 MCP 桥接
|
|
454
508
|
- [审批配置](审批配置.md) — 审批粒度、danger 名单、跳过开关的优先关系、完整判定链
|
|
455
509
|
- [CLAUDE.md](CLAUDE.md) — 模块级职责与代码约定
|
|
456
510
|
- [CHANGELOG.md](CHANGELOG.md) — 各版本变更
|
package/dist/daemon/approval.js
CHANGED
|
@@ -11,11 +11,17 @@ import { json, readBody } from "./http.js";
|
|
|
11
11
|
import { tagBadge, withTagHeader } from "../shared/session-label.js";
|
|
12
12
|
import { sessionNameFor } from "./session-name.js";
|
|
13
13
|
// ── Routing helpers ────────────────────────────────────────────────────
|
|
14
|
-
|
|
14
|
+
// 卡片跟着本轮的频道走: 人在群 B 里 `.fix` 叫住在 A 的 wizard, 这一轮的审批/提问卡
|
|
15
|
+
// 要出现在 B —— 等着点的人在那儿。`principal` (chatKey) 仍是 wizard 自己的 key,
|
|
16
|
+
// 头、缓存、⏱窗口都按它; 只有「发到哪个 chatid」这一步换成频道。mirror 在启动时装上。
|
|
17
|
+
let cardChannel;
|
|
18
|
+
export const bindCardChannel = (fn) => { cardChannel = fn; };
|
|
19
|
+
const targetChatId = (key) => {
|
|
15
20
|
// "user:abc" → "abc" (DM chatid == userid for aibot)
|
|
16
21
|
// "chat:wc..." → "wc..."
|
|
17
22
|
// "user:abc#tag" → "abc" (drop the routing tag; WeCom SDK only knows chatids)
|
|
18
23
|
// raw fallthrough
|
|
24
|
+
const principal = cardChannel?.(key) || key;
|
|
19
25
|
const i = principal.indexOf(":");
|
|
20
26
|
const rest = i >= 0 ? principal.slice(i + 1) : principal;
|
|
21
27
|
const h = rest.indexOf("#");
|
|
@@ -859,7 +865,7 @@ export const buildAskqChatDriveActions = (q) => [
|
|
|
859
865
|
{ kind: "text", text: "聊聊这个" },
|
|
860
866
|
{ kind: "confirm_submit" },
|
|
861
867
|
];
|
|
862
|
-
export const runMirrorAskqFlow = async ({ log, client, sessionId, chatKey, toolInput, voteTimeoutMs, drive }) => {
|
|
868
|
+
export const runMirrorAskqFlow = async ({ cfg, log, client, sessionId, chatKey, toolInput, voteTimeoutMs, drive }) => {
|
|
863
869
|
const questions = parseAskqInput(toolInput);
|
|
864
870
|
if (!questions || questions.length === 0)
|
|
865
871
|
return;
|
|
@@ -1013,7 +1019,7 @@ export const mootMirrorPlan = (sessionId) => {
|
|
|
1013
1019
|
resolvePending(s.reqId, "moot");
|
|
1014
1020
|
mirrorPlanSlots.delete(sessionId);
|
|
1015
1021
|
};
|
|
1016
|
-
export const runMirrorPlanFlow = async ({ log, client, sessionId, chatKey, cwd, jsonlPath, voteTimeoutMs, sendKey }) => {
|
|
1022
|
+
export const runMirrorPlanFlow = async ({ cfg, log, client, sessionId, chatKey, cwd, jsonlPath, voteTimeoutMs, sendKey }) => {
|
|
1017
1023
|
if (!client.isConnected)
|
|
1018
1024
|
return;
|
|
1019
1025
|
const target = targetChatId(chatKey);
|