weflow-cli 1.6.4 → 1.8.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 (205) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/CHANGELOG.md +1966 -0
  3. package/OPERATIONS.md +808 -0
  4. package/README.en.md +93 -7
  5. package/README.md +31 -6
  6. package/bin/weflow-cli.ts +1245 -26
  7. package/dist/bin/weflow-cli.js +1349 -72
  8. package/dist/bin/weflow-cli.js.map +1 -1
  9. package/dist/mcp-server/index.js +39 -13
  10. package/dist/mcp-server/index.js.map +1 -1
  11. package/dist/src/core/appMsgFormat.d.ts +37 -0
  12. package/dist/src/core/appMsgFormat.d.ts.map +1 -0
  13. package/dist/src/core/appMsgFormat.js +106 -0
  14. package/dist/src/core/appMsgFormat.js.map +1 -0
  15. package/dist/src/core/ntCore.d.ts +43 -0
  16. package/dist/src/core/ntCore.d.ts.map +1 -1
  17. package/dist/src/core/ntCore.js +31 -0
  18. package/dist/src/core/ntCore.js.map +1 -1
  19. package/dist/src/core/sqlcipherCore.d.ts +0 -12
  20. package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
  21. package/dist/src/core/sqlcipherCore.js +5 -64
  22. package/dist/src/core/sqlcipherCore.js.map +1 -1
  23. package/dist/src/core/wcdbCore.d.ts +1 -1
  24. package/dist/src/core/wcdbCore.d.ts.map +1 -1
  25. package/dist/src/core/wcdbCore.js +5 -1
  26. package/dist/src/core/wcdbCore.js.map +1 -1
  27. package/dist/src/panel/client.d.ts +25 -0
  28. package/dist/src/panel/client.d.ts.map +1 -0
  29. package/dist/src/panel/client.js +34 -0
  30. package/dist/src/panel/client.js.map +1 -0
  31. package/dist/src/panel/endpoint.d.ts +45 -0
  32. package/dist/src/panel/endpoint.d.ts.map +1 -0
  33. package/dist/src/panel/endpoint.js +132 -0
  34. package/dist/src/panel/endpoint.js.map +1 -0
  35. package/dist/src/panel/launch.d.ts +50 -0
  36. package/dist/src/panel/launch.d.ts.map +1 -0
  37. package/dist/src/panel/launch.js +74 -0
  38. package/dist/src/panel/launch.js.map +1 -0
  39. package/dist/src/panel/server.d.ts +23 -0
  40. package/dist/src/panel/server.d.ts.map +1 -0
  41. package/dist/src/panel/server.js +413 -0
  42. package/dist/src/panel/server.js.map +1 -0
  43. package/dist/src/panel/userId.d.ts +37 -0
  44. package/dist/src/panel/userId.d.ts.map +1 -0
  45. package/dist/src/panel/userId.js +56 -0
  46. package/dist/src/panel/userId.js.map +1 -0
  47. package/dist/src/services/assistantDaemon.d.ts +20 -3
  48. package/dist/src/services/assistantDaemon.d.ts.map +1 -1
  49. package/dist/src/services/assistantDaemon.js +46 -9
  50. package/dist/src/services/assistantDaemon.js.map +1 -1
  51. package/dist/src/services/assistantEval.d.ts +141 -0
  52. package/dist/src/services/assistantEval.d.ts.map +1 -0
  53. package/dist/src/services/assistantEval.js +497 -0
  54. package/dist/src/services/assistantEval.js.map +1 -0
  55. package/dist/src/services/assistantMemory.d.ts +106 -4
  56. package/dist/src/services/assistantMemory.d.ts.map +1 -1
  57. package/dist/src/services/assistantMemory.js +396 -61
  58. package/dist/src/services/assistantMemory.js.map +1 -1
  59. package/dist/src/services/assistantPrivacy.d.ts +15 -2
  60. package/dist/src/services/assistantPrivacy.d.ts.map +1 -1
  61. package/dist/src/services/assistantPrivacy.js +18 -4
  62. package/dist/src/services/assistantPrivacy.js.map +1 -1
  63. package/dist/src/services/assistantRouter.d.ts +41 -0
  64. package/dist/src/services/assistantRouter.d.ts.map +1 -0
  65. package/dist/src/services/assistantRouter.js +151 -0
  66. package/dist/src/services/assistantRouter.js.map +1 -0
  67. package/dist/src/services/assistantService.d.ts +209 -3
  68. package/dist/src/services/assistantService.d.ts.map +1 -1
  69. package/dist/src/services/assistantService.js +590 -75
  70. package/dist/src/services/assistantService.js.map +1 -1
  71. package/dist/src/services/assistantTools.d.ts +205 -1
  72. package/dist/src/services/assistantTools.d.ts.map +1 -1
  73. package/dist/src/services/assistantTools.js +1089 -58
  74. package/dist/src/services/assistantTools.js.map +1 -1
  75. package/dist/src/services/assistantTrace.d.ts +47 -0
  76. package/dist/src/services/assistantTrace.d.ts.map +1 -0
  77. package/dist/src/services/assistantTrace.js +150 -0
  78. package/dist/src/services/assistantTrace.js.map +1 -0
  79. package/dist/src/services/chatService.d.ts +13 -0
  80. package/dist/src/services/chatService.d.ts.map +1 -1
  81. package/dist/src/services/chatService.js +23 -0
  82. package/dist/src/services/chatService.js.map +1 -1
  83. package/dist/src/services/configService.d.ts +9 -0
  84. package/dist/src/services/configService.d.ts.map +1 -1
  85. package/dist/src/services/configService.js +15 -8
  86. package/dist/src/services/configService.js.map +1 -1
  87. package/dist/src/services/messageQuery.d.ts +35 -0
  88. package/dist/src/services/messageQuery.d.ts.map +1 -1
  89. package/dist/src/services/messageQuery.js +61 -0
  90. package/dist/src/services/messageQuery.js.map +1 -1
  91. package/dist/src/services/pythonBridge.d.ts +41 -0
  92. package/dist/src/services/pythonBridge.d.ts.map +1 -0
  93. package/dist/src/services/pythonBridge.js +119 -0
  94. package/dist/src/services/pythonBridge.js.map +1 -0
  95. package/dist/src/services/syncService.d.ts +127 -0
  96. package/dist/src/services/syncService.d.ts.map +1 -0
  97. package/dist/src/services/syncService.js +263 -0
  98. package/dist/src/services/syncService.js.map +1 -0
  99. package/dist/src/services/syncState.d.ts +113 -0
  100. package/dist/src/services/syncState.d.ts.map +1 -0
  101. package/dist/src/services/syncState.js +214 -0
  102. package/dist/src/services/syncState.js.map +1 -0
  103. package/dist/src/services/wechatMessageService.d.ts +25 -0
  104. package/dist/src/services/wechatMessageService.d.ts.map +1 -1
  105. package/dist/src/services/wechatMessageService.js +69 -7
  106. package/dist/src/services/wechatMessageService.js.map +1 -1
  107. package/dist/src/utils/atomicWrite.d.ts +4 -0
  108. package/dist/src/utils/atomicWrite.d.ts.map +1 -0
  109. package/dist/src/utils/atomicWrite.js +60 -0
  110. package/dist/src/utils/atomicWrite.js.map +1 -0
  111. package/dist/src/utils/dateRange.d.ts +15 -0
  112. package/dist/src/utils/dateRange.d.ts.map +1 -1
  113. package/dist/src/utils/dateRange.js +47 -0
  114. package/dist/src/utils/dateRange.js.map +1 -1
  115. package/dist/src/utils/text.d.ts +18 -0
  116. package/dist/src/utils/text.d.ts.map +1 -0
  117. package/dist/src/utils/text.js +20 -0
  118. package/dist/src/utils/text.js.map +1 -0
  119. package/docs/AI_INTERFACE.md +2 -0
  120. package/docs/DECISIONS.md +1484 -20
  121. package/docs/EXTENDING.md +187 -0
  122. package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +338 -0
  123. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -0
  124. package/docs/MCP.md +23 -4
  125. package/docs/PROJECT_STATE.md +299 -96
  126. package/docs/SETUP.md +22 -0
  127. package/docs/SYNC_CONTRACT.md +180 -0
  128. package/mcp-server/index.ts +681 -657
  129. package/package.json +84 -83
  130. package/resources/panel/ball-position.cjs +153 -0
  131. package/resources/panel/index.html +46 -0
  132. package/resources/panel/main.cjs +447 -0
  133. package/resources/panel/mascot-focus.png +0 -0
  134. package/resources/panel/mascot-happy.png +0 -0
  135. package/resources/panel/mascot-sorry.png +0 -0
  136. package/resources/panel/mascot-tired.png +0 -0
  137. package/resources/panel/mascot.png +0 -0
  138. package/resources/panel/package.json +7 -0
  139. package/resources/panel/panel.css +407 -0
  140. package/resources/panel/preload.cjs +72 -0
  141. package/resources/panel/quick-menu.cjs +197 -0
  142. package/resources/panel/renderer.js +441 -0
  143. package/resources/panel/tray-menu.cjs +40 -0
  144. package/resources/panel/tray.png +0 -0
  145. package/scripts/_utils.py +403 -54
  146. package/scripts/annual_report.py +4 -2
  147. package/scripts/article_notes.py +423 -0
  148. package/scripts/backfill_articles.py +438 -0
  149. package/scripts/biz_daily.py +590 -105
  150. package/scripts/chat_notes.py +470 -0
  151. package/scripts/classify_daily.py +10 -18
  152. package/scripts/compile_wiki.py +1059 -46
  153. package/scripts/create_reading_notes.py +199 -14
  154. package/scripts/decide.py +253 -0
  155. package/scripts/draft_reply.py +671 -0
  156. package/scripts/export_chat_html.py +212 -46
  157. package/scripts/extract_todos.py +31 -5
  158. package/scripts/fav_notes.py +318 -0
  159. package/scripts/fix_topics.py +2 -1
  160. package/scripts/generate_ai_report.py +214 -9
  161. package/scripts/generate_html.py +18 -3
  162. package/scripts/generate_review.py +2 -2
  163. package/scripts/jev_client.py +294 -0
  164. package/scripts/jev_probe.py +440 -0
  165. package/scripts/nt_common.py +82 -0
  166. package/scripts/nt_decrypt.py +1755 -1352
  167. package/scripts/pipeline.py +27 -2
  168. package/scripts/promote_ideas.py +19 -4
  169. package/scripts/quality_eval.py +707 -0
  170. package/scripts/rag_chat.py +4 -4
  171. package/scripts/read_image.py +187 -0
  172. package/scripts/reply_debt.py +488 -0
  173. package/scripts/route_cards.py +453 -0
  174. package/scripts/semantic_search.py +208 -49
  175. package/scripts/user_notes.py +315 -0
  176. package/scripts/vault_rag.py +13 -7
  177. package/scripts/vault_search.py +21 -4
  178. package/scripts/wiki_lint.py +397 -0
  179. package/src/core/appMsgFormat.ts +117 -0
  180. package/src/core/ntCore.ts +67 -0
  181. package/src/core/sqlcipherCore.ts +5 -61
  182. package/src/core/wcdbCore.ts +6 -2
  183. package/src/panel/client.ts +49 -0
  184. package/src/panel/endpoint.ts +144 -0
  185. package/src/panel/launch.ts +101 -0
  186. package/src/panel/server.ts +424 -0
  187. package/src/panel/userId.ts +72 -0
  188. package/src/services/assistantDaemon.ts +57 -9
  189. package/src/services/assistantEval.ts +611 -0
  190. package/src/services/assistantMemory.ts +413 -57
  191. package/src/services/assistantPrivacy.ts +18 -4
  192. package/src/services/assistantRouter.ts +191 -0
  193. package/src/services/assistantService.ts +671 -74
  194. package/src/services/assistantTools.ts +1118 -57
  195. package/src/services/assistantTrace.ts +175 -0
  196. package/src/services/chatService.ts +30 -0
  197. package/src/services/configService.ts +403 -385
  198. package/src/services/messageQuery.ts +97 -0
  199. package/src/services/pythonBridge.ts +139 -0
  200. package/src/services/syncService.ts +363 -0
  201. package/src/services/syncState.ts +275 -0
  202. package/src/services/wechatMessageService.ts +73 -7
  203. package/src/utils/atomicWrite.ts +56 -0
  204. package/src/utils/dateRange.ts +46 -0
  205. package/src/utils/text.ts +20 -0
