@quecue/dev-notify 0.1.0-alpha.3 → 0.1.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.
package/dist/cli.js CHANGED
@@ -9,32 +9,39 @@ import { deliver, makePlan } from './channels.js';
9
9
  import { wizard, clearCredentials } from './setup.js';
10
10
  import { terminalPrompts, readText, Cancelled } from './prompts.js';
11
11
  import { CredentialError } from './credentials.js';
12
- import { claudeCodeStatus, configureClaudeCode, offerClaudeCode } from './claude-code.js';
12
+ import { claudeCodeStatus, claudeSettingsPath, configureClaudeCode } from './claude-code.js';
13
13
  import { dispatchClaudeHook, readHookInput } from './claude-hook.js';
14
14
  import { accessReplies, configureReplies, disableReplies } from './reply-state.js';
15
15
  import { startBridge, stopBridge, bridgeStatus, runBridge } from './reply-bridge.js';
16
- const HELP = `dev-notify 0.1.0-alpha.3 — 跨平台开发任务通知
16
+ import { updatePackage } from './update.js';
17
+ export const VERSION = '0.1.1';
18
+ const HELP = `dev-notify ${VERSION} — 跨平台开发任务通知
17
19
 
18
20
  命令:
19
21
  send --title 标题 [--content 正文 | --content-file 文件 | --stdin]
20
22
  [--channel all|telegram|feishu] [--level info|success|warn|error]
21
23
  [--footer 辅助信息] [--dry-run]
22
- setup 配置渠道及系统凭据,可选测试与 Claude Code 通知
23
- setup --template [--yes] 仅创建无密钥模板,不覆盖已有配置、不安装 hooks
24
- credentials clear 确认后移除当前 Node 凭据引用和条目
25
- paths 显示配置路径,不写文件
26
- doctor / status 离线检查,不发消息
24
+ setup [--no-test] [--no-hooks]
25
+ 配置渠道及系统凭据;默认同时发送测试并安装
26
+ Claude Code 通知 hooks,一次确认完成
27
+ setup --template [--yes] 只创建无密钥模板;不会覆盖已有配置,也不会安装 hooks
28
+ credentials clear 移除当前 Node 凭据引用和条目(需要确认)
29
+ paths 显示配置路径(不会写入文件)
30
+ doctor / status 离线检查(不会发送消息)
31
+ update [--check] [--yes] 检查并更新到 npm 最新版本(确认后调用 npm)
27
32
  integration list 列出可用集成
28
33
  integration claude-code install / status / remove
29
34
  单独安装、查询或移除用户级 hooks
30
- reply setup 另行授权可信 Telegram 私聊用户
35
+ reply setup 授权可信的 Telegram 私聊用户
31
36
  reply start / status / stop
32
37
  启动、查询或停止回复桥接
33
- reply disable 确认后撤销远程回复授权并请求停止桥接
38
+ reply disable 撤销远程回复授权并停止桥接(需要确认)
34
39
  reply run 前台运行桥接(排错用,不启用系统自启动)
35
40
 
36
41
  全局选项:--config 路径、--help、--version
37
- 系统凭据使用 Windows Credential Manager / macOS Keychain;不调用 Python,不写入明文密钥。`;
42
+ 环境变量:TELEGRAM_BOT_TOKEN、TELEGRAM_CHAT_ID / TELEGRAM_CHAT_IDS、
43
+ FEISHU_WEBHOOK、FEISHU_SECRET;显式存在时优先。详见 README。
44
+ 系统凭据保存在 Windows Credential Manager / macOS Keychain;不会写入明文密钥。`;
38
45
  const defaults = {
39
46
  env: process.env,
40
47
  out: text => console.log(text),
@@ -44,7 +51,7 @@ const defaults = {
44
51
  hookStdin: () => readHookInput(),
45
52
  confirm: async () => {
46
53
  if (!process.stdin.isTTY)
47
- throw new ConfigError('非交互环境请使用 setup --template --yes');
54
+ throw new ConfigError('当前不是交互终端。自动化场景请使用 setup --template --yes。');
48
55
  return /^(y|yes)$/i.test(await readText('确认创建模板?[y/N] '));
49
56
  },
50
57
  send: deliver,
@@ -60,10 +67,11 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
60
67
  title: { type: 'string' }, content: { type: 'string' }, 'content-file': { type: 'string' }, stdin: { type: 'boolean' },
61
68
  channel: { type: 'string' }, level: { type: 'string' }, footer: { type: 'string' }, 'dry-run': { type: 'boolean' },
62
69
  template: { type: 'boolean' }, yes: { type: 'boolean' },
70
+ 'no-test': { type: 'boolean' }, 'no-hooks': { type: 'boolean' }, check: { type: 'boolean' },
63
71
  } });
64
72
  }
65
73
  catch {
66
- throw new ConfigError('参数无效,请使用 --help;不回显可能含秘密的参数');
74
+ throw new ConfigError('参数无效,请运行 --help 查看用法。');
67
75
  }
68
76
  const { values, positionals } = parsed;
