weflow-cli 1.8.2 → 1.9.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +342 -0
  2. package/OPERATIONS.md +79 -0
  3. package/README.en.md +9 -2
  4. package/README.md +12 -2
  5. package/bin/weflow-cli-electron.cjs +2 -0
  6. package/bin/weflow-cli.ts +648 -79
  7. package/dist/bin/weflow-cli.js +675 -78
  8. package/dist/bin/weflow-cli.js.map +1 -1
  9. package/dist/mcp-server/http.d.ts +14 -0
  10. package/dist/mcp-server/http.d.ts.map +1 -0
  11. package/dist/mcp-server/http.js +84 -0
  12. package/dist/mcp-server/http.js.map +1 -0
  13. package/dist/mcp-server/httpConfig.d.ts +50 -0
  14. package/dist/mcp-server/httpConfig.d.ts.map +1 -0
  15. package/dist/mcp-server/httpConfig.js +104 -0
  16. package/dist/mcp-server/httpConfig.js.map +1 -0
  17. package/dist/mcp-server/index.d.ts +13 -1
  18. package/dist/mcp-server/index.d.ts.map +1 -1
  19. package/dist/mcp-server/index.js +39 -373
  20. package/dist/mcp-server/index.js.map +1 -1
  21. package/dist/src/panel/server.d.ts.map +1 -1
  22. package/dist/src/panel/server.js +26 -1
  23. package/dist/src/panel/server.js.map +1 -1
  24. package/dist/src/services/assistantEval.d.ts +69 -0
  25. package/dist/src/services/assistantEval.d.ts.map +1 -1
  26. package/dist/src/services/assistantEval.js +270 -8
  27. package/dist/src/services/assistantEval.js.map +1 -1
  28. package/dist/src/services/assistantScenes.d.ts +81 -0
  29. package/dist/src/services/assistantScenes.d.ts.map +1 -0
  30. package/dist/src/services/assistantScenes.js +322 -0
  31. package/dist/src/services/assistantScenes.js.map +1 -0
  32. package/dist/src/services/assistantService.d.ts +14 -4
  33. package/dist/src/services/assistantService.d.ts.map +1 -1
  34. package/dist/src/services/assistantService.js +82 -6
  35. package/dist/src/services/assistantService.js.map +1 -1
  36. package/dist/src/services/assistantSkills.d.ts +94 -0
  37. package/dist/src/services/assistantSkills.d.ts.map +1 -0
  38. package/dist/src/services/assistantSkills.js +330 -0
  39. package/dist/src/services/assistantSkills.js.map +1 -0
  40. package/dist/src/services/assistantTools.d.ts +7 -0
  41. package/dist/src/services/assistantTools.d.ts.map +1 -1
  42. package/dist/src/services/assistantTools.js +770 -61
  43. package/dist/src/services/assistantTools.js.map +1 -1
  44. package/dist/src/services/assistantTrace.d.ts +4 -0
  45. package/dist/src/services/assistantTrace.d.ts.map +1 -1
  46. package/dist/src/services/assistantTrace.js +9 -0
  47. package/dist/src/services/assistantTrace.js.map +1 -1
  48. package/dist/src/services/configService.d.ts +7 -0
  49. package/dist/src/services/configService.d.ts.map +1 -1
  50. package/dist/src/services/configService.js +4 -2
  51. package/dist/src/services/configService.js.map +1 -1
  52. package/dist/src/services/outboundSend.d.ts +53 -0
  53. package/dist/src/services/outboundSend.d.ts.map +1 -0
  54. package/dist/src/services/outboundSend.js +90 -0
  55. package/dist/src/services/outboundSend.js.map +1 -0
  56. package/dist/src/services/pythonBridge.d.ts.map +1 -1
  57. package/dist/src/services/pythonBridge.js +1 -0
  58. package/dist/src/services/pythonBridge.js.map +1 -1
  59. package/dist/src/utils/python.js +2 -2
  60. package/dist/src/utils/python.js.map +1 -1
  61. package/docs/BRANCHES.md +37 -0
  62. package/docs/DECISIONS.md +350 -0
  63. package/docs/EXTENDING.md +29 -3
  64. package/docs/MCP.md +56 -2
  65. package/docs/PROJECT_STATE.md +121 -9
  66. package/mcp-server/http.ts +108 -0
  67. package/mcp-server/httpConfig.ts +126 -0
  68. package/mcp-server/index.ts +344 -681
  69. package/package.json +1 -1
  70. package/resources/panel/ball-position.cjs +48 -2
  71. package/resources/panel/index.html +6 -4
  72. package/resources/panel/main.cjs +137 -5
  73. package/resources/panel/mascot-base.png +0 -0
  74. package/resources/panel/mascot-iris.png +0 -0
  75. package/resources/panel/mascot-peek.png +0 -0
  76. package/resources/panel/panel.css +95 -60
  77. package/resources/panel/preload.cjs +26 -0
  78. package/resources/panel/renderer.js +167 -10
  79. package/scripts/_utils.py +47 -1
  80. package/scripts/backfill_articles.py +202 -21
  81. package/scripts/biz_daily.py +8 -31
  82. package/scripts/bonds.py +331 -0
  83. package/scripts/chat_notes.py +551 -470
  84. package/scripts/chat_stats.py +8 -7
  85. package/scripts/compile_wiki.py +24 -2
  86. package/scripts/export_chat_html.py +3 -1
  87. package/scripts/mcp_bridge.py +6 -7
  88. package/scripts/wechat_voice.py +9 -0
  89. package/scripts/wiki_lint.py +30 -7
  90. package/src/panel/server.ts +25 -1
  91. package/src/services/assistantEval.ts +323 -12
  92. package/src/services/assistantScenes.ts +342 -0
  93. package/src/services/assistantService.ts +81 -6
  94. package/src/services/assistantSkills.ts +364 -0
  95. package/src/services/assistantTools.ts +738 -59
  96. package/src/services/assistantTrace.ts +9 -0
  97. package/src/services/configService.ts +412 -403
  98. package/src/services/outboundSend.ts +126 -0
  99. package/src/services/pythonBridge.ts +1 -0
  100. package/src/utils/python.ts +2 -2
package/CHANGELOG.md CHANGED
@@ -2,8 +2,350 @@
2
2
 
3
3
  The npm package is published separately from GitHub. It may lag behind the `master` branch until a release is published.
4
4
 
