weflow-cli 1.7.0 → 1.8.1

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 (200) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/CHANGELOG.md +2108 -2
  3. package/OPERATIONS.md +810 -1
  4. package/README.en.md +96 -10
  5. package/README.md +34 -9
  6. package/bin/weflow-cli.ts +937 -28
  7. package/dist/bin/weflow-cli.js +955 -28
  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 +19 -2
  16. package/dist/src/core/ntCore.d.ts.map +1 -1
  17. package/dist/src/core/ntCore.js +11 -1
  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 +25 -6
  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 +599 -75
  70. package/dist/src/services/assistantService.js.map +1 -1
  71. package/dist/src/services/assistantTools.d.ts +217 -1
  72. package/dist/src/services/assistantTools.d.ts.map +1 -1
  73. package/dist/src/services/assistantTools.js +1116 -59
  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 +1 -1
  80. package/dist/src/services/chatService.d.ts.map +1 -1
  81. package/dist/src/services/chatService.js +5 -2
  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/pythonBridge.d.ts +41 -0
  88. package/dist/src/services/pythonBridge.d.ts.map +1 -0
  89. package/dist/src/services/pythonBridge.js +119 -0
  90. package/dist/src/services/pythonBridge.js.map +1 -0
  91. package/dist/src/services/syncService.d.ts +8 -2
  92. package/dist/src/services/syncService.d.ts.map +1 -1
  93. package/dist/src/services/syncService.js +26 -7
  94. package/dist/src/services/syncService.js.map +1 -1
  95. package/dist/src/services/wechatMessageService.d.ts +25 -0
  96. package/dist/src/services/wechatMessageService.d.ts.map +1 -1
  97. package/dist/src/services/wechatMessageService.js +69 -7
  98. package/dist/src/services/wechatMessageService.js.map +1 -1
  99. package/dist/src/services/wereadService.d.ts +33 -6
  100. package/dist/src/services/wereadService.d.ts.map +1 -1
  101. package/dist/src/services/wereadService.js +27 -2
  102. package/dist/src/services/wereadService.js.map +1 -1
  103. package/dist/src/utils/atomicWrite.d.ts +4 -0
  104. package/dist/src/utils/atomicWrite.d.ts.map +1 -0
  105. package/dist/src/utils/atomicWrite.js +60 -0
  106. package/dist/src/utils/atomicWrite.js.map +1 -0
  107. package/dist/src/utils/dateRange.d.ts +15 -0
  108. package/dist/src/utils/dateRange.d.ts.map +1 -1
  109. package/dist/src/utils/dateRange.js +47 -0
  110. package/dist/src/utils/dateRange.js.map +1 -1
  111. package/dist/src/utils/text.d.ts +18 -0
  112. package/dist/src/utils/text.d.ts.map +1 -0
  113. package/dist/src/utils/text.js +20 -0
  114. package/dist/src/utils/text.js.map +1 -0
  115. package/docs/AI_INTERFACE.md +2 -0
  116. package/docs/BRANCHES.md +13 -4
  117. package/docs/DECISIONS.md +1424 -0
  118. package/docs/EXTENDING.md +187 -0
  119. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -0
  120. package/docs/MCP.md +23 -4
  121. package/docs/PROJECT_STATE.md +210 -9
  122. package/docs/SETUP.md +22 -0
  123. package/docs/SYNC_CONTRACT.md +31 -3
  124. package/mcp-server/index.ts +681 -657
  125. package/package.json +84 -83
  126. package/resources/panel/ball-position.cjs +153 -0
  127. package/resources/panel/index.html +46 -0
  128. package/resources/panel/main.cjs +483 -0
  129. package/resources/panel/mascot-happy.png +0 -0
  130. package/resources/panel/mascot-sorry.png +0 -0
  131. package/resources/panel/mascot-thinking.png +0 -0
  132. package/resources/panel/mascot-tired.png +0 -0
  133. package/resources/panel/mascot.png +0 -0
  134. package/resources/panel/package.json +7 -0
  135. package/resources/panel/panel.css +415 -0
  136. package/resources/panel/preload.cjs +79 -0
  137. package/resources/panel/quick-menu.cjs +197 -0
  138. package/resources/panel/renderer.js +470 -0
  139. package/resources/panel/tray-menu.cjs +40 -0
  140. package/resources/panel/tray.png +0 -0
  141. package/scripts/_utils.py +425 -54
  142. package/scripts/annual_report.py +4 -2
  143. package/scripts/article_notes.py +423 -0
  144. package/scripts/backfill_articles.py +438 -0
  145. package/scripts/biz_daily.py +590 -105
  146. package/scripts/chat_notes.py +470 -0
  147. package/scripts/classify_daily.py +10 -18
  148. package/scripts/compile_wiki.py +1060 -46
  149. package/scripts/create_reading_notes.py +199 -14
  150. package/scripts/decide.py +253 -0
  151. package/scripts/draft_reply.py +671 -0
  152. package/scripts/export_chat_html.py +35 -33
  153. package/scripts/extract_todos.py +31 -5
  154. package/scripts/fav_notes.py +318 -0
  155. package/scripts/fix_topics.py +2 -1
  156. package/scripts/generate_ai_report.py +214 -9
  157. package/scripts/generate_html.py +18 -3
  158. package/scripts/generate_review.py +2 -2
  159. package/scripts/jev_client.py +294 -0
  160. package/scripts/jev_probe.py +440 -0
  161. package/scripts/nt_common.py +82 -0
  162. package/scripts/nt_decrypt.py +1755 -1460
  163. package/scripts/pipeline.py +27 -2
  164. package/scripts/promote_ideas.py +19 -4
  165. package/scripts/quality_eval.py +707 -0
  166. package/scripts/rag_chat.py +4 -4
  167. package/scripts/read_image.py +187 -0
  168. package/scripts/reply_debt.py +488 -0
  169. package/scripts/route_cards.py +453 -0
  170. package/scripts/semantic_search.py +208 -49
  171. package/scripts/user_notes.py +315 -0
  172. package/scripts/vault_rag.py +13 -7
  173. package/scripts/vault_search.py +21 -4
  174. package/scripts/wiki_lint.py +429 -0
  175. package/src/core/appMsgFormat.ts +117 -0
  176. package/src/core/ntCore.ts +27 -2
  177. package/src/core/sqlcipherCore.ts +5 -61
  178. package/src/core/wcdbCore.ts +6 -2
  179. package/src/panel/client.ts +49 -0
  180. package/src/panel/endpoint.ts +144 -0
  181. package/src/panel/launch.ts +101 -0
  182. package/src/panel/server.ts +424 -0
  183. package/src/panel/userId.ts +72 -0
  184. package/src/services/assistantDaemon.ts +57 -9
  185. package/src/services/assistantEval.ts +611 -0
  186. package/src/services/assistantMemory.ts +413 -57
  187. package/src/services/assistantPrivacy.ts +25 -6
  188. package/src/services/assistantRouter.ts +191 -0
  189. package/src/services/assistantService.ts +681 -74
  190. package/src/services/assistantTools.ts +1149 -58
  191. package/src/services/assistantTrace.ts +175 -0
  192. package/src/services/chatService.ts +6 -2
  193. package/src/services/configService.ts +403 -385
  194. package/src/services/pythonBridge.ts +139 -0
  195. package/src/services/syncService.ts +37 -9
  196. package/src/services/wechatMessageService.ts +73 -7
  197. package/src/services/wereadService.ts +43 -7
  198. package/src/utils/atomicWrite.ts +56 -0
  199. package/src/utils/dateRange.ts +46 -0
  200. package/src/utils/text.ts +20 -0