69
77
  if (values.help) {
@@ -71,30 +79,36 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
71
79
  return 0;
72
80
  }
73
81
  if (values.version) {
74
- context.out('dev-notify 0.1.0-alpha.3');
82
+ context.out(`dev-notify ${VERSION}`);
75
83
  return 0;
76
84
  }
77
85
  const [command, action, integrationAction] = positionals;
78
86
  const allowed = {
79
87
  send: ['title', 'content', 'content-file', 'stdin', 'channel', 'level', 'footer', 'dry-run'],
80
- setup: ['template', 'yes'], paths: [], doctor: [], status: [], integration: [], credentials: [], reply: [],
88
+ setup: ['template', 'yes', 'no-test', 'no-hooks'], paths: [], doctor: [], status: [], integration: [], credentials: [], reply: [], update: ['check', 'yes'],
81
89
  };
82
90
  if (!command || !Object.hasOwn(allowed, command) || positionals.length > (command === 'integration' ? 3 : command === 'credentials' || command === 'reply' ? 2 : 1)
83
91
  || Object.keys(values).some(key => key !== 'config' && !allowed[command].includes(key)))
84
- throw new ConfigError('命令或选项组合无效,请使用 --help');
92
+ throw new ConfigError('命令或选项组合不正确,请运行 --help 查看用法。');
85
93
  const paths = appPaths(process.platform, context.env);
86
94
  const file = values.config ? path.resolve(values.config) : paths.config;
87
95
  if (command === 'paths') {
88
96
  context.out(JSON.stringify({ config: file, data: paths.data }, null, 2));
89
97
  return 0;
90
98
  }
99
+ if (command === 'update') {
100
+ return await updatePackage(VERSION, {
101
+ out: context.out, prompts: context.prompts,
102
+ check: !!values.check, yes: !!values.yes,
103
+ });
104
+ }
91
105
  if (command === 'integration') {
92
106
  if (action === 'list' && !integrationAction) {
93
- context.out('claude-code: 可选通知集成、Telegram 回复与结构化问题交互;通知 hooks 与 reply setup 输入授权分开,默认不开启远程输入。');
107
+ context.out('claude-code:支持通知、Telegram 回复和结构化问题交互。通知 hooks 与回复权限分开,默认只发送通知,不接收远程输入。');
94
108
  return 0;
95
109
  }
96
110
  if (action !== 'claude-code' || !['install', 'status', 'remove', 'hook'].includes(integrationAction))
97
- throw new ConfigError('请使用 integration list 或 integration claude-code install / status / remove');
111
+ throw new ConfigError('请使用 integration list,或 integration claude-code install / status / remove。');
98
112
  if (integrationAction === 'hook') {
99
113
  await dispatchClaudeHook(await context.hookStdin(), file, { ...context, response: value => context.out(JSON.stringify(value)) });
100
114
  return 0;
@@ -103,44 +117,44 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
103
117
  const status = await claudeCodeStatus({ env: context.env });
104
118
  context.out(`Claude Code 用户级设置:${status.file}`);
105
119
  context.out(`本工具 hooks:Stop ${status.stop} 个,Notification ${status.notification} 个,会话生命周期 ${status.lifecycle}/3 个,交互及结果确认 ${status.interaction}/5 个;其他处理器 ${status.others} 个。`);
106
- context.out(status.disabled ? '当前文件设置了 disableAllHooks;本工具不会解除禁用。'
107
- : '仅检查用户级文件;实际是否加载或受项目、托管策略限制,请在 Claude Code /hooks 中确认。');
120
+ context.out(status.disabled ? '当前文件启用了 disableAllHooks,安装后 hooks 不会生效;请自行解除禁用。'
121
+ : '这里只检查用户级文件。是否实际加载还受项目和托管策略影响,请在 Claude Code 的 /hooks 中确认。');
108
122
  return status.stop === 1 && status.notification === 1 && status.lifecycle === 3 && status.interaction === 5 && !status.disabled ? 0 : 1;
109
123
  }
110
124
  const prompts = context.prompts ?? terminalPrompts();
111
125
  if (integrationAction === 'install') {
112
126
  const config = await loadConfig(file, true, context.env);
113
127
  if (!ready(config, 'telegram') && !ready(config, 'feishu'))
114
- throw new ConfigError('请先运行 setup 配置可用通知渠道,再安装 Claude Code hooks');
128
+ throw new ConfigError('请先运行 setup 配置至少一个通知渠道,再安装 Claude Code hooks。');
115
129
  }
116
130
  return await configureClaudeCode(file, integrationAction, prompts, { env: context.env }) ? 0 : 130;
117
131
  }
