dsh-kaomoji 0.1.3 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.5] - 2026-09-25
11
+
12
+ ### Fixed
13
+
14
+ - 设置通道从 `connection.rpc` 改为插件自有的 `webServer` 路由
15
+ (`POST /dsh-kaomoji-settings`)。部分部署里 `connection` 服务不会进入插件
16
+ 作用域,通道永远注册不上,浏览器侧只看到 `HTTP 405`,设置卡片因此无法保存。
17
+
18
+ ## [0.1.4] - 2026-09-25
19
+
20
+ ### Changed
21
+
22
+ - 设置卡片在保存失败时直接显示 Host 返回的真实原因(例如文件不可写、来源不受信任),
23
+ 不再只显示通用的“保存失败”,便于定位服务器端问题。
24
+
10
25
  ## [0.1.3] - 2026-09-13
11
26
 
12
27
  ### Fixed
package/README.en.md CHANGED
@@ -44,7 +44,7 @@ largest Japanese kaomoji sites.
44
44
  3. The model emits one of the listed kaomoji where it fits. Because kaomoji
45
45
  are plain text, no custom rendering is needed in the chat UI.
46
46
 
47
- Settings use a dedicated loopback RPC channel (`/dsh-kaomoji-settings`):
47
+ Settings use a same-origin route registered on the web server (`POST /dsh-kaomoji-settings`):
48
48
 
49
49
  1. The Settings → General card reads/writes `~/.dsh/dsh-kaomoji.json`.
50
50
  2. The Host updates the prompt section and emits `system-prompt/change`
@@ -107,19 +107,16 @@ Example `cordis.patch.yml`:
107
107
  | `maxPerTurn` | `number` (1–5) | `1` | Max kaomoji per reply; `frequent` distributes that many across different sentences (fewer in very short replies) |
108
108
  | `customPrompt` | `string` | `''` | Extra style/scene guidance; cannot change mode, whitelist or limits |
109
109
  | `settingsFile` | `string` | `~/.dsh/dsh-kaomoji.json` | (advanced) user-settings file path |
110
- | `settingsAuthority` | `'trusted-host' \| 'loopback'` | `'trusted-host'` | Settings RPC trust scope: by default the local Host plus declared trusted hosts (Tailscale/LAN) may write; set `loopback` to restrict to the local machine |
111
-
112
110
  Card edits take effect immediately; deployment-default edits in
113
111
  `cordis.patch.yml` require a dsh restart.
114
112
 
115
113
  ### Remote access (Tailscale / LAN)
116
114
 
117
- - If the dsh page is reachable remotely, the Host already trusts that origin;
118
- with the default `settingsAuthority: trusted-host` the General card can save
119
- settings from the remote session.
120
- - If the card reports that the origin is not trusted, add the host to the
121
- server’s `trustedHosts` (dsh-client-connection) or temporarily switch to
122
- `settingsAuthority: loopback` and edit on the server itself.
115
+ - The settings route is same-origin with the page (`POST /dsh-kaomoji-settings`),
116
+ so pages opened over Tailscale, LAN or an SSH tunnel can all save settings —
117
+ no `trustedHosts` configuration needed.
118
+ - The only requirement is that the page can reach that Host's HTTP port; any
119
+ failure now shows the server's original error in the card (0.1.4+).
123
120
 
124
121
  ## Kaomoji library & attribution
125
122
 
package/README.md CHANGED
@@ -31,7 +31,7 @@
31
31
  - 用户附加的 `customPrompt`(可选)。
32
32
  3. 模型生成时在合适位置直接输出列表内的颜文字——因为它只是普通文本,聊天界面无需任何自定义渲染即可显示。
33
33
 
34
- 设置部分走独立的 loopback RPC(`/dsh-kaomoji-settings`):
34
+ 设置部分由插件在 webServer 上注册同源路由 `POST /dsh-kaomoji-settings`:
35
35
 
36
36
  1. 「设置 → 通用设置」的卡片读取/写入 `~/.dsh/dsh-kaomoji.json`;
37
37
  2. Host 收到写入后立刻更新提示词段并发出 `system-prompt/change`,**下一次回复就生效**,不需要重启。
@@ -123,14 +123,12 @@ pnpm add file:C:\path\to\dsh-kaomoji
123
123
  | `maxPerTurn` | `number`(1–5) | `1` | 每条回复的颜文字数量上限;`frequent` 会按这个数量分散放在不同句子后,回复太短时自动减少 |
124
124
  | `customPrompt` | `string` | `''` | 附加风格/场景说明;不能改变模式、白名单或数量上限 |
