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 +21 -0
- package/README.md +38 -17
- package/lib/client.js +155 -183
- package/lib/index.js +76 -53
- package/lib/types/client/QqbotSettingsSection.d.ts +6 -5
- package/lib/types/client/index.d.ts +3 -3
- package/lib/types/client/locales.d.ts +10 -6
- package/lib/types/client/settings-controller.d.ts +43 -34
- package/lib/types/index.d.ts +14 -7
- package/package.json +34 -33
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
|
-
|
|
10
|
+
从 npm 装进你运行的 profile(无需本地构建):
|
|
11
11
|
|
|
12
12
|
```sh
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
64
|
+
点击「保存」后**立即生效**:host 半按需从 credential 服务读取(`credentials/updated` 提交即触发),已连接的网关会用新身份自动重连,凭据被清空则自动断开。
|
|
52
65
|
|
|
53
|
-
存储位置:`$DSH_HOME
|
|
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` 四套用例可直接跑(
|
|
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
|
-
|
|
88
|
+
两种安装方式的依赖解析不同:
|
|
74
89
|
|
|
75
|
-
-
|
|
76
|
-
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
12
|
-
|
|
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
|
|
34
|
-
* store. Latest load wins; a refused save reloads the
|
|
35
|
-
* re-renders from the fresh
|
|
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
|
-
|
|
33
|
+
credentials;
|
|
39
34
|
/** uSES-safe state source the section renders through its hooks compartment. */
|
|
40
|
-
store = (0,
|
|
35
|
+
store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
|
|
41
36
|
status: "idle",
|
|
42
|
-
|
|
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
|
|
44
|
+
* @param credentials - the credentials Remote wire face.
|
|
52
45
|
*/
|
|
53
|
-
constructor(
|
|
54
|
-
this.
|
|
46
|
+
constructor(credentials) {
|
|
47
|
+
this.credentials = credentials;
|
|
55
48
|
}
|
|
56
49
|
/**
|
|
57
|
-
* Refresh the snapshot from `
|
|
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.
|
|
69
|
-
if (!response.
|
|
70
|
-
const
|
|
71
|
-
|
|
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.
|
|
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
|
|
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
|
|
90
|
-
if (patch.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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}.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
143
|
+
"title": "TV8fAG_title",
|
|
173
144
|
"secretRow": "TV8fAG_secretRow",
|
|
174
|
-
"
|
|
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
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* field in one ordered write, and the Host
|
|
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
|
|
170
|
+
const dirty = ready && (appId.length > 0 || secret.length > 0);
|
|
208
171
|
const finish = (failure) => {
|
|
209
172
|
setBusy(false);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
219
|
-
if (busy || !ready || !snapshot.writable
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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.
|
|
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.
|
|
273
|
-
className: QqbotSettingsSection_module_css_default.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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.
|
|
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.
|
|
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": "
|
|
422
|
-
"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.
|
|
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": "
|
|
452
|
-
"appId.
|
|
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": "
|
|
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.
|
|
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": "
|
|
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,
|
|
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
|
-
"
|
|
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,
|
|
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.
|
|
493
|
+
const settings = new QqbotSettingsController(ctx.remote.credentials);
|
|
522
494
|
ctx.effect(() => {
|
|
523
|
-
const disposers = [ctx.remote.$on("
|
|
524
|
-
if (
|
|
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)
|
|
439
|
-
*
|
|
440
|
-
* the
|
|
441
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
555
|
-
*
|
|
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
|
-
/**
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
674
|
+
function rejudgeIdentity() {
|
|
657
675
|
const active = gateway;
|
|
658
676
|
if (active === void 0) return;
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
-
|
|
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* field in one ordered write, and the Host
|
|
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-
|
|
13
|
-
import type { ClientContext } from '@deepseek-ai/
|
|
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,
|
|
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.
|
|
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.
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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 {
|
|
9
|
-
import type { SnapshotStore } from '@deepseek-ai/dsh-client-
|
|
10
|
-
/** The
|
|
11
|
-
|
|
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;
|
|
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
|
-
/**
|
|
19
|
-
|
|
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
|
|
37
|
+
/** Snapshot state mirrored from the credential seam's views. */
|
|
24
38
|
export interface QqbotSettingsState {
|
|
25
|
-
/** `loading` until the first accepted
|
|
39
|
+
/** `loading` until the first accepted views; `error` after a failed load. */
|
|
26
40
|
status: 'idle' | 'loading' | 'ready' | 'error';
|
|
27
|
-
/**
|
|
28
|
-
|
|
29
|
-
/**
|
|
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
|
|
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
|
|
42
|
-
* store. Latest load wins; a refused save reloads the
|
|
43
|
-
* re-renders from the fresh
|
|
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
|
|
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
|
|
61
|
+
* @param credentials - the credentials Remote wire face.
|
|
52
62
|
*/
|
|
53
|
-
constructor(
|
|
63
|
+
constructor(credentials: CredentialsWire);
|
|
54
64
|
/**
|
|
55
|
-
* Refresh the snapshot from `
|
|
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
|
|
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 {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -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)
|
|
11
|
-
*
|
|
12
|
-
* the
|
|
13
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
62
|
-
*
|
|
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.
|
|
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-
|
|
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.
|
|
55
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.
|
|
56
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
57
|
-
"@deepseek-ai/dsh-client-
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-
|
|
59
|
-
"@deepseek-ai/dsh-
|
|
60
|
-
"@deepseek-ai/dsh-
|
|
61
|
-
"@deepseek-ai/dsh-
|
|
62
|
-
"@deepseek-ai/dsh-
|
|
63
|
-
"@deepseek-ai/dsh-
|
|
64
|
-
"@deepseek-ai/dsh-
|
|
65
|
-
"@deepseek-ai/dsh-user-approval": "^0.1.
|
|
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.
|
|
71
|
-
"@deepseek-ai/dsh-agent-loop": "^0.1.
|
|
72
|
-
"@deepseek-ai/dsh-agent-loop-testkit": "^0.1.
|
|
73
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.
|
|
74
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.
|
|
75
|
-
"@deepseek-ai/dsh-client-
|
|
76
|
-
"@deepseek-ai/dsh-client-test-runtime": "^0.1.
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.1.
|
|
79
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.
|
|
80
|
-
"@deepseek-ai/dsh-
|
|
81
|
-
"@deepseek-ai/dsh-
|
|
82
|
-
"@deepseek-ai/dsh-invariants": "^0.1.
|
|
83
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
84
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
85
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
86
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.
|
|
87
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
88
|
-
"@deepseek-ai/dsh-user-approval": "^0.1.
|
|
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",
|