118
132
  if (command === 'reply') {
119
133
  if (!['setup', 'start', 'status', 'stop', 'disable', 'run'].includes(action))
120
- throw new ConfigError('请使用 reply setup / start / status / stop / disable / run');
134
+ throw new ConfigError('请使用 reply setup / start / status / stop / disable / run。');
121
135
  if (action === 'setup') {
122
136
  const saved = await configureReplies(file, context.prompts ?? terminalPrompts(), context.env);
123
137
  if (saved)
124
- context.out('授权已保存;请确认已安装本版 hooks,再运行 reply start。新授权不会恢复旧消息映射。');
138
+ context.out('授权已保存。请确认 hooks 已安装,然后运行 reply start。旧消息的回复映射不会保留。');
125
139
  return saved ? 0 : 130;
126
140
  }
127
141
  if (action === 'disable') {
128
142
  if (!await disableReplies(file, context.prompts ?? terminalPrompts()))
129
143
  return 130;
130
144
  const stopped = await stopBridge(file);
131
- context.out(stopped ? '桥接已停止或未运行。' : '已撤销授权并请求停止;正在进行中的调用不能撤回。');
145
+ context.out(stopped ? '桥接已停止或未运行。' : '已撤销授权并请求停止。正在进行中的调用无法撤回。');
132
146
  return 0;
133
147
  }
134
148
  if (action === 'stop') {
135
149
  const stopped = await stopBridge(file);
136
- context.out(stopped ? '桥接已停止或未运行;授权仍保留,下一次事件可再次启动。彻底关闭请用 reply disable。'
137
- : '停止请求已登记;桥接尚未退出,未强杀进程,请用 reply status 检查。');
150
+ context.out(stopped ? '桥接已停止或未运行。授权仍然保留,下次事件可以再次启动;如需彻底关闭,请运行 reply disable。'
151
+ : '已发出停止请求,桥接正在退出。可运行 reply status 查看状态。');
138
152
  return stopped ? 0 : 1;
139
153
  }
140
154
  if (action === 'status') {
141
155
  const access = await accessReplies(file, context.env), status = await bridgeStatus(file);
142
- context.out(access ? `远程回复:已另行授权 ${access.policy.users.length} 个私聊用户;逐次工具权限${access.policy.permissions ? '已启用' : '未启用'}。` : '远程回复:未启用。');
143
- context.out(status.ready ? '桥接:已就绪。' : status.running ? '桥接:进程存在但未就绪。' : '桥接:未运行。');
156
+ context.out(access ? `远程回复:已授权 ${access.policy.users.length} 个私聊用户;工具权限逐次确认${access.policy.permissions ? '已开启' : '未开启'}。` : '远程回复:尚未启用。');
157
+ context.out(status.ready ? '桥接:已就绪。' : status.running ? '桥接:进程存在,但还没有就绪。' : '桥接:未运行。');
144
158
  if (status.error)
145
159
  context.err(status.error);
146
160
  return access && status.ready ? 0 : 1;
@@ -150,22 +164,39 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
150
164
  return 0;
151
165
  }
152
166
  const ready = await startBridge(file, context.env);
153
- context.out(ready ? '回复桥接已就绪;请从新任务通知进入。' : '桥接尚未就绪;请用 reply status / reply run 查看安全错误,不要同时运行旧 Python 桥接。');
167
+ context.out(ready ? '回复桥接已就绪。请从新的任务通知进入。' : '桥接还没有就绪。请运行 reply status 或 reply run 查看原因;如果旧版桥接在运行,请先停用它。');
154
168
  return ready ? 0 : 1;
155
169
  }
156
170
  if (command === 'credentials') {
157
171
  if (action !== 'clear')
158
- throw new ConfigError('当前只支持 credentials clear,且必须交互确认');
172
+ throw new ConfigError('credentials 目前只支持 clear,并且需要在交互终端中确认。');
159
173
  return await clearCredentials(file, context.prompts ?? terminalPrompts()) ? 0 : 130;
160
174
  }