@@ -0,0 +1,671 @@
1
+ #!/usr/bin/env python3
2
+ """起草回复 —— **判断在前、起草在后、绝不发送**。
3
+
4
+ 这套形状借自 `github.com/jev-chat/jev-chat-jarvis`(一个 Android 助手:无障碍读屏 →
5
+ 判断意图/危险 → 起草 3 条候选 → 只填输入框、绝不发送)。**两处它没做好、这里做对了**:
6
+
7
+ 1. 它那边「危险等级」是**纯 UI**——只驱动一块颜色徽章,不拒绝起草、不降级、不阻断
8
+ (全仓库对 `dangerLevel` 的消费只有 `OverlayController.kt:401-405` 一处展示)。
9
+ 这里的闸门**真的会拒绝**:涉钱或风险 ≥ 7 就**不起草**,改成说清风险与该先确认什么。
10
+ 2. 它那边判断结果**没进起草提示词**(`ReplyClient.draft(snapshot, relationship, ctx)` 签名里
11
+ 没有 `Analysis`,判断只喂给了排序题)。这里**注入**:意图/需求/动作/该不该给实质,
12
+ 连同风险档一起写进起草提示词。
13
+
14
+ 它**不需要**的东西这里也不需要——反过来:它是**没有数据访问**才去读屏,
15
+ 我们直接读本机数据库。这不是抄近路,是这套东西在 Windows 上本来就更短。
16
+
17
+ 判断用决策模型(Jev,D-031:判断交给决策模型),起草用生成模型(DeepSeek)。
18
+ **三次调用**:判断一次、起草一次、给候选排序一次。
19
+
20
+ ## 两条输入路径(脱敏逼出来的,不是设计洁癖)
21
+
22
+ `privacyGate` 在 TS 侧,而且它打的是**工具返回值**——管不到"Python 脚本往外发了什么"。
23
+ Python 侧没有任何脱敏实现(全 `scripts/` 只有两处无关的位掩码),所以:
24
+
25
+ - `--talker <名字>`:**脚本自己读库**,不做遮罩。这是命令行路径,与 `reply_debt.py` 的
26
+ 做法一致——用户显式运行、`--dry-run` 里说清要发多少字符给哪两个模型。
27
+ - `--stdin`:从标准输入收一段**已经遮罩好的**对话(助手工具路径,由 TS 侧逐条过
28
+ `maskMessageBody`)。**不在 Python 里再实现一份脱敏**——同一件事两处实现,早晚有一处会漏。
29
+
30
+ 判断、起草、闸门这三段只有一份实现,两条路只差"输入怎么来"。
31
+
32
+ 用法:
33
+
34
+ python scripts/draft_reply.py --talker 老王 --dry-run
35
+ python scripts/draft_reply.py --talker 老王 --yes --json
36
+ echo '{"name":"老王","lines":["[09-24 10:00] 对方:文件发我"]}' | python scripts/draft_reply.py --stdin --yes --json
37
+
38
+ **它不发送任何东西**,也不碰微信窗口。候选只是文本,回不回、怎么回由人决定。
39
+ """
40
+ import argparse
41
+ import json
42
+ import os
43
+ import re
44
+ import sys
45
+ from datetime import datetime, timedelta, timezone
46
+
47
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
48
+ from _utils import call_deepseek, get_api_key, load_config, decrypt_lock # noqa: E402
49
+ from jev_client import JevError, create_client # noqa: E402
50
+ from reply_debt import format_line, TRANSCRIPT_SIZE, TYPE_LABELS, MAX_MSG_CHARS # noqa: E402
51
+
52
+ import nt_decrypt # noqa: E402
53
+
54
+ TZ = timezone(timedelta(hours=8))
55
+ # 转录里单条消息的截断长度。**必须等于** `assistantTools.ts` 的 `TRANSCRIPT_MSG_CHARS`——
56
+ # 否则同一句话走命令行(`--talker`)和走面板(`--stdin`)会不一样长,判断被喂进不同的东西。
57
+ # (`reply_debt` 自己有 120 字的口径,那是欠账雷达 state 的大小,是另一件事,别混。)
58
+ DRAFT_MSG_CHARS = 160
59
+ DEFAULT_COUNT = 3
60
+ MAX_COUNT = 5
61
+ DRAFT_MAX_CHARS = 60
62
+ RISK_REFUSE_AT = 7 # 风险档 ≥ 这个数就不起草(0-9 十档,见 RISK_CRITERIA)
63
+ NOUL_TRUE = 0.5 # noul 是**概率**不是布尔(把 0.31 当 True 会让每问都读成"是")
64
+
65
+
66
+ # ---------------------------------------------------------------- 判断:七道题
67
+ #
68
+ # 判据用英文、语料保留中文原文——这是参考实现的第一手口径(它们的 tools/jev/TASK.md:39:
69
+ # 「Jev 主训练语言是英文,中文效果明显差」),它们的题目集也是据此从中文改成英文的。
70
+ #
71
+ # **本仓库既有的 `reply_debt.py` 用的是中文判据**,这是同一个模型上的两套口径,
72
+ # 我没做过 A/B(记在 D-047 的"已知边界"里)。
73
+
74
+ INTENT_CRITERIA = {
75
+ 'confirm_you_care': 'They want to see that I still care or still notice them - they are testing that. '
76
+ 'If they mainly want a new deliverable or a yes on a time, do not use this.',
77
+ 'vent_anger': 'They are letting off steam or attacking. They want to be heard, not fixed. '
78
+ 'Ending the relationship, telling me not to reply, or deleting me belongs here.',
79
+ 'request_action': 'They want a concrete thing done: send the file, pay, book it, fix it, name a time.',
80
+ 'seek_explanation': 'They want to know why something happened, or why it did not.',
81
+ 'casual_chat': 'Just chatting or sharing; no request and no complaint.',
82
+ 'close_topic': 'Winding the topic down - "ok", "got it", "fine". '
83
+ "Not a breakup, not 'don't contact me', not sarcastic 'I am used to it'.",
84
+ }
85
+
86
+ NEED_CRITERIA = {
87
+ 'apology': 'An apology for a specific thing, said plainly.',
88
+ 'action': 'Something actually done, or a concrete next step with a time.',
89
+ 'explanation': 'The reason why, or the missing fact.',
90
+ 'care': 'Attention and warmth; they want to feel remembered. A sarcastic "I am used to it" belongs here.',
91
+ 'nothing': 'They are genuinely satisfied and the topic is closed. '
92
+ 'A sarcastic remark or an unanswered complaint is NOT satisfaction - do not choose nothing.',
93
+ }
94
+
95
+ ACTION_CRITERIA = {
96
+ 'check_history': 'My reply needs a fact I do not have in this excerpt - read the earlier chat first.',
97
+ 'apologize': 'Acknowledge a specific fault and apologize. '
98
+ 'Not for an unnamed forgotten thing when I should first find out what it was.',
99
+ 'give_commitment': 'Commit to a specific time or deliverable I can actually keep.',
100
+ 'explain': 'Explain what happened or why, without deflecting.',
101
+ 'acknowledge': 'Show I heard it and that it matters, without promising anything.',
102
+ 'say_less': 'Keep it short or add nothing. Extra words would over-explain, reopen a closed topic, '
103
+ 'or pour fuel on an ultimatum that told me not to talk.',
104
+ 'make_plan': 'Turn it into a concrete plan or timeline together.',
105
+ }
106
+
107
+ # 十档,每档写**具体情景**(不写抽象程度,官方要求)。语义借自参考实现
108
+ # `JevQuestions.kt:87-98`,但把"分手/上报领导"那套亲密关系场景换成我们的(工作/家庭/客户)。
109
+ RISK_CRITERIA = [
110
+ 'Light chat or joking; nothing is at stake and a clumsy reply would not matter.',
111
+ 'A mild tease or a small reminder that is easy to laugh off; a clumsy reply would only feel slightly awkward.',
112
+ 'A mild complaint or "please remember next time" said without heat; they still send warm or practical follow-ups.',
113
+ 'Noticeable unhappiness; they mention being forgotten, ignored, or kept waiting, but still give me a chance.',
114
+ 'Sarcasm, cold short replies, or "you better"; they are testing me, and a sloppy or fake-confident reply escalates.',
115
+ 'Openly upset; they say I do not listen or do not care; they expect a real response, not a joke.',
116
+ 'Clearly angry and blaming me; a wrong reply turns this into a fight.',
117
+ 'Last-chance warning: they will not cover for me any more, or will take it to my boss or the client, '
118
+ 'or say trust is almost gone.',
119
+ 'An ultimatum is already on the table even if a practical next step is also named: cancel the contract, '
120
+ 'report it tonight, stop working together if this is missed.',
121
+ 'Active rupture or real-world exposure: it is over, they told me not to reply, or the money or the legal '
122
+ 'consequence is already in motion.',
123
+ ]
124
+
125
+ RISK_WORDS = [(8, '很危险'), (6, '偏危险'), (3, '留神'), (0, '安全')]
126
+
127
+
128
+ def build_questions():
129
+ """一次请求问全(参考实现的实测:多问几道几乎不加成本)。
130
+
131
+ 两条设计约束来自参考实现踩过的坑(`tools/jev/TASK.md:35` 记录了它们两题打架的实测数字):
132
+
133
+ 1. **题目之间不许互相矛盾**:`should_reply` 只问"该不该给**实质内容**",
134
+ `action` 那题显式写 `Do not decide whether to send a message immediately`,
135
+ 否则会出现"该先翻记录 0.60"和"该马上答 0.77"同时成立的矛盾。
136
+ 2. `money` 与 `commitment` 逐字复用 `reply_debt.py` 的中文原文——那是我们自己的措辞,
137
+ 且被测试钉住;新题才用英文。
138
+ """
139
+ return {
140
+ 'intent': {
141
+ 'type': 'choice',
142
+ 'instructions': 'What does the other person actually want from me right now? '
143
+ 'Judge the real intent behind their last message, not its literal wording.',
144
+ 'criteria': INTENT_CRITERIA,
145
+ },
146
+ 'need': {
147
+ 'type': 'choice',
148
+ 'instructions': 'What would actually settle this for them? Choose one.',
149
+ 'criteria': NEED_CRITERIA,
150
+ },
151
+ 'action': {
152
+ 'type': 'choice',
153
+ 'instructions': 'What is the best action for MY next message? '
154
+ 'Do not decide whether to send a message immediately. Ignore timing. '
155
+ 'Choose only the action type. '
156
+ 'If they asked me to recall a specific past message or event and the facts are not '
157
+ 'in this excerpt, choose check_history - do not apologize or invent a plan instead.',
158
+ 'criteria': ACTION_CRITERIA,
159
+ },
160
+ 'should_reply': {
161
+ 'type': 'noul',
162
+ 'instructions': 'Should my next message contain SUBSTANTIVE content - an answer, a decision, a fact, '
163
+ 'a commitment - rather than just an acknowledgement or nothing at all? '
164
+ 'Answer FALSE if the thing they want me to recite or prove is not present in this '
165
+ 'snippet (you would be guessing). '
166
+ "'Then say it' / 'you better' while I am stalling is FALSE. "
167
+ 'Answer FALSE if they already accepted and closed the topic. '
168
+ 'Answer TRUE only if the needed fact, plan, or named fault is already in this snippet '
169
+ 'or is mine to state.',
170
+ },
171
+ 'risk': {
172
+ 'type': 'score',
173
+ 'instructions': 'How close is this to a fight, or to real damage - money, a deadline, the '
174
+ 'relationship? Match the current scene. '
175
+ 'If they genuinely accepted an apology or confirmed a happy plan, score the '
176
+ 'cooled-down present, not an earlier complaint. '
177
+ 'If an ultimatum (cancel the deal, report it, stop working together) is still in '
178
+ 'force and has not been withdrawn, stay in that high bin even if the latest line '
179
+ 'names a specific task.',
180
+ 'criteria': RISK_CRITERIA,
181
+ },
182
+ 'money': {
183
+ 'type': 'noul',
184
+ 'instructions': '涉及金钱、交付物或明确期限这类硬承诺吗?',
185
+ },
186
+ 'commitment': {
187
+ 'type': 'noul',
188
+ 'instructions': '我在这段对话里承诺过什么,而到现在还没有下文?',
189
+ },
190
+ }
191
+
192
+
193
+ def risk_word(score):
194
+ for threshold, word in RISK_WORDS:
195
+ if score >= threshold:
196
+ return word
197
+ return '安全'
198
+
199
+
200
+ def to_judgment(answers):
201
+ """把决策模型的答案摊平成一份判断。**纯函数**——测试要能直接喂一组答案进来。"""
202
+ def noul(key):
203
+ value = (answers.get(key) or {}).get('noul')
204
+ try:
205
+ return float(value)
206
+ except (TypeError, ValueError):
207
+ return None
208
+
209
+ risk = answers.get('risk') or {}
210
+ return {
211
+ 'intent': (answers.get('intent') or {}).get('choice'),
212
+ 'need': (answers.get('need') or {}).get('choice'),
213
+ 'action': (answers.get('action') or {}).get('choice'),
214
+ 'shouldReply': noul('should_reply'),
215
+ 'risk': risk.get('score'),
216
+ 'riskMax': max(len(RISK_CRITERIA) - 1, 1),
217
+ 'money': noul('money'),
218
+ 'commitment': noul('commitment'),
219
+ }
220
+
221
+
222
+ def judge(client, name, lines, premise=''):
223
+ """问一次决策模型。返回 (判断, usage);问不出来就是 (None, None)。"""
224
+ head = '会话:%s' % name
225
+ if premise:
226
+ head += '\n' + premise
227
+ state = '%s\n\n最近的对话(时间从早到晚):\n%s' % (head, '\n'.join(lines))
228
+ try:
229
+ answers, usage = client.decide(state, build_questions())
230
+ except Exception as error:
231
+ print('[WARN] 判断失败(%s):%s' % (type(error).__name__, error), file=sys.stderr)
232
+ return None, None
233
+ return to_judgment(answers), usage
234
+
235
+
236
+ # ---------------------------------------------------------------- 闸门
237
+ #
238
+ # **闸门搭在二值问题上,不搭在 0-9 的分上。** 二值问题只有两个取值,误判成本低得多;
239
+ # 0-9 那档虽有校准过的语义,但阈值是连续量,7 和 6.5 的区别没有意义。0-9 用来**解释**。
240
+
241
+ def evaluate_gate(judgment, gate_commitment=False):
242
+ """返回 (原因, 该怎么办) 或 None(可以起草)。"""
243
+ if (judgment.get('money') or 0) >= NOUL_TRUE:
244
+ return ('这段对话涉及金钱、交付物或明确期限——替你起草等于替你表态',
245
+ ['先想清楚能给什么、什么时候给,再自己写一句',
246
+ '涉及钱的话,把金额与时间写清楚比写得好听重要'])
247
+ risk = judgment.get('risk')
248
+ if risk is not None and risk >= RISK_REFUSE_AT:
249
+ return ('风险 %d/%d(%s)——这个距离上,一条拟好的回复会让你更容易把话说出去'
250
+ % (risk, judgment.get('riskMax') or 9, risk_word(risk)),
251
+ gate_advice(judgment))
252
+ if gate_commitment and (judgment.get('commitment') or 0) >= NOUL_TRUE:
253
+ return ('这段对话里有你承诺过、还没下文的事,而 `--gate-commitment` 打开了',
254
+ ['先说清那件旧事,再谈新的'])
255
+ return None
256
+
257
+
258
+ def gate_advice(judgment):
259
+ """拒绝时给的不是空话,而是"下一步该干什么"——照最佳动作类型来。"""
260
+ by_action = {
261
+ 'check_history': '先去翻一下更早的聊天记录,把事实找齐——现在回什么都是在猜',
262
+ 'apologize': '先想清楚到底哪件事错了,认错要具体,不要笼统地说"抱歉"',
263
+ 'give_commitment': '只承诺你做得到的,并且带上具体时间',
264
+ 'explain': '先把原因想清楚,再决定要不要解释——急着解释容易变成辩解',
265
+ 'acknowledge': '可以先只回一句"我看到了",不必现在给方案',
266
+ 'say_less': '这种时候少说是上策——多一句都可能火上浇油',
267
+ 'make_plan': '把话说成"我们一起定个时间",而不是替对方定',
268
+ }
269
+ advice = [by_action.get(judgment.get('action'), '想清楚对方要的是什么,再决定回不回')]
270
+ if (judgment.get('commitment') or 0) >= NOUL_TRUE:
271
+ advice.append('这段里还有你没兑现的承诺,先把那件事说清楚')
272
+ if (judgment.get('shouldReply') or 0) < NOUL_TRUE:
273
+ advice.append('判断是"这条不必给实质内容"——一句收到就够,不用展开')
274
+ return advice
275
+
276
+
277
+ # ---------------------------------------------------------------- 起草
278
+
279
+ DRAFT_PROMPT = """你是中文即时通讯回复助手。下面是一段对话,以及一段对它的判断。
280
+ 请给出 {count} 条候选回复,**{count} 条必须策略不同**(例如:一条稳妥承接、一条给具体行动或时间、一条简短低姿态)。
281
+
282
+ 要求:
283
+ 1. 只输出一个 JSON 数组,恰好 {count} 个字符串,不要任何解释、不要加引号以外的内容
284
+ 2. 每条不超过 {max_chars} 字,口语、自然,像真人在聊天软件里随手发的
285
+ 3. 用「我」的语气{style_hint}
286
+ 4. **不要编造对话里没有的事实**;不知道的就问,或者先不承诺
287
+ {constraints}
288
+ 判断(供参考,**不要原样复述**):
289
+ - 对方真实意图:{intent}
290
+ - 对方需要的是:{need}
291
+ - 我这条的最佳动作类型:{action}
292
+ - 该不该给实质内容:{should_reply}
293
+ - 风险档位:{risk}
294
+
295
+ 最近对话(时间从早到晚,「我」是我;不是我的那一侧一律写成发言人自己的名字——单聊里就是{name},群聊里是谁说的就写谁):
296
+ {premise}
297
+ {convo}
298
+ """
299
+
300
+ # 没有判断结果时用的那一版:Jev 调不通(没配 key / 免费到期 / 网络断)时的降级路径。
301
+ # **不假装判断过**:直接告诉模型"这次没有判断",让它自己把握分寸,并把"拿不准就问、
302
+ # 不替我许新承诺"这两条底线写死——这正是原版靠判断结果才敢省掉的那部分。
303
+ DRAFT_PROMPT_NO_JUDGE = """你是中文即时通讯回复助手。下面是一段对话。
304
+ 请给出 {count} 条候选回复,**{count} 条必须策略不同**(例如:一条稳妥承接、一条给具体行动或时间、一条简短低姿态)。
305
+
306
+ 要求:
307
+ 1. 只输出一个 JSON 数组,恰好 {count} 个字符串,不要任何解释、不要加引号以外的内容
308
+ 2. 每条不超过 {max_chars} 字,口语、自然,像真人在聊天软件里随手发的
309
+ 3. 用「我」的语气{style_hint}
310
+ 4. **不要编造对话里没有的事实**;不知道的就问,或者先不承诺
311
+ 5. **这次没有判断结果**(判断模型没调通),所以分寸你自己按上下文把握:
312
+ 拿不准就少说、先问;**不要替我做新的承诺**;对方明显带着情绪时先接住情绪再谈事
313
+
314
+ 最近对话(时间从早到晚,「我」是我;不是我的那一侧一律写成发言人自己的名字——单聊里就是{name},群聊里是谁说的就写谁):
315
+ {premise}
316
+ {convo}
317
+ """
318
+
319
+
320
+ def style_hint(own_lines):
321
+ if not own_lines:
322
+ return ''
323
+ sample = '\n'.join(' %s' % line for line in own_lines[-5:])
324
+ return (';**参考下面几行我平时说话的口气**(只学口气,不要照抄内容):\n%s' % sample)
325
+
326
+
327
+ def draft_constraints(judgment):
328
+ lines = []
329
+ if (judgment.get('commitment') or 0) >= NOUL_TRUE:
330
+ lines.append('- **我在这段对话里有承诺还没下文:不要许新的承诺**,可以给时间但不要加码')
331
+ risk = judgment.get('risk') or 0
332
+ if risk >= 4:
333
+ lines.append('- 对方现在带着情绪:**先接住情绪再谈事**,不要辩解、不要讲道理')
334
+ if (judgment.get('shouldReply') or 0) < NOUL_TRUE:
335
+ lines.append('- 判断是"这条不必给实质内容":候选要短,别硬凑内容')
336
+ return '\n'.join(lines)
337
+
338
+
339
+ def own_voice_lines(lines):
340
+ """从渲染好的对话里挑出"我"说过的话,当语气样本。"""
341
+ return [line.split(':', 1)[1] for line in lines if '] 我:' in line][-5:]
342
+
343
+
344
+ def build_draft_prompt(name, lines, judgment, count, premise=''):
345
+ # `judgment=None` = 降级路径(Jev 没调通):换那一版提示词,**不去编一段判断结果出来**
346
+ if not judgment:
347
+ return DRAFT_PROMPT_NO_JUDGE.format(
348
+ count=count, max_chars=DRAFT_MAX_CHARS, name=name,
349
+ style_hint=style_hint(own_voice_lines(lines)),
350
+ premise=premise, convo='\n'.join(lines),
351
+ )
352
+ return DRAFT_PROMPT.format(
353
+ count=count, max_chars=DRAFT_MAX_CHARS, name=name,
354
+ style_hint=style_hint(own_voice_lines(lines)),
355
+ premise=premise,
356
+ constraints=draft_constraints(judgment),
357
+ intent=judgment.get('intent'), need=judgment.get('need'),
358
+ action=judgment.get('action'),
359
+ should_reply='是' if (judgment.get('shouldReply') or 0) >= NOUL_TRUE else '否',
360
+ risk='%s/%s(%s)' % (judgment.get('risk'), judgment.get('riskMax'), risk_word(judgment.get('risk') or 0)),
361
+ convo='\n'.join(lines),
362
+ )
363
+
364
+
365
+ def parse_candidates(text, count):
366
+ """从模型输出里抠出候选。**抠不出来就如实说抠不出来**,不拿占位句凑数
367
+ (参考实现会补一句"(稍等,我看下)"兜底,那是它的取舍;这里宁可少给几条)。"""
368
+ body = (text or '').strip()
369
+ if body.startswith('```'):
370
+ body = body.split('\n', 1)[1] if '\n' in body else ''
371
+ if body.endswith('```'):
372
+ body = body[:-3]
373
+ try:
374
+ parsed = json.loads(body)
375
+ except ValueError:
376
+ parsed = None
377
+ if isinstance(parsed, list):
378
+ items = [str(item).strip() for item in parsed if str(item).strip()]
379
+ else:
380
+ # 退一步:按行切(模型偶尔会加前缀或忘记套 JSON)。
381
+ # 去掉行首的 `1.` / `-` / 引号——**行号不是候选内容**,留着会被原样发出去。
382
+ items = []
383
+ for line in body.split('\n'):
384
+ item = line.strip()
385
+ if item.startswith('```'):
386
+ continue
387
+ item = re.sub(r'^\s*(?:\d+[.、)]|[-*])\s*', '', item) # 行号与项目符号
388
+ item = item.strip().strip('",').strip()
389
+ if item:
390
+ items.append(item)
391
+ return items[:count]
392
+
393
+
394
+ def build_rank_question(candidates):
395
+ """排序题。criteria 的值就是候选的**中文原文**——这是唯一允许 criteria 用中文的地方
396
+ (参考实现的 `tools/jev/TASK.md:53` 也是这么说的:它本身就是待选内容)。"""
397
+ return {
398
+ 'best': {
399
+ 'type': 'choice',
400
+ 'instructions': 'Which candidate reply is the most appropriate next message, given the conversation '
401
+ "and the other person's true need? Prefer a reply that matches the best action type. "
402
+ 'Penalize dismissive, over-promising, or off-topic replies. '
403
+ 'If the facts are not confirmed, prefer the candidate that asks or looks them up '
404
+ 'instead of faking memory or a vague apology.',
405
+ 'criteria': {('candidate_%d' % index): text for index, text in enumerate(candidates, 1)},
406
+ }
407
+ }
408
+
409
+
410
+ def rank(client, name, lines, candidates, judgment):
411
+ """给候选排序。排不出来就按原顺序返回,并如实标注。"""
412
+ if len(candidates) < 2:
413
+ return [{'text': text, 'why': None} for text in candidates], False
414
+ state = ('会话:%s\n\n最近的对话:\n%s\n\n我的最佳动作类型:%s'
415
+ % (name, '\n'.join(lines[-10:]), judgment.get('action')))
416
+ try:
417
+ answers, usage = client.decide(state, build_rank_question(candidates))
418
+ except Exception as error:
419
+ print('[WARN] 排序失败(%s):%s' % (type(error).__name__, error), file=sys.stderr)
420
+ return [{'text': text, 'why': None} for text in candidates], False
421
+
422
+ best = answers.get('best') or {}
423
+ probs = best.get('probabilities') or {}
424
+ order = sorted(range(len(candidates)),
425
+ key=lambda index: -(probs.get('candidate_%d' % (index + 1)) or 0))
426
+ top = best.get('choice')
427
+ ranked = []
428
+ for index in order:
429
+ why = '判断这最合适' if ('candidate_%d' % (index + 1)) == top else None
430
+ ranked.append({'text': candidates[index], 'why': why})
431
+ return ranked, True
432
+
433
+
434
+ # ---------------------------------------------------------------- 取数
435
+
436
+ def read_from_db(talker, days):
437
+ """命令行路径:脚本自己读库(不做遮罩,与 reply_debt.py 一致)。"""
438
+ config = load_config()
439
+ db = config.get('ntDbPath', '')
440
+ if not db:
441
+ print('配置里没有 ntDbPath,先运行 weflow-cli init', file=sys.stderr)
442
+ return None, 2
443
+ conns = nt_decrypt.connect_message_shards(
444
+ db, decrypt_lock(config.get('ntKey', '')), config.get('ntSalt', ''),
445
+ decrypt_lock(config.get('favPassphrase') or config.get('decryptKey') or ''))
446
+ if not conns:
447
+ print('无法打开消息数据库,请检查密钥(weflow-cli check)', file=sys.stderr)
448
+ return None, 2
449
+ try:
450
+ # 名字要经**联系人库**解析:`get_sessions` 不接受 name_map,它给的 displayName
451
+ # 往往就是 wxid 本身,拿备注名去匹配会一个都找不到(实测踩过)。
452
+ name_map = {}
453
+ contact_db = nt_decrypt.find_contact_db_path(db)
454
+ if contact_db:
455
+ name_map = nt_decrypt.load_contact_names(
456
+ contact_db, decrypt_lock(config.get('contactKey', '')),
457
+ config.get('contactSalt', ''))
458
+
459
+ sessions = nt_decrypt.get_sessions(conns).get('sessions') or []
460
+ match = []
461
+ for session in sessions:
462
+ talker_id = session.get('username') or ''
463
+ resolved = name_map.get(talker_id) or session.get('displayName') or talker_id
464
+ # 备注名优先、模糊匹配;全等(含 wxid 本身)也可
465
+ if talker == resolved or talker == talker_id or talker in resolved:
466
+ match.append((session, resolved))
467
+ if not match:
468
+ print('没找到会话:%s(用 weflow-cli sessions 看有哪些)' % talker, file=sys.stderr)
469
+ return None, 1
470
+ # 同名多个时取最近有动静的那个
471
+ match.sort(key=lambda pair: -(pair[0].get('lastTimestamp') or 0))
472
+ picked, resolved = match[0]
473
+ result = nt_decrypt.get_messages(conns, picked['username'], TRANSCRIPT_SIZE,
474
+ name_map=name_map, own_wxid=config.get('wxid', ''))
475
+ messages = result.get('messages') or []
476
+ if not messages:
477
+ print('这个会话最近没有消息', file=sys.stderr)
478
+ return None, 1
479
+ lines = [format_line(m, DRAFT_MSG_CHARS) for m in reversed(messages)]
480
+ # `lines` 是把 messages **反过来**渲染的,所以最后一条 = `messages[0]`。
481
+ # 用结构化字段 `isSend` 而不是去解析那行文本:群聊里别人的标签是**人名**
482
+ # (`format_line` 里 `senderDisplay` 优先),拿「对方」当标记会判错。
483
+ return {'name': resolved, 'lines': lines,
484
+ 'lastFromMe': bool(messages[0].get('isSend'))}, 0
485
+ finally:
486
+ for conn in conns:
487
+ conn.close()
488
+
489
+
490
+ # ---------------------------------------------------------------- 主流程
491
+
492
+ def premise_note(last_from_me):
493
+ """最后一条是我自己发的时,给判断与起草的**前提说明**;其余情况空字符串。
494
+
495
+ 为什么非说不可:那七道题问的是「their last message」。语料里最后一条如果是我发的,
496
+ 这个问题本身就是**问错的**——对方根本没说话。不告诉它,判断会把**我**刚发的那句当成
497
+ 对方的表态(于是"对方真实意图""对方需要什么"全变成对我自己那句话的解读),
498
+ 再当成"参考"喂进写作提示,候选自然答非所问。
499
+
500
+ 只在**确知**是/不是时才有话说:`lastFromMe` 说不清(老调用方没带这个字段)就当不知道。
501
+ 猜错比不说更糟——这条与"没有判断就不假装判断过"是同一条纪律。
502
+ """
503
+ if not last_from_me:
504
+ return ''
505
+ return ('注意:**最后一条是我自己发的,对方还没回**。所以这不是"他在等我回",'
506
+ '而是"我在等他回"——别把我那句话当成对方的表态。')
507
+
508
+
509
+ def run(payload, count, config, gate_commitment=False):
510
+ """判断 → 闸门 → 起草 → 排序。**两条输入路径共用这一段。**"""
511
+ name, lines = payload['name'], payload['lines']
512
+ if not lines:
513
+ return {'success': False, 'error': '没有对话内容可用'}
514
+ premise = premise_note(payload.get('lastFromMe'))
515
+
516
+ # **DeepSeek 是必须的**(候选只有它能写);判断那一步(Jev)是**可降级**的。
517
+ api_key = get_api_key(config)
518
+ if not api_key:
519
+ return {'success': False, 'error': '没有配置 deepseekApiKey,起草这一步跑不了'
520
+ '(weflow-cli config set deepseekApiKey "...")'}
521
+
522
+ # 判断(Jev):能问就问,问不出来**也照样起草**——但那道闸门与排序就没有了,
523
+ # 所以下面会把"这次没有判断"一路带进结果里,页面/命令行都要如实说出来。
524
+ jev_key = os.environ.get('TYPESAFE_API_KEY') or ''
525
+ if not jev_key:
526
+ from _utils import get_typesafe_key
527
+ jev_key = get_typesafe_key(config)
528
+ client = create_client(jev_key, config=config)
529
+ judge_note = ''
530
+ judgment = None
531
+ if client is None:
532
+ judge_note = '没有配置 typesafeApiKey,判断这一步没跑'
533
+ else:
534
+ judgment, usage = judge(client, name, lines, premise)
535
+ if judgment is None:
536
+ judge_note = '判断没调通(看上面的 WARN):可能是 Jev 的免费期已过、key 失效或网络断'
537
+ else:
538
+ judgment['usage'] = usage
539
+
540
+ if judgment is not None:
541
+ refused = evaluate_gate(judgment, gate_commitment)
542
+ if refused:
543
+ reason, advice = refused
544
+ return {'success': True, 'gate': 'refused', 'name': name, 'judged': True,
545
+ 'lastFromMe': payload.get('lastFromMe'), 'premise': premise,
546
+ 'reason': reason, 'advice': advice, 'judgment': judgment, 'drafts': []}
547
+
548
+ prompt = build_draft_prompt(name, lines, judgment, count, premise)
549
+ try:
550
+ raw = call_deepseek(prompt, api_key, max_tokens=800, timeout=90)
551
+ except Exception as error:
552
+ return {'success': False, 'error': '起草失败:%s' % error}
553
+ candidates = parse_candidates(raw, count)
554
+ if not candidates:
555
+ return {'success': False, 'error': '模型没给出可用的候选(原始输出不是 JSON 数组也不是逐行文本)'}
556
+
557
+ if judgment is None:
558
+ # 降级:没有判断就没有排序,也没有闸门。**如实带出去**(`judged: False` + 原因),
559
+ # 不假装判断过——那道闸门正是靠判断结果才存在的。
560
+ return {'success': True, 'gate': 'draft', 'name': name, 'judgment': None,
561
+ 'judged': False, 'judgeNote': judge_note,
562
+ 'lastFromMe': payload.get('lastFromMe'), 'premise': premise,
563
+ 'drafts': [{'text': text, 'why': ''} for text in candidates], 'ranked': False,
564
+ 'candidateCount': len(candidates), 'askedCount': count}
565
+
566
+ drafts, ranked_ok = rank(client, name, lines, candidates, judgment)
567
+ return {'success': True, 'gate': 'draft', 'name': name, 'judgment': judgment,
568
+ 'drafts': drafts, 'ranked': ranked_ok, 'judged': True,
569
+ 'lastFromMe': payload.get('lastFromMe'), 'premise': premise,
570
+ 'candidateCount': len(candidates), 'askedCount': count}
571
+
572
+
573
+ def main():
574
+ parser = argparse.ArgumentParser(description='起草回复(判断在前,起草在后,绝不发送)')
575
+ parser.add_argument('--talker', help='会话名(命令行路径:脚本自己读库)')
576
+ parser.add_argument('--stdin', action='store_true',
577
+ help='从标准输入收一段已遮罩的对话(助手工具路径),JSON: {name, lines}')
578
+ parser.add_argument('--count', type=int, default=DEFAULT_COUNT, help='要几条候选(默认 3)')
579
+ parser.add_argument('--gate-commitment', action='store_true',
580
+ help='承诺未兑现时也拒绝起草(默认不拒,只在提示词里约束)')
581
+ parser.add_argument('--dry-run', action='store_true', help='只报要发多少字符给哪两个模型,不调用')
582
+ parser.add_argument('--yes', action='store_true', help='确认调用两个云端模型')
583
+ parser.add_argument('--json', action='store_true')
584
+ args = parser.parse_args()
585
+
586
+ if not 1 <= args.count <= MAX_COUNT:
587
+ print('--count 必须在 1-%d 之间' % MAX_COUNT, file=sys.stderr)
588
+ return 2
589
+
590
+ if args.stdin:
591
+ try:
592
+ payload = json.loads(sys.stdin.read() or '{}')
593
+ except ValueError as error:
594
+ print('stdin 不是一个合法的 JSON:%s' % error, file=sys.stderr)
595
+ return 2
596
+ payload.setdefault('name', '对方')
597
+ payload.setdefault('lines', [])
598
+ elif args.talker:
599
+ payload, code = read_from_db(args.talker, 7)
600
+ if payload is None:
601
+ return code
602
+ else:
603
+ print('要 --talker <会话名> 或者 --stdin', file=sys.stderr)
604
+ return 2
605
+
606
+ config = load_config()
607
+ state_chars = len('\n'.join(payload['lines']))
608
+ premise = premise_note(payload.get('lastFromMe'))
609
+ if args.dry_run:
610
+ preview = {'success': True, 'dryRun': True, 'action': 'draft-reply',
611
+ 'name': payload['name'], 'messages': len(payload['lines']),
612
+ 'lastFromMe': payload.get('lastFromMe'), 'premise': premise,
613
+ 'stateChars': state_chars, 'count': args.count,
614
+ 'models': {'judge': 'Jev(决策)', 'draft': 'DeepSeek(生成)'},
615
+ 'calls': '判断 1 次 + 起草 1 次 + 排序 1 次(判断那一步调不通时降级:只发生成那一次,'
616
+ '且没有闸门与排序)',
617
+ 'readsLocalChat': True, 'invokesAI': True, 'writesNothing': True}
618
+ if args.json:
619
+ print(json.dumps(preview, ensure_ascii=False, indent=2))
620
+ else:
621
+ print('会话:%s(%d 条消息,%d 字符会发给判断模型与生成模型)'
622
+ % (preview['name'], preview['messages'], state_chars))
623
+ if premise:
624
+ print(premise)
625
+ print('要问的判断:意图 / 需要 / 动作 / 该不该给实质 / 风险 / 涉钱 / 未兑现承诺')
626
+ print('三次调用:判断(Jev)→ 起草 %d 条(DeepSeek)→ 排序(Jev)' % args.count)
627
+ return 0
628
+
629
+ if not args.yes:
630
+ print('会把这段对话发给 Jev 与 DeepSeek 两个云端模型。加 --yes 确认。', file=sys.stderr)
631
+ return 1
632
+
633
+ result = run(payload, args.count, config, args.gate_commitment)
634
+ # 前提先说:这次不是"他在等我回"而是"我在等他回"。它同时进了判断与起草两段提示词,
635
+ # 但**用户也要看见**——否则候选看起来像在回一句根本不存在的话。
636
+ if not args.json and result.get('premise'):
637
+ print(result['premise'])
638
+ print()
639
+ if args.json:
640
+ print(json.dumps(result, ensure_ascii=False, indent=2))
641
+ elif not result.get('success'):
642
+ print('没跑通:%s' % result.get('error'), file=sys.stderr)
643
+ elif result['gate'] == 'refused':
644
+ print('**不起草。** %s' % result['reason'])
645
+ for item in result['advice']:
646
+ print(' · %s' % item)
647
+ else:
648
+ judgment = result['judgment']
649
+ if result.get('judged') is False:
650
+ print('**这次没有判断**(%s)。' % (result.get('judgeNote') or '判断那一步没调通'))
651
+ print('所以"涉钱/风险高就不给草稿"那道闸门没生效,下面几条是生成模型按上下文给的,自己看一眼:')
652
+ for index, item in enumerate(result.get('drafts') or [], 1):
653
+ print(' %d. %s' % (index, item.get('text')))
654
+ return 0
655
+ print('判断:意图 %s · 需要 %s · 动作 %s · 该给实质 %s · 风险 %s(%s)'
656
+ % (judgment.get('intent'), judgment.get('need'), judgment.get('action'),
657
+ '是' if (judgment.get('shouldReply') or 0) >= NOUL_TRUE else '否',
658
+ judgment.get('risk'), risk_word(judgment.get('risk') or 0)))
659
+ print()
660
+ for index, draft in enumerate(result['drafts'], 1):
661
+ print('%d. %s' % (index, draft['text']))
662
+ if not result.get('ranked'):
663
+ print()
664
+ print('(排序那一步没跑通,以上是模型给出的原顺序)')
665
+ print()
666
+ print('这些只是文本,没有发送任何东西;回不回、怎么回由你决定。')
667
+ return 0 if result.get('success') else 1
668
+
669
+
670
+ if __name__ == '__main__':
671
+ sys.exit(main())