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