161
175
  if (command === 'setup') {
162
176
  if (!values.template) {
163
177
  if (values.yes)
164
- throw new ConfigError('--yes 只允许与 --template 同时使用,不能跳过密钥保存确认');
178
+ throw new ConfigError('--yes 只能和 --template 一起使用;保存密钥前仍然需要确认。');
165
179
  const prompts = context.prompts ?? terminalPrompts();
180
+ const notes = [];
181
+ const test = !values['no-test'];
182
+ if (test)
183
+ notes.push('保存后会向最终生效的接收目标发送一条固定内容的测试通知。');
184
+ let hooks = !values['no-hooks'];
185
+ if (hooks) {
186
+ try {
187
+ notes.push(`Claude Code 用户级设置:${claudeSettingsPath(context.env)}`);
188
+ notes.push('会为该设置文件对应的所有项目安装通知 hooks。最终回复正文可能包含敏感内容,将发送到配置实际生效的渠道。');
189
+ notes.push('同时安装会话生命周期和结构化问题 hooks。远程输入只有在运行 reply setup 授权后才会开启。');
190
+ }
191
+ catch (error) {
192
+ hooks = false;
193
+ context.out(error instanceof ConfigError ? `跳过 Claude Code hooks:${error.message}` : '跳过 Claude Code hooks:没有找到用户级设置路径。');
194
+ }
195
+ }
196
+ let integrationFailed = false;
166
197
  const saved = await wizard(file, {
167
- prompts, store: context.store, env: context.env,
168
- testSend: async (config) => {
198
+ prompts, store: context.store, env: context.env, notes,
199
+ testSend: test ? async (config) => {
169
200
  let count = 0, ok = true;
170
201
  for (const channel of ['telegram', 'feishu']) {
171
202
  if (!ready(config, channel))
@@ -173,57 +204,50 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
173
204
  count++;
174
205
  const plan = makePlan(channel, { title: 'dev-notify 配置测试', content: '这是一条由你确认发送的测试通知。', footer: '', level: 'info' }, config);
175
206
  const result = await context.send(plan, config);
176
- context.out(`${channel}: 测试已发送 ${result.sent}/${result.planned}`);
207
+ context.out(`${channel}:测试已发送 ${result.sent}/${result.planned} 条`);
177
208
  ok = ok && result.ok;
178
209
  }
179
210
  return count > 0 && ok;
180
- },
211
+ } : undefined,
212
+ afterSave: hooks ? async () => {
213
+ try {
214
+ await configureClaudeCode(file, 'install', prompts, { env: context.env, confirm: false });
215
+ }
216
+ catch (error) {
217
+ integrationFailed = true;
218
+ throw error;
219
+ }
220
+ } : undefined,
181
221
  });
182
222
  if (!saved)
183
223
  return 130;
184
- try {
185
- await offerClaudeCode(file, prompts, { env: context.env });
186
- const config = await loadConfig(file, true, context.env);
187
- if (ready(config, 'telegram') && /^(y|yes)$/i.test(await prompts.text('是否另行配置可信私聊用户回复 Claude?默认关闭。[y/N] '))) {
188
- await configureReplies(file, prompts, context.env);
189
- context.out('回复授权流程结束;确认 hooks 已安装后可运行 reply start,或由下一次支持的会话事件启动。');
190
- }
191
- }
192
- catch (error) {
193
- if (error instanceof Cancelled) {
194
- context.out('渠道配置已保存;已取消后续 Claude Code 集成/回复配置。');
195
- return 0;
196
- }
197
- context.err('渠道配置已保存,但 Claude Code hook 或回复配置未完成;请运行 integration claude-code status / reply status 检查后重试。');
198
- return 1;
199
- }
200
- return 0;
224
+ return integrationFailed ? 1 : 0;
201
225
  }
202
- context.out(`将创建无密钥模板: ${file};不会联网或修改 hooks`);
226
+ context.out(`将创建无密钥模板:${file}。不会联网,也不会修改 hooks。`);
203
227
  if (!values.yes && !await context.confirm()) {
204
- context.out('已取消,未写入');
228
+ context.out('已取消,没有写入。');
205
229
  return 130;
206
230
  }
207
231
  await createConfig(file);
208
- context.out('已创建模板;请配置环境凭据和接收人');
232
+ context.out('模板已创建。请配置环境凭据和接收人。');
209
233
  return 0;
210
234
  }
211
235
  const config = await loadConfig(file, values.config !== undefined, context.env);
212
236
  if (command === 'doctor' || command === 'status') {
213
237
  context.out(`配置路径: ${file}`);
214
238
  for (const channel of ['telegram', 'feishu'])
215
- context.out(`${channel}: ${ready(config, channel) ? '配置齐全,网络未验证' : '未启用或配置不完整'}`);
216
- context.out('凭据: 环境变量优先,其次 Windows Credential Manager / macOS Keychain;检查不联网,系统可能要求授权');
239
+ context.out(`${channel}:${ready(config, channel) ? '配置完整(尚未验证网络)' : '未启用或配置不完整'}`);
240
+ context.out('凭据:优先使用环境变量,其次系统凭据;本检查不联网,系统可能弹出授权提示。');
217
241
  return ready(config, 'telegram') || ready(config, 'feishu') ? 0 : 1;
218
242
  }
219
243
  if (!values.title?.trim())
220
- throw new ConfigError('send 必须指定非空 --title');
244
+ throw new ConfigError('send 需要提供非空的 --title。');
221
245
  if ([values.content !== undefined, values['content-file'] !== undefined, !!values.stdin].filter(Boolean).length > 1)
222
- throw new ConfigError('正文来源只能指定一个');
246
+ throw new ConfigError('--content、--content-file 和 --stdin 只能选择一个。');
223
247
  const level = values.level ?? 'info';
224
248
  const channel = values.channel ?? 'all';
225
249
  if (!['info', 'success', 'warn', 'error'].includes(level) || !['all', 'telegram', 'feishu'].includes(channel))
226
- throw new ConfigError('渠道或通知级别无效');
250
+ throw new ConfigError('渠道或通知级别不正确。');
227
251
  let content = values.content ?? values.title;
228
252
  if (values['content-file'] !== undefined)
229
253
  content = decodeUtf8(await readFile(values['content-file']));
