@addozhang/dsh-discord 0.5.0-rc.3 → 0.5.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/README.md CHANGED
@@ -29,13 +29,26 @@ No extra process: the adapter is a DSH plugin that mounts straight onto your `ds
29
29
 
30
30
  ## Requirements
31
31
 
32
- - The [dsh CLI](https://www.npmjs.com/package/@deepseek-ai/dsh) `0.1.6-alpha.1` or a compatible newer release, running a web profile. Adapter `0.5.x` uses the 0.1.6 controller services and is not compatible with the former `apiProxy` host surface.
32
+ - The [dsh CLI](https://www.npmjs.com/package/@deepseek-ai/dsh), running a web profile. Match the dsh and adapter channels — see [Versions](#versions).
33
33
  - Node.js `^22.19.0 || >=24`
34
34
  - A Discord application with a bot user and the **MESSAGE CONTENT** privileged intent enabled (Developer Portal → your application → Bot → Privileged Gateway Intents)
35
35
 
36
+ ## Versions
37
+
38
+ Two channels ship in parallel, each pinned to the dsh host surface it was verified against. Pick the adapter and the dsh CLI from the same row:
39
+
40
+ | Channel | Adapter | dsh CLI | Install |
41
+ |---|---|---|---|
42
+ | **stable** | `0.5.0` — npm `latest` | `0.1.7-rc.1` — npm `next` | `dsh plugin --profile web add @addozhang/dsh-discord` |
43
+ | **next** | `>=0.5.0-rc.4` — npm `next` | `0.1.7-alpha` — npm `alpha` | `dsh plugin --profile web add @addozhang/dsh-discord@next` |
44
+
45
+ - **The rows do not mix.** Adapter `>=0.5.0-rc.4` (including `0.5.0`) speaks the 0.1.7 profile-backed settings forms and controller services and will not load on older hosts (the startup probe refuses with one actionable diagnostic); `0.5.0-rc.1`–`rc.3` target dsh `0.1.6-alpha`; adapter `0.4.x` talks to the former `apiProxy` surface, which `0.1.6-alpha` removed — it remains the pairing for the dsh `0.1.5` stable line, reachable by exact version (e.g. `@addozhang/dsh-discord@0.4.0`). A mismatched pair fails at startup.
46
+ - To switch the dsh CLI line: `npm install -g @deepseek-ai/dsh@next` (0.1.7-rc.1, pairs with adapter 0.5.0) or `npm install -g @deepseek-ai/dsh@alpha` (0.1.7-alpha line).
47
+ - Right after a `next` release, the `@next` spec can resolve to a stale cached version. If the installed copy looks old, install by exact version instead (e.g. `@addozhang/dsh-discord@0.5.0-rc.4`) and verify the `version` field inside the profile's `node_modules`.
48
+
36
49
  ## Install
37
50
 
38
- Install with the dsh CLI — it installs the package into the profile and registers the bundle for you:
51
+ Install with the dsh CLI — it installs the package into the profile and registers the bundle for you. The stable channel is shown below; for the next channel use the `@next` command from the [Versions](#versions) table (`up` / `rm` take the same spec):
39
52
 
40
53
  ```sh
41
54
  dsh plugin --profile web add @addozhang/dsh-discord
package/README.zh.md CHANGED
@@ -29,13 +29,26 @@
29
29
 
30
30
  ## 环境要求
31
31
 
