Repository navigation
Expand file tree
/
Copy pathplugins.md
More file actions
620 lines (482 loc) · 62.7 KB
/
Copy pathplugins.md
File metadata and controls
620 lines (482 loc) · 62.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
# 插件作者指南
插件是只含数据的文件夹:一份 `plugin.toml` 清单,加上清单用到的音频或数据文件和可选的说明文字。这份指南里的插件不能带任何可执行内容,也不能申请权限;输入法按固定规则校验每个包,不符合规则的包整包拒绝,不会只装一半。
例外是 macOS、Linux 和 Windows 上实验中的脚本插件(`script`):它带一个 WebAssembly 模块、在清单里申请能力,导入后不运行,要在设置的「插件 → 脚本插件(实验)」里逐项授权才启用,也不能发布到社区。这份指南不覆盖它。
这份指南覆盖八种类型:音效包(`sound`)、音乐包(`music`)、指令表(`command_table`)、特效包(`effect`)、短语表(`phrase_table`)、辅助码表(`helpcode`)、单词本(`wordbook`)和符号集(`symbol_set`)。下面的限制都取自 `crates/client-services/src/plugins/` 里的代码,两者不一致时以代码为准。`crates/client-services/tests/fixtures/plugin-packs/` 里有每种新类型能通过和会被拒绝的示例包,社区后端用同一批包校验。
- 想先看懂插件系统怎么工作:[plugin-architecture.html](plugin-architecture.html) 是一页原理图,讲两种插件、生命周期事件和安全模型,底部有给开发者的模块分层和调用时序。GitHub 上只显示它的源码,下载后用浏览器打开;它由 `scripts/generate_plugin_architecture.py` 生成,改图请改脚本。
- 想直接开始:复制 [plugin-template](plugin-template/),这是一个能通过校验的最小按键音效包。
- 想分享自己的包:在 Windows、macOS 或 Linux 上登录账号,打开设置里的「插件 → 社区插件」,点「发布我的插件」,选一个自己导入的包即可。八种类型都能发布,特效包也一样;内置包不能发布。压缩后的包不超过 8 MB,每个账号最多发布 20 个、合计 32 MB,每小时最多发布 10 次。别人发布的包也在这一页浏览、安装和评分。
## 平台支持
插件靠设置里的「插件」页导入和选用,这一页只在桌面设备上有。Android 和 iOS 另有一个只管辅助码表的入口:直接导入单个 `.txt` 码表,见[在手机上导入单个码表](#在手机上导入单个码表)。
| 平台 | 「插件」页 | 说明 |
| --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Windows、macOS、Linux | 有 | 八种类型都能导入;个别类型在某个平台上的差异写在对应小节里,例如 Linux 不绘制特效。 |
| 鸿蒙电脑(2in1) | 有 | 设置应用和键盘扩展目前各用一个独立的沙箱,键盘读不到设置应用导入的包,也读不到插件页里的选择和开关,所以导入的插件暂时在键盘里不起作用。单词本例外:背单词在设置应用里运行,读的就是设置应用导入的包,照常可用。 |
| 鸿蒙手机和平板 | 没有 | 没有导入入口,键盘不读插件包。 |
| Android | 没有 | 辅助码表可以在「输入 › 辅助码方案」里直接导入单个 `.txt`,导入后在同一个列表里选用,键盘读的就是这里导入的码表。其他类型没有导入入口。按键音只能用内置的 `keys` 模式音效包,在「AI 设计皮肤」的「按键音效」里随皮肤一起选;上屏音、成就音效、按键旋律、背景音乐、特效包和 `/`、`@` 模式都没有。 |
| iOS | 没有 | 辅助码表可以在「输入 › 辅助码 › 更多辅助码设置」里直接导入单个 `.txt`,导入后在「辅助码方案」里选用,键盘读的就是这里导入的码表。其他类型没有导入入口,按键音是系统按键音。 |
## 文件夹结构
```text
my-keys/ 文件夹名随意;安装后的文件夹按 id 命名
plugin.toml 清单,必需
key.wav 清单里提到的音频(辅助码表和单词本则是清单点名的数据文件)
LICENSE.txt 可选的说明文字(.txt 或 .md)
```
- 文件夹里只能有普通文件:不能有子文件夹、符号链接或设备文件。
- 以 `.` 开头的文件(例如 `.DS_Store`、`.git`)会被忽略,不算进包里。
- 除了 `plugin.toml`,每个文件要么是清单里用到的音频或数据文件,要么是 `.txt` / `.md` 说明文件。其他文件都会让整个包被拒绝,包括多余的音频,以及特效包、指令表、短语表、辅助码表、单词本和符号集里的任何音频。
- 清单点名的数据文件(辅助码表的 `.txt`、单词本的 `.tsv`)不按说明文件处理:它必须存在、不能是空文件、不超过该类型的上限,扩展名必须符合(大小写不限)。
- 文件名只能用 ASCII 字母、数字、`_`、`-` 和 `.`,以字母或数字开头,不超过 64 字节。
- 一个包最多 16 个文件(含清单和说明文件),`plugin.toml` 不超过 256 KiB,每个说明文件不超过 64 KiB。
## 通用字段
每种类型的清单都以这些字段开头:
```toml
schema_version = 1
kind = "sound" # sound | music | command_table | effect | phrase_table | helpcode | wordbook | symbol_set
id = "my-keys"
name = "我的按键音"
version = "1.0.0"
license = "CC0-1.0"
author = "你的名字" # 可选
description = "一句话介绍" # 可选
permissions = [] # 可选,只能是空数组
```
| 字段 | 规则 |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema_version` | 必须是整数 `1` |
| `kind` | `sound`、`music`、`command_table`、`effect`、`phrase_table`、`helpcode`、`wordbook` 或 `symbol_set` 之一 |
| `id` | 1 到 64 字节,只含小写字母、数字、`.`、`-`、`_`,以字母或数字开头;安装后的文件夹按 id 命名,源文件夹或压缩包里外层文件夹的名字不要求与它相同 |
| `name` | 必填,不超过 80 字节,不能全是空白,不能含控制字符 |
| `version` | 必填,不超过 32 字节,同上 |
| `license` | 必填,不超过 64 字节,SPDX 许可证表达式(只含字母、数字、`.`、`-`、`+`、`(`、`)` 和空格),例如 `CC0-1.0`、`CC-BY-4.0`、`MIT OR Apache-2.0` |
| `author` | 可选,不超过 120 字节 |
| `description` | 可选,不超过 500 字节 |
| `permissions` | 可选,只能是 `[]`:插件不能申请任何权限 |
- 清单里出现上表和该类型专属键以外的任何键,都会拒绝整个包。专属键之内的未知键同样会被拒绝。
- 以下 id 属于内置包,安装的包不能使用:
- 音效包:`default`、`twinkle`、`msime-typewriter`、`msime-bubble`、`msime-8bit`、`msime-woodblock`、`msime-pentatonic`、`msime-canon`、`msime-ode-to-joy`
- 音乐包:`msime-music-lofi`、`msime-music-ambient`
- 短语表:`latex-symbols`;脚本插件:`uuid`(随桌面安装包分发的官方包,见[内置插件](#内置插件))
- 导入与已安装的包同类型、同 id 的包,会整包替换旧包。
## 音效包(`kind = "sound"`)
音效包有两种模式。
- `keys` 模式:每类按键一个样本,另有上屏音和里程碑音效。
- `sequence` 模式:只有一个样本,按音符序列变调播放,形成按键旋律。
```toml
mode = "keys" # keys | sequence,省略时为 keys
[sounds]
default = "key.wav" # keys 模式必填:其他按键都用它
space = "space.wav" # 以下可选
enter = "enter.wav"
backspace = "backspace.wav"
commit = "commit.wav" # 上屏音,连击升级的提示音也用它(每升一级高 3 个半音)
achievement = "achievement.wav" # 累计上屏数达到里程碑时播放
```
```toml
mode = "sequence"
[sequence]
sample = "tone.wav"
semitones = [0, 0, 7, 7, 9, 9, 7] # 每个音相对样本原音高的半音数
advance = "key" # key | commit,省略时为 key
```
- `keys` 模式必须有 `sounds.default`,不能有 `[sequence]`。
- `sequence` 模式必须有 `[sequence]`。
- `semitones`:1 到 128 个音符,每个是 -24 到 24 的整数,即上下各两个八度。
- `advance`:`key` 表示每按一个键走一个音,`commit` 表示每次上屏走一个音。
- 停止打字 3 秒后,旋律从第一个音重新开始。
### 只接受 WAV
音效包的样本只能是 `.wav` 文件(RIFF/WAVE,文件头必须与扩展名相符),不接受 Ogg。原因有三个:
- 宿主播放按键音前要把样本整个解码进内存。
- 只有 WAV 能在解码前从文件头读出帧数,从而确认时长不超限;Ogg 要解码完才知道多长。
- HarmonyOS 的播放器本来就只播 WAV。如果接受 Ogg,同一个包在桌面有声、在鸿蒙静音。
音乐包是边读边播的,所以仍然接受 WAV 和 Ogg,见下一节。
### 样本限制
| 限制 | 值 |
| -------------- | ------------------------------------------- |
| 不同样本文件数 | 最多 8 个(同一个文件被多个键引用只算一次) |
| 单个样本 | 1 字节到 512 KiB |
| 全部样本合计 | 4 MiB |
| 单个样本时长 | 不超过 1.5 秒 |
| 采样率 | 8000 到 192000 Hz |
时长和采样率不在导入时检查,而是宿主在解码前按文件头检查。超出的样本会被跳过,那个键不出声,所以请自己确认每个样本都不超过 1.5 秒。建议用 16 位 PCM、单声道或双声道、22050 Hz 或 44100 Hz。
## 音乐包(`kind = "music"`)
背景音乐只在输入法处于活动状态时播放;焦点在密码框里时暂停。
```toml
[music]
tracks = ["rain.ogg", "piano.wav"] # 按顺序播放,播完最后一首回到第一首
```
| 限制 | 值 |
| ------------ | -------------------------------------------------- |
| 曲目数 | 1 到 8 首,不能重复 |
| 格式 | `.wav` 或 `.ogg`(文件头必须与扩展名相符) |
| 单首大小 | 1 字节到 16 MiB |
| 全部曲目合计 | 64 MiB |
| 单首时长 | 不超过 15 分钟,宿主播放时检查,超出的曲目会被跳过 |
| 采样率 | 8000 到 192000 Hz |
## 指令表(`kind = "command_table"`)
指令表给 `/` 模式添加指令:输入 `/` 和触发词,候选里会出现模板展开后的文字。
```toml
[[commands]]
trigger = "sig"
title = "签名"
template = "{date:%Y-%m-%d} 张三"
```
| 限制 | 值 |
| ------------ | ----------------------------------------------------------------- |
| 指令条数 | 1 到 256 条;同时启用的所有指令表合计也只用前 256 条 |
| `trigger` | 1 到 32 个小写 ASCII 字母,同一个包里不能重复 |
| `title` | 不能为空,不超过 48 字节 |
| `template` | 不能为空,不超过 199 个 UTF-16 码元,不能含换行、制表符等控制字符 |
| 展开后的文字 | 同样不超过 199 个 UTF-16 码元,也不能含控制字符 |
- 模板是普通文字加三种占位符:
- `{date}` 或 `{date:格式}`,默认格式是 `%Y-%m-%d`
- `{time}` 或 `{time:格式}`,默认格式是 `%H:%M`
- `{weekday}`,展开成「星期一」到「星期日」
- 格式是 strftime 写法。
- 没有其他占位符,也不能嵌套。模板里单独出现的 `{` 或 `}` 会被视为错误。
- 多个指令表启用时,按设置页里的顺序取第一个定义了该触发词的包。
- 指令表只在 `/` 模式里起作用,而 `/` 模式默认关闭:要先在设置的「输入 → 快捷模式」里打开「指令(/ 模式)」。这个开关关着时,启用的指令表不起作用,按 `/` 照常输入标点。
- `/` 模式只在中文标点下、没有正在输入的内容时打开,而且和 K 模式、`@` 模式一样只在全拼、双拼和五笔方案下可用,其他方案下按 `/` 不会进入 `/` 模式。
- 哪些设备能导入和使用指令表,见[平台支持](#平台支持)。
## 特效包(`kind = "effect"`)
特效包不带任何文件,只选用宿主内置的一种打字特效并调整参数。
```toml
[effect]
style = "sparks" # 必填:flash | sparks | power_mode
intensity = 70 # 可选,0 到 100,默认 50
colors = ["#FFB000", "#FF4060"] # 可选,1 到 4 个颜色
duration_ms = 400 # 可选,60 到 1500
particles = 24 # 可选,0 到 64
```
| 键 | 规则 |
| ------------- | -------------------------------------------------------------------------------------------------------------- |
| `style` | 必填,`flash`(一闪)、`sparks`(火花)或 `power_mode`(火花、震动,并随连击增强)之一。特效包不能选 `off` |
| `intensity` | 可选,0 到 100 的整数,默认 50:特效的大小和持续程度 |
| `colors` | 可选,1 到 4 个颜色,每个都必须写成 `#RRGGBB`(`#` 加 6 位十六进制数,大小写均可);不支持简写、透明度和颜色名 |
| `duration_ms` | 可选,60 到 1500 的整数:一次特效持续的毫秒数 |
| `particles` | 可选,0 到 64 的整数:每次按键迸出的火花数 |
- `[effect]` 里只能有这五个键。
- 特效包里除了 `plugin.toml` 只能放 `.txt` / `.md` 说明文件,不能有图片或音频。
- 省略的参数使用宿主对该特效的默认值。
- `colors`、`duration_ms`、`particles` 只是提示,宿主绘制的特效用不上时会忽略。例如 `flash` 没有火花,`particles` 对它无效。
选用和显示:
- 选用一个特效包后,它的 `style` 和参数替代设置里的「打字特效」样式和强度。
- 选中的包被删除或无法载入时不绘制特效,不会退回其他样式。
- Linux 只显示连击计数,不绘制任何特效,所以特效包在 Linux 上没有可见效果。
- 在设置的「插件 → 我的插件」里打开已导入的特效包,选「使用此特效包」。各平台按自己能画的部分取用参数:macOS 和 Windows 用全部参数,火花在光标处迸出,按键之间轮流取 `colors` 里的颜色;鸿蒙电脑只闪烁候选卡片,取 `intensity`、`duration_ms` 和第一个颜色,忽略 `particles`。
## 数据文件的共同规则
辅助码表和单词本的数据文件按同一套规则读:
- 整个文件必须是 UTF-8,开头可以有 BOM。
- 按 `\n` 分行,每行去掉一个行尾的 `\r`,所以 LF 和 CRLF 都可以;行内其他位置的 `\r` 算控制字符,整包拒绝。
- 只有完全为空的行才跳过;只有空格的行不算空行。以 `#` 开头的行是注释。
- 不支持引号转义,引号是普通字符。任何一行不合规,整个包被拒绝,不会跳过坏行。
## 短语表(`kind = "phrase_table"`)
短语表给 K 模式(中文模式下按 Shift+K)添加短语,全部写在清单里,没有数据文件。
```toml
[[phrases]]
key = "dh"
text = "电话"
[[phrases]]
key = "dh"
text = "电话号码"
```
| 限制 | 值 |
| ------ | --------------------------------------------------------------------------- |
| 条数 | 1 到 2000 条,受清单 256 KiB 的上限约束 |
| `key` | 1 到 32 个小写 ASCII 字母 |
| `text` | 不能为空白,1 到 199 个 UTF-16 码元,不能含任何控制字符(包括换行和制表符) |
- 每条只有 `key` 和 `text` 两个键。同一个 `key` 可以对应多段文字,但同一对 `key` 和 `text` 不能重复。
- 在「插件 → 我的插件」里打开短语表,打开「启用」。最多同时启用 16 个,按启用顺序排列。
- K 模式先列词库里的短语(内置的和自己加的),再列编码以输入开头的插件短语;同一编码下靠前的表先列出,已经出现过的文字不再列出,总数最多 100 条。所有启用的表合计最多取 8192 条。
- 设置的「输入 → 快捷模式」里关闭了快捷短语时,短语表不起作用。
- K 模式只在全拼、双拼和五笔方案下可用,其他方案下按 Shift+K 不会进入 K 模式,短语表也就不起作用。
- 短语从不写进词库,删除短语表后直接消失。
- 桌面安装包随带一个官方短语表「LaTeX 符号」(id `latex-symbols`):按 LaTeX 命令名打符号,`Kalpha` 出 α、`Kgamma` 出 γ 和 Γ、`Kmathbbr` 出 ℝ。它和导入的短语表一样要在插件页打开「启用」,见[内置插件](#内置插件);编码规则在仓库的 [plugins/phrase_table/latex-symbols](../plugins/phrase_table/latex-symbols/)。
## 辅助码表(`kind = "helpcode"`)
辅助码表替换全拼或双拼的辅助码方案。
```toml
[helpcode]
table = "table.txt"
```
`[helpcode]` 只有 `table` 一个键,点名包里的一个 `.txt` 文件:
```text
# 注释
啊=a
吧=ba
```
| 限制 | 值 |
| -------- | ---------------------------------------------------------------------- |
| 文件大小 | 1 字节到 1 MiB |
| 条数 | 至少 1 条,无条数上限(仍受 1 MiB 限制),同一个字不能出现两次 |
| 字 | 恰好一个 Unicode 字符,不能是 ASCII、空白或控制字符 |
| 码 | 1 到 2 个小写 ASCII 字母;更长的码会拒绝整个包,而不是像内置表那样截断 |
- 每行是空行、`#` 注释,或者 `字=码`,等号两边不能有空格。
- 在设置的「输入 → 辅助码」里,辅助码方案的下拉框会列出已安装的辅助码表,显示为「名称(插件)」;也可以在插件详情里打开「用于全拼」或「用于双拼」。每个方案最多用一个辅助码表。Android 在「输入 › 辅助码方案」的列表里选,列表跟着当前方案(双拼或全拼)走;iOS 在「输入 › 辅助码」的「辅助码方案」里选(同样跟着当前方案),或在「更多辅助码设置」里分别给双拼、全拼选。
- 选中的辅助码表被删除或无法载入时,退回该方案原来的辅助码方案,并在「我的插件」里显示为未找到。
- 成熟的辅助码方案可能有版权。投稿到社区前请确认自己有权以所写的许可证发布这张表。
### 在手机上导入单个码表
Android 和 iOS 没有「插件」页,但可以直接导入一个 `.txt` 码表,不用自己写 `plugin.toml`:Android 在「输入 › 辅助码方案」里点「导入辅助码表…」,iOS 在「输入 › 辅助码 › 更多辅助码设置」里,在双拼或全拼那一组点「导入辅助码表…」,都用系统文件选择器选文件。输入法把它包成一个辅助码表包装进插件目录,规则与上面完全相同:
- 文件必须是 UTF-8(可以带 BOM),不超过 1 MiB。GBK 等其他编码直接提示改存为 UTF-8,不会去猜编码。
- 码表按上表的规则严格校验:同一个字出现两次、码超过两个字母、码里有大写字母,都会拒绝整张表,提示里写明文件名、原因和行号。桌面上放进 `helpcodes/custom/` 的码表按宽松规则读(后出现的行覆盖前面的,码截到两个字母),能在桌面用的表在这里可能被拒,要先改好。
- 显示名取码表开头的 `# name:` 注释,没有时用去掉扩展名的文件名;名字最长 80 字节,长了在字符边界上截断。
- 包的 id 是 `custom-` 加小写的文件名主干,文件名含中文、空格等时用文件名的哈希。同名文件再次导入时替换原来的码表,正在用它的方案会跟着用新表。
- 导入成功后自动选中到发起导入的方案(双拼或全拼),提示里写明已切换。Android 在列表末尾的「管理已导入的码表」里删除,iOS 在「更多辅助码设置」的「已导入的码表」里左滑删除;删掉正在用的码表时,这个方案退回内置方案。
鸿蒙暂时还没有这个入口:设置应用和键盘各用一个沙箱,键盘读不到设置导入的码表。
## 单词本(`kind = "wordbook"`)
单词本是背单词里的一本词书。
```toml
[wordbook]
file = "words.tsv"
```
`[wordbook]` 只有 `file` 一个键,点名包里的一个 `.tsv` 文件,每行用制表符分成两列或三列:
```text
# 单词 音标 释义
algorithm /ˈælɡərɪðəm/ n. 算法
compiler n. 编译器
cache n. 高速缓存
```
| 限制 | 值 |
| -------- | ------------------------------------------------------------------------------------- |
| 文件大小 | 1 字节到 4 MiB |
| 单词数 | 1 到 20000 个,同一个单词不能出现两次 |
| 每行 | `单词<TAB>释义` 或 `单词<TAB>音标<TAB>释义`;各列不去空白,音标可以留空,释义不能为空 |
| 单词 | 1 到 64 个字符;音标不超过 64 个字符;释义不超过 256 个字符;都不能含控制字符 |
| `id` | 除了通用规则,还只能用小写字母、数字和 `-`,首尾不能是 `-`,不超过 59 个字符 |
| `name` | 除了通用规则,还不能超过 64 个字符 |
- 安装后,这本书出现在背单词的词书列表里,排在内置词书之后、导入的词表之前,标着「插件」。插件详情里的「去背单词」会选中它。
- 插件词书不能在背单词里删除,要在插件页卸载。卸载后复习进度仍然保留,重新安装或升级后可以接着复习;卸载期间背单词会让你重新选一本书。
- 投稿到社区的词表请声明释义的来源和许可证。
## 符号集(`kind = "symbol_set"`)
符号集给符号面板追加符号组或颜文字组,全部写在清单里,没有数据文件。
```toml
[[groups]]
tab = "symbols" # symbols | kaomoji
title = "箭头"
keywords = "jiantou arrow" # 可选,用于搜索
items = ["→", "←", "⇒"]
```
| 限制 | 值 |
| ---------- | ------------------------------------------------------------------------------------------------------- |
| 组数 | 1 到 32 组,合计最多 2048 项 |
| `tab` | `symbols` 或 `kaomoji` |
| `title` | 必填,不能为空白,不超过 48 字节,不能含控制字符;同一个 `tab` 内不能重复,不同 `tab` 可以同名 |
| `keywords` | 可选;写了就不能为空白,不超过 256 字节,不能含控制字符 |
| `items` | 只能是字符串,每组 1 到 512 个;每个 1 到 64 个 UTF-16 码元,不能为空白、不能含控制字符,同组内不能重复 |
- 每组只有这四个键。
- 不需要选用:安装后就出现在符号面板里,卸载后消失。`symbols` 组以插件名作为一个分类,排在内置符号之后;`kaomoji` 组排在颜文字的「All」之后。不与内置符号去重。
- 目前显示插件符号集的是 Windows 和 Linux 的符号面板、macOS 的表情与符号面板,以及鸿蒙电脑的键盘选择器(鸿蒙的键盘目前读不到设置应用导入的包,见[平台支持](#平台支持));不支持的设备会在插件详情里说明。
## 脚本插件(`kind = "script"`)
脚本插件是唯一带可执行内容的类型:包里有一个 core wasm 模块,在桌面输入法进程里由沙箱解释器运行,按用户逐项授予的能力读组字、上屏文本和候选,或者在自己声明的 `/` 指令里给出候选(见[`/` 指令](#-指令))。它还是实验功能。想直接开始:复制 [script-plugin-template](script-plugin-template/),那是一个用 Rust 写的完整例子(中英文之间加空格),附带 C 头文件和 Rust 绑定。拿来就能用的插件在仓库的 [plugins/script](../plugins/script/):生僻字靠后、隐藏生僻字,从源码构建成插件包后导入;以及 UUID(`/uuid`),它是插件指令的参考样板,随桌面安装包分发(见[内置插件](#内置插件))。构建、测试和每个插件的行为见那里的 README。
### 在哪里运行
- 只在桌面:macOS 的输入法进程、Windows 的 `MetasequoiaImeServer`、Linux 的 IBus 引擎进程和 Fcitx5 addon。Windows 上被注入各个应用的 TSF 模块里从不运行脚本。iOS、Android 和鸿蒙(包括鸿蒙电脑)不认这个类型,导入时报「不支持这个 schema_version」。
- macOS、Linux 和 Windows 的发布构建都运行脚本。Windows 上以管理员身份运行的窗口里从不运行脚本。
- 授权文件 `<数据目录>/plugins/script-grants.json` 在用户可写的目录下,不签名:同一用户下的其他程序能伪造一条授权,让脚本在输入法里运行。授权页上写明了这一点;只装你信得过的脚本包。
- 输入法要接入生命周期事件之后才会把事件交给脚本,各平台陆续接入。
- 在设置的「插件 → 我的插件」里导入(macOS、Linux 和 Windows;Windows 是它自己的设置窗口里的同一页)。导入后插件默认停用:在「脚本插件(实验)」里打开它,在授权对话框里逐项勾选清单申请的能力才会运行,几个脚本插件的执行顺序也在那里调整。授权绑定在包的内容哈希上,清单或模块改了一个字节,授权就作废,页面显示「已更新,需要重新授权」。
- 不能发布到社区库:「发布我的插件」不列出脚本包,社区后端也只收 `schema_version = 1` 的包。只能本地导入。
### 清单
```toml
schema_version = 2 # 脚本插件必须写 2,其他类型仍然写 1
kind = "script"
id = "pangu-spacing"
name = "中英文之间加空格"
version = "1.0.0"
license = "MIT"
permissions = ["read_commit", "rewrite_commit"]
[script]
abi = 1 # 只能是 1
module = "pangu.wasm" # 包里的一个 .wasm 文件,1 字节到 256 KiB
hooks = ["before_commit"] # 只投递声明过的钩子
memory_pages = 4 # 可选,1 到 64,默认 16
```
在 `/` 指令里给出候选的插件另外申请 `provide_commands`、声明 `command` 钩子,并在 `[[script.commands]]` 里写出触发词和标题:
```toml
permissions = ["provide_commands"]
[script]
abi = 1
module = "uuid.wasm"
hooks = ["command"]
[[script.commands]]
trigger = "uuid" # 2 到 16 个小写字母
title = "UUID" # 1 到 8 个字符,显示在插件行旁
```
- 通用字段同[上面](#通用字段),只有 `schema_version` 和 `permissions` 不同。
- `permissions` 只能是下表里的能力名,不能重复,依赖要一起申请,`rewrite_commit` 和 `read_composition` 不能同时申请。
- 申请了的能力,模块里必须至少导入一个属于它的函数;导入了的函数,清单必须申请它所属的能力。两个方向都查。
- `[script]` 只有 `abi`、`module`、`hooks`、`memory_pages` 和 `commands` 五个键。模块按数据文件的规则检查:必须存在、不能为空、扩展名 `.wasm`、算进 16 个文件里。
- `[[script.commands]]` 1 到 4 条,每条只有 `trigger` 和 `title`,规则见[`/` 指令](#-指令)。声明了指令就必须申请 `provide_commands` 并声明 `command` 钩子,反过来也一样。
- 导入和 `msime-pack validate` 会按 ABI v1 校验模块(只编译,不执行),输入法加载时用的是同一份校验。
### 能力
| 能力 | 能读或能做 | 依赖 | 函数 |
| --------------------- | ---------------------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| 不需要 | 事件的枚举字段(类型、来源、选法、原因、本地模式等);写日志;`command` 里取这次投递的随机数 | | `event_scalar`、`log`、`random_fill` |
| `read_composition` | 预编辑串、读音、这次打的字符、光标位置 | | `composition_text`、`composition_scalar` |
| `read_commit` | 选中的候选、上屏文本、其中可改写的一段、座位 | | `commit_text`、`commit_scalar` |
| `read_candidates` | 候选窗口里的前 20 行(文本、编码、注释、来源)、页码、高亮位置 | | `candidate_count`、`candidate_text`、`candidate_code`、`candidate_annotation`、`candidate_meta` |
| `rewrite_commit` | 改写上屏文本里可改写的一段(标点、顶字的尾巴只读) | `read_commit`;不能和 `read_composition` 同时 | `rewrite_commit` |
| `rerank_candidates` | 重排候选 | `read_candidates` | `candidates_order` |
| `filter_candidates` | 把候选降到末尾并隐藏 | `read_candidates` | `candidates_hide` |
| `annotate_candidates` | 给候选加注释 | `read_candidates` | `candidates_annotate` |
| `provide_commands` | 在自己声明的 `/` 指令里读到触发词和后面的字母,给出候选行 | | `command_text`、`command_row` |
授权页上「提供 / 指令」这一项的说明是:「你在 / 指令里输入它声明的触发词(例如 /uuid)之后打的字母会交给这个插件,它据此给出候选;给出的候选只取决于这些字母和随机数,带不走你之前打过的内容。你按序号、点选、按空格选中某一行,或者打标点结束输入时,高亮的那一行会输入;在有的系统上,按快捷键或 Tab 等键、用菜单或工具栏结束输入时也会输入这一行。按回车时只输入你打的原文(例如 /uuid),点到别的窗口时插件给出的行不会输入。它看不到你平时打的字、其他指令和别的插件的内容;它给出的文字不进入词库、不计入打字统计、不会作为上文发给 AI 候选,也不会拿去查释义或翻译。」
`rewrite_commit` 到 `annotate_candidates` 这四项是修改类能力:`rewrite_commit` 只在 `before_commit` 里有效,其余三个只在 `candidates` 里有效,在别的钩子里调用返回 `DENIED`。`rewrite_commit` 还要事件的「能否改写」为真(选中候选、原样上屏或暂存段送出,本地模式 `none`,不是以词定字,输入框是普通输入框,宿主没把这一段暂存在自己手里),否则也返回 `DENIED`;它只改标点和顶字尾巴之前的那一段,后面的插件读到的是前面改写过的文本。几个插件的重排、隐藏和注释按授权顺序合成;被隐藏的候选不出现在列表里,也选不到;插件的注释由输入法另外显示,不替换候选原来的注释。改写结果和注释都不能含控制字符、格式字符(双向控制符、零宽字符等)、行和段分隔符,也不能含看不见的填充符和变体选择符(U+034F、韩文填充符、U+17B4、U+17B5、蒙古文自由变体选择符、盲文空白 U+2800、VS1–VS14 和表意文字变体选择符 U+E0100–U+E01EF),否则返回 `INVALID`;emoji 后面的 VS15、VS16 可以用。
### 钩子
| 钩子 | 编号 | 什么时候 | 能读到的字段 |
| --------------------- | ---- | ------------------------------------------------ | ---------------------------------------------------------- |
| `focus_in` | 0 | 输入框获得焦点 | 本地模式 |
| `focus_out` | 1 | 输入框失去焦点 | 本地模式 |
| `input` | 6 | 一次操作开始前 | 操作类别;`read_composition`:这次打的字符 |
| `selection_retracted` | 7 | 撤回暂存词组里的一次选择 | 撤回方式 |
| `candidate_selected` | 8 | 选中了候选 | 选法、结果;`read_commit`:选中的文本、座位 |
| `before_commit` | 9 | 上屏之前(修改类) | 来源、能否改写;`read_commit`:上屏文本和可改写段 |
| `commit` | 10 | 上屏之后 | 来源、是否接着组字、是否引擎生成;`read_commit`:同上 |
| `composition_end` | 11 | 组字结束 | 原因 |
| `composition_start` | 12 | 组字开始 | `read_composition`:预编辑串、读音、光标 |
| `composition_update` | 13 | 组字变化 | 同上 |
| `candidates_shown` | 14 | 候选窗口显示或换了内容 | `read_candidates`:当前页、页码、高亮 |
| `candidates_hidden` | 15 | 候选窗口隐藏 | 原因 |
| `candidates` | 17 | 候选列表排好之后、显示之前(修改类) | `read_candidates`:插件变换之前的顺序和注释 |
| `command` | 18 | 用户在 `/` 指令里打完这个插件的触发词(修改类) | 本地模式(`command`);`provide_commands`:触发词、参数;随机数 |
- 每个事件都带本地模式,`candidate_selected` 和 `commit` 带的是上屏时的模式。
- 同一次操作里的顺序:`input` → `selection_retracted` → `candidate_selected` → `before_commit` → `commit` → `composition_end` 或 `composition_start` → `composition_update` → `candidates_shown` 或 `candidates_hidden`。
- `command` 不是生命周期事件:它只投递给触发词的主人,不和上面这些排在一起,见[`/` 指令](#-指令)。
- 按键本身不投递,免得泄露打字节奏。在线候选、AI 候选、停顿后的重排这类不由按键触发的更新也不投递,免得泄露停顿和网络时序。
- 事件本身不带文本。文本只能在钩子运行期间经授权的函数读取。
### 模块要求(ABI v1)
- 格式是 core wasm(`wasm32-unknown-unknown`),不接受文本格式,没有 WASI,没有 JIT。
- 必须导出 `memory`(恰好一块 32 位线性内存,不能导入,最小页数不超过 `memory_pages`)、`msime_abi() -> i32`(返回 1)和 `msime_on_event(kind: i32) -> i32`(`kind` 是钩子编号,返回负数算这一次失败)。可选导出 `msime_init() -> i32`,实例化后调用一次,返回负数算初始化失败;初始化时读不到任何事件字段。
- import 只能是 `msime` 下上表里的函数,签名必须一致。不能有 start 函数。
- 可以用 MVP 加上 mutable-global、sign-ext、sat-float-to-int、multi-value、bulk-memory、reference-types;不能用 threads、memory64、multi-memory、exceptions、tail-call、extended-const、GC、SIMD。
- 函数返回非负数表示成功(复制的字节数、字段值),负数是 `DENIED`(-1,没有授予所属能力)、`ABSENT`(-2,事件里没有这个字段,或者被隐私规则屏蔽,两者故意不可区分)、`INVALID`(-3,字段编号或参数不对)、`LIMIT`(-4,缓冲区放不下,什么都没复制)。
- 文本都是 UTF-8。取文本的函数依次检查能力(`DENIED`)、字段(`INVALID` 或 `ABSENT`)、整个目标缓冲区是否都在线性内存里(越界直接 trap)、放不放得下(`LIMIT`)。前两步返回时不看目标缓冲区,所以坏指针不一定 trap;复制之前一定已经检查过范围。
- 编号的完整列表在 [sdk/msime_script.h](script-plugin-template/sdk/msime_script.h) 和 [sdk/msime_script.rs](script-plugin-template/sdk/msime_script.rs),两份都和输入法里的定义逐项核对过。编号只会追加,不会改值。
### 限制
| 项 | 值 |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| 模块文件 | 1 字节到 256 KiB |
| 线性内存 | `memory_pages` 页(每页 64 KiB),实例化时一次预留,之后 `memory.grow` 越过它就 trap |
| 初始化 | 每个插件 5 000 000 fuel(`msime_abi` 加 `msime_init`);每次聚焦和每一轮所有插件合计 5 000 000,超出的插件推迟到下一轮 |
| 每一轮 | 每个插件 125 000 fuel,`before_commit` 和 `command` 各有自己的一份 100 000。一轮是宿主交来一个按键到下一个按键之间,同一个键引起的多次调用共用一轮 |
| 单次钩子 | `candidates` 125 000,`before_commit` 和 `command` 各 100 000,其余 25 000;实际可用的是这个和本轮剩下的(`before_commit`、`command` 看各自那一份)较小者 |
| 指令行 | 每次 `command` 最多 9 行,每行最多 64 个 UTF-16 码元,行旁的注释最多 32 个;超出的那一次调用返回 `LIMIT` |
| 随机数 | 每次 `command` 64 字节,按调用顺序取,取完返回 `LIMIT`;别的钩子里没有 |
| 指令参数 | 触发词后面最多 64 个小写字母(输入法的本地输入本来就不超过 64 字节) |
| 函数调用 | 每次调用 `msime` 的函数扣 100 fuel,每复制 4 字节再扣 1 |
| 值栈 | 64 KiB,递归最深 256 层 |
| 表 | 最多 4 张,每张 10 000 个元素 |
| 同时启用 | 8 个 |
| 进程内存总账 | 64 MiB:每个插件按「线性内存 + 模块大小 × 128 + 256 KiB + 64 KiB」估算,启用时检查,放不下就拒绝启用 |
- fuel 是唯一的硬时间限制,大约一条 wasm 指令一个单位。超出单个钩子的上限算这个插件的一次失败,不影响别的插件;一个插件拿到多少配额和别的插件做了什么无关。本轮的配额用完不算失败:修饰键组合、快捷键、失焦这类没有先交来按键的调用和上一个按键共用一轮,花光它的可能是之前几次正常的投递。本轮剩下的为 0 时插件跳过这次投递;运行到一半被本轮剩下的配额截断时这一次的动作全部丢弃,实例和失败时一样丢弃,这一次用掉的不到本钩子上限的一半时不算失败,达到一半就和超出本钩子上限一样算一次 fuel 用完的失败。`candidates` 的单次上限和每轮配额都是 125 000,同一轮里 `input` 先花过 fuel 时它的死循环会先撞上每轮配额,按这条规则照样算失败、照样熔断。
- 最大的模块(256 KiB、64 页)在总账里约 36 MiB,同时只能启用一个;几十 KiB 的普通插件每个约 5 MiB。
### 隐私规则
- 密码框、标为隐私的输入框、Windows 上以管理员身份运行的窗口,以及输入法不知道属性的输入框里,不实例化、不投递任何事件,正在运行的实例当场丢弃。
- 每次聚焦(获得和失去焦点)和输入会话结束时,全部实例都丢弃,所以插件的状态只活在一个输入框里,不能把一个应用里的内容带到另一个应用。
- 没有授权的文本根本不会复制给插件。本地模式不是 `none`、临时英文、临时日文、超级简拼、emoji 或颜文字时(例如网址、命令、快捷短语、提及、拆字 `chaizi`、符号 `symbol`),文本类函数一律返回 `ABSENT`,连光标位置、座位和页码也一样。
- `before_commit` 每次都在 `msime_init` 刚结束时的快照上运行:其他钩子攒下的状态、上一次 `before_commit` 的改动都看不到,这一次的改动也留不下来。表和没有导出的 global 回到模块声明的初值,`msime_init` 对它们的改动不保留(Rust 编译出的栈指针在函数返回时已经复原,不受影响);要在 `before_commit` 里用 `msime_init` 算好的东西,放在线性内存或导出的 global 里。
- `rewrite_commit` 不能和 `read_composition` 同时申请,改写结果只能是这一次上屏文本的函数,不能夹带放弃的组字或更早的输入。
- `command` 和 `before_commit` 一样每次在快照上运行,给出的行只取决于触发词、参数、随机数和模块本身:同时申请了读文本能力的插件在别的钩子里存下的内容,带不进任何一行。`command` 事件里只有本地模式这一个枚举字段,读不到上屏、组字和候选。
- `command` 只投递给触发词的主人,别的插件收不到,也看不到插件行:`/` 指令是本地模式 `command`,别的插件在这里读文本一律是 `ABSENT`,`candidates` 也不运行。
- `log` 在输入法里是空操作,不写任何日志;只有 `msime-pack run` 会打出来。插件可以往 `log` 里写读到的上屏或候选文本(模板和 plugins/script 的插件就靠它在 `msime-pack run` 里核对改写和注释的结果),所以输入法以后若要收集脚本日志,必须把它当作用户输入的文本处理,不能直接写进诊断日志。
### `/` 指令
申请了 `provide_commands` 的插件可以在 `/` 指令里注册自己的触发词:用户在 `/` 之后打完触发词(例如 `/uuid`),输入法把触发词和后面的字母交给这个插件的 `command` 钩子,插件用 `command_row` 给出几行,用户选中其中一行上屏。
- **触发词**:2 到 16 个小写字母;不能等于、也不能和内置指令的触发词(日期 `rq`、`riqi`、`date`,时间 `sj`、`shijian`、`time`,星期 `xq`、`xingqi`、`week`,农历 `nl`、`nongli`、`yinli`,翻译 `fy`、`fanyi`、`translate`)互为前缀;同一个清单里的触发词不重复、不互为前缀。标题 1 到 8 个字符,不含不可见的字符、首尾不是空白、不等于内置指令的标题(日期、时间、星期、农历、翻译)。导入和 `msime-pack validate` 都按这些规则检查。
- **和别的指令冲突**:用户自己启用的指令表永远赢;插件之间按执行顺序先到先得。两个触发词相等或一个是另一个的前缀都算冲突,输掉的那条不生效。授权页在插件详情里列出每条指令和它的状态,例如「与你的指令 /uu 冲突,未生效」「与插件 xx 的 /uui 冲突,未生效」。只算启用、授予了 `provide_commands` 的插件,触发词取自清单,不等模块编译好:模块还没就绪时 `/uuid` 只有原样的那一行,下一次按键再问。被输入法停用的插件(见[失败](#失败))仍占着它的触发词,直到授权变化或输入法进程重启:这期间 `/uuid` 只有原样的那一行,排在后面、声明了同一个触发词的插件不会接手,用户在触发词后面打的字母不会改交给一个没排在前面的插件。
- **什么时候投递**:只在一次按键或其他操作刷新列表时,并且这份输入(`/uuid`、`/uuida`……)还没问过这个插件时投递一次。插件给出的行由输入法按输入缓存起来,移动高亮、翻页、在线候选回来都不再投递;改一个字母再改回来会重新投递。打 `/uui` 时还不是触发词,不投递。密码框、隐私输入框和不运行脚本的地方不投递,`/uuid` 只有原样的那一行。
- **事件里有什么**:`command_text(field, dst, cap)` 读 `TRIGGER`(0)和 `ARGUMENT`(1,触发词后面的字母,可以为空),别的钩子里是 `ABSENT`。`random_fill(dst, len)` 从这次投递的 64 字节系统随机数里按顺序取,不需要能力,取完返回 `LIMIT`,别的钩子里没有随机数。
- **行**:`command_row(text, tlen, note, nlen)` 暂存一行和它的注释,只在 `command` 里可用(别的钩子里返回 `DENIED`),钩子正常返回才采用,失败时这一次的行全部作废。文本不能为空、首尾不能是空白、不能含不可见字符,也不能含 `://`、`www.`、`mailto:`;超出条数或长度返回 `LIMIT`,不合规返回 `INVALID`。注释和标题显示在插件行旁(「标题 注释」),不进候选本身。
- **上屏**:选中、按序号、按空格,或者打标点结束输入时上屏高亮的那一行,原样上屏,全角模式下也不转全角。按回车上屏用户打的原文;输入法替失焦结束组字时(macOS),高亮的是插件行也上屏原文。插件行不进词库、不调词频、不计打字统计、不作为上文发给 AI 候选,页面上有插件行时整页不查释义、不交给翻译服务。
- **`/` 指令默认是关的**:要在「输入 → 快捷模式」里打开「指令(/ 模式)」并使用中文标点,在全拼、双拼、五笔下没有输入时按 `/` 再输入触发词。授权从不替你打开它:提供指令的插件详情里在 `/` 关着时说明这一点,并给一个「打开 / 指令」按钮,点了才打开。
- **残余风险**:授权文件不签名(见[在哪里运行](#在哪里运行)),伪造授权的插件多拿到的只是用户在它的触发词后面打的字母,而 `command` 跑在快照上、没有任何存储,拿到的字母留不到下一次,也带不到别的钩子里。
### 失败
- 一次失败指 trap(`unreachable`、越界、除零等)、超出 fuel 配额、栈溢出、内存越过预留,或 `msime_on_event` 返回负数。失败时丢弃这个插件这一次的全部动作,上屏和候选保持原样,后面的插件照常运行。
- 钩子运行到一半的失败(trap、fuel 用完、栈溢出、内存越过预留)之后实例丢弃,这个插件在当前输入框里不再运行,下一次聚焦时重新实例化。运行到一半被本轮配额截断时实例同样丢弃,这一次用掉的不到本钩子上限的一半时不算失败,达到一半算一次 fuel 用完的失败(见[限制](#限制))。返回负数、本轮配额在调用之前就已用完(不运行,跳过)时实例照用;`before_commit` 和 `command` 每次都在一次性的快照实例上运行,它们的失败也不影响插件的实例。
- `command` 失败时这一次没有行,输入法把这份输入记成「问过了、没有行」,同一份输入不再投递,列表里只有原样的那一行。
- 停用:初始化失败(含 `msime_abi` 不是 1、`msime_init` 返回负数)和模块不合规立即停用;否则最近 50 次投递里失败 3 次,或者最近 20 次里有 3 次超过 4 ms,就停用。停用持续到授权变化或输入法进程重启。
- 输入法自己在投递时出错,会关掉本进程的全部脚本直到重启,上屏照常。
- 停用的插件记在 `<数据目录>/script-health.json`,只有 id、内容哈希和类别(`invalid`、`init`、`failed`、`slow`),不记失败次数和原因。
### 用 msime-pack run 试运行
`msime-pack run` 按一份事件夹具运行一个脚本包,每个事件输出一行 JSON。读包、校验、编译和执行用的都是输入法里的同一份代码:按授权和本地模式复制文本、授权和清单取交集、`before_commit` 的快照、每轮和每个钩子的 fuel 配额、修改类函数的返回码。
```sh
cargo build -p msime-pack-tool --bin msime-pack
target/debug/msime-pack run path/to/pack --events events.jsonl --grant all
```
- `<pack>` 是包的文件夹或 `.zip`,先按 `validate` 的规则检查,不合规时在标准错误上输出和 `validate` 一样的错误行。
- `--grant all` 授予清单申请的全部能力;`--grant read_commit,rewrite_commit` 逐项授予,规则同授权页(必须是清单申请过的、满足依赖和互斥);不写时什么都不授予,和刚导入的包一样。
- 退出码:没有任何失败为 0;有任何失败(包括初始化失败)或包不合规为 1;命令行、`--grant` 或夹具写错为 2。
夹具一行一个 JSON 对象,空行跳过:
```text
{"event":"focus_in"}
{"event":"input","key":true,"input_class":"character","input_char":"n"}
{"event":"composition_update","preedit":"n","reading":"n","caret":1}
{"event":"candidates_shown","candidates":[{"text":"你","code":"ni"},{"text":"呢","code":"ne"}],"page":0,"highlighted":0}
{"event":"before_commit","key":true,"origin":"selection","body":"用Rust写","tail":"。"}
{"event":"commit","origin":"selection","body":"用Rust写","tail":"。"}
```
- `event` 是钩子名,必填。`key: true` 表示这个事件之前宿主交来了一个按键:开始新的一轮,fuel 配额和初始化预算给满。`local_mode` 是本地模式名(`none`、`url`、`temporary_english` 等),不写是 `none`。
- 每种事件只接受输入法会给它的字段,多写或者缺了必填的字段都报错:
| 事件 | 字段(加粗的必填) |
| ---------------------------------------- | --------------------------------------------------------------------- |
| `focus_in`、`focus_out` | 无 |
| `input` | **`input_class`**、`input_char`(一个字符) |
| `selection_retracted` | **`retraction`** |
| `candidate_selected` | **`how`**、**`outcome`**、**`seat`**、`selected` |
| `before_commit` | **`origin`**、**`body`**、`tail`、`rewritable` |
| `commit` | **`origin`**、**`body`**、`tail`、`continues`、`generated` |
| `composition_end`、`candidates_hidden` | **`reason`** |
| `composition_start`、`composition_update` | `preedit`、`reading`(不写是空串)、`caret`(不写时插件读到 `ABSENT`) |
| `candidates_shown`、`candidates` | `candidates`(每行 `text`,可选 `code`、`annotation`、`source`)、`page`(不写是 0)、`highlighted` |
| `command` | **`trigger`**(清单里声明过的)、`argument`(小写字母,不写是空串)、`random`(128 位十六进制,不写是全 0);`local_mode` 只能是 `command` |
- 枚举字段写名字:`origin` 是 `selection`、`raw`、`held_flush`、`scheme_text`、`focus_loss`;`how`、`outcome`、`reason`、`input_class`、`retraction` 的名字同 SDK 里的常量名(小写),例如 `how` 的 `digit`、`reason` 的 `commit`。
- 上屏文本是 `body` 接上 `tail`,`body` 是可改写的那一段。`before_commit` 不写 `rewritable` 时,来源是 `selection`、`raw` 或 `held_flush` 并且本地模式是 `none` 才算可改写;输入法还会看选法、输入框和宿主,夹具里需要时显式写。
输出的每一行:
```text
{"seq":1,"event":"before_commit","status":"ok","fuel":6303,"init_fuel":2,"actions":[{"import":"rewrite_commit","result":"ok"}],"log":[{"level":"debug","text":"用 Rust 写插件"}]}
```
- `seq` 是第几个事件,`event` 是钩子名,`fuel` 是这次投递用掉的 fuel。
- `init_fuel` 只在这个事件之前实例化或尝试实例化时出现,是初始化用掉的 fuel。在 `init:<原因>` 和 `deferred` 行上,它是失败或被截断的那次尝试用掉的 fuel,同样从本轮的初始化预算里扣除,不要把它算成一次完成的实例化。
- `actions` 是这次调用修改类函数的记录和输入法给的结果(`ok`、`denied`、`absent`、`invalid`、`limit`);失败或被本轮配额截断时这些动作在输入法里全部丢弃。
- `log` 是插件经 `log` 写的每一行,`msime_init` 里写的跟在实例化的那个事件上。
- `command` 行多一个 `rows`:插件给出的每一行 `[文本, 注释]`,没有正常返回时为空。
- `status`:`ok`;`not_subscribed`(清单没声明这个钩子);本次钩子自己的 fuel 上限耗尽,或者被本轮配额截断时已经用掉本钩子上限的一半或更多,是失败原因 `out_of_fuel`,其他失败原因有 `stack_overflow`、`memory_limit`、`memory_out_of_bounds`、`unreachable`、`trap`、`negative_return`;本轮共享 fuel 配额先用完时是 `turn_exhausted`(调用之前就已用完,或者被截断时这一次用掉的不到本钩子上限的一半),它不算失败,若执行被截断导致实例不可用,下一次事件是 `suspended`;初始化失败 `init:<原因>`(如 `init:init_failed`、`init:abi_mismatch`),之后的事件是 `disabled`;实例因失败丢弃之后(见[失败](#失败))、下一次 `focus_in` 之前是 `suspended`;初始化预算被本轮用完时是 `deferred`。
实例的生命周期按输入法的规则:第一次需要投递时才实例化,`focus_in` 之前和 `focus_out` 之后丢弃实例,同一轮里重建的实例接着用这一轮剩下的配额。和输入法不同的地方:
- 输入框永远按普通输入框处理,不模拟密码框和隐私字段。
- 不做按次数和按耗时的停用,每次失败都报告,任何失败都让退出码为 1。
- 不检查进程内存总账,单个插件总放得下。
## 内置插件
macOS、Windows 和 Linux 的桌面安装包随带两个官方插件包,在仓库的 [plugins/official-packs.json](../plugins/official-packs.json) 里登记:脚本插件「UUID」(id `uuid`,[`/` 指令](#-指令)的参考样板,打 `/uuid` 出一个随机 UUID 的四种写法)和短语表「LaTeX 符号」(id `latex-symbols`)。移动端安装包不带它们。
- **位置**:macOS 在输入法 bundle 的 `Contents/Resources/plugin-packs`,Windows 在 Program Files 下 Server 目录的 `plugin-packs`,Linux 在 `share/<client>/plugin-packs`(例如 `/usr/share/msime-client/plugin-packs`),都按 `<kind>/<id>/` 排,布局同数据目录下的 `plugins/`。三处都是只读的:macOS 由 bundle 的签名封住,Windows 只有管理员能写,Linux 在系统前缀下。随输入法更新整体替换。从源码构建的 Linux 发行版定义(Arch `msime`、Gentoo、OBS、nix)不跑 wasm 构建,不带官方包;`msime-bin` 重新打包发布页的 deb,带。
- **默认停用**:「插件 → 我的插件」里它们标着「(内置)」、没有删除按钮。短语表要打开「启用」才进 K 模式;脚本插件和导入的一样,要在「脚本插件(实验)」里逐项授权才运行,授权写在数据目录下的 `plugins/script-grants.json`,绑定内置那一份的内容哈希。
- **更新**:新版本的输入法带来内容不同的脚本插件时(改了插件,或者升级了编译器),哈希变了,之前的授权作废,授权页显示「已随输入法更新,需要重新授权」。官方包的哈希锁在 `plugins/official-packs.json` 里,构建时核对,所以这件事只会在改了锁的版本里发生,发版说明会写。
- **保留 id**:`uuid`(脚本插件)和 `latex-symbols`(短语表)和内置音效包的 id 一样保留,导入同 id 的包会被拒绝(`plugin_reserved`),也不能发布到社区。官方包随输入法分发之前导入过的同 id 包仍在数据目录里时,插件页把它列在「无法载入的插件」里,不会被使用;输入法和设置页永远读内置的那一份。
## 校验
用 `msime-pack` 检查插件。它调用的就是输入法导入时用的校验代码:它接受的包,设置页一定能导入;它拒绝的包,设置页也会拒绝。
```sh
cargo build -p msime-pack-tool --bin msime-pack
target/debug/msime-pack validate path/to/my-keys path/to/other-pack.zip
```
- 每个包输出一行,顺序与参数相同:
```text
ok my-keys sound 1.0.0
error path/to/other-pack.zip: plugin_invalid: key.ogg 不是 .wav 文件名,音效包只接受 WAV 音频
```
- 错误行里冒号后面是失败代码:
- `plugin_invalid`:违反了某条规则,后面写着是哪条
- `plugin_archive`:压缩包有问题
- `plugin_unsupported_source`:不是文件夹或 `.zip`
- `plugin_reserved`:用了内置包的 id
- 退出码:全部通过为 0;有任何一个不通过为 1;命令行写错为 2。
- 只检查,不安装,不会改动本机的插件目录。
- `msime-pack hash <包>` 按同样的规则检查一个脚本包,通过时打印它的内容哈希(授权绑定的那个值)。
- `--builtin`(`validate --builtin`、`hash --builtin`、`run ... --builtin`)只给仓库自己的构建脚本用:官方包本来就用保留 id,加上它不报 `plugin_reserved`,其余规则一条不少。
## 导入
在设置的「插件」页点「导入文件夹」选择包的文件夹,或点「导入 .zip」选择压缩包。只有 Windows、macOS、Linux 和鸿蒙电脑有「插件」页,见[平台支持](#平台支持)。Android 和 iOS 只能导入辅助码表,见[在手机上导入单个码表](#在手机上导入单个码表)。
压缩包的要求:
- 不超过 80 MiB,最多 64 个条目。
- 包的文件要么全部在压缩包顶层,要么全部在同一个文件夹里;直接压缩包的文件夹就是后一种。
- 压缩包里不能有更深的子文件夹或符号链接。
- macOS 生成的 `__MACOSX` 文件夹和以 `.` 开头的条目会被忽略。
导入成功后:
- 包装进数据目录下的 `plugins/<kind>/<id>/`。
- 在「插件 → 我的插件」里打开这个包:音效包选「设为当前音效包」(旋律音效包选「设为按键旋律」),音乐包选「设为当前音乐包」,指令表和短语表打开「启用」,辅助码表打开「用于全拼」或「用于双拼」。单词本和符号集装上就能用。
- 选「设为当前音效包」或「设为按键旋律」会同时打开「声音与效果」里的「按键音」,并把「发声方式」相应地设为按键音效或按键旋律;选「设为当前音乐包」会同时打开「背景音乐」。之后关掉开关、调音量都在同一页的「声音与效果」里。
- 指令表还要在「输入 → 快捷模式」里打开「指令(/ 模式)」,见上面「指令表」一节。
- 删除一个正在使用的包后,设置会回到默认选择。