@@ -235,7 +259,7 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
235
259
  for (const name of selected) {
236
260
  if (!ready(config, name)) {
237
261
  if (channel !== 'all')
238
- throw new ConfigError('指定渠道未启用或配置不完整');
262
+ throw new ConfigError('这个渠道未启用,或配置不完整。');
239
263
  continue;
240
264
  }
241
265
  planned++;
@@ -244,31 +268,31 @@ export async function main(argv = process.argv.slice(2), overrides = {}) {
244
268
  plan = makePlan(name, message, config);
245
269
  }
246
270
  catch {
247
- context.err(`${name}: 无法生成安全分段,请缩短标题/辅助信息或检查正文格式`);
271
+ context.err(`${name}:无法安全分段。请缩短标题或底部信息,或检查正文格式。`);
248
272
  failed = true;
249
273
  continue;
250
274
  }
251
275
  if (values['dry-run']) {
252
- context.out(`${name}: 接收目标 ${plan.recipients},预计请求 ${plan.items.length};未发送`);
276
+ context.out(`${name}:接收目标 ${plan.recipients},预计请求 ${plan.items.length} 条(dry-run,没有实际发送)`);
253
277
  continue;
254
278
  }
255
279
  const result = await context.send(plan, config);
256
- context.out(`${name}: 已发送 ${result.sent}/${result.planned}`);
280
+ context.out(`${name}:已发送 ${result.sent}/${result.planned} 条`);
257
281
  if (!result.ok) {
258
282
  failed = true;
259
- context.err(result.error ?? '投递失败');
283
+ context.err(result.error ?? '发送失败。');
260
284
  }
261
285
  }
262
286
  if (!planned)
263
- throw new ConfigError('没有可发送的渠道,请配置环境凭据和接收人');
287
+ throw new ConfigError('没有可用渠道,请先配置环境凭据和接收人。');
264
288
  return failed ? 1 : 0;
265
289
  }
266
290
  catch (error) {
267
291
  if (error instanceof Cancelled) {
268
- context.out('已取消,未继续执行。');
292
+ context.out('已取消。');
269
293
  return isHook ? 0 : 130;
270
294
  }
271
- context.err(error instanceof ConfigError || error instanceof CredentialError ? error.message : '操作失败,请检查文件、权限或输入;不输出可能含秘密的底层异常');
295
+ context.err(error instanceof ConfigError || error instanceof CredentialError ? error.message : '操作失败。请检查文件、权限或输入;为避免泄露密钥,这里不显示底层异常。');
272
296
  // Stop hook 的退出码 2 会要求 Claude 继续;通知失败只能报告,不能阻塞或触发重试循环。
273
297
  return isHook ? 0 : 2;
274
298
  }