32
- - [dsh CLI](https://www.npmjs.com/package/@deepseek-ai/dsh) `0.1.6-alpha.1` 或兼容的更新版本(web profile)。适配器 `0.5.x` 使用 0.1.6 的控制器服务,与旧版 `apiProxy` 宿主面不兼容。
32
+ - [dsh CLI](https://www.npmjs.com/package/@deepseek-ai/dsh)(web profile)。dsh 与适配器的通道需匹配——见[版本支持](#版本支持)。
33
33
  - Node.js `^22.19.0 || >=24`
34
34
  - 一个 Discord 应用(含 Bot 用户),并在开发者门户启用 **MESSAGE CONTENT** 特权 intent(Developer Portal → 你的应用 → Bot → Privileged Gateway Intents)
35
35
 
36
+ ## 版本支持
37
+
38
+ 两条通道并行发布,各自钉在经过真机验证的 dsh 宿主面上。适配器与 dsh CLI 请取同一行:
39
+
40
+ | 通道 | 适配器 | dsh CLI | 安装命令 |
41
+ |---|---|---|---|
42
+ | **stable** | `0.5.0` — npm `latest` | `0.1.7-rc.1` — npm `next` | `dsh plugin --profile web add @addozhang/dsh-discord` |
43
+ | **next** | `>=0.5.0-rc.4` — npm `next` | `0.1.7-alpha` — npm `alpha` | `dsh plugin --profile web add @addozhang/dsh-discord@next` |
44
+
45
+ - **两行不可混搭。** 适配器 `>=0.5.0-rc.4`(含 `0.5.0`)使用 0.1.7 的 profile-backed settings forms 与控制器服务,在更旧的宿主上无法加载(startup 探针会给出一条可操作的诊断后拒绝激活);`0.5.0-rc.1`–`rc.3` 对应 dsh `0.1.6-alpha`;适配器 `0.4.x` 使用旧版 `apiProxy` 宿主面(该面已在 `0.1.6-alpha` 中移除)——它仍是 dsh `0.1.5` stable 线的配对,可用精确版本号安装(如 `@addozhang/dsh-discord@0.4.0`)。错配会在启动时失败。
46
+ - 切换 dsh CLI 通道:`npm install -g @deepseek-ai/dsh@next`(0.1.7-rc.1,配适配器 0.5.0)或 `npm install -g @deepseek-ai/dsh@alpha`(0.1.7-alpha 线)。
47
+ - `next` 刚发版后,`@next` 规格可能解析到过期的缓存版本。若装到的版本偏旧,请改用精确版本号安装(如 `@addozhang/dsh-discord@0.5.0-rc.4`),并核对 profile `node_modules` 中的 `version` 字段。
48
+
36
49
  ## 安装
37
50
 
38
- 使用 dsh CLI 安装——它会自动把包装进 profile 并注册 bundle:
51
+ 使用 dsh CLI 安装——它会自动把包装进 profile 并注册 bundle。下面是 stable 通道;next 通道使用[版本支持](#版本支持)表中的 `@next` 命令(`up` / `rm` 同规格):
39
52
 
40
53
  ```sh
41
54
  dsh plugin --profile web add @addozhang/dsh-discord
@@ -4,14 +4,14 @@
4
4
  * hands to the component: the edit actions plus the `discordCard` snapshot
5
5
  * store the framework binds as the `useDiscordCard` selector hook.
6
6
  */
7
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
7
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
8
8
  import type { AdapterStatusView } from '../features/adapter-status.js';
9
9
  import type { DiscordSettings } from '../settings.js';
10
10
  import { type CardManagement, type DiscordCardFace, type DiscordCardState } from './card-form.js';
11
11
  export declare class DiscordCardController {
12
12
  private readonly form;
13
13
  private management;
14
- constructor(scope: SettingsScope<DiscordSettings>);
14
+ constructor(form: ConfigForm<DiscordSettings>);
15
15
  /** Attach the token write/connect path once the RPC face is known. */
16
16
  setManagement(management: CardManagement): void;
17
17
  /** Publish the Host's sanitized connection status onto the card. */
@@ -7,7 +7,7 @@
7
7
  * is not a list of Discord IDs, which blocks the save rather than dropping it.
8
8
  */
9
9
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
10
- import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client';
10
+ import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
11
11
  import type { AdapterStatusView } from '../features/adapter-status.js';
12
12
  import type { DiscordSettings } from '../settings.js';
13
13
  import { type AdapterStatusPresentation } from './settings-model.js';
@@ -88,7 +88,7 @@ export declare class DiscordCardForm {
88
88
  private saving;
89
89
  private failed;
90
90
  private status;
91
- constructor(scope: SettingsScope<DiscordSettings>);
91
+ constructor(scope: ConfigForm<DiscordSettings>);
92
92
  /**
93
93
  * Publish the Host's sanitized status view. Edits, saves, and failures
94
94
  * never touch it — connection state is the Host's report, not form state.
package/lib/client.js CHANGED
@@ -1022,8 +1022,8 @@ var DiscordCardForm = class {
1022
1022
  var DiscordCardController = class {
1023
1023
  form;
1024
1024
  management;
1025
- constructor(scope) {
1026
- this.form = new DiscordCardForm(scope);
1025
+ constructor(form) {
1026
+ this.form = new DiscordCardForm(form);
1027
1027
  }
1028
1028
  /** Attach the token write/connect path once the RPC face is known. */
1029
1029
  setManagement(management) {
@@ -1469,7 +1469,7 @@ function DiscordSettingsCard(props) {
1469
1469
 
1470
1470
  // src/client/index.ts
1471
1471
  var name = "dsh-discord-client";
1472
- var inject = ["settingsScope", "slots", "connection", "locale"];
1472
+ var inject = ["configForms", "slots", "connection", "locale"];
1473
1473
  var STATUS_POLL_MS = 3e4;
1474
1474
  function isStatusView(value) {
1475
1475
  if (typeof value !== "object" || value === null) return false;
@@ -1477,8 +1477,8 @@ function isStatusView(value) {
1477
1477
  return typeof candidate["token"] === "string" && typeof candidate["connection"] === "string";
1478
1478
  }
1479
1479
  function apply(ctx) {
1480
- const scope = ctx.settingsScope.bind({ namespace: DISCORD_SETTINGS_NAMESPACE });
1481
- const controller = new DiscordCardController(scope);
1480
+ const form = ctx.configForms.get(DISCORD_SETTINGS_NAMESPACE);
1481
+ const controller = new DiscordCardController(form);
1482
1482
  ctx.locale.register("dsh-discord", {
1483
1483
  en: DISCORD_CARD_LOCALE_EN,
1484
1484
  zh: DISCORD_CARD_LOCALE_ZH
package/lib/index.d.ts CHANGED
@@ -1,12 +1,56 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
- import type z from '@deepseek-ai/schemastery';
3
2
  import { type DiscordSettings } from './settings.js';
4
3
  /** Stable Cordis plugin name for diagnostics. */
5
4
  export declare const name = "dsh-discord";
6
5
  /** Host services required by the complete embedded adapter. */
7
6
  export declare const inject: string[];
8
7
  export type Config = DiscordSettings;
9
- export declare const Config: z<Config>;
8
+ /**
9
+ * The plugin's static Config (0.1.7 profile-backed forms): every field is
10
+ * volatile, so the Host delivers each as a stable live reference instead of
11
+ * remounting the fiber on writes. The annotation is deliberately absent —
12
+ * schemastery's volatile mode types field defaults as `Volatile<T>`, which a
13
+ * `z<DiscordSettings>` annotation cannot express.
14
+ */
15
+ export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
16
+ enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
17
+ allowedGuildIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
18
+ memberUserIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
19
+ memberRoleIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
20
+ administratorUserIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
21
+ administratorRoleIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
22
+ deniedUserIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
23
+ deniedRoleIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
24
+ hostOperatorUserIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
25
+ defaultVerbosity: import("@deepseek-ai/schemastery").default<"text-only" | "essential-tools" | "full-tools", "text-only" | "essential-tools" | "full-tools", "volatile-defined">;
26
+ language: import("@deepseek-ai/schemastery").default<"auto" | "zh" | "en", "auto" | "zh" | "en", "volatile-defined">;
27
+ threadAutoArchiveMinutes: import("@deepseek-ai/schemastery").default<60 | 1440 | 4320 | 10080, 60 | 1440 | 4320 | 10080, "volatile-defined">;
28
+ streamUpdateIntervalMs: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
29
+ typingIntervalMs: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
30
+ approvalTimeoutMs: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
31
+ questionTimeoutMs: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
32
+ modelSelectOperatorOnly: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
33
+ permissionSelectOperatorOnly: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
34
+ }>>, Schemastery.ObjectT<NoInfer<{
35
+ enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
36
+ allowedGuildIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
37
+ memberUserIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
38
+ memberRoleIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
39
+ administratorUserIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
40
+ administratorRoleIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
41
+ deniedUserIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
42
+ deniedRoleIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
43
+ hostOperatorUserIds: import("@deepseek-ai/schemastery").default<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
44
+ defaultVerbosity: import("@deepseek-ai/schemastery").default<"text-only" | "essential-tools" | "full-tools", "text-only" | "essential-tools" | "full-tools", "volatile-defined">;
45
+ language: import("@deepseek-ai/schemastery").default<"auto" | "zh" | "en", "auto" | "zh" | "en", "volatile-defined">;
46
+ threadAutoArchiveMinutes: import("@deepseek-ai/schemastery").default<60 | 1440 | 4320 | 10080, 60 | 1440 | 4320 | 10080, "volatile-defined">;
47
+ streamUpdateIntervalMs: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
48
+ typingIntervalMs: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
49
+ approvalTimeoutMs: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
50
+ questionTimeoutMs: import("@deepseek-ai/schemastery").default<number, number, "volatile-defined">;
51
+ modelSelectOperatorOnly: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
52
+ permissionSelectOperatorOnly: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
53
+ }>>, "plain">;
10
54
  /**
11
55
  * Mount the embedded Discord adapter. Composition of the runtime path
12
56
  * (Gateway → ingress → command/mention dispatch → Discord REST, plus the
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { DEFAULT_DISCORD_SETTINGS, DiscordSettingsSchema, installDiscordSettings, normalizeDiscordSettings, } from './settings.js';
1
+ import { DEFAULT_DISCORD_SETTINGS, DiscordSettingsSchema, bindDiscordSettings, } from './settings.js';
2
2
  import { installCancellationRoot } from './lifecycle.js';
3
3
  import { ALLOWED_MENTIONS_NONE, DISCORD_SUPPRESS_NOTIFICATIONS_FLAG, OUTBOUND_EPHEMERAL_FLAGS, safeTitle } from './policy/disclosure.js';
4
4
  import { validateHostCapabilities } from './startup.js';
@@ -39,6 +39,13 @@ import { planWorkspaceChannel, workspaceChannelName } from './features/workspace
39
39
  export const name = 'dsh-discord';
40
40
  /** Host services required by the complete embedded adapter. */
41
41
  export const inject = ['sessionController', 'workspaceController', 'sessionQuery', 'webServer', 'credentials', 'settings', 'storageDomain', 'connection'];
42
+ /**
43
+ * The plugin's static Config (0.1.7 profile-backed forms): every field is
44
+ * volatile, so the Host delivers each as a stable live reference instead of
45
+ * remounting the fiber on writes. The annotation is deliberately absent —
46
+ * schemastery's volatile mode types field defaults as `Volatile<T>`, which a
47
+ * `z<DiscordSettings>` annotation cannot express.
48
+ */
42
49
  export const Config = DiscordSettingsSchema;
43
50
  /**
44
51
  * Fixed Milestone 1 Gateway intents: guilds, messages, content. Member data
@@ -66,18 +73,27 @@ function emitLog(ctx, level, message) {
66
73
  export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
67
74
  validateHostCapabilities(name => ctx.get(name));
68
75
  installCancellationRoot(ctx);
69
- let current = normalizeDiscordSettings(config);
70
76
  /**
71
77
  * Discord-visible copy, re-resolved on every access so a language change
72
78
  * on the settings card applies without rebuilding the composition. The
73
- * 'auto' preference follows the DSH locale (the `locale` namespace the
74
- * Host app registers); non-Chinese locales fall back to English.
79
+ * 'auto' preference follows the DSH locale — read from the settings forms
80
+ * describe projection (the 0.1.7 replacement for the retired
81
+ * `.get(namespace)` service method); non-Chinese locales fall back to
82
+ * English.
75
83
  */
84
+ const readLocalePreference = (service) => {
85
+ const describe = service?.describe;
86
+ if (typeof describe !== 'function')
87
+ return undefined;
88
+ const row = describe.call(service, { redactSecrets: true }).find(entry => entry.ns === 'locale');
89
+ const preference = row?.value?.preference;
90
+ return typeof preference === 'string' ? preference : undefined;
91
+ };
76
92
  const resolveLanguage = () => {
77
93
  if (current.language !== 'auto')
78
94
  return current.language;
79
- const locale = ctx.get('settings').get('locale');
80
- return typeof locale?.preference === 'string' && locale.preference.startsWith('zh') ? 'zh' : 'en';
95
+ const preference = readLocalePreference(ctx.get('settings'));
96
+ return preference !== undefined && preference.startsWith('zh') ? 'zh' : 'en';
81
97
  };
82
98
  const copy = new Proxy({}, {
83
99
  get: (_target, key) => createCopy(resolveLanguage())[key],
@@ -85,7 +101,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
85
101
  // Reassigned once the async composition has built the real registrar.
86
102
  let registerCommands = async () => { };
87
103
  const onGuildsChanged = () => { void registerCommands(); };
88
- installDiscordSettings(ctx, current, (next) => {
104
+ const settingsSource = bindDiscordSettings(ctx, config, (next) => {
89
105
  const guildsChanged = next.allowedGuildIds.join(',') !== current.allowedGuildIds.join(',');
90
106
  current = next;
91
107
  if (guildsChanged)
@@ -96,6 +112,7 @@ export function apply(ctx, config = DEFAULT_DISCORD_SETTINGS) {
96
112
  allowedGuildCount: current.allowedGuildIds.length,
97
113
  });
98
114
  });
115
+ let current = settingsSource.get();
99
116
  // The settings card's status surface: credential presence seeded now, the
100
117
  // Gateway observation fed by the adapter composition as it starts.
101
118
  // Services resolve through the same accessor the startup boundary probes.
package/lib/settings.d.ts CHANGED
@@ -43,10 +43,48 @@ export interface DiscordSettings {
43
43
  export type ThreadAutoArchiveMinutes = 60 | 1440 | 4320 | 10080;
44
44
  export declare const THREAD_AUTO_ARCHIVE_OPTIONS: readonly ThreadAutoArchiveMinutes[];
45
45
  export declare const DEFAULT_DISCORD_SETTINGS: DiscordSettings;
46
- export declare const DiscordSettingsSchema: z<DiscordSettings>;
46
+ export declare const DiscordSettingsSchema: z<Schemastery.ObjectS<NoInfer<{
47
+ enabled: z<boolean, boolean, "volatile-defined">;
48
+ allowedGuildIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
49
+ memberUserIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
50
+ memberRoleIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
51
+ administratorUserIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
52
+ administratorRoleIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
53
+ deniedUserIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
54
+ deniedRoleIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
55
+ hostOperatorUserIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
56
+ defaultVerbosity: z<"text-only" | "essential-tools" | "full-tools", "text-only" | "essential-tools" | "full-tools", "volatile-defined">;
57
+ language: z<"auto" | "zh" | "en", "auto" | "zh" | "en", "volatile-defined">;
58
+ threadAutoArchiveMinutes: z<60 | 1440 | 4320 | 10080, 60 | 1440 | 4320 | 10080, "volatile-defined">;
59
+ streamUpdateIntervalMs: z<number, number, "volatile-defined">;
60
+ typingIntervalMs: z<number, number, "volatile-defined">;
61
+ approvalTimeoutMs: z<number, number, "volatile-defined">;
62
+ questionTimeoutMs: z<number, number, "volatile-defined">;
63
+ modelSelectOperatorOnly: z<boolean, boolean, "volatile-defined">;
64
+ permissionSelectOperatorOnly: z<boolean, boolean, "volatile-defined">;
65
+ }>>, Schemastery.ObjectT<NoInfer<{
66
+ enabled: z<boolean, boolean, "volatile-defined">;
67
+ allowedGuildIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
68
+ memberUserIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
69
+ memberRoleIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
70
+ administratorUserIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
71
+ administratorRoleIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
72
+ deniedUserIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
73
+ deniedRoleIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
74
+ hostOperatorUserIds: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
75
+ defaultVerbosity: z<"text-only" | "essential-tools" | "full-tools", "text-only" | "essential-tools" | "full-tools", "volatile-defined">;
76
+ language: z<"auto" | "zh" | "en", "auto" | "zh" | "en", "volatile-defined">;
77
+ threadAutoArchiveMinutes: z<60 | 1440 | 4320 | 10080, 60 | 1440 | 4320 | 10080, "volatile-defined">;
78
+ streamUpdateIntervalMs: z<number, number, "volatile-defined">;
79
+ typingIntervalMs: z<number, number, "volatile-defined">;
80
+ approvalTimeoutMs: z<number, number, "volatile-defined">;
81
+ questionTimeoutMs: z<number, number, "volatile-defined">;
82
+ modelSelectOperatorOnly: z<boolean, boolean, "volatile-defined">;
83
+ permissionSelectOperatorOnly: z<boolean, boolean, "volatile-defined">;
84
+ }>>, "plain">;
47
85
  export declare function normalizeDiscordSettings(input: DiscordSettings): DiscordSettings;
48
86
  export declare function validateDiscordSettings(input: DiscordSettings): void;
49
87
  export interface DiscordSettingsSource {
50
88
  get(): DiscordSettings;
51
89
  }
52
- export declare function installDiscordSettings(ctx: Context, entry: DiscordSettings, onChange: (value: DiscordSettings) => void): void;
90
+ export declare function bindDiscordSettings(ctx: Context, config: Readonly<Record<string, unknown>>, onChange: (value: DiscordSettings) => void): DiscordSettingsSource;
package/lib/settings.js CHANGED
@@ -22,9 +22,17 @@ export const DEFAULT_DISCORD_SETTINGS = Object.freeze({
22
22
  modelSelectOperatorOnly: false,
23
23
  permissionSelectOperatorOnly: true,
24
24
  });
25
- const discordIdList = z.array(z.string()).default([]);
25
+ /**
26
+ * Every field is volatile (0.1.7 profile-backed forms): values live-update
27
+ * through stable per-field references without remounting the plugin fiber —
28
+ * the exact semantics the 0.1.6 `installSection` registration provided.
29
+ * Snowflake patterns ride the schema so the Host rejects malformed IDs at
30
+ * write time (parity with the retired validate hook).
31
+ */
32
+ const DISCORD_SNOWFLAKE = /^\d{17,20}$/u;
33
+ const discordIdList = z.array(z.string().pattern(DISCORD_SNOWFLAKE)).default([]).volatile();
26
34
  export const DiscordSettingsSchema = z.object({
27
- enabled: z.boolean().default(false),
35
+ enabled: z.boolean().default(false).volatile(),
28
36
  allowedGuildIds: discordIdList,
29
37
  memberUserIds: discordIdList,
30
38
  memberRoleIds: discordIdList,
@@ -34,18 +42,17 @@ export const DiscordSettingsSchema = z.object({
34
42
  deniedRoleIds: discordIdList,
35
43
  hostOperatorUserIds: discordIdList,
36
44
  defaultVerbosity: z.union(['text-only', 'essential-tools', 'full-tools'])
37
- .default('essential-tools'),
38
- language: z.union(['auto', 'zh', 'en']).default('auto'),
45
+ .default('essential-tools').volatile(),
46
+ language: z.union(['auto', 'zh', 'en']).default('auto').volatile(),
39
47
  threadAutoArchiveMinutes: z.union([60, 1440, 4320, 10080])
40
- .default(1440),
41
- streamUpdateIntervalMs: z.number().step(1).min(250).max(10_000).default(800),
42
- typingIntervalMs: z.number().step(1).min(1_000).max(30_000).default(7_000),
43
- approvalTimeoutMs: z.number().step(1).min(30_000).max(86_400_000).default(600_000),
44
- questionTimeoutMs: z.number().step(1).min(30_000).max(86_400_000).default(1_800_000),
45
- modelSelectOperatorOnly: z.boolean().default(false),
46
- permissionSelectOperatorOnly: z.boolean().default(true),
48
+ .default(1440).volatile(),
49
+ streamUpdateIntervalMs: z.number().step(1).min(250).max(10_000).default(800).volatile(),
50
+ typingIntervalMs: z.number().step(1).min(1_000).max(30_000).default(7_000).volatile(),
51
+ approvalTimeoutMs: z.number().step(1).min(30_000).max(86_400_000).default(600_000).volatile(),
52
+ questionTimeoutMs: z.number().step(1).min(30_000).max(86_400_000).default(1_800_000).volatile(),
53
+ modelSelectOperatorOnly: z.boolean().default(false).volatile(),
54
+ permissionSelectOperatorOnly: z.boolean().default(true).volatile(),
47
55
  });
48
- const DISCORD_SNOWFLAKE = /^\d{17,20}$/u;
49
56
  const ID_FIELDS = [
50
57
  'allowedGuildIds',
51
58
  'memberUserIds',
@@ -91,19 +98,22 @@ export function validateDiscordSettings(input) {
91
98
  throw new TypeError('threadAutoArchiveMinutes must be one of 60, 1440, 4320, 10080');
92
99
  }
93
100
  }
94
- export function installDiscordSettings(ctx, entry, onChange) {
95
- let source = () => entry;
96
- const provider = ctx.get('settings');
97
- if (provider === undefined || typeof provider.installSection !== 'function') {
98
- throw new TypeError('settings provider with installSection is unavailable on this Host');
99
- }
100
- provider.installSection(ctx, DISCORD_SETTINGS_NAMESPACE, DiscordSettingsSchema, entry, {
101
- validate: value => { validateDiscordSettings(normalizeDiscordSettings(value)); },
102
- setSource: current => {
103
- source = () => normalizeDiscordSettings(current());
104
- },
105
- onChange: () => {
106
- onChange(source());
107
- },
101
+ function isVolatileField(value) {
102
+ return typeof value === 'object' && value !== null
103
+ && typeof value.get === 'function';
104
+ }
105
+ export function bindDiscordSettings(ctx, config, onChange) {
106
+ const read = () => {
107
+ const raw = {};
108
+ for (const key of Object.keys(DEFAULT_DISCORD_SETTINGS)) {
109
+ const field = config[key];
110
+ raw[key] = isVolatileField(field) ? field.get() : field;
111
+ }
112
+ return normalizeDiscordSettings(raw);
113
+ };
114
+ ctx.on('settings/document-updated', (ns) => {
115
+ if (ns === DISCORD_SETTINGS_NAMESPACE)
116
+ onChange(read());
108
117
  });
118
+ return { get: read };
109
119
  }
package/lib/startup.js CHANGED
@@ -21,10 +21,13 @@ export const REQUIRED_HOST_SERVICES = [
21
21
  'permissionPresets',
22
22
  ];
23
23
  /**
24
- * Minimum contract members per service, confirmed against the 0.1.6
25
- * controller faces (dsh-api-session-controller / dsh-api-workspace-controller
26
- * / dsh-session-query). A service lacking any member means a version drift
27
- * the startup probe should have refused; refuse rather than probe at runtime.
24
+ * Minimum contract members per service, confirmed against the 0.1.7
25
+ * controller and settings faces (dsh-api-session-controller /
26
+ * dsh-api-workspace-controller / dsh-session-query / dsh-settings
27
+ * SettingsForms — describe reads the forms projection including the locale
28
+ * entry; update is the service-side landing of every card write). A service
29
+ * lacking any member means a version drift the startup probe should have
30
+ * refused; refuse rather than probe at runtime.
28
31
  */
29
32
  const REQUIRED_CONTRACT_MEMBERS = {
30
33
  sessionController: ['prompt', 'create', 'list', 'cancel', 'updateQueue', 'selectModel', 'modelCatalog', 'follow', 'control', 'resolveAgent'],
@@ -32,14 +35,14 @@ const REQUIRED_CONTRACT_MEMBERS = {
32
35
  sessionQuery: ['observeSession'],
33
36
  webServer: [],
34
37
  credentials: ['resolve', 'describe', 'set', 'unset'],
35
- settings: ['installSection'],
38
+ settings: ['describe', 'update'],
36
39
  storageDomain: ['open'],
37
40
  connection: ['rpc'],
38
41
  commands: ['execute'],
39
42
  permissionPresets: ['catalog'],
40
43
  };
41
44
  /** Actionable remediation hint appended to every activation failure. */
42
- const REMEDIATION = 'install @addozhang/dsh-discord into a dsh web profile providing the 0.1.6 host controller services';
45
+ const REMEDIATION = 'install @addozhang/dsh-discord into a dsh web profile providing the 0.1.7 host services (profile-backed settings forms)';
43
46
  function missingMembers(service, required) {
44
47
  if (service === undefined || service === null)
45
48
  return [...required];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@addozhang/dsh-discord",
3
- "version": "0.5.0-rc.3",
3
+ "version": "0.5.0",
4
4
  "description": "Discord adapter for DeepSeek Harness · DeepSeek Harness 的 Discord 适配器",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -53,14 +53,15 @@
53
53
  }
54
54
  },
55
55
  "peerDependencies": {
56
- "@deepseek-ai/cordis": "^4.0.1"
56
+ "@deepseek-ai/cordis": "^4.0.3"
57
57
  },
58
58
  "devDependencies": {
59
- "@deepseek-ai/cordis": "4.0.2",
60
- "@deepseek-ai/dsh-client-store": "0.1.6-alpha.2",
61
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.2",
62
- "@deepseek-ai/dsh-client-ui-settings": "0.1.6-alpha.2",
63
- "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.2",
59
+ "@deepseek-ai/cordis": "4.0.3",
60
+ "@deepseek-ai/dsh-client-store": "0.1.7-rc.1",
61
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.1",
62
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.1",
63
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.1",
64
+ "@deepseek-ai/dsh-settings": "0.1.7-rc.1",
64
65
  "@eslint/js": "9.39.1",
65
66
  "@types/node": "24.10.1",
66
67
  "@types/react": "18.3.1",
@@ -75,10 +76,9 @@
75
76
  "vitest": "4.1.11"
76
77
  },
77
78
  "dependencies": {
78
- "@deepseek-ai/dsh-credentials": "0.1.6-alpha.2",
79
- "@deepseek-ai/dsh-settings": "0.1.6-alpha.2",
80
- "@deepseek-ai/dsh-storage-domain": "0.1.6-alpha.2",
81
- "@deepseek-ai/schemastery": "3.18.1",
79
+ "@deepseek-ai/dsh-credentials": "0.1.7-rc.1",
80
+ "@deepseek-ai/dsh-storage-domain": "0.1.7-rc.1",
81
+ "@deepseek-ai/schemastery": "3.18.4",
82
82
  "zod": "^4.4.3"
83
83
  },
84
84
  "pnpm": {},