125
125
  | `settingsFile` | `string` | `~/.dsh/dsh-kaomoji.json` | (进阶)用户设置持久化文件路径 |
126
- | `settingsAuthority` | `'trusted-host' \| 'loopback'` | `'trusted-host'` | 设置 RPC 信任范围:默认允许本机与 Host 声明的受信主机(Tailscale/局域网)保存设置;改成 `loopback` 则仅本机可改 |
127
-
128
126
  卡片里的修改即时写入用户层并热生效;修改 `cordis.patch.yml` 的部署默认值后需重启 dsh。
129
127
 
130
128
  ### 远程访问(Tailscale / 局域网)
131
129
 
132
- - 只要能正常远程打开 dsh 页面,说明 Host 已信任该来源;默认 `settingsAuthority: trusted-host` 下,通用设置卡片可以直接修改并保存。
133
- - 如果卡片提示「当前来源不在 Host 的信任列表里」,说明服务端 `@deepseek-ai/dsh-client-connection` 没有把该域名/IP 加进 `trustedHosts`;先在服务端配置受信主机,或临时改用 `settingsAuthority: loopback` 并到服务器本机编辑。
130
+ - 设置路由和页面同源(`POST /dsh-kaomoji-settings`),所以 Tailscale、局域网或 SSH 端口转发打开的页面都能直接改设置,不需要配置 `trustedHosts`。
131
+ - 唯一要求是页面能访问该 Host 的 HTTP 端口;如果卡片报错,看提示里的具体原因(0.1.4+ 会显示服务端原始错误)。
134
132
 
135
133
  ## 词库来源与许可
136
134
 