package/dist/config.js CHANGED
@@ -13,7 +13,7 @@ export function appPaths(platform = process.platform, env = process.env, home) {
13
13
  const join = platform === 'win32' ? path.win32 : path.posix;
14
14
  if (env.DEV_NOTIFY_HOME) {
15
15
  if (!join.isAbsolute(env.DEV_NOTIFY_HOME))
16
- throw new ConfigError('DEV_NOTIFY_HOME 必须是绝对路径');
16
+ throw new ConfigError('DEV_NOTIFY_HOME 需要是绝对路径。');
17
17
  return { config: join.join(env.DEV_NOTIFY_HOME, 'config', 'config.json'), data: join.join(env.DEV_NOTIFY_HOME, 'data') };
18
18
  }
19
19
  home ??= homedir();
@@ -31,23 +31,23 @@ export function appPaths(platform = process.platform, env = process.env, home) {
31
31
  }
32
32
  export function object(value, allowed, name) {
33
33
  if (!value || typeof value !== 'object' || Array.isArray(value))
34
- throw new ConfigError(`${name} 必须是 JSON 对象`);
34
+ throw new ConfigError(`${name} 需要是 JSON 对象。`);
35
35
  const result = value;
36
36
  if (Object.keys(result).some(key => !allowed.includes(key)))
37
- throw new ConfigError(`${name} 包含不支持的字段;不能直接保存明文密钥`);
37
+ throw new ConfigError(`${name} 包含不支持的字段;不能直接保存明文密钥。`);
38
38
  return result;
39
39
  }
40
40
  function enabled(section) {
41
41
  if (section.enabled === undefined)
42
42
  return true;
43
43
  if (typeof section.enabled !== 'boolean')
44
- throw new ConfigError('enabled 必须是布尔值');
44
+ throw new ConfigError('enabled 需要是布尔值。');
45
45
  return section.enabled;
46
46
  }
47
47
  function secret(section, field, fallback, env, stored) {
48
48
  const name = section[field] ?? fallback;
49
49
  if (typeof name !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
50
- throw new ConfigError('凭据引用必须是合法的环境变量名称');
50
+ throw new ConfigError('凭据引用需要是合法的环境变量名称。');
51
51
  if (Object.hasOwn(env, name))
52
52
  return (env[name] ?? '').trim();
53
53
  return enabled(section) ? stored().trim() : '';
@@ -55,12 +55,12 @@ function secret(section, field, fallback, env, stored) {
55
55
  export function parseConfig(input, env = process.env, resolver = resolveReference) {
56
56
  const data = object(input, ['version', 'telegram', 'feishu', 'credentials'], '配置');
57
57
  if ((data.version ?? 1) !== 1)
58
- throw new ConfigError('不支持的配置版本');
58
+ throw new ConfigError('不支持这个配置版本。');
59
59
  let reference;
60
60
  if (data.credentials !== undefined) {
61
61
  const value = object(data.credentials, ['backend', 'id'], 'credentials');
62
62
  if (!['system', 'keyring'].includes(String(value.backend)) || typeof value.id !== 'string' || !/^[a-f0-9]{32}$/.test(value.id))
63
- throw new ConfigError('系统凭据引用无效');
63
+ throw new ConfigError('系统凭据引用无效。');
64
64
  reference = value;
65
65
  }
66
66
  let cached;
@@ -78,18 +78,18 @@ export function parseConfig(input, env = process.env, resolver = resolveReferenc
78
78
  ids = `${env.TELEGRAM_CHAT_ID ?? ''},${env.TELEGRAM_CHAT_IDS ?? ''}`.split(',').map(v => v.trim()).filter(Boolean);
79
79
  }
80
80
  if (!Array.isArray(ids))
81
- throw new ConfigError('telegram.chat_ids 必须是数组');
81
+ throw new ConfigError('telegram.chat_ids 需要是数组。');
82
82
  const chatIds = [...new Set(ids.map(value => {
83
83
  if (typeof value !== 'string' && !(typeof value === 'number' && Number.isSafeInteger(value)))
84
- throw new ConfigError('接收人必须是数字 chat_id;大整数请使用字符串');
84
+ throw new ConfigError('接收人需要使用数字 chat_id;大整数请写成字符串。');
85
85
  const id = String(value).trim();
86
86
  if (!/^-?[1-9][0-9]*$/.test(id))
87
- throw new ConfigError('接收人必须是非零数字 chat_id');
87
+ throw new ConfigError('接收人需要是非零的数字 chat_id。');
88
88
  return id;
89
89
  }))];
90
90
  const token = secret(tg, 'token_env', 'TELEGRAM_BOT_TOKEN', env, () => stored('telegram_token'));
91
91
  if (token && !/^[0-9]+:[A-Za-z0-9_-]+$/.test(token))
92
- throw new ConfigError('Telegram Token 格式无效');
92
+ throw new ConfigError('Telegram Token 格式不正确。');
93
93
  const webhook = secret(fs, 'webhook_env', 'FEISHU_WEBHOOK', env, () => stored('feishu_webhook'));
94
94
  if (webhook) {
95
95
  let valid = false;
@@ -99,7 +99,7 @@ export function parseConfig(input, env = process.env, resolver = resolveReferenc
99
99
  }
100
100
  catch { /* 不输出含秘密的 URL 异常。 */ }
101
101
  if (!valid)
102
- throw new ConfigError('飞书 webhook 必须是 open.feishu.cn 的 HTTPS 自定义机器人地址');
102
+ throw new ConfigError('飞书 webhook 需要是 open.feishu.cn 的 HTTPS 自定义机器人地址。');
103
103
  }
104
104
  return { telegram: { enabled: tgEnabled, token, chatIds }, feishu: { enabled: fsEnabled, webhook,
105
105
  secret: secret(fs, 'secret_env', 'FEISHU_SECRET', env, () => stored('feishu_secret')) } };
@@ -113,7 +113,7 @@ export function decodeUtf8(data) {
113
113
  return new TextDecoder('utf-8', { fatal: true }).decode(data);
114
114
  }
115
115
  catch {
116
- throw new ConfigError('输入必须是有效的 UTF-8 文本');
116
+ throw new ConfigError('输入需要是有效的 UTF-8 文本。');
117
117
  }
118
118
  }
119
119
  export async function loadConfig(file, required = false, env = process.env) {
@@ -124,7 +124,7 @@ export async function loadConfig(file, required = false, env = process.env) {
124
124
  catch (error) {
125
125
  if (error.code === 'ENOENT' && !required)
126
126
  return parseConfig({}, env);
127
- throw new ConfigError('配置文件缺失、不可读取或不是合法 UTF-8 JSON');
127
+ throw new ConfigError('配置文件缺失、无法读取,或不是合法的 UTF-8 JSON。');
128
128
  }
129
129
  return parseConfig(input, env);
130
130
  }
@@ -135,7 +135,7 @@ export async function createConfig(file) {
135
135
  }
136
136
  catch (error) {
137
137
  if (error.code === 'EEXIST')
138
- throw new ConfigError('配置已存在,未覆盖;Node 版不会修改 Python 原型的配置');
139
- throw new ConfigError('无法创建配置,请检查路径和权限');
138
+ throw new ConfigError('配置已经存在,没有覆盖。Node 版不会修改旧版原型的配置。');
139
+ throw new ConfigError('无法创建配置,请检查路径和权限。');
140
140
  }
141
141
  }
@@ -6,7 +6,7 @@ export class CredentialError extends Error {
6
6
  const fields = ['telegram_token', 'feishu_webhook', 'feishu_secret'];
7
7
  export function validateId(id) {
8
8
  if (typeof id !== 'string' || !/^[0-9a-f]{32}$/.test(id))
9
- throw new CredentialError('系统凭据引用无效');
9
+ throw new CredentialError('系统凭据引用无效。');
10
10
  }
11
11
  export function decodeBundle(raw) {
12
12
  try {
@@ -18,7 +18,7 @@ export function decodeBundle(raw) {
18
18
  return result;
19
19
  }
20
20
  catch {
21
- throw new CredentialError('系统凭据数据无效,请重新运行 setup');
21
+ throw new CredentialError('系统凭据数据无效,请重新运行 setup。');
22
22
  }
23
23
  }
24
24
  export function encodeBundle(values) {
@@ -26,7 +26,7 @@ export function encodeBundle(values) {
26
26
  decodeBundle(encoded);
27
27
  // Windows Credential Manager 通用凭据 blob 上限 2560 字节,跨平台采用保守值。
28
28
  if (encoded.byteLength > 2400)
29
- throw new CredentialError('凭据总长度超过 2400 字节,请检查输入;未保存');
29
+ throw new CredentialError('凭据总长度超过 2400 字节,请检查输入;没有保存。');
30
30
  return encoded;
31
31
  }
32
32
  export class KeyringStore {
@@ -42,7 +42,7 @@ export class KeyringStore {
42
42
  return raw === null ? null : decodeBundle(Uint8Array.from(raw));
43
43
  }
44
44
  catch {
45
- throw new CredentialError('系统凭据无法读取或内容无效,请解锁系统存储并允许访问,或使用 setup 重配');
45
+ throw new CredentialError('无法读取系统凭据,或内容无效。请解锁系统凭据存储并允许访问,或重新运行 setup 配置。');
46
46
  }
47
47
  }
48
48
  create(id, values) {
@@ -52,12 +52,12 @@ export class KeyringStore {
52
52
  try {
53
53
  entry = this.entry(id);
54
54
  if (entry.getSecret() !== null)
55
- throw new CredentialError('随机凭据引用已存在,未覆盖,请重试');
55
+ throw new CredentialError('这个随机凭据引用已经存在,没有覆盖,请重试。');
56
56
  }
57
57
  catch (error) {
58
58
  if (error instanceof CredentialError)
59
59
  throw error;
60
- throw new CredentialError('无法访问系统凭据存储,请检查系统权限');
60
+ throw new CredentialError('无法访问系统凭据存储,请检查系统权限。');
61
61
  }
62
62
  try {
63
63
  entry.setSecret(encoded);
@@ -68,9 +68,9 @@ export class KeyringStore {
68
68
  entry.deleteCredential();
69
69
  }
70
70
  catch {
71
- throw new CredentialError(`凭据写入状态未知且清理失败,需检查引用 ID:${id}`);
71
+ throw new CredentialError(`凭据写入状态未知,清理也失败了。请检查引用 ID:${id}`);
72
72
  }
73
- throw new CredentialError('系统凭据写入失败,未切换配置');
73
+ throw new CredentialError('系统凭据写入失败,配置没有切换。');
74
74
  }
75
75
  }
76
76
  remove(id) {
@@ -79,27 +79,27 @@ export class KeyringStore {
79
79
  this.entry(id).deleteCredential();
80
80
  }
81
81
  catch {
82
- throw new CredentialError('系统凭据删除失败,请检查存储权限');
82
+ throw new CredentialError('系统凭据删除失败,请检查存储权限。');
83
83
  }
84
84
  }
85
85
  }
86
86
  const require = createRequire(import.meta.url);
87
87
  export function nativeStore(platform = process.platform) {
88
88
  if (platform !== 'win32' && platform !== 'darwin')
89
- throw new CredentialError('系统凭据仅支持 Windows/macOS;其他平台请使用环境变量');
89
+ throw new CredentialError('系统凭据仅支持 Windows 和 macOS;其他平台请使用环境变量。');
90
90
  try {
91
91
  const binding = require('@napi-rs/keyring');
92
92
  return new KeyringStore(binding.Entry);
93
93
  }
94
94
  catch {
95
- throw new CredentialError('原生凭据组件无法加载,请确认 npm 安装保留 optional 依赖且架构匹配;不会降级为明文');
95
+ throw new CredentialError('原生凭据组件无法加载。请确认 npm 安装保留了 optional 依赖,并且架构匹配;不会降级为明文存储。');
96
96
  }
97
97
  }
98
98
  export function resolveReference(reference) {
99
99
  if (reference.backend === 'system')
100
- throw new CredentialError('这是 Python 原型的凭据引用;请用 setup 重新输入并保存为 Node 凭据,或显式提供环境变量。不会自动读取或删除旧凭据');
100
+ throw new CredentialError('这是旧版原型保存的凭据引用。请用 setup 重新输入并保存为 Node 凭据,或显式提供环境变量;不会自动读取或删除旧凭据。');
101
101
  const values = nativeStore().read(reference.id);
102
102
  if (values === null)
103
- throw new CredentialError('系统凭据条目缺失,请运行 setup 重新配置');
103
+ throw new CredentialError('系统凭据条目缺失,请运行 setup 重新配置。');
104
104
  return values;
105
105
  }
@@ -104,7 +104,7 @@ export function visibleText(blocks) {
104
104
  }
105
105
  function cut(text, budget, measure) {
106
106
  if (budget < 1)
107
- throw new Error('分段空间不足');
107
+ throw new Error('可用空间不足,无法分段。');
108
108
  let used = 0, end = 0;
109
109
  for (const ch of text) {
110
110
  const size = measure(ch);
@@ -114,7 +114,7 @@ function cut(text, budget, measure) {
114
114
  end += ch.length;
115
115
  }
116
116
  if (!end && text)
117
- throw new Error('分段空间不足以容纳一个字符');
117
+ throw new Error('可用空间连一个字符都放不下。');
118
118
  if (end < text.length) {
119
119
  for (const boundary of ['\n\n', '\n', ' ']) {
120
120
  const offset = text.slice(0, end).lastIndexOf(boundary);
@@ -128,7 +128,7 @@ function cut(text, budget, measure) {
128
128
  }
129
129
  export function telegramParts(blocks, budget) {
130
130
  if (budget < 2)
131
- throw new Error('Telegram 标题或辅助信息过长');
131
+ throw new Error('Telegram 标题或底部信息太长,没有剩余空间放正文。');
132
132
  const output = [];
133
133
  let html = '', plain = '';
134
134
  const flush = () => { if (plain)
@@ -156,7 +156,7 @@ export function telegramParts(blocks, budget) {
156
156
  }
157
157
  export function feishuParts(blocks, budget) {
158
158
  if (budget < 128)
159
- throw new Error('飞书标题或辅助信息过长');
159
+ throw new Error('飞书标题或底部信息太长,没有剩余空间放正文。');
160
160
  const output = [];
161
161
  let current = '';
162
162
  const size = (text) => Buffer.byteLength(text, 'utf8');
package/dist/prompts.js CHANGED
@@ -5,7 +5,7 @@ export class Cancelled extends Error {
5
5
  }
6
6
  export function terminalPrompts() {
7
7
  if (!process.stdin.isTTY || !process.stdout.isTTY)
8
- throw new ConfigError('配置向导需要交互终端;自动化请使用 setup --template --yes 和环境变量');
8
+ throw new ConfigError('配置向导需要在交互终端中运行。自动化场景可以改用 setup --template --yes 搭配环境变量。');
9
9
  return {
10
10
  out: text => console.log(text),
11
11
  text: label => readText(label),
@@ -22,7 +22,7 @@ export async function readText(label, input = process.stdin, output = process.st
22
22
  return (await terminal.question(label, { signal: controller.signal })).trim();
23
23
  }
24
24
  catch {
25
- throw new Cancelled('已取消');
25
+ throw new Cancelled('已取消。');
26
26
  }
27
27
  finally {
28
28
  terminal.off('SIGINT', cancel);
@@ -32,7 +32,7 @@ export async function readText(label, input = process.stdin, output = process.st
32
32
  }
33
33
  export async function readHidden(label, input = process.stdin, output = process.stdout) {
34
34
  if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== 'function')
35
- throw new ConfigError('当前终端不支持隐藏输入,拒绝回显密钥');
35
+ throw new ConfigError('当前终端不支持隐藏输入。为了不暴露密钥,这里不会改用明文输入;请在支持的终端中运行,或用环境变量配置。');
36
36
  const wasRaw = input.isRaw, wasPaused = input.isPaused();
37
37
  return new Promise((resolve, reject) => {
38
38
  let value = '';
@@ -58,8 +58,8 @@ export async function readHidden(label, input = process.stdin, output = process.
58
58
  value = '';
59
59
  error ? reject(error) : resolve(result);
60
60
  };
61
- const failed = () => finish(new ConfigError('隐藏输入失败,未保存凭据'));
62
- const cancelled = () => finish(new Cancelled('已取消'));
61
+ const failed = () => finish(new ConfigError('隐藏输入没有完成,凭据没有保存。'));
62
+ const cancelled = () => finish(new Cancelled('已取消。'));
63
63
  const keypress = (text, key = {}) => {
64
64
  if (key.ctrl && (key.name === 'c' || key.name === 'd'))
65
65
  return cancelled();
@@ -76,7 +76,7 @@ export async function readHidden(label, input = process.stdin, output = process.
76
76
  if (key.ctrl || key.meta || !text || /[\x00-\x1f\x7f]/.test(text))
77
77
  return;
78
78
  if (value.length + text.length > 8192)
79
- return finish(new ConfigError('凭据输入过长,未保存'));
79
+ return finish(new ConfigError('凭据输入过长,没有保存。'));
80
80
  value += text;
81
81
  };
82
82
  try {