dsh-context-toolkit 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +129 -2
- package/README.zh.md +130 -0
- package/client.js +334 -0
- package/cordis.patch.yml +14 -0
- package/icon.svg +5 -0
- package/index.js +1250 -0
- package/lib/runner.js +265 -0
- package/lib/store.js +243 -0
- package/locale/en.json +83 -0
- package/locale/zh.json +83 -0
- package/package.json +59 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chimneyQ
|
|
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
|
@@ -1,3 +1,130 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-context-toolkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> [简体中文](./README.zh.md) | **English**
|
|
4
|
+
|
|
5
|
+
A DeepSeek Harness bundle whose single job is **keeping bulk output out of the conversation**.
|
|
6
|
+
|
|
7
|
+
It separates *raw bytes* from *conclusions*: commands run in a child process, files are analysed in a sandbox, and long material is filed into a local knowledge base — while the conversation receives only conclusions, excerpts and counts. A routing gate refuses, before execution, the calls that would inevitably flood the transcript.
|
|
8
|
+
|
|
9
|
+
**No third-party runtime dependencies.** The knowledge base rides on Node's own `node:sqlite` (FTS5 included) and code execution uses interpreters already on the machine, so installing it pulls nothing else.
|
|
10
|
+
|
|
11
|
+
## What it is for
|
|
12
|
+
|
|
13
|
+
| Problem | What this does |
|
|
14
|
+
|---|---|
|
|
15
|
+
| A command prints thousands of lines; you only need a count | `ctk_execute` runs it in a child process and returns the answer |
|
|
16
|
+
| A large file needs statistics, not a full read | `ctk_execute_file` keeps its bytes in the sandbox |
|
|
17
|
+
| Many commands, one question | `ctk_batch_execute` answers in one call with per-command status |
|
|
18
|
+
| A page or spec is read once and never reused | `ctk_index` files it; `ctk_search` pulls excerpts on demand |
|
|
19
|
+
| `curl` / `wget` paste whole responses into the conversation | The routing gate refuses them and names the alternative |
|
|
20
|
+
| You cannot tell how much was saved | `ctk_stats` reports the ledger; the settings page shows a live occupancy panel |
|
|
21
|
+
|
|
22
|
+
## Tools
|
|
23
|
+
|
|
24
|
+
| Tool | Purpose |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `ctk_execute` | Run code in a sandboxed child process (javascript / esm / typescript / shell), return only the answer |
|
|
27
|
+
| `ctk_execute_file` | Run code over a file for statistics or extraction; the file text stays in the sandbox |
|
|
28
|
+
| `ctk_batch_execute` | Several commands in one call, per-command status, conclusions only |
|
|
29
|
+
| `ctk_index` | File text or a file into the local FTS5 knowledge base |
|
|
30
|
+
| `ctk_search` | Multi-query retrieval; excerpts are centred on the match |
|
|
31
|
+
| `ctk_fetch_and_index` | Fetch pages → strip markup → file them |
|
|
32
|
+
| `ctk_purge` | Delete filed material (requires `confirm: true`) |
|
|
33
|
+
| `ctk_stats` | Savings ledger: refusals, sandbox bytes, filed bytes, call counts |
|
|
34
|
+
| `ctk_doctor` | Self-check: interpreters really start, scratch dir writable, knowledge round-trip |
|
|
35
|
+
| `ctk_context_report` | This conversation's context occupancy and composition |
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
From npm (recommended):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dsh plugin --profile desktop add dsh-context-toolkit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
From GitHub:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dsh plugin --profile desktop add github:chimneyQ/dsh-context-toolkit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
From a local checkout:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
dsh plugin --profile desktop add D:\path\to\dsh-context-toolkit
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Confirm `dsh-context-toolkit` appears in `dsh.profile.bundles` inside `<DSH_HOME>/profiles/desktop/package.json`.
|
|
58
|
+
|
|
59
|
+
Remove:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
dsh plugin --profile desktop remove dsh-context-toolkit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Use it well
|
|
66
|
+
|
|
67
|
+
**Reach for `ctk_*` first** whenever the work is analysis, counting, filtering or summarising — reading that output into the conversation is the exact problem this bundle exists to solve.
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// How many .log files, and how big in total?
|
|
71
|
+
await ctk_execute({
|
|
72
|
+
language: 'javascript',
|
|
73
|
+
code: `
|
|
74
|
+
const fs = require('fs');
|
|
75
|
+
const dir = process.argv[2];
|
|
76
|
+
let n = 0, bytes = 0;
|
|
77
|
+
for (const f of fs.readdirSync(dir)) {
|
|
78
|
+
if (!f.endsWith('.log')) continue;
|
|
79
|
+
n++; bytes += fs.statSync(dir + '/' + f).size;
|
|
80
|
+
}
|
|
81
|
+
console.log(n + ' files, ' + bytes + ' bytes');
|
|
82
|
+
`
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
File long material once, then pull sections:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
await ctk_index({ path: 'D:/specs/api.md' }); // the full text stays in the store
|
|
90
|
+
await ctk_search({ queries: ['rate limit', 'auth'] }); // only the relevant excerpts come back
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
| Key | Default | Meaning |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `routing` | `true` | Master switch for the routing gate |
|
|
98
|
+
| `denyCurlWget` | `true` | Block curl / wget outright |
|
|
99
|
+
| `bashNudgeMinCommandBytes` | `4096` | Nudge threshold for long commands (0 disables) |
|
|
100
|
+
| `maxReadBytesBeforeAsk` | `51200` | Nudge threshold for whole-file reads (0 disables) |
|
|
101
|
+
| `trustedReadBasenames` | `package.json` etc. | Filenames exempt from the read nudge |
|
|
102
|
+
| `compressNudgeTokens` | `120000` | Occupancy warning threshold (tokens) |
|
|
103
|
+
| `searchLimit` | `5` | Excerpts per query in `ctk_search` |
|
|
104
|
+
| `snippetChars` | `900` | Character ceiling per excerpt |
|
|
105
|
+
| `fetchMaxBytes` | `2000000` | Per-page fetch ceiling |
|
|
106
|
+
| `storageDir` | empty | Knowledge-base root; empty = `<DSH_HOME>/context-toolkit` |
|
|
107
|
+
| `tools.*` | see below | Per-tool switches |
|
|
108
|
+
|
|
109
|
+
Tool switches: `tools.execute`, `tools.executeFile`, `tools.batchExecute`, `tools.index`, `tools.search`, `tools.fetchAndIndex`, `tools.purge`, `tools.stats`, `tools.contextReport`, `tools.doctor` — all on by default.
|
|
110
|
+
|
|
111
|
+
## Verify
|
|
112
|
+
|
|
113
|
+
The offline suites need no DSH runtime:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
node smoke.mjs # 52 checks: contract, runtime discovery, tools, gate, knowledge base, observability
|
|
117
|
+
node test-client.mjs # 27 checks: browser-half contract and bilingual dictionary parity
|
|
118
|
+
node test-package.mjs # 18 checks: publish manifest and the zero-dependency constraint
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Known limits
|
|
122
|
+
|
|
123
|
+
- **Outbound fetching depends on this machine's network.** If host names resolve to a loopback address, `ctk_fetch_and_index` fails with that reason spelled out — not a bare "fetch failed" — and `ctk_index` is the way to file local material instead.
|
|
124
|
+
- **`ctk_execute` timeouts are hard kills**: the child is terminated and `timedOut` is reported.
|
|
125
|
+
- **A single run captures at most 256 KB**; anything beyond is counted but not retained. The text handed back is additionally capped at 4000 characters, so even a 1 MB single-line output cannot enter the conversation whole.
|
|
126
|
+
- **The knowledge base deduplicates by path**: filing the same path again replaces the earlier copy.
|
|
127
|
+
|
|
128
|
+
## License
|
|
129
|
+
|
|
130
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# 上下文工具箱(dsh-context-toolkit)
|
|
2
|
+
|
|
3
|
+
> **简体中文** | [English](./README.md)
|
|
4
|
+
|
|
5
|
+
一个给 DeepSeek Harness 用的插件:**让大块输出不进对话**。
|
|
6
|
+
|
|
7
|
+
它的做法是把"原始字节"和"结论"分开——命令在子进程里跑、文件在沙箱里分析、网页与长材料归档进本地知识库,对话里只留下结论、片段和计数。另有一道路由门禁,在工具调用前挡掉那些注定会灌满对话的操作。
|
|
8
|
+
|
|
9
|
+
**零第三方运行时依赖**:知识库用 Node 自带的 `node:sqlite`(含 FTS5),跑码用系统已有的解释器,安装后不需要再拉任何包。
|
|
10
|
+
|
|
11
|
+
## 它有什么用
|
|
12
|
+
|
|
13
|
+
| 问题 | 这个插件的做法 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| 跑个命令,输出几千行,只想知道"有几个" | `ctk_execute` 在子进程里跑,只把结论带回来 |
|
|
16
|
+
| 读大文件做统计,不想把全文塞进上下文 | `ctk_execute_file` 让文件留在沙箱里 |
|
|
17
|
+
| 一次要问很多条命令 | `ctk_batch_execute` 一条调用跑完,逐条报状态 |
|
|
18
|
+
| 网页/长文档看完就废,下次还要重看 | `ctk_index` 归档,`ctk_search` 按需取片段 |
|
|
19
|
+
| `curl` / `wget` 直接把整页灌进对话 | 路由门禁硬拦,并给出替代方案 |
|
|
20
|
+
| 不知道到底省了多少 | `ctk_stats` 报台账;设置页有实时占用面板 |
|
|
21
|
+
|
|
22
|
+
## 工具
|
|
23
|
+
|
|
24
|
+
| 工具 | 用途 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `ctk_execute` | 在沙箱子进程里跑代码(javascript / esm / typescript / shell),只回结论 |
|
|
27
|
+
| `ctk_execute_file` | 对某个文件跑代码做统计或抽取,文件原文不出沙箱 |
|
|
28
|
+
| `ctk_batch_execute` | 一次跑多条命令,逐条状态,输出只有结论 |
|
|
29
|
+
| `ctk_index` | 把文本或文件存进本地 FTS5 知识库 |
|
|
30
|
+
| `ctk_search` | 多查询检索知识库,返回围绕命中词的片段 |
|
|
31
|
+
| `ctk_fetch_and_index` | 抓网页 → 去标签转文本 → 直接入库 |
|
|
32
|
+
| `ctk_purge` | 删除已归档内容(必须 `confirm: true`) |
|
|
33
|
+
| `ctk_stats` | 节约台账:拦截次数、沙箱保留字节、归档字节、调用计数 |
|
|
34
|
+
| `ctk_doctor` | 自检:解释器能否真启动、临时目录可写、知识库能否往返 |
|
|
35
|
+
| `ctk_context_report` | 当前会话的上下文占用与构成 |
|
|
36
|
+
|
|
37
|
+
## 安装
|
|
38
|
+
|
|
39
|
+
从 npm 安装(推荐):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dsh plugin --profile desktop add dsh-context-toolkit
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
从 GitHub 安装:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dsh plugin --profile desktop add github:chimneyQ/dsh-context-toolkit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
本地 checkout 时可直接指定目录:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
dsh plugin --profile desktop add D:\path\to\dsh-context-toolkit
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
装完确认 `<DSH_HOME>/profiles/desktop/package.json` 的 `dsh.profile.bundles` 里出现 `dsh-context-toolkit`。
|
|
58
|
+
|
|
59
|
+
卸载:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
dsh plugin --profile desktop remove dsh-context-toolkit
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 怎么用更划算
|
|
66
|
+
|
|
67
|
+
**默认就该走 `ctk_*`**。尤其是要分析、计数、过滤、看摘要的场合——直接把输出读进对话是这套插件要解决的原始问题。
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
// 想知道一个大目录里有多少 .log 文件、总共多大
|
|
71
|
+
await ctk_execute({
|
|
72
|
+
language: 'javascript',
|
|
73
|
+
code: `
|
|
74
|
+
const fs = require('fs');
|
|
75
|
+
const dir = process.argv[2];
|
|
76
|
+
let n = 0, bytes = 0;
|
|
77
|
+
for (const f of fs.readdirSync(dir)) {
|
|
78
|
+
if (!f.endsWith('.log')) continue;
|
|
79
|
+
n++; bytes += fs.statSync(dir + '/' + f).size;
|
|
80
|
+
}
|
|
81
|
+
console.log(n + ' files, ' + bytes + ' bytes');
|
|
82
|
+
`
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
长材料先归档,之后按需取:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
await ctk_index({ path: 'D:/specs/api.md' }); // 全文留在库里
|
|
90
|
+
await ctk_search({ queries: ['rate limit', 'auth'] }); // 只取相关片段
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## 配置
|
|
94
|
+
|
|
95
|
+
| 键 | 默认 | 含义 |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `routing` | `true` | 路由门禁总开关 |
|
|
98
|
+
| `denyCurlWget` | `true` | 硬拦 curl / wget |
|
|
99
|
+
| `bashNudgeMinCommandBytes` | `4096` | 超长命令的引导阈值(0 = 关闭) |
|
|
100
|
+
| `maxReadBytesBeforeAsk` | `51200` | 大文件整读的引导阈值(0 = 关闭) |
|
|
101
|
+
| `trustedReadBasenames` | `package.json` 等 | 不受整读门禁限制的文件名 |
|
|
102
|
+
| `compressNudgeTokens` | `120000` | 占用提示阈值(tokens) |
|
|
103
|
+
| `searchLimit` | `5` | `ctk_search` 每次查询的片段数上限 |
|
|
104
|
+
| `snippetChars` | `900` | 每个片段的字符上限 |
|
|
105
|
+
| `fetchMaxBytes` | `2000000` | 单页抓取上限 |
|
|
106
|
+
| `storageDir` | 空 | 知识库根目录,空 = `<DSH_HOME>/context-toolkit` |
|
|
107
|
+
| `tools.*` | 见下 | 逐个工具的开关 |
|
|
108
|
+
|
|
109
|
+
工具开关:`tools.execute`、`tools.executeFile`、`tools.batchExecute`、`tools.index`、`tools.search`、`tools.fetchAndIndex`、`tools.purge`、`tools.stats`、`tools.contextReport`、`tools.doctor`,默认全部开启。
|
|
110
|
+
|
|
111
|
+
## 校验
|
|
112
|
+
|
|
113
|
+
离线套件不依赖 DSH 运行时:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
node smoke.mjs # 52 项:契约、运行时探测、三工具、门禁、知识库、观测
|
|
117
|
+
node test-client.mjs # 27 项:浏览器半体契约与双语字典一致性
|
|
118
|
+
node test-package.mjs # 18 项:发布清单与零依赖约束
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## 已知边界
|
|
122
|
+
|
|
123
|
+
- **出站抓取受本机网络影响**:若本机把域名解析到回环地址,`ctk_fetch_and_index` 会失败并明确说明原因(而不是只报 "fetch failed");此时改用 `ctk_index` 归档本地材料。
|
|
124
|
+
- **`ctk_execute` 的超时是硬杀**:超时会终止子进程并把 `timedOut` 标出来。
|
|
125
|
+
- **单次捕获上限 256 KB**,超出部分只计数不保留;回给对话的文本另有 4000 字符硬帽,因此 1 MB 的单行输出也不会整段进入上下文。
|
|
126
|
+
- **知识库按路径去重**:同一路径再次归档是替换,不是追加。
|
|
127
|
+
|
|
128
|
+
## 许可
|
|
129
|
+
|
|
130
|
+
MIT
|
package/client.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
// dsh-context-toolkit — browser half: the settings page and the occupancy panel.
|
|
2
|
+
//
|
|
3
|
+
// The harness module loader expects exactly one __ModuleLoader__.load() with an id
|
|
4
|
+
// and a factory. The server half renders nothing; every field staged here must
|
|
5
|
+
// exist in the plugin's Config, and the panel reads the plugin's own HTTP route so
|
|
6
|
+
// it keeps working while the host has not served the settings namespace.
|
|
7
|
+
window.__ModuleLoader__.load({
|
|
8
|
+
id: "dsh-context-toolkit",
|
|
9
|
+
factory: (require) => {
|
|
10
|
+
var module = { exports: {} };
|
|
11
|
+
var exports = module.exports;
|
|
12
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
13
|
+
let React = require("react");
|
|
14
|
+
let jsxRuntime = require("react/jsx-runtime");
|
|
15
|
+
let P = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
16
|
+
let jsx = jsxRuntime.jsx;
|
|
17
|
+
let jsxs = jsxRuntime.jsxs;
|
|
18
|
+
|
|
19
|
+
//#region locales
|
|
20
|
+
const zh = {"title":"上下文工具箱","description":"在沙箱里跑代码、把大块材料归档成可检索的本地知识库、并挡住会灌满对话的抓取与整读。附一张实时上下文占用面板。","hint":"下列开关即时生效,无需重启:门禁在每次工具调用前读取。","routing":"路由门禁","routingHint":"总开关。开启后,每次工具调用执行前都会经过门禁检查。","denyCurlWget":"硬拦 curl / wget","denyCurlWgetHint":"这两个工具的设计就是把整页内容塞进对话;拦掉,改用 ctk_fetch_and_index。","bashNudgeMinCommandBytes":"长命令引导阈值(字节)","bashNudgeMinCommandBytesHint":"命令超过这个长度时提示改用 ctk_batch_execute。填 0 表示不提示。","maxReadBytesBeforeAsk":"大文件整读引导阈值(字节)","maxReadBytesBeforeAskHint":"整读超过这个大小的文件时提示改用 ctk_execute_file。填 0 表示不提示。","trustedReadBasenames":"不受整读门禁限制的文件名","trustedReadBasenamesHint":"逗号分隔;这些文件即使很大也允许整读。","compressNudgeTokens":"占用提示阈值(tokens)","compressNudgeTokensHint":"表面占用达到此值时,占用报告给出压缩建议。填 0 表示不提示。","searchLimit":"检索返回条数上限","searchLimitHint":"ctk_search 每次查询返回的片段数上限。","snippetChars":"片段最大字符数","snippetCharsHint":"ctk_search 每个片段返回的字符上限,超出部分省略。","fetchMaxBytes":"单页抓取上限(字节)","fetchMaxBytesHint":"ctk_fetch_and_index 抓取单页的最大字节数,超出部分丢弃。","storageDir":"知识库根目录","storageDirHint":"留空 = <DSH_HOME>/context-toolkit。","toolsTitle":"工具开关","toolsHint":"逐个启用或停用 ctk_* 工具。","overridden":"已覆盖","reset":"恢复默认","readOnly":"本部署的设置为只读。","unavailable":"该插件当前未加载,暂时无法配置。","save":"保存","saving":"保存中…","saveFailed":"本部署没有接受这些值,已保留供你修改。","invalidNumber":"请填数字;留空表示使用默认值。","note":"门禁开关即时生效;工具开关改动后建议重开页面确认。","ctxTitle":"上下文情况","ctxLoading":"正在读取…","ctxReload":"刷新","ctxFailed":"读取失败:","ctxNoSession":"本会话暂无可测量的投影。","ctxStale":"以下为最近一次实测快照({time}):网页请求没有会话边界,实时值请在对话中调用 ctk_context_report。","ctxTotal":"总压力 {n} tokens","ctxSurface":"表面占用 {n} tokens","ctxBudget":"提示阈值 {n} tokens","ctxOver":"已超过提示阈值,建议把大块输出改走 ctk_execute / ctk_index。","ctxUnder":"低于提示阈值,暂不需要干预。","ctxSystem":"系统提示","ctxTools":"工具 schema","ctxMessages":"会话消息","ctxEstimated":"(估算)","ctxNodes":"占用大头(前 {n} 项)","ctxLedger":"本插件节约台账","ctxLedgerLine":"门禁已拦截 {n} 次,约 {bytes} 字节未进入上下文;沙箱保留 {sandbox} 字节;已注册 {tools} 个 ctk_* 工具。","ctxKnowledge":"知识库:{docs} 文档 / {chunks} 片段({bytes} 字节)","ctxStorage":"知识库位置:{path}","tools.execute":"ctk_execute(在沙箱里跑代码,只把结论带回来)","tools.executeHint":"用于「只想要结论、不想让原始输出占满上下文」的分析型任务。","tools.executeFile":"ctk_execute_file(对某个文件跑代码,只回结论)","tools.executeFileHint":"适合读大文件后做统计或抽取,文件原文留在沙箱里。","tools.batchExecute":"ctk_batch_execute(一次跑多条命令,逐条状态)","tools.batchExecuteHint":"多条命令一次调用,只有结论回来。","tools.index":"ctk_index(把大块材料存进本地知识库)","tools.indexHint":"把长文档、日志、规范归档,之后用 ctk_search 按需取用。","tools.search":"ctk_search(按需检索知识库,取片段而非整份)","tools.searchHint":"知识库里已有的内容优先用它检索,不要整份读进来。","tools.fetchAndIndex":"ctk_fetch_and_index(抓网页并直接并入索引)","tools.fetchAndIndexHint":"抓整页网页又不让它淹没对话时用;curl/wget 会被门禁拦掉。","tools.purge":"ctk_purge(删除已索引内容,不可恢复)","tools.purgeHint":"清理知识库用;会永久删除,调用必须带 confirm:true。","tools.stats":"ctk_stats(节约台账与知识库用量)","tools.statsHint":"想知道这个插件省了多少、归档了多少时看它。","tools.doctor":"ctk_doctor(自检:运行时、临时目录、知识库往返)","tools.doctorHint":"出现异常(子进程跑不起来、索引失败)时先跑它定位。","tools.contextReport":"ctk_context_report(会话上下文占用报告)","tools.contextReportHint":"让模型可以主动查询本会话的上下文占用与构成。","restart":"需重启"};
|
|
21
|
+
const en = {"title":"Context Toolkit","description":"Run code in a sandbox, file large material into a local searchable knowledge base, and block the fetches and whole-file reads that flood a conversation. Ships a live context-occupancy panel.","hint":"These switches apply immediately, no restart: the gate is read before every tool call.","routing":"Routing gate","routingHint":"Master switch. When on, every tool call passes through the gate before running.","denyCurlWget":"Block curl / wget","denyCurlWgetHint":"Both exist to paste whole responses into the conversation; blocked here, use ctk_fetch_and_index instead.","bashNudgeMinCommandBytes":"Long-command nudge (bytes)","bashNudgeMinCommandBytesHint":"Commands longer than this get a nudge toward ctk_batch_execute. 0 disables it.","maxReadBytesBeforeAsk":"Whole-file read nudge (bytes)","maxReadBytesBeforeAskHint":"Reading a file larger than this gets a nudge toward ctk_execute_file. 0 disables it.","trustedReadBasenames":"Filenames exempt from the read nudge","trustedReadBasenamesHint":"Comma separated; these may be read whole however large they are.","compressNudgeTokens":"Occupancy warning (tokens)","compressNudgeTokensHint":"At this surface occupancy the report suggests compaction. 0 disables it.","searchLimit":"Search result limit","searchLimitHint":"How many excerpts ctk_search returns per query.","snippetChars":"Excerpt size (chars)","snippetCharsHint":"Character ceiling on each excerpt from ctk_search.","fetchMaxBytes":"Per-page fetch ceiling (bytes)","fetchMaxBytesHint":"ctk_fetch_and_index keeps at most this many bytes of each page.","storageDir":"Knowledge-base root","storageDirHint":"Empty = <DSH_HOME>/context-toolkit.","toolsTitle":"Tool switches","toolsHint":"Enable or disable individual ctk_* tools.","overridden":"overridden","reset":"reset","readOnly":"This deployment is read-only.","unavailable":"The plugin is not loaded right now; it cannot be configured.","save":"Save","saving":"Saving…","saveFailed":"This deployment did not accept those values; they are kept for you to edit.","invalidNumber":"Numbers only; blank means the default.","note":"Gate switches apply immediately; reopen the page to confirm tool-switch changes.","ctxTitle":"Context occupancy","ctxLoading":"Reading…","ctxReload":"Refresh","ctxFailed":"Read failed: ","ctxNoSession":"No measurable projection in this session yet.","ctxStale":"Snapshot from the last real measurement ({time}): a page request has no session boundary; ask ctk_context_report in the conversation for a live reading.","ctxTotal":"{n} tokens total","ctxSurface":"{n} tokens on the surface","ctxBudget":"warning at {n} tokens","ctxOver":"Above the warning threshold — route bulk output through ctk_execute / ctk_index.","ctxUnder":"Below the warning threshold; nothing to do.","ctxSystem":"System prompt","ctxTools":"Tool schemas","ctxMessages":"Conversation","ctxEstimated":" (estimated)","ctxNodes":"Biggest contributors (top {n})","ctxLedger":"This plugin's ledger","ctxLedgerLine":"The gate blocked {n} calls, about {bytes} bytes kept out of context; the sandbox kept out {sandbox} bytes; {tools} ctk_* tools registered.","ctxKnowledge":"Knowledge base: {docs} documents / {chunks} chunks ({bytes} bytes)","ctxStorage":"Knowledge base at {path}","tools.execute":"ctk_execute (run code in the sandbox, return only the answer)","tools.executeHint":"For analysis where you want the conclusion, not the raw output.","tools.executeFile":"ctk_execute_file (run code over a file, return only the answer)","tools.executeFileHint":"Read a big file, compute over it, keep its bytes in the sandbox.","tools.batchExecute":"ctk_batch_execute (many commands in one call, per-command status)","tools.batchExecuteHint":"One call for many commands; only conclusions come back.","tools.index":"ctk_index (file large material into the local knowledge base)","tools.indexHint":"Archive long docs, logs or specs, then pull sections with ctk_search.","tools.search":"ctk_search (retrieve excerpts on demand, not the whole thing)","tools.searchHint":"Prefer this for material already in the knowledge base.","tools.fetchAndIndex":"ctk_fetch_and_index (fetch a page straight into the index)","tools.fetchAndIndexHint":"When a whole page would flood the conversation; curl/wget are gated.","tools.purge":"ctk_purge (delete indexed content — permanent)","tools.purgeHint":"Clears the knowledge base; permanent, so the call must confirm:true.","tools.stats":"ctk_stats (this plugin's savings ledger and knowledge usage)","tools.statsHint":"How much this plugin saved, and how much it filed.","tools.doctor":"ctk_doctor (self-check: runtimes, scratch dir, knowledge round-trip)","tools.doctorHint":"Run it first when a child process or indexing misbehaves.","tools.contextReport":"ctk_context_report (session context occupancy report)","tools.contextReportHint":"Lets the model ask for this session context occupancy and composition.","restart":"needs a restart"};
|
|
22
|
+
//#endregion
|
|
23
|
+
|
|
24
|
+
/** Fill the {name} holes in a translated sentence. */
|
|
25
|
+
function fill(text, values) {
|
|
26
|
+
return String(text).replace(/\{(\w+)\}/g, (whole, name) => {
|
|
27
|
+
const v = values[name];
|
|
28
|
+
return v === undefined ? whole : String(v);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#region fields
|
|
33
|
+
const FIELDS = [{"field":"routing","kind":"bool"},{"field":"denyCurlWget","kind":"bool"},{"field":"bashNudgeMinCommandBytes","kind":"number"},{"field":"maxReadBytesBeforeAsk","kind":"number"},{"field":"compressNudgeTokens","kind":"number"},{"field":"searchLimit","kind":"number"},{"field":"snippetChars","kind":"number"},{"field":"fetchMaxBytes","kind":"number"},{"field":"trustedReadBasenames","kind":"text"},{"field":"storageDir","kind":"text"}];
|
|
34
|
+
const TOOL_FIELDS = ["execute","executeFile","batchExecute","index","search","fetchAndIndex","purge","stats","contextReport","doctor"];
|
|
35
|
+
const STATUS_PATH = "/_dsh/context-toolkit/status";
|
|
36
|
+
//#endregion
|
|
37
|
+
|
|
38
|
+
//#region styles
|
|
39
|
+
const rowStyle = { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px", padding: "10px 0" };
|
|
40
|
+
const labelStyle = { display: "flex", flexDirection: "column", gap: "2px" };
|
|
41
|
+
const nameStyle = { fontSize: "13px" };
|
|
42
|
+
const hintStyle = { fontSize: "12px", opacity: 0.7, maxWidth: "46em" };
|
|
43
|
+
const controlsStyle = { display: "flex", alignItems: "center", gap: "8px", flex: "0 0 auto" };
|
|
44
|
+
const resetStyle = { background: "transparent", border: "none", color: "inherit", opacity: 0.75, cursor: "pointer", fontSize: "12px", textDecoration: "underline", padding: 0 };
|
|
45
|
+
const sectionStyle = { marginTop: "18px", borderTop: "1px solid rgba(128,128,128,0.25)", paddingTop: "14px" };
|
|
46
|
+
const sectionTitleStyle = { fontSize: "13px", fontWeight: 600 };
|
|
47
|
+
const sectionHintStyle = { fontSize: "12px", opacity: 0.7, margin: "4px 0 8px", maxWidth: "46em" };
|
|
48
|
+
const panelStyle = { marginTop: "18px", border: "1px solid rgba(128,128,128,0.25)", borderRadius: "6px", padding: "12px 14px" };
|
|
49
|
+
const panelHeadStyle = { display: "flex", alignItems: "center", justifyContent: "space-between", gap: "10px", marginBottom: "6px" };
|
|
50
|
+
const statRowStyle = { display: "flex", alignItems: "center", gap: "10px", padding: "4px 0", fontSize: "12px" };
|
|
51
|
+
const statNameStyle = { flex: "0 0 9em" };
|
|
52
|
+
const barWrapStyle = { flex: "1 1 auto", height: "10px", background: "rgba(128,128,128,0.18)", borderRadius: "5px", overflow: "hidden", display: "flex" };
|
|
53
|
+
const statValueStyle = { flex: "0 0 8em", textAlign: "right", fontFamily: "monospace", fontSize: "11px", opacity: 0.85 };
|
|
54
|
+
const monoStyle = { fontFamily: "monospace", fontSize: "11px", opacity: 0.8 };
|
|
55
|
+
const smallButtonStyle = { background: "transparent", border: "1px solid rgba(128,128,128,0.4)", borderRadius: "4px", color: "inherit", cursor: "pointer", fontSize: "11px", padding: "2px 8px" };
|
|
56
|
+
const OVER_COLOR = "#bf8700";
|
|
57
|
+
const OK_COLOR = "#2ea043";
|
|
58
|
+
const BAR_COLOR = { system: "#8250df", tools: "#0969da", messages: "#2ea043" };
|
|
59
|
+
//#endregion
|
|
60
|
+
|
|
61
|
+
//#region BoolField
|
|
62
|
+
function BoolField(props) {
|
|
63
|
+
const t = props.t;
|
|
64
|
+
const fieldState = props.state[props.spec.field] === undefined ? {} : props.state[props.spec.field];
|
|
65
|
+
return jsxs("div", {
|
|
66
|
+
style: rowStyle,
|
|
67
|
+
children: [
|
|
68
|
+
jsxs("div", { key: "label", style: labelStyle, children: [
|
|
69
|
+
jsx("div", { key: "name", style: nameStyle, children: t(props.spec.field) }),
|
|
70
|
+
jsx("div", { key: "hint", style: hintStyle, children: t(props.spec.field + "Hint") })
|
|
71
|
+
]}),
|
|
72
|
+
jsxs("div", { key: "controls", style: controlsStyle, children: [
|
|
73
|
+
fieldState.overridden === true
|
|
74
|
+
? jsx("button", { key: "reset", type: "button", style: resetStyle, disabled: !props.state.writable, onClick: () => props.onReset(), children: t("reset") })
|
|
75
|
+
: null,
|
|
76
|
+
jsx(P.Switch, {
|
|
77
|
+
key: "switch",
|
|
78
|
+
checked: fieldState.text === "true",
|
|
79
|
+
disabled: !props.state.writable,
|
|
80
|
+
label: t(props.spec.field),
|
|
81
|
+
onChange: (next) => props.onEdit(next ? "true" : "false")
|
|
82
|
+
})
|
|
83
|
+
]})
|
|
84
|
+
]
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
|
|
89
|
+
//#region panel
|
|
90
|
+
/**
|
|
91
|
+
* A live read of this session's context occupancy.
|
|
92
|
+
*
|
|
93
|
+
* It reloads on mount and on demand — a settings page has no business polling
|
|
94
|
+
* forever. When the route can only offer a snapshot it says so, with the time,
|
|
95
|
+
* so an old number is never mistaken for a live one.
|
|
96
|
+
*/
|
|
97
|
+
function ContextPanel(props) {
|
|
98
|
+
const t = props.t;
|
|
99
|
+
const [state, setState] = React.useState({ status: "loading", data: null, error: "" });
|
|
100
|
+
const load = React.useCallback(() => {
|
|
101
|
+
setState({ status: "loading", data: null, error: "" });
|
|
102
|
+
Promise.resolve()
|
|
103
|
+
.then(() => fetch(STATUS_PATH, { headers: { accept: "application/json" } }))
|
|
104
|
+
.then((res) => (res.ok ? res.json() : Promise.reject(new Error("HTTP " + res.status))))
|
|
105
|
+
.then((data) => setState({ status: "ready", data: data, error: "" }))
|
|
106
|
+
.catch((error) => setState({ status: "failed", data: null, error: String((error && error.message) || error) }));
|
|
107
|
+
}, []);
|
|
108
|
+
React.useEffect(() => { load(); }, [load]);
|
|
109
|
+
|
|
110
|
+
const children = [];
|
|
111
|
+
children.push(jsxs("div", { key: "head", style: panelHeadStyle, children: [
|
|
112
|
+
jsx("div", { key: "title", style: sectionTitleStyle, children: t("ctxTitle") }),
|
|
113
|
+
jsx("button", { key: "reload", type: "button", style: smallButtonStyle, onClick: load, children: t("ctxReload") })
|
|
114
|
+
]}));
|
|
115
|
+
|
|
116
|
+
if (state.status === "loading") {
|
|
117
|
+
children.push(jsx("div", { key: "loading", style: hintStyle, children: t("ctxLoading") }));
|
|
118
|
+
return jsx("div", { "data-context-panel": true, style: panelStyle, children: children });
|
|
119
|
+
}
|
|
120
|
+
if (state.status === "failed") {
|
|
121
|
+
children.push(jsx("div", { key: "failed", style: hintStyle, children: t("ctxFailed") + state.error }));
|
|
122
|
+
return jsx("div", { "data-context-panel": true, style: panelStyle, children: children });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const data = state.data || {};
|
|
126
|
+
const context = data.context || {};
|
|
127
|
+
const ledger = data.ledger || {};
|
|
128
|
+
|
|
129
|
+
if (context.ok !== true) {
|
|
130
|
+
children.push(jsx("div", { key: "none", style: hintStyle, children: context.why ? t("ctxFailed") + context.why : t("ctxNoSession") }));
|
|
131
|
+
} else {
|
|
132
|
+
if (context.stale === true) {
|
|
133
|
+
children.push(jsx("div", { key: "stale", style: Object.assign({}, hintStyle, { color: OVER_COLOR }), children: fill(t("ctxStale"), { time: new Date(Number(context.at) || Date.now()).toLocaleTimeString() }) }));
|
|
134
|
+
}
|
|
135
|
+
const total = Number(context.totalTokens) || 0;
|
|
136
|
+
const surface = Number(context.surfaceTokens) || 0;
|
|
137
|
+
const budget = Number(context.budget) || 0;
|
|
138
|
+
children.push(jsx("div", { key: "totals", style: monoStyle, children: fill(t("ctxTotal"), { n: total }) + " · " + fill(t("ctxSurface"), { n: surface }) + " · " + fill(t("ctxBudget"), { n: budget }) }));
|
|
139
|
+
const over = context.overBudget === true;
|
|
140
|
+
children.push(jsx("div", { key: "verdict", style: Object.assign({}, hintStyle, { color: over ? OVER_COLOR : OK_COLOR }), children: over ? t("ctxOver") : t("ctxUnder") }));
|
|
141
|
+
const bd = context.breakdown;
|
|
142
|
+
if (bd && bd.total > 0) {
|
|
143
|
+
const rows = [
|
|
144
|
+
{ key: "system", tokens: bd.system, label: t("ctxSystem") },
|
|
145
|
+
{ key: "tools", tokens: bd.tools, label: t("ctxTools") },
|
|
146
|
+
{ key: "messages", tokens: bd.messages, label: t("ctxMessages") }
|
|
147
|
+
];
|
|
148
|
+
for (const row of rows) {
|
|
149
|
+
const pct = Math.round((row.tokens / bd.total) * 100);
|
|
150
|
+
children.push(jsxs("div", { key: "bd-" + row.key, style: statRowStyle, children: [
|
|
151
|
+
jsx("span", { key: "n", style: statNameStyle, children: row.label + (bd.estimated ? t("ctxEstimated") : "") }),
|
|
152
|
+
jsx("span", { key: "bar", style: barWrapStyle, children: jsx("span", { style: { width: pct + "%", background: BAR_COLOR[row.key] || "#8b949e", display: "block" } }) }),
|
|
153
|
+
jsx("span", { key: "v", style: statValueStyle, children: row.tokens + " (" + pct + "%)" })
|
|
154
|
+
]}));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (Array.isArray(context.nodes) && context.nodes.length > 0) {
|
|
158
|
+
const shown = context.nodes.slice(0, 10);
|
|
159
|
+
children.push(jsx("div", { key: "nodes-title", style: Object.assign({}, hintStyle, { marginTop: "10px" }), children: fill(t("ctxNodes"), { n: shown.length }) }));
|
|
160
|
+
children.push(jsx("div", { key: "nodes", children: shown.map((node, index) => jsxs("div", { key: "node-" + index, style: statRowStyle, children: [
|
|
161
|
+
jsx("span", { key: "n", style: statNameStyle, children: "seq " + node.seq + (node.system ? " [system]" : "") }),
|
|
162
|
+
jsx("span", { key: "v", style: statValueStyle, children: node.tokens + " tokens" })
|
|
163
|
+
]})) }));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
children.push(jsx("div", { key: "ledger-title", style: Object.assign({}, hintStyle, { marginTop: "12px" }), children: t("ctxLedger") }));
|
|
168
|
+
children.push(jsx("div", { key: "ledger", style: hintStyle, children: fill(t("ctxLedgerLine"), { n: Number(ledger.denied) || 0, bytes: Number(ledger.deniedBytes) || 0, sandbox: Number(ledger.bytesSandboxed) || 0, tools: Number(ledger.registered) || 0 }) }));
|
|
169
|
+
if (data.knowledge && typeof data.knowledge.documents === "number") {
|
|
170
|
+
children.push(jsx("div", { key: "kb", style: monoStyle, children: fill(t("ctxKnowledge"), { docs: data.knowledge.documents, chunks: data.knowledge.chunks, bytes: data.knowledge.bytes }) }));
|
|
171
|
+
}
|
|
172
|
+
if (typeof data.storage === "string" && data.storage !== "") {
|
|
173
|
+
children.push(jsx("div", { key: "storage", style: monoStyle, children: fill(t("ctxStorage"), { path: data.storage }) }));
|
|
174
|
+
}
|
|
175
|
+
return jsx("div", { "data-context-panel": true, style: panelStyle, children: children });
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
|
|
179
|
+
//#region card
|
|
180
|
+
function ToolkitCard(props) {
|
|
181
|
+
const t = props.t;
|
|
182
|
+
if (props.view === "summary") return t("description");
|
|
183
|
+
const state = props.useToolkitCard((snapshot) => snapshot);
|
|
184
|
+
const children = [];
|
|
185
|
+
children.push(jsx("p", { key: "hint", style: Object.assign({}, hintStyle, { margin: "0 0 6px" }), children: t("hint") }));
|
|
186
|
+
|
|
187
|
+
for (const spec of FIELDS) {
|
|
188
|
+
const fieldState = state[spec.field] === undefined ? {} : state[spec.field];
|
|
189
|
+
if (spec.kind === "bool") {
|
|
190
|
+
children.push(jsx(BoolField, { key: spec.field, t: t, state: state, spec: spec,
|
|
191
|
+
onEdit: (text) => props.edit(spec.field, text), onReset: () => props.resetField(spec.field) }));
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
children.push(jsx(P.SettingsValueField, {
|
|
195
|
+
key: spec.field,
|
|
196
|
+
id: "plugin-config-context-toolkit-" + spec.field,
|
|
197
|
+
label: t(spec.field),
|
|
198
|
+
hint: t(spec.field + "Hint"),
|
|
199
|
+
overriddenLabel: t("overridden"),
|
|
200
|
+
resetLabel: t("reset"),
|
|
201
|
+
invalidLabel: t("invalidNumber"),
|
|
202
|
+
numeric: spec.kind === "number",
|
|
203
|
+
disabled: !state.writable,
|
|
204
|
+
text: fieldState.text,
|
|
205
|
+
overridden: fieldState.overridden,
|
|
206
|
+
invalid: fieldState.invalid,
|
|
207
|
+
onEdit: (text) => props.edit(spec.field, text),
|
|
208
|
+
onReset: () => props.resetField(spec.field)
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
children.push(jsxs("div", { key: "tools-head", style: sectionStyle, children: [
|
|
213
|
+
jsx("div", { key: "t", style: sectionTitleStyle, children: t("toolsTitle") }),
|
|
214
|
+
jsx("div", { key: "h", style: sectionHintStyle, children: t("toolsHint") })
|
|
215
|
+
]}));
|
|
216
|
+
for (const name of TOOL_FIELDS) {
|
|
217
|
+
const field = "tools." + name;
|
|
218
|
+
children.push(jsx(BoolField, { key: field, t: t, state: state, spec: { field: field },
|
|
219
|
+
onEdit: (text) => props.edit(field, text), onReset: () => props.resetField(field) }));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Rendered BESIDE the form, not inside it: the host's SettingsForm returns
|
|
223
|
+
// early — dropping every child — while the namespace is unserved, and the
|
|
224
|
+
// panel must survive exactly that state to be worth having.
|
|
225
|
+
const panel = jsx(ContextPanel, { key: "panel", t: t });
|
|
226
|
+
const note = jsx("p", { key: "note", style: Object.assign({}, hintStyle, { marginTop: "12px" }), children: t("note") });
|
|
227
|
+
|
|
228
|
+
return jsxs("div", { key: "context-toolkit-page", children: [
|
|
229
|
+
jsx(P.SettingsForm, {
|
|
230
|
+
key: "form",
|
|
231
|
+
labels: { unavailable: t("unavailable"), readOnly: t("readOnly"), saveFailed: t("saveFailed"), save: t("save"), saving: t("saving") },
|
|
232
|
+
state: state,
|
|
233
|
+
onSave: props.save,
|
|
234
|
+
onDiscard: props.discard,
|
|
235
|
+
children: children
|
|
236
|
+
}),
|
|
237
|
+
panel,
|
|
238
|
+
note
|
|
239
|
+
]});
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
|
|
243
|
+
//#region controller
|
|
244
|
+
/** Dictionary namespace owned by this plugin. */
|
|
245
|
+
const NS = "settings.contextToolkit";
|
|
246
|
+
/** The plugin's own settings namespace (its bundle id). */
|
|
247
|
+
const CTX_MODE_NS = "dsh-context-toolkit";
|
|
248
|
+
const ToolkitCardController = class {
|
|
249
|
+
constructor(scope) {
|
|
250
|
+
const specs = [];
|
|
251
|
+
for (const spec of FIELDS) {
|
|
252
|
+
if (spec.kind === "number") specs.push(P.settingsNumberField(spec.field));
|
|
253
|
+
else if (spec.kind === "bool") specs.push({ field: spec.field, format: formatBoolean, parse: parseBoolean });
|
|
254
|
+
else specs.push({ field: spec.field, format: formatText, parse: parseText });
|
|
255
|
+
}
|
|
256
|
+
for (const name of TOOL_FIELDS) specs.push({ field: "tools." + name, format: formatBoolean, parse: parseBoolean });
|
|
257
|
+
this.form = new P.SettingsFormModel(scope, specs);
|
|
258
|
+
this.store = this.form.bind(() => this.projection());
|
|
259
|
+
}
|
|
260
|
+
projection() {
|
|
261
|
+
const snapshot = Object.assign({}, this.form.shell());
|
|
262
|
+
for (const spec of FIELDS) snapshot[spec.field] = this.form.field(spec.field);
|
|
263
|
+
for (const name of TOOL_FIELDS) snapshot["tools." + name] = this.form.field("tools." + name);
|
|
264
|
+
return snapshot;
|
|
265
|
+
}
|
|
266
|
+
inject() {
|
|
267
|
+
return Object.assign({ hooks: { toolkitCard: this.store } }, this.form.actions());
|
|
268
|
+
}
|
|
269
|
+
dispose() { this.form.dispose(); }
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
function formatBoolean(value) {
|
|
273
|
+
if (value === true) return "true";
|
|
274
|
+
if (value === false) return "false";
|
|
275
|
+
return "";
|
|
276
|
+
}
|
|
277
|
+
function parseBoolean(text) {
|
|
278
|
+
const trimmed = String(text).trim().toLowerCase();
|
|
279
|
+
if (trimmed === "") return { kind: "clear" };
|
|
280
|
+
if (trimmed === "true") return { kind: "set", value: true };
|
|
281
|
+
if (trimmed === "false") return { kind: "set", value: false };
|
|
282
|
+
return { kind: "invalid", message: "expected true or false" };
|
|
283
|
+
}
|
|
284
|
+
function formatText(value) {
|
|
285
|
+
if (typeof value === "string") return value;
|
|
286
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
287
|
+
return "";
|
|
288
|
+
}
|
|
289
|
+
function parseText(text) {
|
|
290
|
+
const trimmed = String(text).trim();
|
|
291
|
+
if (trimmed === "") return { kind: "clear" };
|
|
292
|
+
return { kind: "set", value: trimmed };
|
|
293
|
+
}
|
|
294
|
+
//#endregion
|
|
295
|
+
|
|
296
|
+
//#region mount
|
|
297
|
+
/**
|
|
298
|
+
* Mount the settings page.
|
|
299
|
+
*
|
|
300
|
+
* Two host containers publish a slot this form can occupy:
|
|
301
|
+
* - settings.section is the Settings shell navigation list;
|
|
302
|
+
* - plugins.item is the Plugins panel card list, which renders the same
|
|
303
|
+
* component as its card body.
|
|
304
|
+
*
|
|
305
|
+
* Registration is unconditional: gating it on the host having served our own
|
|
306
|
+
* namespace fails silently (no error, zero cells) and hides the entry, and the
|
|
307
|
+
* card already renders its own unavailable state when no form is served.
|
|
308
|
+
*/
|
|
309
|
+
function apply(ctx) {
|
|
310
|
+
const t = ctx.locale.bind(NS);
|
|
311
|
+
ctx.effect(() => ctx.locale.register(NS, { zh: zh, en: en }), "context-toolkit: dictionaries");
|
|
312
|
+
const card = new ToolkitCardController(ctx.configForms.get(CTX_MODE_NS));
|
|
313
|
+
ctx.effect(() => () => { card.dispose(); }, "context-toolkit: form subscription");
|
|
314
|
+
const definition = (name, order) => ({
|
|
315
|
+
name: name,
|
|
316
|
+
id: "context-toolkit",
|
|
317
|
+
order: order,
|
|
318
|
+
label: () => t("title"),
|
|
319
|
+
locale: NS,
|
|
320
|
+
inject: () => card.inject()
|
|
321
|
+
});
|
|
322
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register(definition("settings.section", 30), ToolkitCard));
|
|
323
|
+
ctx.slots.inject("plugins.item", () => ctx.slots.register(definition("plugins.item", 55), ToolkitCard));
|
|
324
|
+
}
|
|
325
|
+
/** Required services (cordis fiber inject). */
|
|
326
|
+
const inject = ["slots", "locale", "configForms"];
|
|
327
|
+
//#endregion
|
|
328
|
+
|
|
329
|
+
exports.NS = NS;
|
|
330
|
+
exports.apply = apply;
|
|
331
|
+
exports.inject = inject;
|
|
332
|
+
return module.exports;
|
|
333
|
+
}
|
|
334
|
+
});
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# dsh-context-toolkit bundle patch
|
|
2
|
+
#
|
|
3
|
+
# Registers ctk_* tools natively through the harness tools service.
|
|
4
|
+
# No child MCP process, no mcp__ prefix.
|
|
5
|
+
#
|
|
6
|
+
# Every value below is validated against the plugin's Config schema; the full
|
|
7
|
+
# set of controls and their meaning is documented in README.md.
|
|
8
|
+
- insert:
|
|
9
|
+
- id: dsh-context-toolkit
|
|
10
|
+
name: 'dsh-context-toolkit'
|
|
11
|
+
config:
|
|
12
|
+
routing: true
|
|
13
|
+
denyCurlWget: true
|
|
14
|
+
caPemPath: 'auto'
|