dsh-qqbot 1.0.0 → 1.0.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 belowthetree
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -7,26 +7,40 @@ QQ 官方机器人插件,作为 DeepSeek Harness 的**树外插件**维护(
7
7
 
8
8
  ## 安装
9
9
 
10
- 构建,然后装进你运行的 profile
10
+ npm 装进你运行的 profile(无需本地构建):
11
11
 
12
12
  ```sh
13
- pnpm install
14
- pnpm run build
15
- pnpm dsh plugin --profile <name> add -w link:E:/Project/dsh-plugins/dsh-qqbot
13
+ dsh plugin --profile <name> add -w dsh-qqbot # 最新版
14
+ dsh plugin --profile <name> add -w dsh-qqbot@1.0.1 # 或固定版本
16
15
  ```
17
16
 
18
- 或者手动进入 profile 目录执行等价的 pnpm 命令:
17
+ 等价的手动方式(进入 profile 目录执行 pnpm):
19
18
 
20
19
  ```sh
21
20
  cd "$env:DSH_HOME/profiles/<name>" # DSH_HOME 未设置时是 ~/.dsh/profiles/<name>
22
- pnpm add -w link:E:/Project/dsh-plugins/dsh-qqbot
21
+ pnpm add -w dsh-qqbot
23
22
  ```
24
23
 
25
24
  注意:
26
25
 
27
26
  - **必须带 `-w`**:profile 目录自带 `pnpm-workspace.yaml`,不带 `-w` 时 pnpm 报 `ERR_PNPM_ADDING_TO_ROOT` 直接失败。
28
- - **用 `link:` 而不是裸路径**:裸路径是 `file:` 语义(快照副本),改源码不生效;`link:` 直接指向源码目录,重新 `pnpm run build` 后即时生效,无需重新 add。
29
27
  - 本包没有 `dsh.bundle` 声明,不会进入 profile 的 bundles 层,这符合预期——由下面的 patch 行按包名挂载;client 半由 `dsh.client` 扫描同一条挂载行自动进浏览器。
28
+ - 升级到新版本:重新 `add` 一次新版本号,然后重启 dsh。registry 版是安装时的快照(不再指向源码目录),改本地源码不影响它。
29
+
30
+ ### 本地开发(link 到源码)
31
+
32
+ 改插件源码时用 `link:` 直接指向源码目录:
33
+
34
+ ```sh
35
+ cd E:\Project\dsh-plugins\dsh-qqbot
36
+ pnpm install
37
+ pnpm run build
38
+ dsh plugin --profile <name> add -w link:E:/Project/dsh-plugins/dsh-qqbot
39
+ ```
40
+
41
+ - **用 `link:` 而不是裸路径**:裸路径是 `file:` 语义(快照副本),改源码不生效;`link:` 直接指向源码目录,重新 `pnpm run build` 后即时生效,无需重新 add。
42
+ - 改完发布版后切回本地版用同一条 `link:` 命令(`pnpm add` 会把依赖 spec 从 registry 改写成 link)。
43
+ - 反之,切回发布版:`dsh plugin --profile <name> add -w dsh-qqbot@<版本>`。
30
44
 
31
45
  在 **profile 自己的** `cordis.patch.yml`(`$DSH_HOME/profiles/<name>/cordis.patch.yml`)挂载(凭据在设置界面填写,不再写在这里):
32
46
 
@@ -44,13 +58,14 @@ pnpm add -w link:E:/Project/dsh-plugins/dsh-qqbot
44
58
 
45
59
  | 字段 | 说明 |
46
60
  | --- | --- |
47
- | AppID | QQ 开放平台机器人应用的 AppID |
48
- | AppSecret | **写穿不回显**:保存后输入框留空表示「保持现有密钥」;页面显示「已配置」,可用「清除密钥」移除。 |
49
- | 使用沙箱环境 | 勾选后走沙箱网关(`sandbox.api.sgroup.qq.com`),用于开发调试。 |
61
+ | AppID | QQ 开放平台机器人应用的 AppID。**写穿不回显**:页面只显示「已配置/未配置」状态,输入框填写新值以覆盖;「清除 AppID」可移除。 |
62
+ | AppSecret | **写穿不回显**:页面只显示「已配置/未配置」,留空表示保持现有密钥;「清除密钥」可移除。 |
50
63
 
51
- 点击「保存」后**立即生效**:host 半通过 settings 服务热读取(`settings/updated` 提交即触发),已连接的网关会用新身份自动重连,凭据被清空则自动断开。
64
+ 点击「保存」后**立即生效**:host 半按需从 credential 服务读取(`credentials/updated` 提交即触发),已连接的网关会用新身份自动重连,凭据被清空则自动断开。
52
65
 
53
- 存储位置:`$DSH_HOME/settings.yaml``qqbot:` 段(明文,等价于环境变量)。配置解析顺序:**settings 用户层 cordis.yml 行的 config 基础层 环境变量 `QQBOT_APP_ID` / `QQBOT_APP_SECRET`**(无头环境可用环境变量兜底)。三处都没有时插件照常挂载,`/qqbot on` 会报错并指向设置页。
66
+ 存储位置:`$DSH_HOME/.credentials.yaml`(明文,等价于环境变量;由 harness credential 服务管理)。页面写入的引用名是 **`QQBOT_CRED_APP_ID` / `QQBOT_CRED_APP_SECRET`**——故意与 `QQBOT_APP_ID`/`QQBOT_APP_SECRET` 环境变量不同名:credential 服务的环境层会遮蔽同名引用并使其只读,shell 里一旦导出过 `QQBOT_APP_ID`,页面就会永远变灰,因此设置页用自己的引用名,UI 写入永远优先于环境变量。之所以走 credential 服务而不是 settings 命名空间:harness 的 settings wire 面只服务硬编码的命名空间白名单(`agent-loop`、`shell` 等),树外插件注册的命名空间不会被暴露;credential 面接受任意引用名,无需改 harness。
67
+
68
+ 配置解析顺序:**credential 库(设置页写入)→ `settings.yaml` 的 `qqbot:` 段 → cordis.yml 行的 config 基础层 → 环境变量 `QQBOT_APP_ID` / `QQBOT_APP_SECRET`**(无头环境可用环境变量兜底)。**沙箱开关**不在此页:仅在 cordis.yml 的 `config.sandbox: true` 里配置。四处都没有 AppID/AppSecret 时插件照常挂载,`/qqbot on` 会报错并指向设置页。
54
69
 
55
70
  ## 测试
56
71
 
@@ -58,7 +73,7 @@ pnpm add -w link:E:/Project/dsh-plugins/dsh-qqbot
58
73
  pnpm test
59
74
  ```
60
75
 
61
- host 的 `protocol` / `gateway` / `qqbot` / `settings-controller` 四套用例可直接跑(57 例,settings-controller 通过 `vi.mock` 替换 runtime 客户端模块以绕过浏览器 bundle)。`tests/*.client.spec.ts(x)` 被 vitest 排除:它们 import `@deepseek-ai/dsh-client-runtime/client`,而 registry 上发布的是浏览器 bundle(`lib/client.js`,import 时执行 `window.__ModuleLoader__.load(...)`),node/jsdom 无法加载。这些用例在 harness 源码树内开发期已验证,客户端开关与设置页的验证走浏览器手动确认(见「使用」)。
76
+ host 的 `protocol` / `gateway` / `qqbot` / `settings-controller` 四套用例可直接跑(59 例,settings-controller 通过 `vi.mock` 替换 runtime 客户端模块以绕过浏览器 bundle)。`tests/*.client.spec.ts(x)` 被 vitest 排除:它们 import `@deepseek-ai/dsh-client-runtime/client`,而 registry 上发布的是浏览器 bundle(`lib/client.js`,import 时执行 `window.__ModuleLoader__.load(...)`),node/jsdom 无法加载。这些用例在 harness 源码树内开发期已验证,客户端开关与设置页的验证走浏览器手动确认(见「使用」)。
62
77
 
63
78
  ## 使用
64
79
 
@@ -70,16 +85,18 @@ host 的 `protocol` / `gateway` / `qqbot` / `settings-controller` 四套用例
70
85
 
71
86
  ## peer 依赖解析
72
87
 
73
- `link:` 安装的 symlink 目标在 harness 之外,Node 从真实路径(realpath)向上解析依赖,所以 `@deepseek-ai/cordis`、`dsh-*` Service Definition 这些 peer 命中 **dsh-qqbot 项目自己的 `node_modules`**(`pnpm install` 装的最新 registry 版),不是 profile 的 `node_modules` 或 `$DSH_HOME/profiles/node_modules` 回退。这带来两个后果:
88
+ 两种安装方式的依赖解析不同:
74
89
 
75
- - 插件与 harness 各持一份 `@deepseek-ai/cordis`(双实例)。当前启动与挂载正常,但若插件代码对 `ctx` 做跨实例判断(`instanceof` 等)会失效。
76
- - peer 版本按 `^` 取最新(例如 `dsh-session@0.1.0-rc.6`),可能比正在运行的 harness(rc.5)新。运行时服务走 harness 注入的 ctx,一般兼容;harness 升级或插件用到新 API 时需对齐 peer 版本。
90
+ - **registry 版(推荐)**:包从 profile 的 `node_modules` 向上解析 peer(`@deepseek-ai/cordis`、`dsh-*` Service Definition 等),与 harness 及 profile 的 `$DSH_HOME/profiles/node_modules` 回退**共享同一份实例**,没有双实例问题。
91
+ - **`link:` 开发版**:symlink 目标在 harness 之外,Node 从真实路径(realpath)向上解析,peer 命中 **dsh-qqbot 项目自己的 `node_modules`**(`pnpm install` 装的最新 registry 版)。这带来两个后果:
92
+ - 插件与 harness 各持一份 `@deepseek-ai/cordis`(双实例)。当前启动与挂载正常,但若插件代码对 `ctx` 做跨实例判断(`instanceof` 等)会失效。
93
+ - peer 版本按 `^` 取最新(例如 `dsh-session@0.1.0-rc.6`),可能比正在运行的 harness(rc.5)新。运行时服务走 harness 注入的 ctx,一般兼容;harness 升级或插件用到新 API 时需对齐 peer 版本。
77
94
 
78
95
  ## 版本耦合说明
79
96
 
80
97
  client 半的浏览器 bundle 必须用 harness 的客户端加载协议构建:`tsdown.config.ts` 里硬编码了 `PLATFORM_MODULES`(模块表 external 列表,含 `@deepseek-ai/dsh-client-runtime/client` 的 store 豁免)和 `__ModuleLoader__.load({ id })` 的包名 id。如果 harness 更新了这份平台模块列表或 `__ModuleLoader__` 协议,需要同步更新 `tsdown.config.ts`,否则浏览器端加载会失败。
81
98
 
82
- 设置页依赖 harness settings 能力(host 的 `settings` 服务 + `settings.section` 槽位 + `api.settings` wire 面),这些都在 web 基础组合里,无需额外挂载行;如果换成自定义 profile,需保证 `@deepseek-ai/dsh-settings-file` `@deepseek-ai/dsh-client-ui-settings(-general)` 在场。
99
+ 设置页依赖 harness 的:credential 能力(host 的 `credentials` 服务 + `api.credentials` wire 面,由 base 组合的 `@deepseek-ai/dsh-credentials-local` 提供)和 settings 面板(`settings.section` 槽位 + settings 服务,用于 `qqbot:` 段的 base 层解析),这些都在 web 基础组合里,无需额外挂载行;如果换成自定义 profile,需保证上述行在场。
83
100
 
84
101
  ## 发布
85
102
 
@@ -96,3 +113,7 @@ npm publish
96
113
  ```sh
97
114
  dsh plugin --profile web add dsh-qqbot -w
98
115
  ```
116
+
117
+ ## 协议
118
+
119
+ 本项目以 [MIT License](LICENSE) 发布(Copyright © 2025 belowthetree),详见 [LICENSE](LICENSE) 文件。
package/lib/client.js CHANGED
@@ -4,58 +4,51 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
7
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
8
8
  let react = require("react");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
10
  //#region src/client/settings-controller.ts
11
- /** The settings namespace the Host plugin registers its identity under. */
12
- const QQBOT_SETTINGS_NAMESPACE = "qqbot";
11
+ /**
12
+ * Credential reference holding the robot application id. Deliberately NOT the
13
+ * environment-variable name: the credential store's env layer shadows a
14
+ * same-named ref and makes it read-only, which would disable the settings
15
+ * page wherever `QQBOT_APP_ID` is exported.
16
+ */
17
+ const QQBOT_APP_ID_REF = "QQBOT_CRED_APP_ID";
18
+ /**
19
+ * Credential reference holding the robot application secret; distinct from
20
+ * `QQBOT_APP_SECRET` for the same shadowing reason.
21
+ */
22
+ const QQBOT_APP_SECRET_REF = "QQBOT_CRED_APP_SECRET";
13
23
  /** Human text for a rejected wire call or transport failure. */
14
24
  function messageOf(error) {
15
25
  return error instanceof Error ? error.message : String(error);
16
26
  }
17
- /** Read the redacted resolved AppID, defaulting absent values to ''. */
18
- function appIdIn(view) {
19
- if (typeof view.value !== "object" || view.value === null) return "";
20
- const value = view.value["appId"];
21
- return typeof value === "string" ? value : "";
22
- }
23
- /** Read the redacted resolved sandbox flag, defaulting to false. */
24
- function sandboxIn(view) {
25
- if (typeof view.value !== "object" || view.value === null) return false;
26
- return view.value["sandbox"] === true;
27
- }
28
- /** Whether the redacted view reports a stored AppSecret. */
29
- function secretSetIn(view) {
30
- return view.secrets.some((secret) => secret.path.length === 1 && secret.path[0] === "appSecret" && secret.set);
31
- }
32
27
  /**
33
- * Serializes the qqbot namespace's Host reads and writes behind one snapshot
34
- * store. Latest load wins; a refused save reloads the namespace so the form
35
- * re-renders from the fresh view and the user can re-apply the edit.
28
+ * Serializes the two credential references' Host reads and writes behind one
29
+ * snapshot store. Latest load wins; a refused save reloads the views so the
30
+ * form re-renders from the fresh state and the user can re-apply the edit.
36
31
  */
37
32
  var QqbotSettingsController = class {
38
- api;
33
+ credentials;
39
34
  /** uSES-safe state source the section renders through its hooks compartment. */
40
- store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)({
35
+ store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
41
36
  status: "idle",
42
- appId: "",
43
- sandbox: false,
37
+ appIdSet: false,
44
38
  secretSet: false,
45
39
  writable: false,
46
- revision: void 0,
47
40
  error: null
48
41
  });
49
42
  generation = 0;
50
43
  /**
51
- * @param api - the settings wire face.
44
+ * @param credentials - the credentials Remote wire face.
52
45
  */
53
- constructor(api) {
54
- this.api = api;
46
+ constructor(credentials) {
47
+ this.credentials = credentials;
55
48
  }
56
49
  /**
57
- * Refresh the snapshot from `settings.describe`; a failure keeps the last
58
- * good state and surfaces the error.
50
+ * Refresh the snapshot from `credentials.describe`; a failure keeps the
51
+ * last good state and surfaces the error.
59
52
  * @returns settlement after the read.
60
53
  */
61
54
  async load() {
@@ -65,12 +58,20 @@ window.__ModuleLoader__.load({
65
58
  state.error = null;
66
59
  });
67
60
  try {
68
- const response = await this.api.settings.describe({});
69
- if (!response.result.ok) throw new Error(response.result.error.message);
70
- const view = response.result.value.namespaces.find((candidate) => candidate.ns === QQBOT_SETTINGS_NAMESPACE);
71
- if (view === void 0) throw new Error(`settings namespace "${QQBOT_SETTINGS_NAMESPACE}" is not registered`);
61
+ const response = await this.credentials.describe([QQBOT_APP_ID_REF, QQBOT_APP_SECRET_REF]);
62
+ if (!response.ok) throw new Error(response.error.message);
63
+ const views = response.value;
64
+ const appIdView = views[QQBOT_APP_ID_REF];
65
+ const secretView = views[QQBOT_APP_SECRET_REF];
66
+ if (appIdView === void 0 || secretView === void 0) throw new Error("credentials domain did not answer the qqbot references");
72
67
  if (generation !== this.generation) return;
73
- this.accept(view, response.result.value.writable);
68
+ this.store.update((state) => {
69
+ state.status = "ready";
70
+ state.appIdSet = appIdView.configured;
71
+ state.secretSet = secretView.configured;
72
+ state.writable = appIdView.writable && secretView.writable;
73
+ state.error = null;
74
+ });
74
75
  } catch (error) {
75
76
  if (generation !== this.generation) return;
76
77
  this.store.update((state) => {
@@ -80,67 +81,35 @@ window.__ModuleLoader__.load({
80
81
  }
81
82
  }
82
83
  /**
83
- * Apply one user edit as ordered path ops; a refusal reloads the namespace
84
- * and returns its message.
84
+ * Apply one user edit as ordered credential writes, stopping at the first
85
+ * refusal; a refusal reloads the views and returns its message.
85
86
  * @param patch - the fields the user changed.
86
87
  * @returns null on success; the Host's failure text otherwise.
87
88
  */
88
89
  async save(patch) {
89
- const ops = [];
90
- if (patch.appId !== void 0) ops.push(patch.appId.length === 0 ? {
91
- op: "unset",
92
- path: ["appId"]
93
- } : {
94
- op: "set",
95
- path: ["appId"],
96
- value: patch.appId
97
- });
98
- if (patch.sandbox !== void 0) ops.push({
99
- op: "set",
100
- path: ["sandbox"],
101
- value: patch.sandbox
102
- });
103
- if (patch.clearSecret === true) ops.push({
104
- op: "unset",
105
- path: ["appSecret"]
106
- });
107
- if (patch.appSecret !== void 0 && patch.appSecret.length > 0) ops.push({
108
- op: "set",
109
- path: ["appSecret"],
110
- value: patch.appSecret
111
- });
112
- if (ops.length === 0) return null;
113
- const revision = this.store.getSnapshot().revision;
114
- let response;
90
+ const writes = [];
91
+ if (patch.clearAppId === true) writes.push(() => this.credentials.unset(QQBOT_APP_ID_REF));
92
+ const appId = patch.appId;
93
+ if (appId !== void 0 && appId.length > 0) writes.push(() => this.credentials.set(QQBOT_APP_ID_REF, appId));
94
+ if (patch.clearSecret === true) writes.push(() => this.credentials.unset(QQBOT_APP_SECRET_REF));
95
+ const appSecret = patch.appSecret;
96
+ if (appSecret !== void 0 && appSecret.length > 0) writes.push(() => this.credentials.set(QQBOT_APP_SECRET_REF, appSecret));
97
+ if (writes.length === 0) return null;
115
98
  try {
116
- response = await this.api.settings.mutate({
117
- ns: QQBOT_SETTINGS_NAMESPACE,
118
- ops,
119
- ...revision === void 0 ? {} : { expectedRevision: revision }
120
- });
99
+ for (const write of writes) {
100
+ const response = await write();
101
+ if (!response.ok) {
102
+ await this.load();
103
+ return response.error.message;
104
+ }
105
+ }
121
106
  } catch (error) {
122
107
  await this.load();
123
108
  return messageOf(error);
124
109
  }
125
- if (!response.result.ok) {
126
- await this.load();
127
- return response.result.error.message;
128
- }
129
- this.accept(response.result.value, this.store.getSnapshot().writable);
110
+ await this.load();
130
111
  return null;
131
112
  }
132
- /** Fold one redacted wire view into the snapshot. */
133
- accept(view, writable) {
134
- this.store.update((state) => {
135
- state.status = "ready";
136
- state.appId = appIdIn(view);
137
- state.sandbox = sandboxIn(view);
138
- state.secretSet = secretSetIn(view);
139
- state.writable = writable;
140
- state.revision = view.revision;
141
- state.error = null;
142
- });
143
- }
144
113
  };
145
114
  /**
146
115
  * Refresh only after the section left idle, so an unopened page never fetches
@@ -153,7 +122,7 @@ window.__ModuleLoader__.load({
153
122
  }
154
123
  //#endregion
155
124
  //#region \0dsh-css:E:\Project\dsh-plugins\dsh-qqbot\src\client\QqbotSettingsSection.module.css.mjs
156
- const css$1 = ".TV8fAG_section{flex-direction:column;gap:12px;width:100%;padding-bottom:8px;display:flex}.TV8fAG_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:600;line-height:24px}.TV8fAG_desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:20px}.TV8fAG_field{flex-direction:column;gap:6px;display:flex}.TV8fAG_fieldLabel{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}.TV8fAG_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;max-width:360px;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px;line-height:22px}.TV8fAG_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.TV8fAG_input::placeholder{color:var(--dsw-alias-label-dimmed)}.TV8fAG_input:disabled{opacity:.6;cursor:default}.TV8fAG_fieldHint{color:var(--dsw-alias-label-dimmed);margin:0;font-size:12px;line-height:18px}.TV8fAG_secretRow{align-items:center;gap:8px;width:100%;max-width:360px;display:flex}.TV8fAG_secretRow .TV8fAG_input{flex:auto}.TV8fAG_clearButton{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-state-error-primary);cursor:pointer;background:0 0;border-radius:8px;flex:none;padding:0 10px;font-size:13px;line-height:18px}.TV8fAG_clearButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.TV8fAG_clearButton:disabled{opacity:.6;cursor:default}.TV8fAG_fieldCheck{width:fit-content;color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:8px;font-size:13px;line-height:20px;display:flex}.TV8fAG_fieldCheck input:disabled{cursor:default}.TV8fAG_actions{align-items:center;gap:12px;display:flex}.TV8fAG_saveButton{height:32px;font:inherit;background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary-invert);cursor:pointer;border:none;border-radius:8px;padding:0 16px;font-size:14px;font-weight:500;line-height:20px}.TV8fAG_saveButton:hover:not(:disabled){opacity:.9}.TV8fAG_saveButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.TV8fAG_saveButton:disabled{opacity:.5;cursor:default}.TV8fAG_bound{color:var(--dsw-alias-state-warn-label);font-size:12px;line-height:18px}.TV8fAG_messageOk{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}.TV8fAG_messageError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}";
125
+ const css$1 = ".TV8fAG_section{flex-direction:column;gap:12px;width:100%;padding-bottom:8px;display:flex}.TV8fAG_title{color:var(--dsw-alias-label-primary);margin:0;font-size:16px;font-weight:600;line-height:24px}.TV8fAG_desc{color:var(--dsw-alias-label-secondary);margin:0;font-size:13px;line-height:20px}.TV8fAG_field{flex-direction:column;gap:6px;display:flex}.TV8fAG_fieldLabel{color:var(--dsw-alias-label-primary);align-items:center;gap:8px;font-size:13px;font-weight:500;line-height:20px;display:flex}.TV8fAG_chipSet{background:var(--dsw-alias-state-warn-tertiary);height:18px;color:var(--dsw-alias-state-warn-label);border-radius:999px;align-items:center;padding:0 6px;font-size:11px;font-weight:500;line-height:18px;display:inline-flex}.TV8fAG_chipUnset{background:var(--dsw-alias-bg-layer-1);height:18px;color:var(--dsw-alias-label-dimmed);border-radius:999px;align-items:center;padding:0 6px;font-size:11px;font-weight:500;line-height:18px;display:inline-flex}.TV8fAG_input{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);width:100%;max-width:360px;height:32px;font:inherit;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 10px;font-size:14px;line-height:22px}.TV8fAG_input:focus{border-color:var(--dsw-alias-brand-primary);outline:none}.TV8fAG_input::placeholder{color:var(--dsw-alias-label-dimmed)}.TV8fAG_input:disabled{opacity:.6;cursor:default}.TV8fAG_fieldHint{color:var(--dsw-alias-label-dimmed);margin:0;font-size:12px;line-height:18px}.TV8fAG_secretRow{align-items:center;gap:8px;width:100%;max-width:360px;display:flex}.TV8fAG_secretRow .TV8fAG_input{flex:auto}.TV8fAG_clearButton{border:1px solid var(--dsw-alias-border-l2);height:28px;font:inherit;color:var(--dsw-alias-state-error-primary);cursor:pointer;background:0 0;border-radius:8px;flex:none;padding:0 10px;font-size:13px;line-height:18px}.TV8fAG_clearButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-danger)}.TV8fAG_clearButton:disabled{opacity:.6;cursor:default}.TV8fAG_actions{align-items:center;gap:12px;display:flex}.TV8fAG_saveButton{height:32px;font:inherit;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);cursor:pointer;border:none;border-radius:8px;padding:0 16px;font-size:14px;font-weight:500;line-height:20px}.TV8fAG_saveButton:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}.TV8fAG_saveButton:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}.TV8fAG_saveButton:disabled{opacity:.5;cursor:default}.TV8fAG_bound{color:var(--dsw-alias-state-warn-label);font-size:12px;line-height:18px}.TV8fAG_messageOk{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px;line-height:18px}.TV8fAG_messageError{color:var(--dsw-alias-state-error-primary);margin:0;font-size:12px;line-height:18px}";
157
126
  const tagId$1 = "dsh-qqbot/QqbotSettingsSection.module.css";
158
127
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
159
128
  const tag = document.createElement("style");
@@ -164,66 +133,60 @@ window.__ModuleLoader__.load({
164
133
  }
165
134
  var QqbotSettingsSection_module_css_default = {
166
135
  "actions": "TV8fAG_actions",
167
- "fieldLabel": "TV8fAG_fieldLabel",
168
136
  "bound": "TV8fAG_bound",
169
- "title": "TV8fAG_title",
137
+ "section": "TV8fAG_section",
138
+ "chipUnset": "TV8fAG_chipUnset",
139
+ "desc": "TV8fAG_desc",
140
+ "saveButton": "TV8fAG_saveButton",
170
141
  "messageError": "TV8fAG_messageError",
171
142
  "input": "TV8fAG_input",
172
- "fieldCheck": "TV8fAG_fieldCheck",
143
+ "title": "TV8fAG_title",
173
144
  "secretRow": "TV8fAG_secretRow",
174
- "desc": "TV8fAG_desc",
145
+ "fieldLabel": "TV8fAG_fieldLabel",
146
+ "chipSet": "TV8fAG_chipSet",
175
147
  "field": "TV8fAG_field",
176
148
  "fieldHint": "TV8fAG_fieldHint",
177
149
  "clearButton": "TV8fAG_clearButton",
178
- "saveButton": "TV8fAG_saveButton",
179
- "section": "TV8fAG_section",
180
150
  "messageOk": "TV8fAG_messageOk"
181
151
  };
182
152
  //#endregion
183
153
  //#region src/client/QqbotSettingsSection.tsx
184
154
  /**
185
- * The QQ Bot settings page: AppID, AppSecret, and sandbox fields over the
186
- * Host's redacted namespace view. The AppSecret is write-only on the wire, so
187
- * the field renders blank and "keep unchanged" is the empty-value meaning;
188
- * the clear button removes a stored secret. Saving applies every changed
189
- * field in one ordered write, and the Host restarts a live gateway in place.
155
+ * The QQ Bot settings page: AppID and AppSecret over the credential seam's
156
+ * value-free views. Both values are write-only on the wire — the page shows
157
+ * configured/not-configured chips and the inputs mean "enter a new value";
158
+ * an empty field keeps the stored one, and the clear buttons remove it.
159
+ * Saving applies every changed field in one ordered write, and the Host
160
+ * restarts a live gateway in place.
190
161
  */
191
162
  function QqbotSettingsSection({ useQqbotSettings, useQqbotBound, save, t }) {
192
163
  const snapshot = useQqbotSettings((state) => state);
193
164
  const boundSessionId = useQqbotBound((state) => state.sessionId);
194
165
  const [appId, setAppId] = (0, react.useState)("");
195
166
  const [secret, setSecret] = (0, react.useState)("");
196
- const [sandbox, setSandbox] = (0, react.useState)(false);
197
167
  const [busy, setBusy] = (0, react.useState)(false);
198
168
  const [message, setMessage] = (0, react.useState)(null);
199
- const revision = snapshot.status === "ready" ? snapshot.revision : void 0;
200
- (0, react.useEffect)(() => {
201
- if (snapshot.status !== "ready") return;
202
- setAppId(snapshot.appId);
203
- setSecret("");
204
- setSandbox(snapshot.sandbox);
205
- }, [revision]);
206
169
  const ready = snapshot.status === "ready";
207
- const dirty = ready && (appId !== snapshot.appId || secret.length > 0 || sandbox !== snapshot.sandbox);
170
+ const dirty = ready && (appId.length > 0 || secret.length > 0);
208
171
  const finish = (failure) => {
209
172
  setBusy(false);
210
- setMessage(failure === null ? {
211
- kind: "ok",
212
- text: t("save.ok")
213
- } : {
173
+ if (failure === null) {
174
+ setAppId("");
175
+ setSecret("");
176
+ setMessage({
177
+ kind: "ok",
178
+ text: t("save.ok")
179
+ });
180
+ } else setMessage({
214
181
  kind: "error",
215
182
  text: failure
216
183
  });
217
184
  };
218
- const onClickSave = () => {
219
- if (busy || !ready || !snapshot.writable || !dirty) return;
185
+ const runSave = (patch) => {
186
+ if (busy || !ready || !snapshot.writable) return;
220
187
  setBusy(true);
221
188
  setMessage(null);
222
- save({
223
- appId,
224
- ...secret.length > 0 ? { appSecret: secret } : {},
225
- sandbox
226
- }).then(finish, (error) => {
189
+ save(patch).then(finish, (error) => {
227
190
  setBusy(false);
228
191
  setMessage({
229
192
  kind: "error",
@@ -231,20 +194,25 @@ window.__ModuleLoader__.load({
231
194
  });
232
195
  });
233
196
  };
197
+ const onClickSave = () => {
198
+ if (!dirty) return;
199
+ runSave({
200
+ ...appId.length > 0 ? { appId } : {},
201
+ ...secret.length > 0 ? { appSecret: secret } : {}
202
+ });
203
+ };
204
+ const onClickClearAppId = () => {
205
+ runSave({
206
+ ...appId.length > 0 ? { appId } : {},
207
+ ...secret.length > 0 ? { appSecret: secret } : {},
208
+ clearAppId: true
209
+ });
210
+ };
234
211
  const onClickClearSecret = () => {
235
- if (busy || !ready || !snapshot.writable) return;
236
- setBusy(true);
237
- setMessage(null);
238
- save({
239
- appId,
240
- sandbox,
212
+ runSave({
213
+ ...appId.length > 0 ? { appId } : {},
214
+ ...secret.length > 0 ? { appSecret: secret } : {},
241
215
  clearSecret: true
242
- }).then(finish, (error) => {
243
- setBusy(false);
244
- setMessage({
245
- kind: "error",
246
- text: error instanceof Error ? error.message : String(error)
247
- });
248
216
  });
249
217
  };
250
218
  let statusLine = null;
@@ -265,20 +233,32 @@ window.__ModuleLoader__.load({
265
233
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
266
234
  className: QqbotSettingsSection_module_css_default.field,
267
235
  children: [
268
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
236
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
269
237
  className: QqbotSettingsSection_module_css_default.fieldLabel,
270
- children: t("appId.label")
238
+ children: [t("appId.label"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
239
+ className: snapshot.appIdSet ? QqbotSettingsSection_module_css_default.chipSet : QqbotSettingsSection_module_css_default.chipUnset,
240
+ children: snapshot.appIdSet ? t("chip.set") : t("chip.unset")
241
+ })]
271
242
  }),
272
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
273
- className: QqbotSettingsSection_module_css_default.input,
274
- value: appId,
275
- onChange: (event) => {
276
- setAppId(event.target.value);
277
- },
278
- placeholder: t("appId.placeholder"),
279
- autoComplete: "off",
280
- spellCheck: false,
281
- disabled: !ready || !snapshot.writable
243
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
244
+ className: QqbotSettingsSection_module_css_default.secretRow,
245
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
246
+ className: QqbotSettingsSection_module_css_default.input,
247
+ value: appId,
248
+ onChange: (event) => {
249
+ setAppId(event.target.value);
250
+ },
251
+ placeholder: snapshot.appIdSet ? t("appId.placeholder.set") : t("appId.placeholder"),
252
+ autoComplete: "off",
253
+ spellCheck: false,
254
+ disabled: !ready || !snapshot.writable
255
+ }), snapshot.appIdSet ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
256
+ type: "button",
257
+ className: QqbotSettingsSection_module_css_default.clearButton,
258
+ onClick: onClickClearAppId,
259
+ disabled: busy,
260
+ children: t("clear.appId")
261
+ }) : null]
282
262
  }),
283
263
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
284
264
  className: QqbotSettingsSection_module_css_default.fieldHint,
@@ -289,9 +269,12 @@ window.__ModuleLoader__.load({
289
269
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
290
270
  className: QqbotSettingsSection_module_css_default.field,
291
271
  children: [
292
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
272
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
293
273
  className: QqbotSettingsSection_module_css_default.fieldLabel,
294
- children: t("appSecret.label")
274
+ children: [t("appSecret.label"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
275
+ className: snapshot.secretSet ? QqbotSettingsSection_module_css_default.chipSet : QqbotSettingsSection_module_css_default.chipUnset,
276
+ children: snapshot.secretSet ? t("chip.set") : t("chip.unset")
277
+ })]
295
278
  }),
296
279
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
297
280
  className: QqbotSettingsSection_module_css_default.secretRow,
@@ -310,7 +293,7 @@ window.__ModuleLoader__.load({
310
293
  className: QqbotSettingsSection_module_css_default.clearButton,
311
294
  onClick: onClickClearSecret,
312
295
  disabled: busy,
313
- children: t("clear.label")
296
+ children: t("clear.secret")
314
297
  }) : null]
315
298
  }),
316
299
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -319,21 +302,6 @@ window.__ModuleLoader__.load({
319
302
  })
320
303
  ]
321
304
  }),
322
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
323
- className: QqbotSettingsSection_module_css_default.fieldCheck,
324
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
325
- type: "checkbox",
326
- checked: sandbox,
327
- onChange: (event) => {
328
- setSandbox(event.target.checked);
329
- },
330
- disabled: !ready || !snapshot.writable
331
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sandbox.label") })]
332
- }),
333
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
334
- className: QqbotSettingsSection_module_css_default.fieldHint,
335
- children: t("sandbox.hint")
336
- }),
337
305
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
338
306
  className: QqbotSettingsSection_module_css_default.actions,