5
+ **The GitHub Releases page is cut by hand and can therefore lag npm by several versions.** That decoupling is
6
+ deliberate (`docs/RELEASING.md`: "GitHub Release 与本清单解耦:源码、编译包、npm 三者版本差异要对用户可见"), and it comes
7
+ with a requirement: the difference has to stay **visible**. So if a version you expect is missing from the Releases
8
+ page, check the two sources rather than assuming it was withdrawn:
9
+
10
+ ```
11
+ npm view weflow-cli version # 权威的最新已发布版本
12
+ gh release list # GitHub 上手工建过哪些
13
+ ```
14
+
15
+ Everything published to npm has a section in this file. **No version numbers are written into this paragraph on
16
+ purpose** - a hand-written "latest is X" is exactly the claim that goes stale here; those two commands never do.
17
+
5
18
  All notable user-facing changes are recorded here. This project follows [Semantic Versioning](https://semver.org/).
6
19
 
20
+ ## 1.9.0
21
+
22
+ ### Added
23
+
24
+ - **`draft --pick N --send`: a candidate can now be sent, by a human who names which one.** The hard half already
25
+ existed - judge the intent, draft three deliberately different candidates, rank them with the decision model - and it
26
+ ended by saying the candidates are only text and that sending is your call. That call can now be carried out from the
27
+ same command. Three rules keep it honest: `--send` **requires** `--pick` (no "draft three, let it choose, let it
28
+ send" path exists); without `--yes` it only returns `CONFIRMATION_REQUIRED` (with the text in the payload) or asks
29
+ interactively; and the blacklist -> whitelist -> rate limit -> audit chain is now **one shared implementation**
30
+ (`src/services/outboundSend.ts`) used by both `send` and `draft --send`, because a second copy of that chain is how a
31
+ send path ends up quietly skipping the whitelist. Sending stays **absent from the assistant and MCP tool tables** (a
32
+ test asserts it), so nothing model-driven can send; `draft_reply` stays present because drafting is not sending.
33
+ `--dry-run` means zero egress, and since candidates can only come from a model it cannot preview one - it reports the
34
+ character count and says so. Usage errors (`--send` without `--pick`, `--pick 9` when there are 3) are reported
35
+ **before any model call**, which is also what the tests assert (D-063).
36
+ - **`weflow-cli bonds` - relationship temperature: who is going quiet, who is heating up, who is reachable only here.** Not a
37
+ leaderboard of who you message most (that tells you nothing you did not know); the useful signal is the *shape*. It
38
+ reports relationships with substantial history that have gone silent (ranked by messages x silent days, so it
39
+ surfaces "294 messages last year, nothing for 282 days"), relationships whose last 7 days run at least twice the
40
+ long-run rate, and contacts whose entire history never mentions another channel - phone, email, meeting up - which
41
+ is the data-derived version of "if this account disappeared tomorrow". Read-only, local, no model, no network.
42
+ Three measurements shaped it, and all three overturned a simpler design: ranking by volume produced no information;
43
+ the first irreplaceability rule was **inverted** (it is trivially true of bots, so the top of the list was a coffee
44
+ shop's bot); and `flag` - the field that looked like a clean human/bot marker - would have silently deleted two
45
+ real friends, since two of the 38 people on this machine carry extra bits from however they were added. What
46
+ remains is the **shape of the id** (`@` anything, `filehelper`, `gh_` are out), plus accounts that look structurally
47
+ like a person but whose name reads as a shop are **flagged for you to decide** rather than dropped, with a skip file
48
+ and `--skip`. Every exclusion is printed with its reason. Deliberately **not** in the assistant or MCP tool tables:
49
+ its output is an inference about other people (D-062).
50
+ - **Voice messages reach the conversation cards as text, not as `[语音]`.** The chat line fed the model a bare
51
+ `[语音]` placeholder for every voice message - no length, no words - while the transcripts were already on disk:
52
+ the local faster-whisper pass writes a content-addressed cache, and measured over the last 30 days **1,780 of
53
+ 1,869 clips** in the busiest conversations already had a transcript that nothing was reading. `chat-notes` now
54
+ reads that cache and substitutes the text, labelled `[语音·本机转写·未校对]` - the label is not decoration: it is
55
+ someone's speech as a machine heard it and it will contain errors, so the model has to know not to treat it as
56
+ quoted fact (the same discipline as `verified: false` on concept pages). `--transcribe-voice` fills what is
57
+ missing (local model, never leaves the machine, resumable - the cache is the progress); without it the run only
58
+ reads, and the count of what is still missing is printed rather than quietly omitted. First real run: **54 of the
59
+ 125 voice messages in the 30-day window** carry text into the cards that previously said `[语音]`.
60
+ Finding this also surfaced a quieter bug in the same feature: the cache directory was being derived from
61
+ `chat-notes`'s *card* output directory, so it read a cache it had just created itself (`output/chat-notes/.voice-cache`,
62
+ 16 entries) instead of the real one (`output/.voice-cache`, 1,788) - and the run reported "16 hits" that were
63
+ internally consistent and wrong. The cache's location is now defined once, in the module that owns the cache, and
64
+ a test asserts the three readers agree.
65
+ - **The MCP server can speak Streamable HTTP, so clients that cannot spawn a process can use it too.** stdio stays
66
+ the default and is unchanged; `--http` (or `WEFLOW_MCP_HTTP=1`) serves the **same tools** over HTTP, because the
67
+ table is derived from one place (`TOOL_DEFS` -> `MCP_TOOL_DEFS`). Serving private messages on a listening socket is
68
+ a different risk from writing to stdout, so four rules are enforced in code and each is mutation-checked: it is
69
+ **off unless asked for**; a **token is mandatory with no default** (missing or under 24 characters, the process
70
+ refuses to start and prints how to generate one); the bind is **loopback only** (`0.0.0.0` is rejected with the
71
+ tunnel command in the message, because widening the bind is what would put chat data on the network); and
72
+ **DNS-rebinding protection is on**, which the SDK leaves off by default - the realistic attack on a loopback server
73
+ is a web page you happen to visit reaching `127.0.0.1`, so the `Host` header must match and any `Origin` must be
74
+ allowlisted. Requests are stateless and the token is never logged. Default port 8790 (5030 is chatlog's, and two
75
+ servers fighting over a port is not a useful failure). The decisions live in `mcp-server/httpConfig.ts` as pure
76
+ functions with 13 offline tests, five of which were shown to go red by mutation.
77
+ - **Tool coverage is now a test-watched table instead of a paragraph.** The list of "which tools have no eval
78
+ case" used to be prose inside the case file, and prose does not fail when it goes stale - two of its claims had.
79
+ It is now `EVAL_UNCOVERED`, a map from tool name to reason, with `test/assistant-eval.test.ts` asserting both
80
+ directions: every tool is either claimed by some case's assertions or registered here, and no registered name has
81
+ stopped existing. Adding a tool now means writing a case or writing a reason; skipping both fails. That walk also
82
+ caught two cases whose comments promised a distinction they never asserted - `daily-review` says it is testing
83
+ "get_review, not get_daily_report" and `skills-health` says the same about `check_skills` vs `list_skills`, and
84
+ neither had the `mustNotCall` half. Both have it now, verified against the real model.
85
+ - **Five more behaviour-eval cases, and a limitation list that had gone stale.** Walking the 31 tools one by one
86
+ against `assistantEval.ts` turned up five with no assertions at all - and one stub that made its own case
87
+ untestable: `getSnsTimeline` returned a hard-coded empty timeline, so `get_sns` could only ever be asserted as "no
88
+ cached data", the same defect `listContacts` had. The `sns` fixture is per-case now, and the new cases cover
89
+ `get_sns`, `read_skill` (the body, not just `list_skills`' catalogue), `get_stats`' knowledge-base half,
90
+ `export_chat` (into a temp root, via `WEFLOW_ASSISTANT_EXPORT_ROOT`), and `draft_reply` under strict privacy - that
91
+ last one is a guard for a real regression: with `aiEngine=ollama` the old code still handed the conversation to the
92
+ script, whose two remote calls have nothing to do with that setting. The block of comments claiming which tools
93
+ were untestable was rewritten too, because two of its claims were no longer true (`get_concepts`/`get_review` had
94
+ been covered since 2026-09-30, and the harness does set environment variables). `get_weread` was filed as untestable too, for the same reason - which mistook a missing mechanism for an
95
+ untestable tool, since the harness had been overriding `assistantPrivacy` per case all along without
96
+ generalising it. A general per-case `config` override fixed that, and `weread-notebooks` now asserts the book
97
+ title actually reaches the answer (reading `b.title` off the notebooks response returns `undefined` while the
98
+ output still looks fine - the silent version of that bug is what the case is for). What is genuinely uncovered
99
+ is **one** tool: `search_semantic`, which needs a `dashscopeApiKey` and a built index. Run against the real
100
+ model the same day: **37 passed, 0 failed** once `weread-notebooks` was added. The strict-draft case is worth one note for what it did *not* do - it
101
+ called `get_messages` and then refused, never reaching for `draft_reply`, so an expectation of
102
+ `mustCall: ['draft_reply']` (that case's first version) would have failed on its first run and sent someone chasing
103
+ a defect that does not exist. Asserting the outcome rather than the call is what the memory cases already argued
104
+ for, and here it is what kept the case honest.
105
+ - **`backfill_articles.py --topic` fetches one topic instead of a whole day - and a day fetched that way knows it is
106
+ partial.** The filter is the classifier the script already had: `_guess_topic` reads only the article's title and its
107
+ account name, never the body, so `--topic AI` selects exactly the articles a full run would have filed under `AI`.
108
+ The saving is the point - over 2025-09-05 ~ 2026-03-02 the untouched backlog is 9,207 articles across 156 days
109
+ (official accounts only: `gh_%`), 1,758 of them AI, and because 5,278 of the 9,207 already hold enough text in the
110
+ database a full run needs **3,929** network fetches against the AI run's **1,208**. What makes it safe rather than merely convenient is the bookkeeping: the day
111
+ records `topicFilter` in `.articles.json` and names the missing topics in its README, and `day_done` refuses to call
112
+ it finished, so a later full run still covers those days. Without that marker the other five topics would have been
113
+ dropped with nothing to report it - the same shape as the `backfilled`-marker mistake of 2026-09-28 (D-060). The
114
+ filter is applied inside `write_day` too, so a caller that forgets cannot stamp "only AI" on a directory holding all
115
+ six topics. An unknown `--topic` is rejected before the database is opened: a typo would otherwise select zero
116
+ articles and report "nothing to do", which is indistinguishable from a day that genuinely had none.
117
+ - **Two more behaviour-eval cases cover the tools that had no testable data at all.** `daily-review`
118
+ (`get_review`) and `knowledge-overview` (`get_concepts`) both read directories rather than services, and
119
+ with the eval running in a temporary home those directories are always empty - which is why neither tool
120
+ could be asserted beyond "nothing here yet". `WEFLOW_ASSISTANT_VAULT_DIR` was added as a third per-call
121
+ injection point (the same mechanism `WEFLOW_ASSISTANT_BIZ_DAILY_DIR` and `WEFLOW_ASSISTANT_REVIEWS_DIR`
122
+ already use), and the eval can now lay down fixture files and set those variables for a case. Same day, a
123
+ real gap surfaced: `listContacts` had never been stubbed, so `list_contacts` was calling the real service
124
+ against a temp home and always coming back empty - two cases were running on that accident while the
125
+ harness claims synthetic data. It is stubbed now, with contacts derived from the case's own sessions.
126
+ - **The assistant's behaviour eval now covers the tools the widened surface added.** Ten tools went in on
127
+ 2026-09-30 and nine of them had no assertions at all - which matters because that is exactly how "did routing get
128
+ worse once there were ten more tools to choose from" goes unnoticed. Six cases were added (`format-for-wechat`,
129
+ `themes-listed`, `skills-health`, `wiki-health`, `todo-done`, `todo-ambiguous-refused`); the two that leave the
130
+ machine cannot be evaluated there by design, and two more have no injection point yet - all four are written down
131
+ in the case file rather than skipped quietly. The write cases came with a new floor, `neverSucceeds`: if the named
132
+ tool was called, that call must have produced nothing, so "asked the user instead of writing" and "called the tool
133
+ and was refused" both pass while an actual write fails. Full run the same day: **29 passed, 0 failed**.
134
+
135
+ - **Skills: the assistant can read the skill packages you already have.** A skill is a directory with a `SKILL.md`
136
+ (`name`/`description`), scanned from `skillDirs` - by default `~/.claude/skills` and `~/.weflow-cli/skills` - so the
137
+ skills already on this machine are usable immediately. The system prompt carries the catalogue and two read-only
138
+ tools (`list_skills`, `read_skill`) return it and one body; a scene can reference a skill inline with
139
+ `{{skill:<id>}}`. **A skill grants no new powers**: nothing is installed, nothing is executed, no tool is added to
140
+ the model's table, and `read_skill` can only return a file named `SKILL.md` that the scan already found. Referencing
141
+ a skill that is missing, disabled or unreadable renders exactly that state and asks the model to continue and say
142
+ what it could not do rather than dropping the reference. Disable one with
143
+ `config set skillDisabled <id, ...>` or with `enabled: false` inside the skill itself.
144
+ `weflow-cli skill list|show|check|--json`; `skill check` reports unreadable frontmatter, cross-root collisions,
145
+ non-conforming ids and disabled skills. On this machine `skill check` finds **28 skills, 0 unreadable, 0 collisions**,
146
+ and warns about 5 ids that are usable here but would not conform to the Anthropic Agent Skills naming rule
147
+ (`clz_docx_to_mp` and friends). Rationale and the two rules taken from real data (read only the leading `---` block;
148
+ support block scalars) are in D-056.
149
+
150
+ - **Scenes: per-conversation prompt presets.** A scene is keywords + an extra instruction + an output spec + required
151
+ skills, stored in `~/.weflow-cli/assistant_scenes.json` (`weflow-scenes/v1`, unknown version quarantined rather than
152
+ migrated). Each turn picks at most one through three tiers - **explicit binding → keyword hit → last used in that
153
+ conversation** - and no match means no scene section at all, leaving the default behaviour byte-identical. Two
154
+ equally long keyword matches produce **no** scene plus a trace note saying why, instead of picking one. Manage them
155
+ with `weflow-cli scene list|show|add|remove|enable|disable|bind|unbind` (every mutation is `--dry-run` then `--yes`),
156
+ or from a conversation with the built-in `场景 <id>` / `场景 无` / `场景`, which accepts only an argument that names an
157
+ existing scene so an ordinary sentence starting with 场景 is left to the model. **Scenes cannot be read or written by
158
+ the model** - there is no such tool - and a scene's content enters the prompt through `frameLocalData`. Which scene
159
+ applied, and which tier matched, is recorded in the turn trace you already see under 「思考过程」 in the panel.
160
+ Scene ids are validated against quotes and angle brackets before they reach the prompt label. Rationale, the MCP
161
+ collapse cost and the deliberately skipped fourth tier are in D-057.
162
+
163
+ - **The assistant can reach everything it already could over MCP, and only what the project allows.** Three
164
+ surfaces had drifted: the CLI, the 21-tool assistant table, and 11 tools hand-written in the MCP server that the
165
+ chat path could not call at all. Ten tools are now shared - `list_contacts` (names only: never `wxid`, never the
166
+ avatar URL), `get_review`, `get_concepts`, `fetch_article`, `search_public`, `format_article`, `list_themes`,
167
+ `lint_wiki`, `check_skills`, and `set_todo_status` - seven MCP-only tools were **moved** into the shared table
168
+ (same tool names and arguments, single implementation; the hand-written table went from 11 entries to 4), and four
169
+ tools were **widened instead of duplicated**: `get_daily_report` now returns the human-readable daily with
170
+ `full: true` and searches across dates when given a keyword without a date (that was the MCP-only
171
+ `search_articles`), `get_todos` shows each item's id and groups by urgency (that was `todos remind`), `get_weread`
172
+ gained `stats` / `book` / `review` / `discover` / `profile` modes, and `get_stats` now reports the knowledge-base
173
+ half it had been missing while the MCP copy had it. Almost all of this is **alignment rather than new access**:
174
+ every capability added was already reachable by an MCP client.
175
+ Two things are genuinely new and both are **recorded with their residual risk** (D-058) rather than left to be
176
+ found: `search_public` sends a **model-authored query** to a third-party search page with no allowlist and no
177
+ preview (chosen deliberately - `fetch_article`, by contrast, accepts only `https://mp.weixin.qq.com` and
178
+ re-validates that allowlist on **every redirect hop**), and `set_todo_status` can flip one existing todo between
179
+ done/pending - it **cannot create, delete or edit anything else**, it refuses an ambiguous task-text match instead
180
+ of guessing, and it is deliberately **excluded from the MCP surface** ("mutating todos stays out").
181
+ What stays out of reach from a model-driven path is unchanged and named in D-058: sending, publishing,
182
+ configuration, access-list writes, `evidence-review`, `vault promote`, scenes, interactive login, and `decide`.
183
+ Side effect worth noting: `WEFLOW_ASSISTANT_BIZ_DAILY_DIR` and `WEFLOW_ASSISTANT_REVIEWS_DIR` were added as
184
+ per-call injection points, which closed the **last uncovered tool branch** - `get_daily_report` had never been
185
+ executed because its directory was a frozen module constant, so the only possible assertion was a shape check that
186
+ asserted nothing. All 31 tool branches are now executed against fixtures.
187
+ - **Push the ball to a screen edge and it hides behind it, peeking out; click it to come back.** Drag the ball
188
+ until it touches the left or right edge of a monitor and let go: the ball ducks behind the edge and reappears as
189
+ the cat **peeking out from behind it** - head and front paws out, body hidden, with a dedicated drawing whose
190
+ left side is a straight cut that lands exactly on the screen edge (so it reads as "behind the bezel" rather than
191
+ "a ball that got clipped"). The first click (or drag) brings the normal ball back; that click deliberately does
192
+ *not* open the conversation, because on a hidden ball a click means "come back", not "let's talk" - a second
193
+ click expands as usual. Only the left and right edges: top and bottom would fight the taskbar and Windows' own
194
+ edge snapping. The window itself stays **fully on screen**, flush against the edge - the illusion comes from the
195
+ drawing, so nothing is pushed off-screen, the whole thing stays clickable, and a restart just puts the ball back
196
+ at the edge (not lost). Two rules make or break the effect and both have tests, because everything else stays
197
+ green without them: the ball's circular clip has to be off in this state (a circle would slice that straight cut
198
+ into an arc), and hover-zoom has to be off too (scaling by 1.02 lifts the cut 1px off the edge - a visible seam).
199
+ The detection threshold is deliberately below the normal 24px edge margin, so a ball that is merely parked is
200
+ never mistaken for one being pushed off (also tested).
201
+
202
+ - **The ball's eyes follow your mouse.** Move the pointer and the mascot looks that way; stop moving and it settles
203
+ and stops animating entirely (no 60 fps loop running for decoration on a window that is always open). The cursor
204
+ position has to come from the main process - the ball is 96x96, so the pointer is outside the window almost all of
205
+ the time and the page never sees a mousemove - and it is sampled at 120 ms, pushed **only when the coordinates
206
+ change**, kept strictly in-process, and never logged or sent anywhere (D-059). How far the eyes can travel is
207
+ **measured from the artwork**, not picked: the iris has about 7 px of sclera to its left and none above it, which
208
+ at the ball's size is roughly 2.6 px sideways and 0.8 px up - past that the eye reads as a sticker sliding, so the
209
+ movement is clamped to an ellipse. The eyes drift only on the idle and "pressed" faces; the thinking / offline /
210
+ tired faces have their eyes drawn in, so the moving layer is hidden there rather than showing two pupils. The ball
211
+ is otherwise pixel-for-pixel what it was: the new base image plus the iris layer, composited at rest, are asserted
212
+ to equal the old ball image exactly. If your system asks for reduced motion, the eyes stay still.
213
+
214
+
215
+ - **The panel shows what the assistant did, collapsed under each reply.** The user asked for the
216
+ thinking process to be visible but not in the way - so each answer now carries a `<details>` block
217
+ ("思考过程") that is **closed by default**: routing decision, any guard that pushed the turn back,
218
+ each tool call with its argument summary and byte count, the round count and the stop reason. The
219
+ lines come from the **same** `describeForChat` that the WeChat side's 「轨迹」 command prints, so the
220
+ two surfaces cannot drift into disagreeing about what happened - and that also means the panel
221
+ inherits the guarantee that `userId` never appears. When the model actually returns
222
+ `reasoning_content` it is appended (clipped, marked when cut); the current default model returns
223
+ none, and the trace says so rather than pretending.
224
+
225
+ One thing had to be defended: **the trace is an accessory, the reply is the result.** The first
226
+ version called the lookup unguarded, and the panel test went red immediately - its stub service
227
+ had no such method, so every ask became a 500. A throw there would have cost the user their answer
228
+ to gain nothing, so it is caught, and the test pins both halves (the trace comes through, and a
229
+ failing trace still returns the reply).
230
+
231
+ ### Fixed
232
+
233
+ - **A concept name containing a double quote lost a character on every read.** `parse_frontmatter` split
234
+ list values with `item.strip().strip('"\'')`, and `strip` removes *every* leading and trailing quote character - not a matched
235
+ pair - so `AI 长出"手脚"` came back as `AI 长出"手脚`; `wiki_lint` then repeated the mistake, stripping
236
+ quotes a second time off values the parser had already unwrapped. One character, no error, and the visible
237
+ effect was a page nobody could explain: the card's `[[AI 长出"手脚"]]` matched no page name, so the page was
238
+ reported as an orphan. Both now unwrap a **matched pair** only. Related and fixed with it: the file name of a
239
+ concept page is the name sanitised (`:` `/ ? * " < > |` → `_`) and cut to 60 characters, so pages could not be
240
+ reached by the link that pointed at them - the builder now records the original name in `aliases:`, and
241
+ `wiki_lint` counts an alias as an inbound edge (it already accepted one as "exists", which is why no broken link
242
+ was ever reported). Measured on the live knowledge base: **28 orphan pages → 1** (`DESIGN`, which no card mentions
243
+ at all), with 0 broken links across 26 pages that had been silently unlinked. The case-only half of the same
244
+ transform is fixed with it: two names differing only in case are one file, and the builder wrote both, so the second
245
+ overwrote the first - **53 pages were lost this way** in the two builds of 2026-10-01 alone (44 AI, 9 non-AI).
246
+ Names are now grouped by file name: one page, the other spellings as `aliases` (which is what
247
+ `wiki compile --merge-duplicates` already did for one concept written two ways), and the **233 pages** that were
248
+ missing a spelling got it back - `[[AI agent]]` and `[[AI Agent]]` now reach the same page.
249
+ - **Backfilling a day in two passes dropped the first pass from the index.** `--topic` was written for the
250
+ one-topic-at-a-time case, and `write_day` rewrote `.articles.json` from that run's results - so fetching `AI` on
251
+ Monday and `学术` on Tuesday left Tuesday's file listing only the 学术 articles while Monday's `AI` files sat on
252
+ disk, an index shorter than the directory and nothing reported. The index now describes **the day**, not the run:
253
+ entries for other topics are carried over and merged (de-duplicated on the name the file actually has), the
254
+ `topicFilter` mark becomes the union, and it is **removed** once the union covers all six topics - otherwise such a
255
+ day could never converge and every later full run would refetch the whole window. `truncated` is now sticky, and an
256
+ unreadable index is preserved as `.articles.json.bad` instead of being silently replaced. `--topic` also takes
257
+ repeated flags now, not only commas: the same flag in `article_notes.py` is repeatable, so `--topic AI --topic 学术`
258
+ was silently acting on only the last one. Verified on a real day (8 `AI` entries became 42, none lost, directory and
259
+ index equal) and across the window (8,754 entries over 157 days, 0 days with a mismatch, 0 entries lost).
260
+ - **A day backfilled with `--limit-per-day` reported itself as complete.** The day was sliced, written, and then
261
+ `day_done` saw a non-empty article list and skipped it from then on, so everything past the limit was silently out
262
+ of scope - and had been since the flag was added. Such a day now carries a `truncated` marker and is redone on the
263
+ next run. Note the behaviour change: a limited day is no longer incremental, so re-running it re-fetches the same
264
+ first N articles. Written down as D-060 alongside `--topic`, because it is the same defect - an incomplete
265
+ criterion read as "done".
266
+ - **The subscription-account database key was derived in three places, and two of them could never
267
+ have worked.** `chat_stats.py` and `mcp_bridge.py` read `bizKey` / `bizSalt` from the configuration -
268
+ two keys that do not exist on this machine, because WeChat 4.x derives each database's key from the
269
+ account-wide passphrase (PBKDF2 over the file's own 16-byte header salt). Both paths therefore reported
270
+ "missing key", and the hint they printed (`config set bizKey`) leads somewhere worse: that key is not in
271
+ the CLI's writable allowlist, so following the advice hits `INVALID_CONFIG_KEY`. `biz_daily.py` held the
272
+ working derivation - and computed it **twice in a row**, the first result immediately overwritten by the
273
+ second, which is what a fork in the logic leaves behind. All three now call one shared
274
+ `_utils.biz_message_db()`, and a source-level test asserts that no other script reads `bizKey` at all -
275
+ so a script added later is covered too, rather than only today's three. Verified by running the path
276
+ that used to fail: `daily-stats` now returns per-account counts, and a daily dry run opens the database.
277
+ - **`wiki lint` was O(links x pages) and took 9.5 minutes on the article line.** The link-existence
278
+ predicate passed `resolvable_names(pages)` *inside a lambda*, so the set of all page names plus aliases was
279
+ rebuilt once per link instead of once per run - measured on this machine: 3,623 pages took **9m29s**, while the
280
+ 1,527-page chat line took 3.2s. The set is now computed once, outside the lambda: the same page count now takes
281
+ **3.4s**. Behaviour is unchanged, and that was checked rather than assumed - the old and new versions produce
282
+ **byte-identical JSON** (sha256 match, 366,638 bytes) on the chat line, and hoisting a pure function out of a
283
+ per-call position is a no-op by construction. This also unblocked the new `lint_wiki` assistant tool, whose
284
+ first real-machine run is what surfaced the timeout. Side note for whoever touches that code: two other places in
285
+ `assistantTools.ts` split a path with `/[\/]/` for the same reason (telling the two knowledge-base lines
286
+ apart) and one of them had lost a backslash to a shell heredoc, so on Windows both lines were labelled
287
+ 文章线 - silently, because the output still looked plausible. The helper `wikiIndexPath` and the line label are
288
+ now covered by a mutation-checked test.
289
+
290
+
291
+ - **The ball is never backed by anything now - the glow layer is gone entirely.** The user reported
292
+ it a second time: "the background is not transparent while it is thinking". That is the *same*
293
+ observation that led to the 2026-09-26 change, where an always-on glow was narrowed to
294
+ "only while there is a state" - so the remaining case was exactly the busy one. **The same
295
+ complaint arriving twice means the ask is not "draw less of it" but "not at all"**, so the whole
296
+ layer is removed: the element, the three state rules, the `radial-gradient`, `@property --hue`,
297
+ and the `drift` / `breathe` animations, including the two selectors still named inside
298
+ `prefers-reduced-motion` (the tests caught that leftover - a half-removal is what this change was
299
+ most likely to leave behind). The state light is not lost: all four states already have their own
300
+ face (thinking / sorry / tired / idle), and that is the primary signal.
301
+
302
+ - **The pressed face is preloaded, because fetching it cost a frame.** The user reported that
303
+ clicking the ball flickers. Measured: a face served from the daemon takes **~15 ms** against a
304
+ 16.7 ms frame at 60 Hz, and the faces are CSS backgrounds - fetched **when first used**, which is
305
+ the instant the ball is pressed. So the first press can render an empty face for a frame. The
306
+ repo's own comment had recorded the symptom before ("球在按下的一瞬间会闪成一张空图") and fixed
307
+ the whitelist at the time; the whitelist is correct, but the first use still waits for the
308
+ network. All five faces are now fetched when the page loads, inside the `hasShell` branch (the
309
+ browser fallback never shows the ball).
310
+
311
+ - **Expanding the ball no longer makes it jump first.** The user described it precisely: click the
312
+ ball and it flashes off to one side, snaps back, and only then does the bubble open. The cause is
313
+ an ordering rule the collapse path already followed and the expand path did not: `setMode` resized
314
+ the window **first** and told the page **afterwards**. The ball is pinned to a *corner* of the
315
+ window, and which corner depends on the layout (`anchor-top` when the bubble opens upward,
316
+ `bubble-right` when it opens to the right) - so during those frames the ball was still drawn with
317
+ the default anchor, i.e. at a **different corner of the already-larger window**. Measured against
318
+ the real layout the vertical error is (bubble height - ball size), which is why it reads as a jump
319
+ rather than a flicker. Collapsing had the same trap and dodged it deliberately ("tell the page
320
+ first, then shrink, and wait at least one frame - otherwise a 76x76 window shows a sliver of the
321
+ bubble"); expanding now does the same: reposition the anchor, **wait for the page to actually
322
+ paint** (two `requestAnimationFrame`s via `executeJavaScript`, with a ceiling so a busy renderer
323
+ cannot stall the expansion), resize, and only then switch modes. It is two messages rather than
324
+ one because switching modes early shows the bubble inside the still-96px window - the same trap
325
+ from the other side.
326
+
327
+ - **Clicking anywhere in the panel threw a `ReferenceError`.** `renderer.js` called
328
+ `closeQuickMenu()` on every document click and on Escape, and that function **does not exist** -
329
+ the menu is native now, opened by the main process outside the window, so there is no in-page menu
330
+ to close. The two calls were leftovers from the in-page implementation. Harmless in that a throw
331
+ inside one listener does not stop the others, but it fired on every click, and it was the
332
+ unhandled error jsdom kept reporting while the panel tests passed. Deleted, with the reason left
333
+ in place.
334
+
335
+
336
+ - **Every subprocess now runs without a console window.** The user reported a black command-line
337
+ window popping up while the assistant was thinking and using tools. The cause was not one bad
338
+ call: the daemon itself is started with `detached` + `stdio: 'ignore'` + `windowsHide`, so it has
339
+ **no console** - and Windows therefore creates a fresh one for every child it spawns unless that
340
+ call site says `windowsHide: true`. Fourteen of the fifteen sites did not, including the assistant
341
+ tool path itself (`pythonBridge`) and the Python probes (`--version`, `import sqlcipher3`).
342
+
343
+ Nothing was watching that before, which is why it was missed everywhere at once, so a test now
344
+ requires it: it walks `src/`, `bin/` and `resources/panel/`, strips comments (a comment that
345
+ mentions `spawn(` is not a call - that tripped the first draft), and fails listing any call site
346
+ without `windowsHide`. Writing the scan found a fifteenth site that a hand pass had missed
347
+ (`bin/weflow-cli-electron.cjs`), and the mutation check confirms removing one is caught.
348
+
7
349
  ## 1.8.2
8
350
 
9
351
  ### Changed
package/OPERATIONS.md CHANGED
@@ -1087,6 +1087,85 @@ npm run eval:assistant -- --json # 附机器可读报告
1087
1087
  会把守护进程刚写的状态覆盖掉)。用的 key 是你配置里那一个(`lock:` 密文,只在本机解),
1088
1088
  临时配置里只放这一个字段。
1089
1089
 
1090
+ ### 技能:让助手读上你自己的技能包
1091
+
1092
+ 技能就是一份 `SKILL.md`(`name` + `description` frontmatter)。助手**不会安装、不会执行**它们——
1093
+ 它把技能当资料:提示词里列出有哪些,要用时用 `read_skill` 读正文再照着做。所以装一百个技能也不会让模型多出一个工具。
1094
+
1095
+ 默认扫两个目录:`~/.claude/skills`(你已有的技能立刻可用)和 `~/.weflow-cli/skills`(本项目自己的落脚点)。
1096
+ 换目录:`weflow-cli config set skillDirs "D:/我的技能,~/.claude/skills"`。
1097
+
1098
+ ```powershell
1099
+ weflow-cli skill list # 有哪些(带版本与状态)
1100
+ weflow-cli skill list --query 日报 # 按 id/名称/说明/触发词过滤
1101
+ weflow-cli skill show memory-organizer # 打印正文
1102
+ weflow-cli skill check # 体检:读不出来的、跨目录同名的、id 不合规的、被禁用的
1103
+ ```
1104
+
1105
+ `skill check` 值得偶尔跑一次:少一个技能不会报错,只会少一个技能——它会告诉你少了哪个、为什么。
1106
+ 两种禁用方式:`weflow-cli config set skillDisabled "某技能,另一个"`,或在技能自己的 `SKILL.md` 里写 `enabled: false`。
1107
+
1108
+ **边界(有意为之)**:`read_skill` 只可能读到名字叫 `SKILL.md`、且被目录扫描扫到的文件,读不到配置、数据库或任意路径。
1109
+ 符号链接进来的技能**算数**(在 git 仓库里开发技能、再 `ln -s` 到技能目录是常见做法)。
1110
+
1111
+ ### 助手现在能调哪些工具(以及两条要注意的)
1112
+
1113
+ 2026-09-29 把三个界面拉平了:助手能调的,基本就是 MCP 客户端早就能调的那一批(详见 `docs/DECISIONS.md` 的 D-058)。
1114
+ 新增里平时最用得上的几条:
1115
+
1116
+ - **抓公众号正文**:贴一个 `https://mp.weixin.qq.com/...` 链接让它读(白名单 + 逐跳复检;非这个域一律拒绝)
1117
+ - **搜公开文章**:按关键词搜公开的公众号文章标题与摘要(走第三方搜索页,**搜索词会离开本机且没有预览**)
1118
+ - **排版**:「把这段 Markdown 排成公众号格式」→ 它给 HTML 片段,主题用 `list_themes` 查
1119
+ - **通讯录**:「我有哪些联系人」——只给名字,**不给 wxid**
1120
+ - **学习回顾 / 知识库体检 / 技能体检**:`get_review`、`lint_wiki`、`check_skills`
1121
+ - **把待办标记完成**:「交季度报表那条做完了」——它能改状态,**不能删待办**;指代不清(重名)时它会拒绝而不是猜
1122
+
1123
+ 两条边界要记住:
1124
+
1125
+ 1. **`search_public` 的搜索词没有闸门**。这是明确的取舍(D-058),不是遗漏——从一条微信消息就能让模型去搜任意词。
1126
+ 介意的话就别在群里开助手,或者把它从工具表里去掉(改代码)。
1127
+ 2. **待办修改在 MCP 那条路上不可达**。助手在微信/面板里能改,外部 MCP 客户端不能(项目自己的天花板:
1128
+ mutating todos stays out)。
1129
+
1130
+ ### 场景:让某个会话有固定的做法
1131
+
1132
+ 一个场景 = 关键词 + 附加指令 + 输出规范 + 所需技能。消息进来时按三档挑:**绑定**(你显式指定)→
1133
+ **关键词**(这条消息里出现了它的关键词)→ **上次用过的**。都没中就没有场景段,助手行为与以前一字不差。
1134
+
1135
+ ```powershell
1136
+ # 加一个:关键词命中就带上(长文本用 --from-file,别在命令行里塞多行中文)
1137
+ weflow-cli scene add --id 日报 --name "日报整理" --keywords 日报,推文 `
1138
+ --instruction "把这些推文按主题分组,标注来源。" --output-spec "先列表后总结" --dry-run
1139
+ weflow-cli scene add ... --yes # 确认执行(所有写操作都要先 --dry-run 再 --yes)
1140
+
1141
+ weflow-cli scene list # 有哪些、本会话绑的是哪个
1142
+ weflow-cli scene show 日报 # 看全部字段
1143
+ weflow-cli scene disable 日报 # 停用(不再参与匹配)
1144
+ weflow-cli scene remove 日报 # 删掉(连指向它的绑定一起清)
1145
+ weflow-cli scene bind wxid_xxx --scene 日报 --yes # 固定绑到某个会话
1146
+ weflow-cli scene unbind wxid_xxx --yes
1147
+ ```
1148
+
1149
+ 在对话里(微信或面板)也能用:
1150
+
1151
+ - `场景` —— 列出所有场景,标出本会话生效的那个
1152
+ - `场景 <id>` —— 把这个场景绑到当前会话
1153
+ - `场景 无` —— 解绑
1154
+
1155
+ 只有后半段**是真实存在的场景 id** 时才算指令,所以「场景切换怎么用」这种问话不会被吃掉。
1156
+ 指令不调用模型(省一次调用,也省配额)。
1157
+
1158
+ **改完场景要重启助手才生效**:场景是助手启动时读一次(和配置一样),而**技能是每一轮现扫的**——
1159
+ 所以新装一个技能立刻可用,但 `scene add` 出来的场景在运行中的助手那里看不见。
1160
+ 要 `weflow-cli assistant stop` 再 `assistant start` 之后才生效(**没有 `assistant restart` 这个命令**)。
1161
+
1162
+ 场景里可以写 `{{skill:技能id}}` 行内引用技能;技能缺失时会明确写成「技能文件不可用,请直接完成」,
1163
+ 并让助手在回答里说明哪部分没做——**不会静默跳过**。
1164
+
1165
+ **场景只由你在电脑上改**:没有任何助手工具能读写场景,一条微信消息改不了它(同 `隐私` 那条的立场)。
1166
+ 助手回答下面展开的「思考过程」里会写清这一次用了哪个场景、因为哪一档命中;如果有两个场景同样匹配,
1167
+ 它**不带场景**,并在那里说明原因。
1168
+
1090
1169
  ## 7. 常见问题
1091
1170
 
1092
1171
  | 现象 | 处理 |
package/README.en.md CHANGED
@@ -98,7 +98,10 @@ Automatic initialization on Linux depends on the WeChat distribution, the Python
98
98
  | AI collaboration | Expose article crawling, knowledge-base retrieval, digests, and selected local-data tools to MCP-compatible clients. The inventory follows the current code and `docs/MCP.md`. |
99
99
  | Personal review | Monthly chat reports, annual reports, todo extraction, and local Moments cache queries. |
100
100
  | WeChat favorites | Read WeChat "Favorites" (official-account articles, text, images, videos, chat records) with type filters, keyword search, and Markdown/JSON export. |
101
- | Second-brain agent | Chat with a local AI assistant inside WeChat: natural-language queries over chats, favorites, Moments, digests, WeRead, todos, and the knowledge base; three-tier memory across sessions, daemonized background service. |
101
+ | Second-brain agent | Chat with a local AI assistant inside WeChat: natural-language queries over chats, favorites, Moments, digests, WeRead, todos, and the knowledge base; three-tier memory across sessions, daemonized background service. It can also **fetch an official-account article**, search public articles, format Markdown for the WeChat editor, list contacts, read your learning reviews, health-check the knowledge base and skill packages, and mark a todo done (**never delete one**). What it can reach, and what stays out of reach, is in `docs/PROJECT_STATE.md` and `docs/DECISIONS.md` (D-058). |
102
+ | On-device panel | A floating ball on your desktop: click to chat with **the same brain as the WeChat side** (same memory, same quota), and each reply expands into a **thinking process** - what tool it called, how many round trips, why it stopped. The ball itself is transparent with a face per state. |
103
+ | Skill packages | The assistant reads the `SKILL.md` packages already on your machine (by default `~/.claude/skills` and `~/.weflow-cli/skills`): the prompt lists what exists, and a body is read on demand before following it. **A skill grants the assistant no new tools** - it is material: not executed, not installed, and unable to read configuration or databases. `weflow-cli skill check` reports unreadable frontmatter, cross-directory collisions, and non-conforming ids. |
104
+ | Conversation scenes | Give one conversation a fixed way of working: keywords plus an extra instruction, an output spec, and required skills. Each message picks one by **binding → keyword → last used**, and picks none when nothing matches, leaving the previous behaviour untouched. Manage with `weflow-cli scene`, or bind from the conversation with `场景 <id>`. **Scenes are not readable or writable by the model**, and which scene applied is recorded in the thinking process. |
102
105
 
103
106
  > **⚠️ Intended use**: this tool only accesses data of **your own** WeChat account, on your own machine, with your own consent. Do not use it to monitor a partner, employee, or any third party without explicit consent, deploy it onto someone else's device, or run it remotely and silently. Review WeChat's terms, account-risk policies, and local legal requirements. See [SECURITY.md](SECURITY.md).
104
107
 
@@ -229,7 +232,11 @@ weflow-cli panel # open the on-device panel window (usable without
229
232
 
230
233
  `weflow-cli panel` opens a small always-on window that talks to **the same brain as the WeChat side** — the same memory, the same daily quota. **No WeChat login is required**: the messaging channel is optional. The window is only a client; its messages go to the daemon's entry point on `127.0.0.1`, so the two entry points never keep separate copies of memory.
231
234
 
232
- The floating ball needs Electron (`npm i -g electron`); without it the panel degrades to an Edge/Chrome `--app` window — same UI, but no frameless always-on-top ball, no tray, no global shortcut. With the ball (a 96px mascot): **click it and the chat bubble opens beside it (the ball itself stays put; click again to collapse)**. The bubble opens to the left of the ball by default, flips to the right when there is no room on the left, and top-aligns when vertical space runs short; drag the ball to move it, and the position is remembered. The ball doubles as a status light — busy, offline, and quota-exhausted are three real states, each with its own face and glow colour, while an idle ball shows no glow at all. **Right-click anywhere in the panel** to draft a reply for someone on your list (`weflow-cli config set quickReplyContacts "Alice,Bob"` — a click sends that conversation to two cloud models and returns text only, never sending for you). The last item in that menu tucks the ball away; the tray icon brings it back.
235
+ The floating ball needs Electron (`npm i -g electron`); without it the panel degrades to an Edge/Chrome `--app` window — same UI, but no frameless always-on-top ball, no tray, no global shortcut. With the ball (a 96px mascot): **click it and the chat bubble opens beside it (the ball itself stays put; click again to collapse)**. The bubble opens to the left of the ball by default, flips to the right when there is no room on the left, and top-aligns when vertical space runs short; drag the ball to move it, and the position is remembered. **The ball is transparent - only the mascot shows through** - calm at rest, and switching to a *thinking* face for the seconds you are waiting on an answer (offline and quota-exhausted have faces of their own).
236
+
237
+ **Every reply carries a collapsible "thinking process" (closed by default)**: how that turn actually went - the routing decision, which tool it called, its arguments, how many round trips, and why it stopped. It is the same record you get in WeChat by sending 轨迹 (or 思考过程), so the two cannot drift apart.
238
+
239
+ **Right-click anywhere in the panel** to draft a reply for someone on your list (`weflow-cli config set quickReplyContacts "Alice,Bob"` — a click sends that conversation to two cloud models and returns text only, never sending for you). The last item in that menu tucks the ball away; the tray icon brings it back. Running a tool **never pops up a console window**, and the window recovers by itself after the assistant restarts - you do not have to reopen it.
233
240
 
234
241
  Then just talk to ClawBot on your phone. The assistant queries local chats and favorites to answer, with memory that survives across sessions:
235
242
 
package/README.md CHANGED
@@ -98,7 +98,10 @@ Linux 的自动初始化能力取决于微信发行版、Python 依赖和当前
98
98
  | AI 协作 | 通过 MCP 把文章、知识库、日报和受控的本地数据能力交给兼容客户端;工具清单以 `weflow-cli mcp-config` 和 `docs/MCP.md` 为准。 |
99
99
  | 个人回顾 | 聊天月报、年度报告、待办提取与朋友圈本地缓存查询。 |
100
100
  | 微信收藏 | 读取微信"收藏"内容(公众号文章、文字、图片、视频、聊天记录),支持类型过滤、关键词搜索与 Markdown/JSON 导出。 |
101
- | 第二大脑 Agent | 在微信里和本地 AI 助手对话:自然语言查询聊天记录、收藏、朋友圈、日报、微信读书、待办与知识库;三层记忆跨会话延续,守护进程常驻后台。 |
101
+ | 第二大脑 Agent | 在微信里和本地 AI 助手对话:自然语言查询聊天记录、收藏、朋友圈、日报、微信读书、待办与知识库;三层记忆跨会话延续,守护进程常驻后台。也能**抓一篇公众号正文**、在公开文章里搜、把 Markdown 排成公众号格式、列通讯录、读学习回顾、体检知识库与技能包、把待办标记完成(**不能删除**)。它调得到的工具与不可达的边界见 `docs/PROJECT_STATE.md` 与 `docs/DECISIONS.md`(D-058)。 |
102
+ | 本机面板 | 桌面常驻的悬浮球:点开即对话(与微信里**同一个大脑**、同一份记忆与配额),每条回答下面可展开**「思考过程」**(它调了什么工具、几轮、为什么停),球本身有状态表情、透明底。 |
103
+ | 技能包 | 助手能读你机器上已有的 `SKILL.md` 技能包(默认扫 `~/.claude/skills` 与 `~/.weflow-cli/skills`):提示词里列出有哪些,要用时读正文再照着做。**技能不给助手增加任何工具**——它只是资料,不执行、不安装、读不到配置和数据库。`weflow-cli skill check` 报出读不出来、跨目录同名和 id 不合规的技能。 |
104
+ | 会话场景 | 给某个会话一套固定的做法:关键词 + 附加指令 + 输出规范 + 所需技能。消息进来按「绑定 → 关键词 → 上次用过的」挑一个,都没中就完全按原来的行为。用 `weflow-cli scene` 管理,或在对话里发「场景 <id>」绑定。**场景不让模型读写**,本次用了哪个场景会记在「思考过程」里。 |
102
105
 
103
106
  > **⚠️ 合法使用边界**:本工具仅限访问**你自己拥有并登录的微信账号**在自己设备上的数据。未经他人明确同意,不得用它监控配偶、员工或任何第三方,也不得部署到他人设备或远程静默运行。请同时留意微信服务条款、账号风控和当地法律要求。详见 [SECURITY.md](SECURITY.md)。
104
107
 
@@ -222,7 +225,14 @@ weflow-cli panel # 打开本机面板窗口 (不扫码也能用)
222
225
  悬浮球需要 Electron(`npm i -g electron`);没装的话会降级成 Edge/Chrome 的 `--app` 小窗,
223
226
  界面一样,但没有无边框置顶、没有托盘与快捷键。有球时:**点一下它在旁边展开对话气泡
224
227
  (球不动,再点一下收起)**;气泡默认开在球的左边,左边放不下会自己翻到右边,上方不够就
225
- 改成顶对齐;拖动球可以换位置(位置会记住)。**在面板里点右键**可以给名单上的人快速起草回复(`weflow-cli config set quickReplyContacts "某人,另一人"`;点一下就是把那段对话发给两个云端模型,只产出文本、不会替你发送),菜单最后一项能把球收起来(点托盘图标叫回来)。
228
+ 改成顶对齐;拖动球可以换位置(位置会记住)。**球是透明底的,只有吉祥物本身**——空闲时一脸
229
+ 平静,**在你等它回答的那几秒换成"思考"的表情**(连不上、额度用完时各有各的脸)。
230
+
231
+ **每条回答下面可以展开「思考过程」(默认收起)**:里面是它这一轮怎么走的——路由判断、
232
+ 调了哪个工具、参数摘要、往返几次、为什么结束。在微信里发「轨迹」(或「思考过程」)看的是
233
+ 同一份记录,两处不会各说各话。
234
+
235
+ **在面板里点右键**可以给名单上的人快速起草回复(`weflow-cli config set quickReplyContacts "某人,另一人"`;点一下就是把那段对话发给两个云端模型,只产出文本、不会替你发送),菜单最后一项能把球收起来(点托盘图标叫回来)。助手跑工具时**不会弹出黑色命令行窗口**;助手重启之后窗口也会自己恢复,不用重新打开。
226
236
 
227
237
  之后在手机微信的 ClawBot 对话里直接说话,助手会自动查询本地聊天记录和收藏作答,并具备跨会话记忆:
228
238
 
@@ -46,6 +46,8 @@ const electronArgs = ['-e', script, '--', ...args];
46
46
 
47
47
  try {
48
48
  execFileSync(electronPath, electronArgs, {
49
+ // 从无控制台的父进程(守护进程/托盘)跑起来时,不写这个 Windows 会弹一个命令行窗口
50
+ windowsHide: true,
49
51
  stdio: 'inherit',
50
52
  env,
51
53
  cwd: process.cwd()