dsh-kaomoji 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,506 @@
1
+ /**
2
+ * dsh-kaomoji Web client half.
3
+ *
4
+ * 在「设置 → 通用设置」中注册一张配置卡片,通过 loopback RPC
5
+ * (/dsh-kaomoji-settings)读写 Host 的 ~/.dsh/dsh-kaomoji.json。
6
+ * 保存即生效:Host 收到 save 后更新提示词段,下一次回复就按新设置来。
7
+ *
8
+ * 采用 dsh Web 客户端的 ModuleLoader 格式(与 dsh-emoji/dsh-meme 相同)。
9
+ */
10
+ window.__ModuleLoader__.load({
11
+ id: "dsh-kaomoji",
12
+ factory: (require) => {
13
+ const module = { exports: {} };
14
+ const exports = module.exports;
15
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
16
+
17
+ const react = require("react");
18
+ const { useState, useEffect } = react;
19
+ const h = react.createElement;
20
+
21
+ const NS = "dsh-kaomoji";
22
+ const RPC_CHANNEL = "/dsh-kaomoji-settings";
23
+ const MODES = ["off", "auto", "frequent"];
24
+ const PLACEMENTS = ["inline", "end"];
25
+ const DEFAULT_SETTINGS = Object.freeze({
26
+ mode: "auto",
27
+ placement: "inline",
28
+ maxPerTurn: 1,
29
+ customPrompt: "",
30
+ });
31
+
32
+ function normalizeSettings(value) {
33
+ const mode = MODES.includes(value.mode) ? value.mode : DEFAULT_SETTINGS.mode;
34
+ const placement = PLACEMENTS.includes(value.placement)
35
+ ? value.placement
36
+ : DEFAULT_SETTINGS.placement;
37
+ const maxPerTurn =
38
+ Number.isInteger(value.maxPerTurn) && value.maxPerTurn >= 1 && value.maxPerTurn <= 5
39
+ ? value.maxPerTurn
40
+ : DEFAULT_SETTINGS.maxPerTurn;
41
+ const customPrompt =
42
+ typeof value.customPrompt === "string" && value.customPrompt.length <= 4000
43
+ ? value.customPrompt
44
+ : "";
45
+ return { mode, placement, maxPerTurn, customPrompt };
46
+ }
47
+
48
+ function cloneSettings(settings) {
49
+ return { ...settings, customPrompt: settings.customPrompt };
50
+ }
51
+
52
+ const zh = {
53
+ "row.title": "颜文字(dsh-kaomoji)",
54
+ "row.description": "给对话回复自动加日式颜文字,词库精选自 kaomojiya.org。",
55
+ "field.mode": "使用频率",
56
+ "mode.off": "关闭",
57
+ "mode.off.desc": "不使用颜文字",
58
+ "mode.auto": "智能",
59
+ "mode.auto.desc": "适合表达友好、共情或俏皮情绪时自然使用",
60
+ "mode.frequent": "高频",
61
+ "mode.frequent.desc": "每条对话回复都带一个(纯代码/正式交付除外)",
62
+ "field.placement": "放置位置",
63
+ "placement.inline": "贴合句子",
64
+ "placement.end": "回复结尾",
65
+ "field.max": "每条上限",
66
+ "field.prompt": "附加提示词",
67
+ "prompt.placeholder": "例如:正式场景克制一点,优先使用「感谢 / 鼓励」类颜文字",
68
+ "prompt.help": "只细化风格与场景,不能改变模式、白名单或数量上限。",
69
+ "action.reset": "恢复默认",
70
+ "status.loading": "正在读取设置…",
71
+ "status.saved": "已保存,下一次回复生效。",
72
+ "status.unsupported": "设置仅在 Host 本机(loopback)页面可修改。",
73
+ "status.error": "保存失败,请重试。",
74
+ };
75
+
76
+ const en = {
77
+ "row.title": "Kaomoji (dsh-kaomoji)",
78
+ "row.description": "Add Japanese kaomoji to replies; curated from kaomojiya.org.",
79
+ "field.mode": "Frequency",
80
+ "mode.off": "Off",
81
+ "mode.off.desc": "No kaomoji",
82
+ "mode.auto": "Smart",
83
+ "mode.auto.desc": "Use naturally when a friendly, empathetic, or playful mood fits",
84
+ "mode.frequent": "Frequent",
85
+ "mode.frequent.desc": "One kaomoji in every conversational reply (code-only/formal excluded)",
86
+ "field.placement": "Placement",
87
+ "placement.inline": "After best-matching sentence",
88
+ "placement.end": "End of reply",
89
+ "field.max": "Max per reply",
90
+ "field.prompt": "Extra guidance",
91
+ "prompt.placeholder": "e.g. Stay professional; prefer thanks/encourage kaomoji",
92
+ "prompt.help": "Refines tone/scenes only; cannot change mode, whitelist, or limits.",
93
+ "action.reset": "Reset to defaults",
94
+ "status.loading": "Loading settings…",
95
+ "status.saved": "Saved — applies to the next reply.",
96
+ "status.unsupported": "Settings can only be changed on the Host’s local (loopback) page.",
97
+ "status.error": "Save failed, please retry.",
98
+ };
99
+
100
+ const styles = {
101
+ card: {
102
+ display: "flex",
103
+ flexDirection: "column",
104
+ gap: 12,
105
+ padding: "14px 16px",
106
+ border: "1px solid var(--dsw-alias-border-l2)",
107
+ borderRadius: 12,
108
+ background: "var(--dsw-alias-bg-layer-3)",
109
+ color: "var(--dsw-alias-label-primary)",
110
+ },
111
+ header: { display: "flex", flexDirection: "column", gap: 4, minWidth: 0 },
112
+ title: { fontSize: 15, lineHeight: "21px", fontWeight: 600 },
113
+ description: {
114
+ fontSize: 13,
115
+ lineHeight: "19px",
116
+ color: "var(--dsw-alias-label-tertiary)",
117
+ },
118
+ grid: {
119
+ display: "grid",
120
+ gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))",
121
+ gap: 12,
122
+ },
123
+ field: { display: "flex", flexDirection: "column", gap: 6, minWidth: 0 },
124
+ label: { fontSize: 13, lineHeight: "18px", fontWeight: 600 },
125
+ modeRow: {
126
+ display: "grid",
127
+ gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
128
+ gap: 8,
129
+ },
130
+ mode: {
131
+ minWidth: 0,
132
+ display: "flex",
133
+ flexDirection: "column",
134
+ gap: 4,
135
+ padding: "8px 6px",
136
+ border: "1px solid var(--dsw-alias-border-l2)",
137
+ borderRadius: 8,
138
+ background: "var(--dsw-alias-bg-module-platform)",
139
+ color: "inherit",
140
+ font: "inherit",
141
+ cursor: "pointer",
142
+ },
143
+ modeName: { fontSize: 12, lineHeight: "18px", fontWeight: 600 },
144
+ modeDesc: {
145
+ fontSize: 11,
146
+ lineHeight: "16px",
147
+ color: "var(--dsw-alias-label-tertiary)",
148
+ },
149
+ select: {
150
+ boxSizing: "border-box",
151
+ width: "100%",
152
+ padding: "7px 10px",
153
+ border: "1px solid var(--dsw-alias-border-l2)",
154
+ borderRadius: 8,
155
+ background: "var(--dsw-alias-bg-module-platform)",
156
+ color: "var(--dsw-alias-label-primary)",
157
+ font: "inherit",
158
+ fontSize: 13,
159
+ },
160
+ number: {
161
+ boxSizing: "border-box",
162
+ width: "100%",
163
+ padding: "7px 10px",
164
+ border: "1px solid var(--dsw-alias-border-l2)",
165
+ borderRadius: 8,
166
+ background: "var(--dsw-alias-bg-module-platform)",
167
+ color: "var(--dsw-alias-label-primary)",
168
+ font: "inherit",
169
+ fontSize: 13,
170
+ },
171
+ prompt: {
172
+ boxSizing: "border-box",
173
+ width: "100%",
174
+ minHeight: 72,
175
+ resize: "vertical",
176
+ padding: "8px 10px",
177
+ border: "1px solid var(--dsw-alias-border-l2)",
178
+ borderRadius: 8,
179
+ background: "var(--dsw-alias-bg-module-platform)",
180
+ color: "var(--dsw-alias-label-primary)",
181
+ font: "inherit",
182
+ fontSize: 13,
183
+ lineHeight: 1.55,
184
+ },
185
+ help: {
186
+ fontSize: 12,
187
+ lineHeight: "18px",
188
+ color: "var(--dsw-alias-label-tertiary)",
189
+ },
190
+ footer: {
191
+ display: "flex",
192
+ alignItems: "center",
193
+ justifyContent: "space-between",
194
+ gap: 12,
195
+ },
196
+ status: { fontSize: 12, lineHeight: "18px", color: "var(--dsw-alias-label-tertiary)" },
197
+ reset: {
198
+ flex: "0 0 auto",
199
+ padding: "6px 10px",
200
+ border: "1px solid var(--dsw-alias-border-l2)",
201
+ borderRadius: 8,
202
+ background: "none",
203
+ color: "var(--dsw-alias-label-secondary)",
204
+ font: "inherit",
205
+ fontSize: 12,
206
+ cursor: "pointer",
207
+ },
208
+ };
209
+
210
+ /**
211
+ * 轻量快照 store(getSnapshot/subscribe),slot runtime 会把它包装成
212
+ * useKaomoji(selector) hook。所有写操作都串行经过 loopback RPC。
213
+ */
214
+ function createSettingsStore(rpc, isLoopback) {
215
+ let snapshot = {
216
+ status: isLoopback ? "loading" : "unavailable",
217
+ settings: cloneSettings(DEFAULT_SETTINGS),
218
+ revision: undefined,
219
+ writable: Boolean(isLoopback),
220
+ saveError: null,
221
+ justSaved: false,
222
+ };
223
+ const listeners = new Set();
224
+ let tail = Promise.resolve();
225
+
226
+ const emit = () => {
227
+ for (const listener of listeners) listener();
228
+ };
229
+
230
+ const publish = (patch) => {
231
+ snapshot = { ...snapshot, ...patch };
232
+ emit();
233
+ };
234
+
235
+ const enqueue = (operation) => {
236
+ const task = tail.then(operation);
237
+ tail = task.catch(() => {});
238
+ return task;
239
+ };
240
+
241
+ const callRpc = async (endpoint, payload) => {
242
+ if (!isLoopback) {
243
+ publish({ status: "unavailable", saveError: null });
244
+ return undefined;
245
+ }
246
+ try {
247
+ const result = await rpc.call(RPC_CHANNEL, endpoint, payload);
248
+ return result && result.ok === true ? result.value : undefined;
249
+ } catch {
250
+ return undefined;
251
+ }
252
+ };
253
+
254
+ return {
255
+ getSnapshot: () => snapshot,
256
+ subscribe(listener) {
257
+ listeners.add(listener);
258
+ return () => listeners.delete(listener);
259
+ },
260
+ async refresh() {
261
+ const value = await callRpc("get", {});
262
+ if (value === undefined) {
263
+ if (isLoopback) publish({ status: "ready", saveError: "load" });
264
+ return;
265
+ }
266
+ publish({
267
+ status: "ready",
268
+ settings: normalizeSettings(value.settings),
269
+ revision: value.revision,
270
+ writable: value.writable === true,
271
+ saveError: null,
272
+ });
273
+ },
274
+ patch(field) {
275
+ const next = normalizeSettings({ ...snapshot.settings, ...field });
276
+ publish({
277
+ status: "ready",
278
+ settings: next,
279
+ saveError: null,
280
+ justSaved: false,
281
+ });
282
+ return enqueue(async () => {
283
+ const value = await callRpc("save", {
284
+ settings: next,
285
+ expectedRevision: snapshot.revision,
286
+ });
287
+ if (value === undefined) {
288
+ publish({ saveError: "save", justSaved: false });
289
+ return;
290
+ }
291
+ publish({
292
+ settings: normalizeSettings(value.settings),
293
+ revision: value.revision,
294
+ writable: value.writable === true,
295
+ status: "ready",
296
+ saveError: null,
297
+ justSaved: true,
298
+ });
299
+ });
300
+ },
301
+ async reset() {
302
+ if (!isLoopback) return;
303
+ publish({ saveError: null, justSaved: false });
304
+ const value = await callRpc("reset", { expectedRevision: snapshot.revision });
305
+ if (value === undefined) {
306
+ publish({ saveError: "save", justSaved: false });
307
+ return;
308
+ }
309
+ publish({
310
+ settings: normalizeSettings(value.settings),
311
+ revision: value.revision,
312
+ status: "ready",
313
+ saveError: null,
314
+ justSaved: true,
315
+ });
316
+ },
317
+ };
318
+ }
319
+
320
+ /**
321
+ * 通用设置行(settings.general.item)。props 由 slot runtime 组装:
322
+ * t(本插件字典)+ useKaomoji(selector) + 写操作。
323
+ */
324
+ function KaomojiSettingsRow(props) {
325
+ const { t, useKaomoji } = props;
326
+ const state = useKaomoji((value) => value);
327
+ const settings = state.settings;
328
+ const [promptDraft, setPromptDraft] = useState(settings.customPrompt);
329
+
330
+ useEffect(() => {
331
+ setPromptDraft(settings.customPrompt);
332
+ }, [settings.customPrompt]);
333
+
334
+ const statusText = state.saveError
335
+ ? t("status.error")
336
+ : state.status === "loading"
337
+ ? t("status.loading")
338
+ : !state.writable
339
+ ? t("status.unsupported")
340
+ : state.justSaved
341
+ ? t("status.saved")
342
+ : t("prompt.help");
343
+ const controlsDisabled = !state.writable || state.status === "loading";
344
+
345
+ return h(
346
+ "section",
347
+ { style: styles.card, "data-dsh-kaomoji-settings": "true" },
348
+ h(
349
+ "div",
350
+ { style: styles.header },
351
+ h("div", { style: styles.title }, t("row.title")),
352
+ h("div", { style: styles.description }, t("row.description")),
353
+ ),
354
+ h(
355
+ "div",
356
+ { style: styles.grid },
357
+ h(
358
+ "div",
359
+ { style: styles.field },
360
+ h("div", { style: styles.label }, t("field.mode")),
361
+ h(
362
+ "div",
363
+ { style: styles.modeRow },
364
+ ["off", "auto", "frequent"].map((mode) =>
365
+ h(
366
+ "button",
367
+ {
368
+ key: mode,
369
+ type: "button",
370
+ style: {
371
+ ...styles.mode,
372
+ borderColor:
373
+ settings.mode === mode
374
+ ? "var(--dsw-alias-label-primary)"
375
+ : "var(--dsw-alias-border-l2)",
376
+ },
377
+ "aria-pressed": settings.mode === mode,
378
+ disabled: controlsDisabled,
379
+ onClick: () => props.setMode(mode),
380
+ },
381
+ h("span", { style: styles.modeName }, t(`mode.${mode}`)),
382
+ h("span", { style: styles.modeDesc }, t(`mode.${mode}.desc`)),
383
+ ),
384
+ ),
385
+ ),
386
+ ),
387
+ h(
388
+ "div",
389
+ { style: styles.field },
390
+ h("label", { style: styles.label }, t("field.placement")),
391
+ h(
392
+ "select",
393
+ {
394
+ style: styles.select,
395
+ value: settings.placement,
396
+ disabled: controlsDisabled,
397
+ onChange: (event) => props.setPlacement(event.target.value),
398
+ },
399
+ h("option", { value: "inline" }, t("placement.inline")),
400
+ h("option", { value: "end" }, t("placement.end")),
401
+ ),
402
+ ),
403
+ h(
404
+ "div",
405
+ { style: styles.field },
406
+ h("label", { htmlFor: "dsh-kaomoji-max", style: styles.label }, t("field.max")),
407
+ h("input", {
408
+ id: "dsh-kaomoji-max",
409
+ type: "number",
410
+ min: 1,
411
+ max: 5,
412
+ step: 1,
413
+ style: styles.number,
414
+ value: String(settings.maxPerTurn),
415
+ disabled: controlsDisabled,
416
+ onChange: (event) => {
417
+ const parsed = Number(event.target.value);
418
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 5) {
419
+ props.setMaxPerTurn(parsed);
420
+ }
421
+ },
422
+ }),
423
+ ),
424
+ ),
425
+ h(
426
+ "div",
427
+ { style: styles.field },
428
+ h("label", { htmlFor: "dsh-kaomoji-prompt", style: styles.label }, t("field.prompt")),
429
+ h("textarea", {
430
+ id: "dsh-kaomoji-prompt",
431
+ style: styles.prompt,
432
+ placeholder: t("prompt.placeholder"),
433
+ value: promptDraft,
434
+ disabled: controlsDisabled,
435
+ onChange: (event) => setPromptDraft(event.target.value),
436
+ onBlur: () => {
437
+ if (promptDraft !== settings.customPrompt) {
438
+ props.setCustomPrompt(promptDraft);
439
+ }
440
+ },
441
+ }),
442
+ ),
443
+ h(
444
+ "div",
445
+ { style: styles.footer },
446
+ h("div", { style: styles.status }, statusText),
447
+ h(
448
+ "button",
449
+ {
450
+ type: "button",
451
+ style: styles.reset,
452
+ disabled: controlsDisabled,
453
+ onClick: () => void props.reset(),
454
+ },
455
+ t("action.reset"),
456
+ ),
457
+ ),
458
+ );
459
+ }
460
+
461
+ const inject = ["slots", "locale", "connection"];
462
+
463
+ function apply(ctx) {
464
+ ctx.effect(
465
+ () => ctx.locale.register(NS, { zh, en }),
466
+ "dsh-kaomoji: dictionaries",
467
+ );
468
+ const connection = ctx.get("connection");
469
+ const controller = createSettingsStore(connection.rpc, connection.isLoopback);
470
+ void controller.refresh();
471
+
472
+ ctx.effect(() => {
473
+ const disposeReset = ctx.on("connection/reset", () => void controller.refresh());
474
+ return () => disposeReset();
475
+ }, "dsh-kaomoji: settings invalidations");
476
+
477
+ ctx.slots.inject("settings.general.item", () =>
478
+ ctx.slots.register(
479
+ {
480
+ name: "settings.general.item",
481
+ id: "dsh-kaomoji",
482
+ order: 30,
483
+ locale: NS,
484
+ inject: () => ({
485
+ hooks: { kaomoji: controller },
486
+ setMode: (mode) => void controller.patch({ mode }),
487
+ setPlacement: (placement) => void controller.patch({ placement }),
488
+ setMaxPerTurn: (maxPerTurn) => void controller.patch({ maxPerTurn }),
489
+ setCustomPrompt: (customPrompt) => void controller.patch({ customPrompt }),
490
+ reset: () => controller.reset(),
491
+ }),
492
+ },
493
+ KaomojiSettingsRow,
494
+ ),
495
+ );
496
+ }
497
+
498
+ exports.apply = apply;
499
+ exports.inject = inject;
500
+ exports.NS = NS;
501
+ exports.RPC_CHANNEL = RPC_CHANNEL;
502
+ exports.createSettingsStore = createSettingsStore;
503
+ exports.KaomojiSettingsRow = KaomojiSettingsRow;
504
+ return module.exports;
505
+ },
506
+ });
package/lib/index.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * dsh-kaomoji — 给 DeepSeek Harness (dsh) 回复自动添加日式颜文字。
3
+ *
4
+ * 纯提示词插件:零第三方运行时依赖,不需要 client 端或 webServer。
5
+ */
6
+
7
+ export type KaomojiMode = "off" | "auto" | "frequent";
8
+ export type KaomojiPlacement = "inline" | "end";
9
+
10
+ export interface KaomojiConfig {
11
+ mode?: KaomojiMode;
12
+ placement?: KaomojiPlacement;
13
+ maxPerTurn?: number;
14
+ customPrompt?: string;
15
+ }
16
+
17
+ export interface KaomojiGroup {
18
+ id: string;
19
+ label: string;
20
+ sourceUrl: string;
21
+ examples: string[];
22
+ }
23
+
24
+ export const name: string;
25
+ export const inject: string[];
26
+ export const SECTION_NAME: string;
27
+ export const SECTION_ORDER: number;
28
+ export const SETTINGS_NAMESPACE: string;
29
+ export const SETTINGS_RPC_CHANNEL: string;
30
+ export const SETTINGS_FILE_NAME: string;
31
+ export const SETTINGS_FILE_SCHEMA_VERSION: number;
32
+ export const MODES: KaomojiMode[];
33
+ export const PLACEMENTS: KaomojiPlacement[];
34
+ export const DEFAULT_CONFIG: Required<KaomojiConfig>;
35
+ export const CATALOG: readonly KaomojiGroup[];
36
+
37
+ export function normalizeConfig(config?: KaomojiConfig): Required<KaomojiConfig>;
38
+
39
+ /** mode=off 时返回空字符串(不会注入任何内容)。 */
40
+ export function buildGuidance(config?: KaomojiConfig): string;
41
+
42
+ /** Cordis 插件入口。 */
43
+ export function apply(ctx: any, config?: KaomojiConfig): void;
44
+
45
+ declare const plugin: { name: string; inject: string[]; apply: typeof apply };
46
+ export default plugin;