339
307
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -416,25 +384,27 @@ window.__ModuleLoader__.load({
416
384
  "toggle.off.label": "连接 QQ",
417
385
  "settings.nav": "QQ Bot",
418
386
  "settings.title": "QQ 官方机器人",
419
- "settings.desc": "填写 QQ 开放平台机器人应用的 AppID 与 AppSecret 后,在会话中使用 /qqbot on 或输入框的「连接 QQ」开关接入。配置保存后立即生效。",
387
+ "settings.desc": "填写 QQ 开放平台机器人应用的 AppID 与 AppSecret 后,在会话中使用 /qqbot on 或输入框的「连接 QQ」开关接入。两者保存后都不回显,页面仅显示配置状态;保存立即生效。",
388
+ "chip.set": "已配置",
389
+ "chip.unset": "未配置",
420
390
  "appId.label": "AppID",
421
- "appId.placeholder": "机器人的 AppID",
422
- "appId.hint": "QQ 开放平台机器人应用的 AppID。",
391
+ "appId.placeholder": "输入新的 AppID",
392
+ "appId.placeholder.set": "已配置(不回显),输入以覆盖",
393
+ "appId.hint": "QQ 开放平台机器人应用的 AppID;留空表示保持现有值。",
394
+ "clear.appId": "清除 AppID",
423
395
  "appSecret.label": "AppSecret",
424
396
  "appSecret.placeholder": "输入新的 AppSecret",
425
- "appSecret.placeholder.set": "已保存(不回显),输入以覆盖",
397
+ "appSecret.placeholder.set": "已配置(不回显),输入以覆盖",
426
398
  "appSecret.hint": "保存后不回显;留空表示保持现有密钥。",
427
399
  "appSecret.hint.set": "已配置 AppSecret;留空保存将保持不变,点击「清除密钥」可移除。",
428
- "clear.label": "清除密钥",
429
- "sandbox.label": "使用沙箱环境",
430
- "sandbox.hint": "启用后连接沙箱网关(sandbox.api.sgroup.qq.com),用于开发调试。",
400
+ "clear.secret": "清除密钥",
431
401
  "save.label": "保存",
432
402
  "save.busy": "保存中…",
433
403
  "save.ok": "已保存,立即生效。",
434
404
  "bound.prefix": "已绑定会话:",
435
405
  "status.loading": "正在读取配置…",
436
406
  "status.error": "读取配置失败:",
437
- "status.readonly": "设置存储为只读,无法修改。"
407
+ "status.readonly": "凭据被只读来源(如环境变量)遮蔽,无法在界面修改。"
438
408
  };
439
409
  /** English dictionary, checked complete against the zh key set. */
440
410
  const en = {
@@ -446,25 +416,27 @@ window.__ModuleLoader__.load({
446
416
  "toggle.off.label": "Connect QQ",
447
417
  "settings.nav": "QQ Bot",
448
418
  "settings.title": "QQ Official Bot",
449
- "settings.desc": "Enter the AppID and AppSecret of your QQ open-platform bot application, then use /qqbot on or the \"Connect QQ\" toggle in a session. Saved changes apply immediately.",
419
+ "settings.desc": "Enter the AppID and AppSecret of your QQ open-platform bot application, then use /qqbot on or the \"Connect QQ\" toggle in a session. Both are write-only: the page shows only configured status. Saved changes apply immediately.",
420
+ "chip.set": "Configured",
421
+ "chip.unset": "Not configured",
450
422
  "appId.label": "AppID",
451
- "appId.placeholder": "Bot AppID",
452
- "appId.hint": "The AppID of the bot application on the QQ open platform.",
423
+ "appId.placeholder": "Enter a new AppID",
424
+ "appId.placeholder.set": "Configured (not shown back), enter to replace",
425
+ "appId.hint": "The AppID of the bot application on the QQ open platform; leave empty to keep the stored value.",
426
+ "clear.appId": "Clear AppID",
453
427
  "appSecret.label": "AppSecret",
454
428
  "appSecret.placeholder": "Enter a new AppSecret",
455
- "appSecret.placeholder.set": "Saved (not shown back), enter to replace",
429
+ "appSecret.placeholder.set": "Configured (not shown back), enter to replace",
456
430
  "appSecret.hint": "Never shown back after saving; leave empty to keep the stored value.",
457
431
  "appSecret.hint.set": "An AppSecret is configured; saving with an empty field keeps it. Use \"Clear secret\" to remove it.",
458
- "clear.label": "Clear secret",
459
- "sandbox.label": "Use the sandbox environment",
460
- "sandbox.hint": "Route token, gateway, and sends through the sandbox host (sandbox.api.sgroup.qq.com) for development.",
432
+ "clear.secret": "Clear secret",
461
433
  "save.label": "Save",
462
434
  "save.busy": "Saving…",
463
435
  "save.ok": "Saved, applied immediately.",
464
436
  "bound.prefix": "Bound session: ",
465
437
  "status.loading": "Loading configuration…",
466
438
  "status.error": "Failed to load configuration: ",
467
- "status.readonly": "Settings storage is read-only."
439
+ "status.readonly": "The credentials are shadowed by a read-only source (such as environment variables) and cannot be changed here."
468
440
  };
469
441
  //#endregion
470
442
  //#region src/client/index.ts
@@ -481,12 +453,12 @@ window.__ModuleLoader__.load({
481
453
  */
482
454
  /** Dictionary namespace owned by this plugin. */
483
455
  const NS = "qqbot";
484
- /** Required services: the seats' slot registry, connection and commands remotes, and locale. */
456
+ /** Required services: the seats' slot registry, commands and credentials remotes, and locale. */
485
457
  const inject = [
486
458
  "slots",
487
459
  "remote",
488
460
  "remote.commands",
489
- "connection",
461
+ "remote.credentials",
490
462
  "locale"
491
463
  ];
492
464
  /**
@@ -499,7 +471,7 @@ window.__ModuleLoader__.load({
499
471
  zh,
500
472
  en
501
473
  }), "ui-qqbot: dictionaries");
502
- const bound = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)({ sessionId: null });
474
+ const bound = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({ sessionId: null });
503
475
  ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
504
476
  name: "conversation.input.left",
505
477
  id: "qqbot",
@@ -510,7 +482,7 @@ window.__ModuleLoader__.load({
510
482
  toggle: async () => {
511
483
  const active = bound.getSnapshot().sessionId === sessionId;
512
484
  const line = active ? "/qqbot off" : "/qqbot on";
513
- const result = await ctx.remote.commands.execute(sessionId, line);
485
+ const result = await ctx.remote.commands.execute(sessionId, line, []);
514
486
  if (!result.ok) return `${result.error.message} (${result.error.code})`;
515
487
  if (result.value === void 0) return `unknown command: ${line}`;
516
488
  bound.set({ sessionId: active ? null : sessionId });
@@ -518,10 +490,10 @@ window.__ModuleLoader__.load({
518
490
  }
519
491
  })
520
492
  }, QqbotToggle));
521
- const settings = new QqbotSettingsController(ctx.get("connection").api);
493
+ const settings = new QqbotSettingsController(ctx.remote.credentials);
522
494
  ctx.effect(() => {
523
- const disposers = [ctx.remote.$on("settings/document-updated", (ns) => {
524
- if (ns === "qqbot") refreshIfLoaded(settings);
495
+ const disposers = [ctx.remote.$on("credentials/reference-updated", (ref) => {
496
+ if (ref === "QQBOT_CRED_APP_ID" || ref === "QQBOT_CRED_APP_SECRET") refreshIfLoaded(settings);
525
497
  }), ctx.on("connection/reset", () => {
526
498
  refreshIfLoaded(settings);
527
499
  })];
package/lib/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import Schema from "@deepseek-ai/schemastery";
2
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
2
3
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
3
4
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
4
5
  //#region lib/types/protocol.js
@@ -435,10 +436,12 @@ var QqGatewayClient = class {
435
436
  * QQ gateway; `/qqbot off` disconnects. Nothing connects until a session opts
436
437
  * in, and QQ messages then arrive as ordinary user messages on that agent.
437
438
  *
438
- * The robot identity (AppID/AppSecret) lives in the `qqbot` settings section —
439
- * the Settings QQ Bot page of the web GUI — with the composition entry as
440
- * the base layer and `QQBOT_APP_ID`/`QQBOT_APP_SECRET` as an environment
441
- * fallback. Committed settings changes restart a live gateway in place.
439
+ * The robot identity (AppID/AppSecret) resolves in order: the credential
440
+ * references `QQBOT_APP_ID`/`QQBOT_APP_SECRET` (written by the Settings QQ
441
+ * Bot page into the harness credential store), then the `qqbot` settings
442
+ * section (whose base layer is the composition entry), then the same-named
443
+ * environment variables. Committed credential or settings changes restart a
444
+ * live gateway in place.
442
445
  *
443
446
  * The bridge is also an approval answerer for its bound agent: a tool call that
444
447
  * asks for confirmation is forwarded to the originating QQ chat as a prompt,
@@ -459,7 +462,11 @@ const Config = Schema.object({
459
462
  const QQBOT_APP_ID_ENV = "QQBOT_APP_ID";
460
463
  /** Environment variable fallback for the robot application secret. */
461
464
  const QQBOT_APP_SECRET_ENV = "QQBOT_APP_SECRET";
462
- /** Settings namespace carrying the robot identity (Settings → QQ Bot). */
465
+ /** Credential reference for the robot application id (written by Settings → QQ Bot). */
466
+ const QQBOT_APP_ID_REF = credentialRef("QQBOT_CRED_APP_ID");
467
+ /** Credential reference for the robot application secret (written by Settings → QQ Bot). */
468
+ const QQBOT_APP_SECRET_REF = credentialRef("QQBOT_CRED_APP_SECRET");
469
+ /** Settings namespace carrying the identity base layer (the cordis.yml config). */
463
470
  const QQBOT_SETTINGS_NAMESPACE = settingsNamespace("qqbot");
464
471
  /** Prompt section name registered on the bound agent's scope. */
465
472
  const QQBOT_CHANNEL_SECTION = "qqbot:channel";
@@ -531,28 +538,13 @@ function parseAnswer(text) {
531
538
  if (REJECT_ANSWERS.has(first)) return "rejected";
532
539
  }
533
540
  /**
534
- * Resolve robot credentials from a config section, then environment, then
535
- * fail loud.
536
- * @param config - the validated section (settings-resolved or composition entry).
537
- * @returns the concrete credentials.
538
- */
539
- function resolveCredentials(config) {
540
- const appId = config.appId ?? process.env["QQBOT_APP_ID"];
541
- const appSecret = config.appSecret ?? process.env["QQBOT_APP_SECRET"];
542
- if (appId === void 0 || appId.length === 0 || appSecret === void 0 || appSecret.length === 0) throw new Error(`qqbot: appId and appSecret are required; set them in Settings → QQ Bot, or export ${QQBOT_APP_ID_ENV}/${QQBOT_APP_SECRET_ENV}`);
543
- return {
544
- appId,
545
- appSecret
546
- };
547
- }
548
- /**
549
541
  * Mount the QQ bot bridge. It registers the `/qqbot` command and connects the
550
542
  * gateway only when a session runs `/qqbot on`; the bound agent's own scope,
551
543
  * history, and cwd are reused unchanged. While bound, it also answers that
552
544
  * agent's approval requests through the originating QQ chat.
553
545
  * @param ctx - Cordis context carrying the agent registry, commands, and session events.
554
- * @param config - composition entry; the user layer of the `qqbot` settings
555
- * namespace resolves over it once a settings provider is mounted.
546
+ * @param config - composition entry; the `qqbot` settings section resolves its
547
+ * user layer over it once a settings provider is mounted.
556
548
  */
557
549
  function apply(ctx, config) {
558
550
  const logger = ctx.logger;
@@ -561,14 +553,36 @@ function apply(ctx, config) {
561
553
  let boundSectionDispose;
562
554
  let gateway;
563
555
  let gatewaySettings;
556
+ let rejudgeGeneration = 0;
564
557
  const originByMessage = /* @__PURE__ */ new Map();
565
558
  const originByTurn = /* @__PURE__ */ new Map();
566
559
  const replyText = /* @__PURE__ */ new Map();
567
560
  let currentOrigin;
568
561
  let pendingApproval;
569
- /** Resolve the identity the bridge uses right now, or throw for the caller. */
570
- function resolveCurrentCredentials() {
571
- return resolveCredentials(readSection());
562
+ /**
563
+ * Resolve the identity the bridge uses right now, or throw for the caller.
564
+ * Layer ladder: the credential store (written by Settings → QQ Bot), then
565
+ * the settings section (settings.yaml over the cordis.yml base), then the
566
+ * same-named environment variables.
567
+ * @returns the concrete credentials.
568
+ */
569
+ async function resolveCurrentCredentials() {
570
+ const section = readSection();
571
+ const credentials = ctx.get("credentials");
572
+ let appId = section.appId;
573
+ let appSecret = section.appSecret;
574
+ if (credentials !== void 0) {
575
+ const [appIdCredential, appSecretCredential] = await Promise.all([credentials.resolve(QQBOT_APP_ID_REF), credentials.resolve(QQBOT_APP_SECRET_REF)]);
576
+ if (appIdCredential !== void 0) appId = appIdCredential.value;
577
+ if (appSecretCredential !== void 0) appSecret = appSecretCredential.value;
578
+ }
579
+ appId = appId ?? process.env["QQBOT_APP_ID"];
580
+ appSecret = appSecret ?? process.env["QQBOT_APP_SECRET"];
581
+ if (appId === void 0 || appId.length === 0 || appSecret === void 0 || appSecret.length === 0) throw new Error(`qqbot: appId and appSecret are required; set them in Settings → QQ Bot, or export ${QQBOT_APP_ID_ENV}/${QQBOT_APP_SECRET_ENV}`);
582
+ return {
583
+ appId,
584
+ appSecret
585
+ };
572
586
  }
573
587
  /** Resolve the reply path for one inbound message. */
574
588
  function replyPathOf(message) {
@@ -611,9 +625,10 @@ function apply(ctx, config) {
611
625
  agent.followup(userMessage);
612
626
  }
613
627
  /** Connect the gateway for the first bound session, or reuse the live one. */
614
- function ensureGateway() {
628
+ async function ensureGateway() {
615
629
  if (gateway === void 0) {
616
- const creds = resolveCurrentCredentials();
630
+ const creds = await resolveCurrentCredentials();
631
+ rejudgeGeneration += 1;
617
632
  gatewaySettings = {
618
633
  appId: creds.appId,
619
634
  appSecret: creds.appSecret,
@@ -634,48 +649,53 @@ function apply(ctx, config) {
634
649
  return gateway;
635
650
  }
636
651
  /** Bind a session: register its channel section, then connect the gateway. */
637
- function bind(agent) {
652
+ async function bind(agent) {
653
+ let live;
638
654
  try {
639
- resolveCurrentCredentials();
655
+ live = await ensureGateway();
640
656
  } catch (_missingCredentials) {
641
657
  return `QQ bot credentials are not configured. Set AppID/AppSecret in Settings → QQ Bot, or export ${QQBOT_APP_ID_ENV}/${QQBOT_APP_SECRET_ENV}.`;
642
658
  }
643
659
  boundAgent = agent;
644
660
  boundSectionDispose = agent.ctx.systemPrompt.section(channelSection);
645
- ensureGateway().start().catch((error) => {
661
+ live.start().catch((error) => {
646
662
  const detail = error instanceof Error ? error.message : String(error);
647
663
  logger.warn(`qqbot: gateway start failed: ${detail}`);
648
664
  });
649
665
  return null;
650
666
  }
651
667
  /**
652
- * Re-evaluate the settings section against the live gateway: removed
668
+ * Re-evaluate the credential layers against the live gateway: removed
653
669
  * credentials close it; a changed identity or sandbox flag restarts it with
654
- * the new values, keeping the binding.
670
+ * the new values, keeping the binding. Asynchronous by nature (credential
671
+ * resolution is a per-call read), so a generation counter and a
672
+ * gateway-identity check discard stale evaluations.
655
673
  */
656
- function rejudgeSettings() {
674
+ function rejudgeIdentity() {
657
675
  const active = gateway;
658
676
  if (active === void 0) return;
659
- let creds;
660
- try {
661
- creds = resolveCurrentCredentials();
662
- } catch (_missingCredentials) {
677
+ const generation = ++rejudgeGeneration;
678
+ resolveCurrentCredentials().then((creds) => {
679
+ if (generation !== rejudgeGeneration || gateway !== active) return;
680
+ const sandbox = readSection().sandbox === true;
681
+ const current = gatewaySettings;
682
+ if (current !== void 0 && current.appId === creds.appId && current.appSecret === creds.appSecret && current.sandbox === sandbox) return;
683
+ active.close();
684
+ gateway = void 0;
685
+ gatewaySettings = void 0;
686
+ if (boundAgent === void 0) return;
687
+ ensureGateway().then((live) => {
688
+ live.start();
689
+ }, (error) => {
690
+ const detail = error instanceof Error ? error.message : String(error);
691
+ logger.warn(`qqbot: gateway restart failed: ${detail}`);
692
+ });
693
+ }, () => {
694
+ if (generation !== rejudgeGeneration || gateway !== active) return;
663
695
  logger.warn("qqbot: credentials removed while connected; gateway closed");
664
696
  active.close();
665
697
  gateway = void 0;
666
698
  gatewaySettings = void 0;
667
- return;
668
- }
669
- const sandbox = readSection().sandbox === true;
670
- const current = gatewaySettings;
671
- if (current !== void 0 && current.appId === creds.appId && current.appSecret === creds.appSecret && current.sandbox === sandbox) return;
672
- active.close();
673
- gateway = void 0;
674
- gatewaySettings = void 0;
675
- if (boundAgent === void 0) return;
676
- ensureGateway().start().catch((error) => {
677
- const detail = error instanceof Error ? error.message : String(error);
678
- logger.warn(`qqbot: gateway restart failed: ${detail}`);
679
699
  });
680
700
  }
681
701
  /** Disconnect and drop the binding, unwinding the scoped section. */
@@ -769,10 +789,10 @@ function apply(ctx, config) {
769
789
  ctx.commands.register({
770
790
  name: "qqbot",
771
791
  description: "Connect or disconnect the QQ official bot for this session (/qqbot on | off)",
772
- handler: (invocation) => {
792
+ handler: async (invocation) => {
773
793
  const input = invocation.rawInput.trim();
774
794
  if (input === "on") {
775
- const failure = bind(invocation.agent);
795
+ const failure = await bind(invocation.agent);
776
796
  if (failure !== null) return {
777
797
  kind: "error",
778
798
  text: failure
@@ -795,12 +815,15 @@ function apply(ctx, config) {
795
815
  };
796
816
  }
797
817
  });
818
+ ctx.on("credentials/reference-updated", (ref) => {
819
+ if (ref === QQBOT_APP_ID_REF || ref === QQBOT_APP_SECRET_REF) rejudgeIdentity();
820
+ });
798
821
  installSettingsSection(ctx, QQBOT_SETTINGS_NAMESPACE, Config, config, {
799
822
  setSource: (current) => {
800
823
  readSection = current;
801
824
  },
802
825
  onChange: () => {
803
- rejudgeSettings();
826
+ rejudgeIdentity();
804
827
  }
805
828
  });
806
829
  ctx.effect(() => () => {
@@ -808,4 +831,4 @@ function apply(ctx, config) {
808
831
  }, "qqbot");
809
832
  }
810
833
  //#endregion
811
- export { Config, QQBOT_APP_ID_ENV, QQBOT_APP_SECRET_ENV, QQBOT_CHANNEL_ORDER, QQBOT_CHANNEL_PROMPT, QQBOT_CHANNEL_SECTION, QQBOT_SETTINGS_NAMESPACE, apply, inject, name, parseAnswer };
834
+ export { Config, QQBOT_APP_ID_ENV, QQBOT_APP_ID_REF, QQBOT_APP_SECRET_ENV, QQBOT_APP_SECRET_REF, QQBOT_CHANNEL_ORDER, QQBOT_CHANNEL_PROMPT, QQBOT_CHANNEL_SECTION, QQBOT_SETTINGS_NAMESPACE, apply, inject, name, parseAnswer };
@@ -3,10 +3,11 @@ import type { QqbotSettingsInjected } from './index.ts';
3
3
  /** Full settings.section component props: runtime share + injected share + the locale seat. */
4
4
  export type QqbotSettingsSectionProps = PropsRuntime<'settings.section'> & InjectFace<QqbotSettingsInjected> & PropsLocale<'qqbot'>;
5
5
  /**
6
- * The QQ Bot settings page: AppID, AppSecret, and sandbox fields over the
7
- * Host's redacted namespace view. The AppSecret is write-only on the wire, so
8
- * the field renders blank and "keep unchanged" is the empty-value meaning;
9
- * the clear button removes a stored secret. Saving applies every changed
10
- * field in one ordered write, and the Host restarts a live gateway in place.
6
+ * The QQ Bot settings page: AppID and AppSecret over the credential seam's
7
+ * value-free views. Both values are write-only on the wire — the page shows
8
+ * configured/not-configured chips and the inputs mean "enter a new value";
9
+ * an empty field keeps the stored one, and the clear buttons remove it.
10
+ * Saving applies every changed field in one ordered write, and the Host
11
+ * restarts a live gateway in place.
11
12
  */
12
13
  export declare function QqbotSettingsSection({ useQqbotSettings, useQqbotBound, save, t }: QqbotSettingsSectionProps): import("react").JSX.Element;
@@ -9,8 +9,8 @@
9
9
  * `remote.commands`, and settings edits write through the Host's settings
10
10
  * wire face.
11
11
  */
12
- import { type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
13
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
12
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
13
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
14
14
  import { type QqbotSettingsPatch, type QqbotSettingsState } from './settings-controller.ts';
15
15
  import { type QqbotKey } from './locales.ts';
16
16
  export type { QqbotSettingsPatch, QqbotSettingsState } from './settings-controller.ts';
@@ -50,7 +50,7 @@ export interface QqbotSettingsInjected {
50
50
  */
51
51
  save: (patch: QqbotSettingsPatch) => Promise<string | null>;
52
52
  }
53
- /** Required services: the seats' slot registry, connection and commands remotes, and locale. */
53
+ /** Required services: the seats' slot registry, commands and credentials remotes, and locale. */
54
54
  export declare const inject: string[];
55
55
  /**
56
56
  * Client plugin body: register the connection toggle over the command channel
@@ -10,17 +10,19 @@ export declare const zh: {
10
10
  'settings.nav': string;
11
11
  'settings.title': string;
12
12
  'settings.desc': string;
13
+ 'chip.set': string;
14
+ 'chip.unset': string;
13
15
  'appId.label': string;
14
16
  'appId.placeholder': string;
17
+ 'appId.placeholder.set': string;
15
18
  'appId.hint': string;
19
+ 'clear.appId': string;
16
20
  'appSecret.label': string;
17
21
  'appSecret.placeholder': string;
18
22
  'appSecret.placeholder.set': string;
19
23
  'appSecret.hint': string;
20
24
  'appSecret.hint.set': string;
21
- 'clear.label': string;
22
- 'sandbox.label': string;
23
- 'sandbox.hint': string;
25
+ 'clear.secret': string;
24
26
  'save.label': string;
25
27
  'save.busy': string;
26
28
  'save.ok': string;
@@ -42,17 +44,19 @@ export declare const en: {
42
44
  'settings.nav': string;
43
45
  'settings.title': string;
44
46
  'settings.desc': string;
47
+ 'chip.set': string;
48
+ 'chip.unset': string;
45
49
  'appId.label': string;
46
50
  'appId.placeholder': string;
51
+ 'appId.placeholder.set': string;
47
52
  'appId.hint': string;
53
+ 'clear.appId': string;
48
54
  'appSecret.label': string;
49
55
  'appSecret.placeholder': string;
50
56
  'appSecret.placeholder.set': string;
51
57
  'appSecret.hint': string;
52
58
  'appSecret.hint.set': string;
53
- 'clear.label': string;
54
- 'sandbox.label': string;
55
- 'sandbox.hint': string;
59
+ 'clear.secret': string;
56
60
  'save.label': string;
57
61
  'save.busy': string;
58
62
  'save.ok': string;
@@ -1,71 +1,79 @@
1
1
  /**
2
- * Settings-page transport for the qqbot namespace: reads the Host's redacted
3
- * settings view and writes user edits as path ops, mirroring the result in a
4
- * snapshot store the section renders through. The Host is the single fact
5
- * source every mutation writes through the wire and the page re-renders
6
- * from the response view (or a reload after a refusal).
2
+ * Settings-page transport for the QQ bot identity: reads the credential seam's
3
+ * value-free views (`configured`/`source`/`writable`, never values) and writes
4
+ * user edits through `credentials.set`/`credentials.unset`, mirroring the
5
+ * result in a snapshot store the section renders through. The credentials
6
+ * domain is used because the settings wire face serves only a hardcoded
7
+ * namespace allowlist the harness owns; credentials take arbitrary reference
8
+ * names, so an out-of-tree plugin needs no harness patch.
7
9
  */
8
- import type { IApiClient } from '@deepseek-ai/dsh-api-remotes/client';
9
- import type { SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
10
- /** The settings namespace the Host plugin registers its identity under. */
11
- export declare const QQBOT_SETTINGS_NAMESPACE = "qqbot";
10
+ import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client';
11
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
12
+ /** The credentials Remote methods the QQ settings page reads and writes through. */
13
+ type CredentialsWire = Pick<ClientRemote['credentials'], 'describe' | 'set' | 'unset'>;
14
+ /**
15
+ * Credential reference holding the robot application id. Deliberately NOT the
16
+ * environment-variable name: the credential store's env layer shadows a
17
+ * same-named ref and makes it read-only, which would disable the settings
18
+ * page wherever `QQBOT_APP_ID` is exported.
19
+ */
20
+ export declare const QQBOT_APP_ID_REF = "QQBOT_CRED_APP_ID";
21
+ /**
22
+ * Credential reference holding the robot application secret; distinct from
23
+ * `QQBOT_APP_SECRET` for the same shadowing reason.
24
+ */
25
+ export declare const QQBOT_APP_SECRET_REF = "QQBOT_CRED_APP_SECRET";
12
26
  /** One user edit the section sends: absent fields stay untouched. */
13
27
  export interface QqbotSettingsPatch {
14
- /** New AppID; an empty string clears the field (re-inherits the base layer). */
28
+ /** New AppID; omitted or empty keeps the stored value. */
15
29
  appId?: string;
16
30
  /** New AppSecret; omitted or empty keeps the stored value. */
17
31
  appSecret?: string;
18
- /** New sandbox flag. */
19
- sandbox?: boolean;
32
+ /** Remove the stored AppID. */
33
+ clearAppId?: boolean;
20
34
  /** Remove the stored AppSecret. */
21
35
  clearSecret?: boolean;
22
36
  }
23
- /** Snapshot state mirrored from the Host's redacted namespace view. */
37
+ /** Snapshot state mirrored from the credential seam's views. */
24
38
  export interface QqbotSettingsState {
25
- /** `loading` until the first accepted view; `error` after a failed load. */
39
+ /** `loading` until the first accepted views; `error` after a failed load. */
26
40
  status: 'idle' | 'loading' | 'ready' | 'error';
27
- /** Resolved AppID (base layer overridden by the user section); '' when unset. */
28
- appId: string;
29
- /** Resolved sandbox flag. */
30
- sandbox: boolean;
31
- /** Whether the user section stores an AppSecret (the value never rides the wire). */
41
+ /** Whether an AppID is currently stored (the value never rides the wire). */
42
+ appIdSet: boolean;
43
+ /** Whether an AppSecret is currently stored (the value never rides the wire). */
32
44
  secretSet: boolean;
33
- /** Whether the Host settings document accepts writes. */
45
+ /** Whether the credential store accepts writes for both references. */
34
46
  writable: boolean;
35
- /** Namespace revision the next write fences against. */
36
- revision: number | undefined;
37
47
  /** Human failure text of the latest load or save; null while healthy. */
38
48
  error: string | null;
39
49
  }
40
50
  /**
41
- * Serializes the qqbot namespace's Host reads and writes behind one snapshot
42
- * store. Latest load wins; a refused save reloads the namespace so the form
43
- * re-renders from the fresh view and the user can re-apply the edit.
51
+ * Serializes the two credential references' Host reads and writes behind one
52
+ * snapshot store. Latest load wins; a refused save reloads the views so the
53
+ * form re-renders from the fresh state and the user can re-apply the edit.
44
54
  */
45
55
  export declare class QqbotSettingsController {
46
- private readonly api;
56
+ private readonly credentials;
47
57
  /** uSES-safe state source the section renders through its hooks compartment. */
48
58
  readonly store: SnapshotStore<QqbotSettingsState>;
49
59
  private generation;
50
60
  /**
51
- * @param api - the settings wire face.
61
+ * @param credentials - the credentials Remote wire face.
52
62
  */
53
- constructor(api: Pick<IApiClient, 'settings'>);
63
+ constructor(credentials: CredentialsWire);
54
64
  /**
55
- * Refresh the snapshot from `settings.describe`; a failure keeps the last
56
- * good state and surfaces the error.
65
+ * Refresh the snapshot from `credentials.describe`; a failure keeps the
66
+ * last good state and surfaces the error.
57
67
  * @returns settlement after the read.
58
68
  */
59
69
  load(): Promise<void>;
60
70
  /**
61
- * Apply one user edit as ordered path ops; a refusal reloads the namespace
62
- * and returns its message.
71
+ * Apply one user edit as ordered credential writes, stopping at the first
72
+ * refusal; a refusal reloads the views and returns its message.
63
73
  * @param patch - the fields the user changed.
64
74
  * @returns null on success; the Host's failure text otherwise.
65
75
  */
66
76
  save(patch: QqbotSettingsPatch): Promise<string | null>;
67
- /** Fold one redacted wire view into the snapshot. */
68
- private accept;
69
77
  }
70
78
  /**
71
79
  * Refresh only after the section left idle, so an unopened page never fetches
@@ -73,3 +81,4 @@ export declare class QqbotSettingsController {
73
81
  * @param controller - the page controller whose current status gates the reload.
74
82
  */
75
83
  export declare function refreshIfLoaded(controller: QqbotSettingsController): void;
84
+ export {};
@@ -7,10 +7,12 @@
7
7
  * QQ gateway; `/qqbot off` disconnects. Nothing connects until a session opts
8
8
  * in, and QQ messages then arrive as ordinary user messages on that agent.
9
9
  *
10
- * The robot identity (AppID/AppSecret) lives in the `qqbot` settings section —
11
- * the Settings QQ Bot page of the web GUI — with the composition entry as
12
- * the base layer and `QQBOT_APP_ID`/`QQBOT_APP_SECRET` as an environment
13
- * fallback. Committed settings changes restart a live gateway in place.
10
+ * The robot identity (AppID/AppSecret) resolves in order: the credential
11
+ * references `QQBOT_APP_ID`/`QQBOT_APP_SECRET` (written by the Settings QQ
12
+ * Bot page into the harness credential store), then the `qqbot` settings
13
+ * section (whose base layer is the composition entry), then the same-named
14
+ * environment variables. Committed credential or settings changes restart a
15
+ * live gateway in place.
14
16
  *
15
17
  * The bridge is also an approval answerer for its bound agent: a tool call that
16
18
  * asks for confirmation is forwarded to the originating QQ chat as a prompt,
@@ -20,6 +22,7 @@
20
22
  */
21
23
  import type { Context } from '@deepseek-ai/cordis';
22
24
  import Schema from '@deepseek-ai/schemastery';
25
+ import { type CredentialRef } from '@deepseek-ai/dsh-credentials';
23
26
  import type { ApprovalOutcome } from '@deepseek-ai/dsh-user-approval';
24
27
  import type { QqbotConfig } from './types.ts';
25
28
  export type { QqbotConfig } from './types.ts';
@@ -32,7 +35,11 @@ export declare const Config: Schema<QqbotConfig>;
32
35
  export declare const QQBOT_APP_ID_ENV = "QQBOT_APP_ID";
33
36
  /** Environment variable fallback for the robot application secret. */
34
37
  export declare const QQBOT_APP_SECRET_ENV = "QQBOT_APP_SECRET";
35
- /** Settings namespace carrying the robot identity (Settings → QQ Bot). */
38
+ /** Credential reference for the robot application id (written by Settings → QQ Bot). */
39
+ export declare const QQBOT_APP_ID_REF: CredentialRef;
40
+ /** Credential reference for the robot application secret (written by Settings → QQ Bot). */
41
+ export declare const QQBOT_APP_SECRET_REF: CredentialRef;
42
+ /** Settings namespace carrying the identity base layer (the cordis.yml config). */
36
43
  export declare const QQBOT_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
37
44
  /** Prompt section name registered on the bound agent's scope. */
38
45
  export declare const QQBOT_CHANNEL_SECTION = "qqbot:channel";
@@ -58,7 +65,7 @@ export declare function parseAnswer(text: string): ApprovalOutcome | undefined;
58
65
  * history, and cwd are reused unchanged. While bound, it also answers that
59
66
  * agent's approval requests through the originating QQ chat.
60
67
  * @param ctx - Cordis context carrying the agent registry, commands, and session events.
61
- * @param config - composition entry; the user layer of the `qqbot` settings
62
- * namespace resolves over it once a settings provider is mounted.
68
+ * @param config - composition entry; the `qqbot` settings section resolves its
69
+ * user layer over it once a settings provider is mounted.
63
70
  */
64
71
  export declare function apply(ctx: Context, config: QqbotConfig): void;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-qqbot",
3
3
  "description": "QQ official bot bridge for DeepSeek Harness: /qqbot command, bound-session routing, QQ-side approvals, the composer connection toggle, and the QQ Bot settings page",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -27,7 +27,7 @@
27
27
  "@deepseek-ai/dsh-api-remotes",
28
28
  "@deepseek-ai/dsh-client-connection",
29
29
  "@deepseek-ai/dsh-client-locale",
30
- "@deepseek-ai/dsh-client-runtime",
30
+ "@deepseek-ai/dsh-client-store",
31
31
  "@deepseek-ai/dsh-client-ui-conversation",
32
32
  "@deepseek-ai/dsh-client-ui-settings"
33
33
  ],
@@ -46,46 +46,47 @@
46
46
  "test": "vitest run"
47
47
  },
48
48
  "dependencies": {
49
+ "@deepseek-ai/dsh-credentials": "^0.1.0-rc.5",
49
50
  "@deepseek-ai/dsh-settings": "^0.1.0-rc.5",
50
51
  "@deepseek-ai/schemastery": "^3.18.1"
51
52
  },
52
53
  "peerDependencies": {
53
54
  "@deepseek-ai/cordis": "^4.0.1",
54
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.5",
55
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.5",
56
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.5",
57
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.5",
58
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.5",
59
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.5",
60
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.5",
61
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.5",
62
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.5",
63
- "@deepseek-ai/dsh-session": "^0.1.0-rc.5",
64
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.5",
65
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.5",
55
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.1",
56
+ "@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.1",
57
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.1",
58
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.1",
59
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.1",
60
+ "@deepseek-ai/dsh-commands": "^0.1.2-alpha.1",
61
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.1",
62
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.1",
63
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.1",
64
+ "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.1",
65
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.1",
66
+ "@deepseek-ai/dsh-user-approval": "^0.1.2-alpha.1",
66
67
  "react": "^18.2.0"
67
68
  },
68
69
  "devDependencies": {
69
70
  "@deepseek-ai/cordis": "^4.0.1",
70
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.5",
71
- "@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.5",
72
- "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.0-rc.5",
73
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.5",
74
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.5",
75
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.5",
76
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.0-rc.5",
77
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.5",
78
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.5",
79
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.5",
80
- "@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.5",
81
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.5",
82
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.5",
83
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.5",
84
- "@deepseek-ai/dsh-session": "^0.1.0-rc.5",
85
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.5",
86
- "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.5",
87
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.5",
88
- "@deepseek-ai/dsh-user-approval": "^0.1.0-rc.5",
71
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.1",
72
+ "@deepseek-ai/dsh-agent-loop": "^0.1.2-alpha.1",
73
+ "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.2-alpha.1",
74
+ "@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.1",
75
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.1",
76
+ "@deepseek-ai/dsh-client-store": "^0.1.2-alpha.1",
77
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.2-alpha.1",
78
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.1",
79
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.1",
80
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.1",
81
+ "@deepseek-ai/dsh-commands": "^0.1.2-alpha.1",
82
+ "@deepseek-ai/dsh-credentials": "^0.1.2-alpha.1",
83
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.1",
84
+ "@deepseek-ai/dsh-llm": "^0.1.2-alpha.1",
85
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.1",
86
+ "@deepseek-ai/dsh-settings": "^0.1.2-alpha.1",
87
+ "@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.1",
88
+ "@deepseek-ai/dsh-tools": "^0.1.2-alpha.1",
89
+ "@deepseek-ai/dsh-user-approval": "^0.1.2-alpha.1",
89
90
  "@testing-library/react": "^16.3.2",
90
91
  "@types/node": "^22.20.0",
91
92
  "@types/react": "~18.3.1",