package/lib/client.js CHANGED
@@ -213,7 +213,8 @@ window.__ModuleLoader__.load({
213
213
  * 轻量快照 store(getSnapshot/subscribe),slot runtime 会把它包装成
214
214
  * useKaomoji(selector) hook。所有写操作都串行经过 loopback RPC。
215
215
  */
216
- function createSettingsStore(rpc) {
216
+ function createSettingsStore(fetchImpl) {
217
+ const doFetch = typeof fetchImpl === "function" ? fetchImpl : (...args) => fetch(...args);
217
218
  let snapshot = {
218
219
  status: "loading",
219
220
  settings: cloneSettings(DEFAULT_SETTINGS),
@@ -230,7 +231,9 @@ window.__ModuleLoader__.load({
230
231
  };
231
232
 
232
233
  const publish = (patch) => {
233
- snapshot = { ...snapshot, ...patch };
234
+ snapshot = patch.saveError === null
235
+ ? { ...snapshot, ...patch, saveErrorMessage: null }
236
+ : { ...snapshot, ...patch };
234
237
  emit();
235
238
  };
236
239
 
@@ -240,24 +243,45 @@ window.__ModuleLoader__.load({
240
243
  return task;
241
244
  };
242
245
 
243
- const fail = (code) => {
246
+ const fail = (code, message) => {
244
247
  publish({
245
248
  status: "unavailable",
246
249
  writable: false,
247
250
  saveError: code,
251
+ saveErrorMessage: message || null,
248
252
  justSaved: false,
249
253
  });
250
254
  };
251
255
 
252
256
  const callRpc = async (endpoint, payload) => {
253
257
  try {
254
- const result = await rpc.call(RPC_CHANNEL, endpoint, payload);
258
+ const response = await doFetch(RPC_CHANNEL, {
259
+ method: "POST",
260
+ headers: { "Content-Type": "application/json" },
261
+ body: JSON.stringify({ endpoint, payload }),
262
+ });
263
+ const status = Number((response && response.status) || 0);
264
+ let result;
265
+ try {
266
+ result = await response.json();
267
+ } catch {
268
+ result = undefined;
269
+ }
255
270
  if (result && result.ok === true) return { value: result.value };
256
- const code = result && result.error && result.error.code;
257
- return { error: code === "forbidden" ? "forbidden" : "rejected" };
271
+ const error = (result && result.error) || {};
272
+ if (status === 403 || error.code === "forbidden") {
273
+ return { error: "forbidden", message: error.message || "forbidden" };
274
+ }
275
+ return {
276
+ error: "rejected",
277
+ message: typeof error.message === "string" ? error.message : `HTTP ${String(status)}`,
278
+ };
258
279
  } catch (error) {
259
280
  const message = String((error && error.message) || error || "");
260
- return { error: /403|forbidden/i.test(message) ? "forbidden" : "unreachable" };
281
+ return {
282
+ error: /403|forbidden/i.test(message) ? "forbidden" : "unreachable",
283
+ message,
284
+ };
261
285
  }
262
286
  };
263
287
 
@@ -270,7 +294,7 @@ window.__ModuleLoader__.load({
270
294
  async refresh() {
271
295
  const result = await callRpc("get", {});
272
296
  if (result.error !== undefined) {
273
- fail(result.error === "forbidden" ? "forbidden" : "load");
297
+ fail(result.error === "forbidden" ? "forbidden" : "load", result.message);
274
298
  return;
275
299
  }
276
300
  const value = result.value;
@@ -297,7 +321,7 @@ window.__ModuleLoader__.load({
297
321
  expectedRevision: snapshot.revision,
298
322
  });
299
323
  if (result.error !== undefined) {
300
- fail(result.error === "forbidden" ? "forbidden" : "save");
324
+ fail(result.error === "forbidden" ? "forbidden" : "save", result.message);
301
325
  return;
302
326
  }
303
327
  const value = result.value;
@@ -316,7 +340,7 @@ window.__ModuleLoader__.load({
316
340
  publish({ saveError: null, justSaved: false });
317
341
  const result = await callRpc("reset", { expectedRevision: snapshot.revision });
318
342
  if (result.error !== undefined) {
319
- fail(result.error === "forbidden" ? "forbidden" : "save");
343
+ fail(result.error === "forbidden" ? "forbidden" : "save", result.message);
320
344
  return;
321
345
  }
322
346
  const value = result.value;
@@ -348,7 +372,9 @@ window.__ModuleLoader__.load({
348
372
  const statusText = state.saveError
349
373
  ? state.saveError === "forbidden"
350
374
  ? t("status.forbidden")
351
- : t("status.error")
375
+ : state.saveErrorMessage
376
+ ? `${t("status.error")} ${state.saveErrorMessage}`
377
+ : t("status.error")
352
378
  : state.status === "loading"
353
379
  ? t("status.loading")
354
380
  : !state.writable
@@ -474,15 +500,14 @@ window.__ModuleLoader__.load({
474
500
  );
475
501
  }
476
502
 
477
- const inject = ["slots", "locale", "connection"];
503
+ const inject = ["slots", "locale"];
478
504
 
479
505
  function apply(ctx) {
480
506
  ctx.effect(
481
507
  () => ctx.locale.register(NS, { zh, en }),
482
508
  "dsh-kaomoji: dictionaries",
483
509
  );
484
- const connection = ctx.get("connection");
485
- const controller = createSettingsStore(connection.rpc);
510
+ const controller = createSettingsStore();
486
511
  void controller.refresh();
487
512
 
488
513
  ctx.effect(() => {
package/lib/index.js CHANGED
@@ -30,8 +30,6 @@ export const SETTINGS_FILE_SCHEMA_VERSION = 1;
30
30
 
31
31
  export const MODES = ["off", "auto", "frequent"];
32
32
  export const PLACEMENTS = ["inline", "end"];
33
- /** 设置 RPC 的信任范围:trusted-host = 本机 + 服务端声明的受信主机(如 Tailscale)。 */
34
- export const SETTINGS_AUTHORITIES = ["trusted-host", "loopback"];
35
33
 
36
34
  export const DEFAULT_CONFIG = Object.freeze({
37
35
  /** off=关闭;auto=智能(确实有助于表达才用);frequent=高频(每条对话回复都带)。 */
@@ -219,11 +217,6 @@ function cloneSettings(settings) {
219
217
  return { ...settings, customPrompt: settings.customPrompt };
220
218
  }
221
219
 
222
- /** 设置 RPC 的信任范围;未显式配置时用 trusted-host(本机 + 服务端声明的受信主机)。 */
223
- function resolveAuthority(config) {
224
- return config?.settingsAuthority === "loopback" ? "loopback" : "trusted-host";
225
- }
226
-
227
220
  function defaultSettingsFile(config) {
228
221
  if (typeof config?.settingsFile === "string" && config.settingsFile.trim() !== "") {
229
222
  return resolve(config.settingsFile);
@@ -372,10 +365,9 @@ function createSettingsRpcHandler(ctx, settingsFile, getState, commit) {
372
365
  return rpcError("bad-request", `Unknown dsh-kaomoji settings operation: ${endpoint}`);
373
366
  });
374
367
  } catch (error) {
375
- ctx.logger?.warn?.(
376
- `[dsh-kaomoji] settings rpc failed: ${error instanceof Error ? error.message : String(error)}`,
377
- );
378
- return rpcError("settings-rejected", "The Host rejected the kaomoji settings.");
368
+ const reason = error instanceof Error ? error.message : String(error);
369
+ console.warn(`[dsh-kaomoji] settings rpc failed: ${reason}`);
370
+ return rpcError("settings-rejected", `The Host rejected the kaomoji settings: ${reason}`);
379
371
  }
380
372
  };
381
373
  }
@@ -383,7 +375,6 @@ function createSettingsRpcHandler(ctx, settingsFile, getState, commit) {
383
375
  export function apply(ctx, config = {}) {
384
376
  const baseSettings = normalizeConfig(config);
385
377
  const settingsFile = defaultSettingsFile(config);
386
- const authority = resolveAuthority(config);
387
378
  const stored = loadSettingsDocument(settingsFile);
388
379
 
389
380
  // currentSettings = 用户层(~/.dsh/dsh-kaomoji.json)覆盖部署默认值后的实时设置。
@@ -421,21 +412,59 @@ export function apply(ctx, config = {}) {
421
412
  "dsh-kaomoji: guidance",
422
413
  );
423
414
 
424
- // 设置卡片通过 loopback RPC 读写 ~/.dsh/dsh-kaomoji.json;写入后立即
425
- // 更新提示词段(emit system-prompt/change),无需重启。
426
- ctx.inject(["connection"], (connectionCtx) => {
427
- const handler = createSettingsRpcHandler(ctx, settingsFile, getState, adoptSettings);
428
- connectionCtx.effect(
415
+ // 设置读写走 webServer 直连路由,不用 connection.rpc:后者在部分部署里
416
+ // connection 服务不进入插件作用域,通道永远注册不上(浏览器侧表现为 405)。
417
+ const settingsHandler = createSettingsRpcHandler(ctx, settingsFile, getState, adoptSettings);
418
+ const attachSettingsRoute = (scope) => {
419
+ scope.effect(
429
420
  () =>
430
- connectionCtx.connection.rpc.handle(SETTINGS_RPC_CHANNEL, handler, {
431
- authority,
421
+ scope.webServer.register({
422
+ kind: "exact",
423
+ path: SETTINGS_RPC_CHANNEL,
424
+ handler: async (request, response) => {
425
+ const reply = (status, value) => {
426
+ response.writeHead(status, {
427
+ "Content-Type": "application/json; charset=utf-8",
428
+ "Cache-Control": "no-store",
429
+ });
430
+ response.end(JSON.stringify(value));
431
+ };
432
+ if (request.method !== "POST") {
433
+ reply(405, rpcError("bad-request", "POST required"));
434
+ return;
435
+ }
436
+ let raw = "";
437
+ try {
438
+ for await (const chunk of request) raw += chunk;
439
+ } catch {
440
+ // 客户端中断:按空 body 处理,下面的解析会返回 400。
441
+ }
442
+ let endpoint = "";
443
+ let payload;
444
+ try {
445
+ const parsed = JSON.parse(raw === "" ? "{}" : raw);
446
+ endpoint = typeof parsed.endpoint === "string" ? parsed.endpoint : "";
447
+ payload = parsed.payload;
448
+ } catch {
449
+ reply(400, rpcError("bad-request", "invalid JSON body"));
450
+ return;
451
+ }
452
+ reply(200, await settingsHandler(endpoint, payload));
453
+ },
432
454
  }),
433
- "dsh-kaomoji: settings rpc",
455
+ "dsh-kaomoji: settings route",
434
456
  );
435
- });
457
+ console.log(`[dsh-kaomoji] settings route 已注册(path=${SETTINGS_RPC_CHANNEL})`);
458
+ };
459
+ const webServer = typeof ctx.get === "function" ? ctx.get("webServer") : undefined;
460
+ if (webServer !== undefined) {
461
+ attachSettingsRoute(ctx);
462
+ } else {
463
+ ctx.inject(["webServer"], attachSettingsRoute);
464
+ }
436
465
 
437
- ctx.logger?.info?.(
438
- `[dsh-kaomoji] 已挂载(mode=${currentSettings.mode}, placement=${currentSettings.placement}, authority=${authority}, settingsFile=${settingsFile})`,
466
+ console.log(
467
+ `[dsh-kaomoji] 已挂载(mode=${currentSettings.mode}, placement=${currentSettings.placement}, settingsFile=${settingsFile})`,
439
468
  );
440
469
  }
441
470
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-kaomoji",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Add mood-aware Japanese kaomoji to DeepSeek Harness (dsh) replies; prompt-injection plugin with a curated library from kaomojiya.org.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",