@@ -0,0 +1,187 @@
1
+ # Extending WeFlow CLI
2
+
3
+ **Status: this is a checklist, not a plugin system.** There is no plugin loader, no third-party import surface,
4
+ and no `exports` field in `package.json` (`bin` is the only entry point). So "extending" this project today means
5
+ one of two things, and they are not equivalent:
6
+
7
+ 1. **In-repo** - add the capability to this source tree (fork + PR). Every recipe below is about this path, and each
8
+ one lists the places you must touch **and the test that will catch you if you miss one**.
9
+ 2. **Out-of-process** - call the project from a program you keep elsewhere, through **MCP** (`mcp-server/`, 27 tools)
10
+ or the CLI's `--json` surface. This is the only boundary that exists for code that does not live in this repository.
11
+
12
+ ## Why there is no plugin loader
13
+
14
+ A plugin that runs inside this process would get, by construction, the same reach as the code it joins: the decrypted
15
+ database path, the config (including every API key), the filesystem, and the message channel. The project's standing
16
+ constraint is that **sending, assistant, MCP and cloud AI all keep an explicit permission boundary** - so the boundary
17
+ that must be crossed to extend this thing is a process boundary, not a function call. MCP is that boundary: it is
18
+ enumerable (`capabilities --json` lists what is exposed), scoped (per-tool confirmation for anything that leaves the
19
+ machine), and can be turned off without touching this repository.
20
+
21
+ A plugin/adapter mechanism is on the roadmap on purpose, with its constraint written down: third-party extensions must
22
+ not read configuration, databases or arbitrary files directly, and must be disableable individually
23
+ (`docs/ROADMAP.md`, "插件/适配器机制"). It is not implemented.
24
+
25
+ ## Recipe A - a new assistant tool
26
+
27
+ The most common extension. All of it is in `src/services/assistantTools.ts` unless noted.
28
+
29
+ | Step | Where | If you skip it |
30
+ | --- | --- | --- |
31
+ | 1. Declare it | `TOOL_DEFS` | The model never sees it - dead code. A test fails. |
32
+ | 2. Implement it | `executeTool`, one `case '<name>':` | The model sees it, calls it, and gets `(未知工具: <name>)`. A test fails. |
33
+ | 3. Config prerequisite (only if it cannot run without a key) | `TOOL_REQUIREMENTS` - one entry, `{ key, why }` | The tool is offered on machines where it cannot work. That is the failure this project already measured once: a model tried both search tools, both errored, and the answer said "both search tools are broken". |
34
+ | 4. Cloud egress (only if user data leaves the machine) | In the handler: `if (ctx.requiresConfirm && args.confirm !== true) return confirmPreview(...)`; then claim it in `bin/weflow-cli.ts` under `safety.mcpSurface.callsCloudModels` **and** `requiresConfirm` | An external MCP caller sends the user's data with no preview. Tests fail - the assertion counts `ctx.requiresConfirm` occurrences in the source, so it cannot drift from the field. |
35
+ | 5. Keep it off MCP (only if it cannot do its job over that transport) | `MCP_EXCLUDED` - one entry whose **value is the reason** | It is advertised on a transport where it silently cannot work (`look_at_image` is the worked example). |
36
+ | 6. Coverage note | The header of `test/assistant-tools-branches.test.ts` states how many tools are executed; update it | The coverage claim becomes false while still reading as true. |
37
+
38
+ Notes that are easy to get wrong:
39
+
40
+ - **The description is the interface.** The model decides whether to call the tool from the description alone - keep it
41
+ a sentence about *when* to use it, not a restatement of the name.
42
+ - **`parameters` must be a JSON-schema `object`.** An empty `properties` is fine for a no-argument tool.
43
+ - **Failures are `(...)`**, successes are not. `producedContent()` in the same file reads that convention, and the
44
+ trace uses it. A success message that starts with a bracket has to be added to `PAREN_SUCCESS_PREFIXES` or it will be
45
+ reported as "no content".
46
+ - **MCP needs no change.** The MCP surface is *derived* from `TOOL_DEFS` (`MCP_TOOL_DEFS`) - adding a tool adds it to
47
+ both paths at once. That is deliberate, and it is also why the surface is not read-only: see `docs/MCP.md`.
48
+
49
+ ## Recipe B - a new config key
50
+
51
+ A key must appear in **five** places, and each one fails differently:
52
+
53
+ | Where | Failure if missing |
54
+ | --- | --- |
55
+ | `src/services/configService.ts` - the `CliConfig` interface | Type errors elsewhere; the compiler catches this one. |
56
+ | same file - the default literal | - |
57
+ | same file - the `load()` mapping | The value silently disappears on the next start. The user sees "I set that already". |
58
+ | same file - the `clear()` reset | `config reset` **loses** the old value and `getAll()` then reports it as never set. |
59
+ | `bin/weflow-cli.ts` - `configurableKeys` | `config set` refuses the key outright. This one happened: `dashscopeApiKey`. |
60
+
61
+ `test/config-keys.test.ts` reads all five and fails if a key is missing from any of them. It is a static text
62
+ assertion, so changing the *format* of those literals turns it red - which is the intent, not a nuisance.
63
+
64
+ Keys that hold secrets belong in `ENCRYPTED_KEYS` in the same file (they are stored encrypted at rest; reading the raw
65
+ file shows ciphertext, not the value).
66
+
67
+ ## Recipe C - a new CLI command
68
+
69
+ Two places in `bin/weflow-cli.ts`: the `program.command('...')` registration, and - if it should appear in the
70
+ interactive menu - `showInteractiveMenu()` plus the `switch (action)` that maps a menu choice back to the command.
71
+
72
+ `test/cli-menu.test.ts` keeps those in sync: menu entries and `switch` cases must match **in both directions**, and
73
+ every `runCmd('x')` / `runSubCmd('p','c')` the menu calls must name a command that actually exists (that test asks the
74
+ CLI's own `--help`, because the source cannot tell top-level commands from subcommands - 99 `.command('...')` call sites
75
+ as of 2026-09-28, against however many real commands `--help` reports). This matters because both failure modes are silent: a menu entry with no `case` does nothing
76
+ when picked, and `runCmd` is written as `if (cmd) await ...`, so a renamed command makes the entry do nothing too.
77
+
78
+ Two conventions that hold across the command surface: every command that a script or an AI might drive has a `--json`
79
+ mode, and anything destructive is two-phase (`--dry-run` preview, then `--yes`).
80
+
81
+ ## Recipe D - a new hand-written MCP tool
82
+
83
+ Three places: the `ListTools` table in `mcp-server/index.ts`, a `case` in its `CallTool` switch, and the table in
84
+ `docs/MCP.md`. `test/tool-registry.test.ts` compares the doc table against the actually-served set (hand-written +
85
+ derived) and fails on a missing row, an extra row, or a duplicate - all three have happened or nearly happened.
86
+
87
+ Prefer a **derived** tool (Recipe A) unless it needs something the assistant layer cannot provide. The hand-written
88
+ list exists for tools with no assistant counterpart (article fetching, the versioned `weflow-message/v1` export).
89
+
90
+ ## Recipe E - a new Python workflow
91
+
92
+ Scripts live in `scripts/` and are invoked through `src/services/pythonBridge.ts` (`runPythonJson` when the script
93
+ prints JSON). Three rules that this project has been bitten by:
94
+
95
+ - **Encoding: the bridge handles it, your hand-run does not.** Every script spawned through the bridge gets
96
+ `PYTHONIOENCODING=utf-8` (`src/utils/pythonProcessEnv.ts`), so stdout is UTF-8 there and `json.dumps(...,
97
+ ensure_ascii=False)` - what the scripts use - round-trips correctly. Running the same script by hand on this machine
98
+ gives a **GBK** stdout: Chinese comes out as GBK bytes (mojibake if you were expecting UTF-8) and a character
99
+ outside GBK (`✓`, an emoji) raises `UnicodeEncodeError` and kills the run. 34 of the 55 scripts call
100
+ `sys.stdout.reconfigure(encoding='utf-8', errors='replace')` in `main()` so both paths behave the same; if you add a
101
+ script you intend to run by hand, do the same. A probe that forgets to set that env var will show you a decoding bug
102
+ that does not exist in the real path.
103
+ - **Fail loudly.** Exit non-zero, or return a JSON object whose `success` is false with an `error` string. "No JSON on
104
+ stdout" must never be read as "no results" - that conflation is why `get_todos` distinguishes "no pending todos" from
105
+ "extraction has never been run".
106
+ - **State goes in over stdin, not argv.** Command lines are visible in the process list; the drafting tool passes the
107
+ conversation over stdin for exactly that reason, and `semantic_search.py` takes the query through the environment.
108
+
109
+ Tests for Python live in `test/*_test.py` and run on the same filesystem as everything else (`npm test` covers
110
+ TypeScript; `python -m unittest discover -s test -p '*_test.py'` covers these).
111
+
112
+ ## Recipe F - a new knowledge source
113
+
114
+ The knowledge base is fed by "material that carries `[[wikilinks]]`": `compile_wiki` scans a source directory
115
+ (`--source`, already a flag - no code change needed to add a source), aggregates every wikilink into concepts, and
116
+ writes concept pages into the Vault. There are four producers today, all writing the same card shape into
117
+ `output/*-notes/`: `article_notes.py` (concepts extracted from the Vault's article notes - it exists because
118
+ those notes, measured over the whole corpus, carry **no** concept links), `chat_notes.py` (conversation cards),
119
+ `fav_notes.py` (WeChat favourites, whose article bodies come from the daily line's cached fetcher rather than a
120
+ second downloader) and `user_notes.py` (the user's own hand-written notes, read-only, whose summaries are marked
121
+ as the model's reading). Adding a fifth means writing one producer that satisfies the table below; the
122
+ aggregator, the lint and the assistant-facing search do not change - the lint finds card directories with an
123
+ `output/*-notes` glob for exactly that reason.
124
+
125
+ To add another, the producer must satisfy the consumer's contract exactly:
126
+
127
+ | The consumer (`scan_articles`) needs | Notes |
128
+ | --- | --- |
129
+ | `*.md` under the source dir, with frontmatter | `title`, `source`, `topic`, `tags` are read; `README.md` is skipped |
130
+ | A `## AI 摘要` (or `## 深度解析`) section | the paragraph after it becomes the note's summary |
131
+ | `[[name]] — description` lines | anywhere in the body; the description is what gets fed to page generation, so keep it clean (no trailing labels) |
132
+
133
+ **Two rules that are not obvious from that table.** Links should be rendered by your code, not written by a model -
134
+ the consumer collects *every* wikilink in a body and cannot tell who wrote it, so a stray one invents a concept
135
+ (this is why `chat_notes.plain()` strips `[[...]]` from model-authored free text). And a wikilink whose description
136
+ is polluted - a trailing `(话题)`, a stray marker - silently degrades every concept page it feeds, because that
137
+ description is the material for the page.
138
+
139
+ **A new source also has to pick a line.** Since 2026-09-27 the concept pages live in two directories -
140
+ `Wiki/Concepts` for the article line, `Chat/Concepts` for the conversation line - so a source that writes into a
141
+ directory nobody reads is invisible, and `compile_wiki` writes whichever line you point `--output` at (one line per
142
+ run, by design). If your source belongs to an existing line, you are done; if it deserves its own, it goes into
143
+ `_utils.CONCEPT_DIRS` and into every other declaration that `test/concept-dirs-agreement.test.ts` pins (that test
144
+ counts the sites itself rather than trusting a number written here) - that test
145
+ fails until every reader of concept pages knows about the new directory, which is the point.
146
+
147
+ The guard to copy: `test/chat_notes_test.py` writes a card and then runs the **real consumer**
148
+ (`compile_wiki.scan_articles`) over it, asserting the links and descriptions come back. That is the shape of every
149
+ producer/consumer pair in this project - do not assert on the markdown text, assert that the other side reads it.
150
+
151
+ ## What will catch you
152
+
153
+ | Invariant | Test |
154
+ | --- | --- |
155
+ | Declaration table and `executeTool` cases match, **both directions**, names unique, every declaration usable | `test/tool-registry.test.ts` |
156
+ | Availability rules and the MCP exclusion table name real tools | `test/tool-registry.test.ts` |
157
+ | `docs/MCP.md`'s table equals the served MCP surface, no duplicates | `test/tool-registry.test.ts` |
158
+ | Every inbound-egress tool carries the `confirm` gate, and `capabilities --json` agrees | `test/assistant-tools.test.ts` |
159
+ | A config key exists in all five places | `test/config-keys.test.ts` |
160
+ | The MCP subset contains no write/send/publish tool | `test/assistant-tools.test.ts` |
161
+ | Panel IPC surface is exactly the declared method list | `test/panel-packaging.test.ts` |
162
+ | Interactive-menu entries match `switch` cases, and the commands they call exist | `test/cli-menu.test.ts` |
163
+ | A knowledge source's output is readable by the wiki aggregator, links and descriptions intact | `test/chat_notes_test.py`, `test/compile_wiki_test.py` |
164
+ | Every declaration of the concept directories agrees, and nothing that imports the constant stops iterating it | `test/concept-dirs-agreement.test.ts` |
165
+
166
+ ## Red lines
167
+
168
+ These hold for any extension, and the full statements live in `AGENTS.md` and `CLAUDE.md`:
169
+
170
+ - Local data is the user's. Never put database paths, keys, tokens, account identifiers, chat content, exports or real
171
+ local paths into source, docs, fixtures, issues, commits or command output.
172
+ - **Sending is structurally unreachable from a model-driven path.** No extension may add a path that sends a message,
173
+ fills an input box, or drives another application's UI on the model's behalf.
174
+ - Anything that leaves the machine goes through `privacyGate`. Redaction, not discretion.
175
+ - Do not widen an assertion or a test to make a number look better. A static check that has to be relaxed is usually
176
+ reporting a real change.
177
+ - Do not record a guess as a verified fact. When something has not been measured, say so where the claim is made.
178
+
179
+ ## What does not exist yet
180
+
181
+ Named here so that the gaps are not mistaken for omissions:
182
+
183
+ - **A plugin loader / adapter interface.** Roadmap, not implemented (see above).
184
+ - **A public import surface.** No `exports`, `main` or `types`; the published package is a CLI, and `src/` is shipped
185
+ without a stability promise.
186
+ - **A versioned extension contract.** The versioned contracts that exist are data contracts (`weflow-message/v1`,
187
+ `weflow-sync/v1`), not code contracts.
@@ -0,0 +1,338 @@
1
+ # WeFlow CLI 功能增强技术方案
2
+
3
+ > 状态:提案
4
+ >
5
+ > 更新时间:2026-09-19
6
+ >
7
+ > 适用范围:CLI、MCP、本地阅读器、聊天导出、日报、知识库和可选助手
8
+
9
+ 本文把 WeFlow CLI 下一阶段的功能改进整理为可拆分的技术方案。它补充 [ROADMAP.md](ROADMAP.md) 的方向说明,面向实现、评审和验收;已经实现的能力以 [PROJECT_STATE.md](PROJECT_STATE.md) 为准。
10
+
11
+ ## 1. 产品目标
12
+
13
+ WeFlow CLI 已经覆盖本地聊天读取、联系人与收藏、HTML/JSON/TXT/Excel 导出、公众号日报、知识库检索、RAG、只读 MCP 和 Bot 助手。下一阶段不以增加零散命令为主要目标,而是把这些能力连接成四条可恢复、可验证的工作流:
14
+
15
+ 1. **同步:** 从本地数据源读取增量变化,并明确哪些数据已覆盖、哪些数据读取失败。
16
+ 2. **整理:** 将消息、媒体、文章和笔记写入统一的本地索引,保留原始来源。
17
+ 3. **使用:** 通过搜索、阅读器、导出和 MCP 查询这些数据,并能回到原始记录。
18
+ 4. **分析:** 在用户选择的范围内调用本地或云端 AI,输出带来源的结果。
19
+
20
+ 功能完成的标准不是命令数量,而是用户可以在失败后继续运行、在结果中看到覆盖边界,并能验证 AI 或导出结果来自哪些原始资料。
21
+
22
+ ## 2. 现状与功能缺口
23
+
24
+ | 能力 | 当前基础 | 需要补齐的功能 |
25
+ | --- | --- | --- |
26
+ | 本地数据 | 支持会话、联系人、消息、收藏、朋友圈缓存和多种导出;已处理部分消息分片 | 增量同步、稳定游标、覆盖报告、失败分片重试和统一索引 |
27
+ | 媒体导出 | 支持图片、表情、视频、链接卡片,部分场景支持语音转写 | 媒体清单、缺失原因、去重、导出断点和可迁移资料包 |
28
+ | 日报阅读 | 支持来源筛选、分类、摘要、HTML 阅读器和收藏同步 | 来源健康、抓取重试、文章去重、阅读状态和离线归档 |
29
+ | 搜索与知识库 | 支持语义搜索、RAG、Vault 和阅读笔记 | 混合检索、增量索引、引用定位、结果解释和评估集 |
30
+ | MCP | 默认只读、有限制、使用版本化消息契约 | 稳定游标、更多只读资源、统一错误码和任务状态查询 |
31
+ | 助手 | 有隐私模式、白名单、Bot 通道和确认边界 | 收件箱/发件箱、幂等发送、投递状态和会话级记忆控制 |
32
+
33
+ ## 3. 优先级和交付顺序
34
+
35
+ ### P0:数据可信和任务可恢复
36
+
37
+ 先完成增量同步、统一任务状态和导出覆盖报告。这一阶段解决“有没有读全”“导出到哪一步”“缺的媒体是什么”三个问题。
38
+
39
+ ### P1:检索和阅读闭环
40
+
41
+ 在可信数据基础上完成混合搜索、引用定位、日报增量抓取和离线阅读。此阶段应让用户能够从一篇 AI 摘要或一个搜索结果返回到原始消息或文章。
42
+
43
+ ### P2:受控 AI 和只读生态
44
+
45
+ 统一 AI 执行层、MCP 增量接口和助手收发队列。所有云端分析和外发操作继续沿用预览、确认和隐私模式。
46
+
47
+ ### P3:迁移和平台扩展
48
+
49
+ 最后增加资料包迁移、Markdown/Obsidian 等互操作格式,以及 Linux/macOS 的能力适配。平台扩展必须建立在能力检测和明确降级结果上。
50
+
51
+ ## 4. 统一数据同步和索引
52
+
53
+ ### 4.1 模块划分
54
+
55
+ 建议把同步流程拆成以下模块:
56
+
57
+ | 模块 | 责任 |
58
+ | --- | --- |
59
+ | `SourceAdapter` | 从 NT 数据库、传统数据库、日报目录或文章源读取记录 |
60
+ | `Normalizer` | 将不同来源转换为统一记录,保留未知字段 |
61
+ | `IdentityResolver` | 生成消息、文章和媒体的稳定身份,拒绝不可靠匹配 |
62
+ | `CheckpointStore` | 保存分片、时间窗、游标和失败重试状态 |
63
+ | `LocalIndex` | 写入全文索引、向量索引和元数据索引 |
64
+ | `CoverageReporter` | 统计读取范围、缺失分片、媒体覆盖和重复记录 |
65
+
66
+ ### 4.2 消息身份
67
+
68
+ 在稳定游标尚未得到所有后端支持前,使用组合身份并允许重叠读取:
69
+
70
+ ```text
71
+ messageKey = source + talker + shard + localId + serverId + createTime
72
+ ```
73
+
74
+ 其中 `serverId` 为非零值时优先使用;`localId` 不能单独作为全局身份。时间窗同步应保留少量重叠范围,由本地去重保证边界消息不丢失。
75
+
76
+ ### 4.3 同步状态示例
77
+
78
+ 状态文件不得包含密钥、完整聊天正文或无必要的本地绝对路径。可以保存如下信息:
79
+
80
+ ```json
81
+ {
82
+ "schema": "weflow-sync/v1",
83
+ "source": "wechat-nt",
84
+ "scope": "示例联系人",
85
+ "lastSuccessfulRun": "2026-09-19T09:00:00+08:00",
86
+ "coveredFrom": "2026-09-01T00:00:00+08:00",
87
+ "coveredTo": "2026-09-19T08:59:59+08:00",
88
+ "recordsRead": 1200,
89
+ "recordsDeduplicated": 14,
90
+ "shardsRead": 4,
91
+ "shardsFailed": 0,
92
+ "mayHaveMore": false,
93
+ "warnings": []
94
+ }
95
+ ```
96
+
97
+ ### 4.4 建议入口
98
+
99
+ 具体命令名称可以由实现者决定,但应覆盖以下能力:
100
+
101
+ - `sync --dry-run --json`:只返回范围、预计记录数和风险标记;
102
+ - `sync run`:执行全量或增量同步;
103
+ - `sync status --json`:查看最后一次运行、覆盖范围和失败项;
104
+ - `sync retry`:只重跑失败分片或失败来源;
105
+ - `sync verify`:检查索引与原始数据的时间边界和计数摘要。
106
+
107
+ ## 5. 统一任务中心
108
+
109
+ 导出、日报、索引、语音转写和知识处理应共享任务模型,而不是各自实现一套进度逻辑。
110
+
111
+ ### 5.1 生命周期
112
+
113
+ ```text
114
+ planned → waiting_confirmation → running → paused
115
+ ↘ completed
116
+ ↘ failed
117
+ ↘ cancelled
118
+ ```
119
+
120
+ 任务必须具备稳定的 `jobId` 和幂等键。重复提交同一范围的任务时,应返回已有任务或明确创建新任务,不能悄悄重复写入。
121
+
122
+ ### 5.2 任务记录
123
+
124
+ ```json
125
+ {
126
+ "schema": "weflow-job/v1",
127
+ "jobId": "job_20260919_0001",
128
+ "kind": "export.html",
129
+ "state": "running",
130
+ "createdAt": "2026-09-19T09:00:00+08:00",
131
+ "updatedAt": "2026-09-19T09:02:10+08:00",
132
+ "progress": { "completed": 800, "total": 1200 },
133
+ "resumeable": true,
134
+ "result": { "records": 800, "mediaMissing": 3 },
135
+ "error": null
136
+ }
137
+ ```
138
+
139
+ 任务输出应区分“已完成”“部分完成”和“失败”。部分完成不能返回成功状态,也不能删除已生成的可用结果。
140
+
141
+ ### 5.3 用户体验
142
+
143
+ 所有长任务至少提供:
144
+
145
+ - 预览和预计影响;
146
+ - 进度、当前阶段和耗时;
147
+ - 暂停、恢复、取消;
148
+ - 失败原因和可重试动作;
149
+ - 人类可读输出与机器可读 JSON 输出。
150
+
151
+ ## 6. 搜索、引用和知识库
152
+
153
+ ### 6.1 混合检索流程
154
+
155
+ ```text
156
+ 用户查询
157
+ → 查询解析(关键词、日期、联系人、会话、消息类型)
158
+ → 全文检索 + 向量检索
159
+ → 元数据过滤
160
+ → 去重和重排
161
+ → 片段裁剪
162
+ → 引用生成
163
+ → AI 回答或结构化结果
164
+ ```
165
+
166
+ 关键词检索负责精确命中姓名、编号和原句;向量检索负责语义相近内容。两者都必须受到日期、会话和结果数量限制。
167
+
168
+ ### 6.2 引用对象
169
+
170
+ 每条引用至少包含:
171
+
172
+ ```json
173
+ {
174
+ "sourceType": "wechat-message",
175
+ "sourceId": "messageKey",
176
+ "talker": "示例联系人",
177
+ "createdAt": "2026-09-19T08:30:00+08:00",
178
+ "snippet": "经脱敏或用户允许的原文片段",
179
+ "locator": { "export": "示例联系人", "part": 1 }
180
+ }
181
+ ```
182
+
183
+ AI 输出中的事实性结论应能回到一个或多个引用;没有引用的内容应标记为推断或建议。原始消息和 AI 生成文本必须分开保存。
184
+
185
+ ### 6.3 索引更新
186
+
187
+ - 只对新增或变更记录建立索引;
188
+ - 内容哈希变化时重建对应条目;
189
+ - 删除或失效记录进入 tombstone,避免旧结果继续出现;
190
+ - 向量模型变化时支持新旧索引并行切换;
191
+ - 建立不含真实聊天内容的评估集,测试召回率、引用准确率和重复率。
192
+
193
+ ## 7. 导出资料包和媒体覆盖
194
+
195
+ ### 7.1 导出配置
196
+
197
+ 导出应从单一命令扩展为可保存的配置:
198
+
199
+ - 会话或联系人范围;
200
+ - 日期范围;
201
+ - 输出格式;
202
+ - 是否包含图片、表情、视频、语音和远程封面;
203
+ - 是否脱敏;
204
+ - 每页条数和媒体大小上限;
205
+ - 离线模式或允许远程读取。
206
+
207
+ ### 7.2 媒体清单
208
+
209
+ 每个媒体项应记录状态,而不是只在页面中显示占位符:
210
+
211
+ ```json
212
+ {
213
+ "mediaKey": "content-or-message-identity",
214
+ "kind": "image",
215
+ "status": "embedded",
216
+ "mime": "image/jpeg",
217
+ "bytes": 123456,
218
+ "sha256": "redacted-example",
219
+ "reason": null
220
+ }
221
+ ```
222
+
223
+ 推荐状态:`embedded`、`cached`、`remote-fetched`、`missing`、`untrusted-match`、`unsupported`。无法可靠匹配时必须保留缺失原因,不能用相邻消息或裸 `localId` 猜测媒体。
224
+
225
+ ### 7.3 离线资料包
226
+
227
+ 资料包建议包含:
228
+
229
+ ```text
230
+ manifest.json
231
+ messages.json
232
+ index.html
233
+ media/
234
+ attachments/
235
+ README.txt
236
+ ```
237
+
238
+ `manifest.json` 保存数据范围、生成时间、版本、覆盖统计和脱敏选项;不得保存数据库密钥、完整本地路径或未授权的配置内容。
239
+
240
+ ## 8. 日报和本地阅读器
241
+
242
+ ### 8.1 来源状态
243
+
244
+ 每个公众号或文章来源应有本地状态:启用、停用、最近成功时间、最近失败原因、连续失败次数和最后抓取文章时间。状态用于提醒用户,不应自动扩大抓取范围。
245
+
246
+ ### 8.2 文章去重
247
+
248
+ 优先使用文章 URL,其次使用来源、标题、发布时间和正文哈希组合判断重复。重复文章应保留来源关系,但不重复生成日报和向量索引。
249
+
250
+ ### 8.3 阅读状态
251
+
252
+ 阅读器可以增加未读、已读、稍后阅读、收藏和笔记关联。所有状态保存在本地,刷新、重新生成 HTML 或重启服务后仍能恢复。
253
+
254
+ ## 9. 受控 AI 执行层
255
+
256
+ 统一 AI 配置应至少包含:
257
+
258
+ | 配置 | 作用 |
259
+ | --- | --- |
260
+ | provider/model | 选择云端或本地模型 |
261
+ | privacyMode | `strict`、`balanced`、`local` |
262
+ | inputLimit | 限制消息数量、字符数或 token 数 |
263
+ | timeLimit | 限制单任务耗时 |
264
+ | budget | 限制调用次数或费用 |
265
+ | outputSchema | 约束结构化输出 |
266
+ | citationRequired | 要求返回引用 |
267
+
268
+ AI 调用前应生成不含正文的预览,说明数据范围、是否联网、预计规模和写入位置。云端调用失败时,保留本地抓取和无 AI 结果,不把整个工作流判定为不可用。
269
+
270
+ ## 10. MCP 和助手功能
271
+
272
+ ### 10.1 MCP 只读扩展
273
+
274
+ 优先扩展以下只读能力:
275
+
276
+ - 会话发现和唯一名称解析;
277
+ - 分页读取消息和覆盖状态;
278
+ - 联系人、收藏、日报和知识条目查询;
279
+ - 导出任务创建、查询和结果摘要;
280
+ - 能力、契约和限制发现。
281
+
282
+ 分页建议使用不透明游标,响应带 `nextCursor`、`returnedCount`、`mayHaveMore` 和请求范围。错误响应使用稳定错误码,避免让客户端解析中文错误文本。
283
+
284
+ ### 10.2 Bot 助手收发队列
285
+
286
+ 助手只围绕已验证的 Bot 通道增强,不扩展为个人微信账号自动化。建议引入:
287
+
288
+ - 入站事件 ID 和去重表;
289
+ - 出站消息队列;
290
+ - `queued`、`sent`、`delivered`、`failed` 状态;
291
+ - 重试次数和最后失败原因;
292
+ - 会话级记忆开关和数据范围;
293
+ - 外发前的预览和确认。
294
+
295
+ ## 11. 验收指标
296
+
297
+ 每个阶段至少记录以下指标:
298
+
299
+ | 指标 | 目标 |
300
+ | --- | --- |
301
+ | 增量同步重复率 | 重复读取可被本地去重,不产生重复索引 |
302
+ | 分片覆盖率 | 可报告成功、失败和未扫描分片 |
303
+ | 媒体错配率 | 可靠匹配失败时宁缺毋滥,禁止跨会话错配 |
304
+ | 导出可恢复率 | 中断后从最近检查点继续,不必从头开始 |
305
+ | 搜索引用准确率 | 引用能定位到实际返回片段 |
306
+ | 日报重复率 | 同一文章重复运行不重复生成内容 |
307
+ | AI 数据边界 | 每次云端调用均可说明输入范围和隐私模式 |
308
+ | MCP 响应边界 | 结果数量、时间范围和字段均受限制 |
309
+
310
+ ## 12. 风险和明确不做的事
311
+
312
+ - 不使用裸 `localId`、相邻记录或模糊名称猜测消息和媒体身份。
313
+ - 不把数据库路径、密钥、完整聊天正文或任意本地目录暴露给 MCP 客户端或合作应用。
314
+ - 不在默认工作流中加入个人微信账号自动发消息、拉群、群管理或未验证的客户端自动化。
315
+ - 不把云端 AI 作为日报、导出和本地搜索的必需依赖。
316
+ - 不在没有迁移方案、兼容性测试和回滚方式时修改现有 JSON 数据契约。
317
+
318
+ ## 13. 建议的首轮开发任务
319
+
320
+ 1. 定义 `weflow-sync/v1` 和 `weflow-job/v1` 两个内部状态契约。
321
+ 2. 为消息同步增加检查点、重叠时间窗和覆盖摘要。
322
+ 3. 为 HTML/JSON 导出增加媒体覆盖报告和部分完成状态。
323
+ 4. 将导出、日报和索引任务接入统一任务状态查询。
324
+ 5. 为搜索结果增加消息/文章引用对象,不改变现有原始数据字段。
325
+ 6. 为 MCP 增加不透明游标和稳定错误码。
326
+ 7. 使用合成数据建立同步、搜索、导出和 MCP 的端到端验收样例。
327
+
328
+ 首轮完成后,再根据实际失败记录决定是否进入阅读状态、AI 执行层和助手收发队列,避免在数据边界尚未稳定时继续扩大功能面。
329
+
330
+ ## 14. 关联文档
331
+
332
+ - [整体路线图](ROADMAP.md)
333
+ - [项目当前状态](PROJECT_STATE.md)
334
+ - [消息数据契约](DATA_CONTRACT.md)
335
+ - [MCP 接口说明](MCP.md)
336
+ - [AI 接口和确认协议](AI_INTERFACE.md)
337
+ - [技术决策记录](DECISIONS.md)
338
+ - [安全说明](../SECURITY.md)