@michengai/dsh-btw 0.1.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/CHANGELOG.md +10 -0
- package/CHANGELOG.zh-CN.md +10 -0
- package/LICENSE +201 -0
- package/README.md +204 -0
- package/README.zh-CN.md +200 -0
- package/assets/screenshots/btw-bubbles.png +0 -0
- package/assets/screenshots/btw-command-menu.png +0 -0
- package/assets/screenshots/btw-conversation.png +0 -0
- package/cordis.patch.yml +3 -0
- package/docs/00-/344/272/244/346/216/245/345/205/245/345/217/243/00-/351/230/205/350/257/273/345/257/274/350/210/252.md +12 -0
- package/docs/00-/344/272/244/346/216/245/345/205/245/345/217/243/02-/345/275/223/345/211/215/347/212/266/346/200/201.md +16 -0
- package/docs/00-/344/272/244/346/216/245/345/205/245/345/217/243/03-/345/276/205/345/212/236/344/270/216/351/230/273/345/241/236.md +11 -0
- package/docs/01-/345/275/223/345/211/215/345/267/245/344/275/234/I005-/351/246/226/346/254/241/345/217/221/345/270/203/344/270/216/345/217/257/344/277/241/345/217/221/345/270/203/351/205/215/347/275/256/00-/350/277/255/344/273/243/346/200/273/350/247/210.md +27 -0
- package/docs/05-/345/267/245/347/250/213/344/272/244/344/273/230/01-/350/207/252/345/212/250/345/217/221/345/270/203.md +58 -0
- package/docs/07-/350/277/255/344/273/243/345/275/222/346/241/243/2026/I001-/344/270/200/346/254/241/346/200/247/346/227/201/351/227/256/346/260/224/346/263/241/344/270/216/350/276/223/345/205/245/345/216/206/345/217/262/00-/350/277/255/344/273/243/346/200/273/350/247/210.md +44 -0
- package/docs/07-/350/277/255/344/273/243/345/275/222/346/241/243/2026/I001-/344/270/200/346/254/241/346/200/247/346/227/201/351/227/256/346/260/224/346/263/241/344/270/216/350/276/223/345/205/245/345/216/206/345/217/262/01-/351/252/214/346/224/266/350/256/260/345/275/225.md +32 -0
- package/docs/07-/350/277/255/344/273/243/345/275/222/346/241/243/2026/I002-/345/256/236/351/231/205DSH/346/217/222/344/273/266/346/216/245/345/205/245/00-/350/277/255/344/273/243/346/200/273/350/247/210.md +45 -0
- package/docs/07-/350/277/255/344/273/243/345/275/222/346/241/243/2026/I002-/345/256/236/351/231/205DSH/346/217/222/344/273/266/346/216/245/345/205/245/01-/346/265/213/350/257/225/347/216/257/345/242/203/351/232/224/347/246/273.md +38 -0
- package/docs/07-/350/277/255/344/273/243/345/275/222/346/241/243/2026/I002-/345/256/236/351/231/205DSH/346/217/222/344/273/266/346/216/245/345/205/245/02-/345/256/242/346/210/267/347/253/257/346/263/250/345/205/245/344/270/216/346/260/224/346/263/241/345/256/275/345/272/246/344/277/256/345/244/215.md +28 -0
- package/docs/07-/350/277/255/344/273/243/345/275/222/346/241/243/2026/I003-/345/256/277/344/270/273/345/244/226/350/247/202/344/270/216/345/233/275/351/231/205/345/214/226/00-/350/277/255/344/273/243/346/200/273/350/247/210.md +47 -0
- package/docs/07-/350/277/255/344/273/243/345/275/222/346/241/243/2026/I003-/345/256/277/344/270/273/345/244/226/350/247/202/344/270/216/345/233/275/351/231/205/345/214/226/01-/350/247/206/350/247/211/351/252/214/346/224/266.md +35 -0
- package/docs/07-/350/277/255/344/273/243/345/275/222/346/241/243/2026/I004-/344/273/223/345/272/223/344/270/216/350/207/252/345/212/250/345/217/221/345/270/203/00-/350/277/255/344/273/243/346/200/273/350/247/210.md +38 -0
- package/lib/client.js +13473 -0
- package/lib/index.js +322 -0
- package/lib/types/client/BubbleView.d.ts +13 -0
- package/lib/types/client/bubbles.d.ts +32 -0
- package/lib/types/client/index.d.ts +3 -0
- package/lib/types/client/styles.d.ts +1 -0
- package/lib/types/index.d.ts +5 -0
- package/lib/types/locales.d.ts +49 -0
- package/lib/types/server/command-visibility.d.ts +3 -0
- package/lib/types/server/jobs.d.ts +43 -0
- package/lib/types/server/tool-guard.d.ts +13 -0
- package/lib/types/shared.d.ts +15 -0
- package/package.json +110 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// src/locales.ts
|
|
2
|
+
var zh = {
|
|
3
|
+
"locale.id": "zh",
|
|
4
|
+
"bubble.label": "\u65C1\u95EE\u56DE\u7B54",
|
|
5
|
+
"bubble.image": "\u56FE\u7247",
|
|
6
|
+
"action.copy": "\u590D\u5236\u56DE\u7B54",
|
|
7
|
+
"action.copied": "\u5DF2\u590D\u5236",
|
|
8
|
+
"action.expand": "\u5C55\u5F00\u56DE\u7B54",
|
|
9
|
+
"action.collapse": "\u6298\u53E0\u56DE\u7B54",
|
|
10
|
+
"action.close": "\u5173\u95ED\u65C1\u95EE",
|
|
11
|
+
"action.retryClose": "\u91CD\u8BD5\u5173\u95ED",
|
|
12
|
+
"status.answering": "\u6B63\u5728\u56DE\u7B54\u2026",
|
|
13
|
+
"status.closing": "\u6B63\u5728\u5173\u95ED\u2026",
|
|
14
|
+
"command.hint": "\u65C1\u95EE\u5185\u5BB9",
|
|
15
|
+
"menu.group": "\u65C1\u95EE",
|
|
16
|
+
"command.description": "\u6839\u636E\u5F53\u524D\u4E0A\u4E0B\u6587\u56DE\u7B54\u65C1\u95EE",
|
|
17
|
+
"error.copy": "\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u9009\u62E9\u56DE\u7B54\u6587\u5B57\u590D\u5236\u3002",
|
|
18
|
+
"error.images": "\u65C1\u95EE\u53EA\u63A5\u53D7\u6587\u5B57\uFF0C\u56FE\u7247\u4ECD\u4FDD\u7559\u5728\u8F93\u5165\u6846\u4E2D\u3002",
|
|
19
|
+
"error.removeImages": "\u65C1\u95EE\u53EA\u63A5\u53D7\u6587\u5B57\uFF0C\u8BF7\u5148\u79FB\u9664\u56FE\u7247\u3002",
|
|
20
|
+
"error.backend": "BTW \u540E\u7AEF\u5C1A\u672A\u542F\u7528\uFF0C\u8BF7\u68C0\u67E5\u63D2\u4EF6\u914D\u7F6E\u3002",
|
|
21
|
+
"error.empty": "\u8BF7\u8F93\u5165\u65C1\u95EE\u5185\u5BB9\u3002",
|
|
22
|
+
"error.length": "\u65C1\u95EE\u6700\u591A\u652F\u6301 8000 \u4E2A\u5B57\u7B26\u3002",
|
|
23
|
+
"error.capacity": "\u6B63\u5728\u5904\u7406\u7684\u65C1\u95EE\u8F83\u591A\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
24
|
+
"error.close": "\u5173\u95ED\u5931\u8D25\uFF0C\u53EF\u91CD\u8BD5\u3002{detail}",
|
|
25
|
+
"error.requestLength": "\u65C1\u95EE\u5185\u5BB9\u8FC7\u957F\u3002",
|
|
26
|
+
"error.request": "\u65C1\u95EE\u8BF7\u6C42\u65E0\u6548\u3002",
|
|
27
|
+
"error.id": "\u65C1\u95EE\u6807\u8BC6\u65E0\u6548\u3002",
|
|
28
|
+
"error.provider": "\u5F53\u524D fork provider \u4E0D\u652F\u6301\u7EE7\u627F\u4E0A\u4E0B\u6587\u5E76\u7981\u7528\u5DE5\u5177\uFF0C\u65C1\u95EE\u672A\u542F\u52A8\u3002",
|
|
29
|
+
"error.stopped": "\u65C1\u95EE\u63D2\u4EF6\u5DF2\u505C\u6B62\u3002",
|
|
30
|
+
"error.duplicate": "\u8BE5\u65C1\u95EE\u5DF2\u63D0\u4EA4\u6216\u5DF2\u5173\u95ED\u3002",
|
|
31
|
+
"error.cancelled": "\u65C1\u95EE\u5DF2\u53D6\u6D88\u3002",
|
|
32
|
+
"error.timeout": "\u65C1\u95EE\u8D85\u65F6\uFF0C\u5DF2\u53D6\u6D88\u3002",
|
|
33
|
+
"error.incomplete": "\u65C1\u95EE\u672A\u5B8C\u6210\uFF08{reason}\uFF09\u3002{detail}",
|
|
34
|
+
"error.noText": "\u6A21\u578B\u672A\u8FD4\u56DE\u6587\u5B57\u56DE\u7B54\u3002",
|
|
35
|
+
"error.cleanup": "\u8D44\u6E90\u6E05\u7406\u5931\u8D25\uFF0C\u8BF7\u518D\u6B21\u5173\u95ED\u91CD\u8BD5\u3002{detail}"
|
|
36
|
+
};
|
|
37
|
+
var en = {
|
|
38
|
+
"locale.id": "en",
|
|
39
|
+
"bubble.label": "Side question answer",
|
|
40
|
+
"bubble.image": "Image",
|
|
41
|
+
"action.copy": "Copy answer",
|
|
42
|
+
"action.copied": "Copied",
|
|
43
|
+
"action.expand": "Expand answer",
|
|
44
|
+
"action.collapse": "Collapse answer",
|
|
45
|
+
"action.close": "Close side question",
|
|
46
|
+
"action.retryClose": "Retry closing",
|
|
47
|
+
"status.answering": "Answering\u2026",
|
|
48
|
+
"status.closing": "Closing\u2026",
|
|
49
|
+
"command.hint": "Side question",
|
|
50
|
+
"menu.group": "Side Questions",
|
|
51
|
+
"command.description": "Ask a side question using the current context",
|
|
52
|
+
"error.copy": "Copy failed. Select the answer text to copy it.",
|
|
53
|
+
"error.images": "Side questions accept text only. Your images remain in the input.",
|
|
54
|
+
"error.removeImages": "Side questions accept text only. Remove the images first.",
|
|
55
|
+
"error.backend": "The BTW backend is unavailable. Check the plugin configuration.",
|
|
56
|
+
"error.empty": "Enter a side question.",
|
|
57
|
+
"error.length": "Side questions are limited to 8,000 characters.",
|
|
58
|
+
"error.capacity": "Too many side questions are running. Try again shortly.",
|
|
59
|
+
"error.close": "Could not close. Try again. {detail}",
|
|
60
|
+
"error.requestLength": "The side question request is too long.",
|
|
61
|
+
"error.request": "Invalid side question request.",
|
|
62
|
+
"error.id": "Invalid side question ID.",
|
|
63
|
+
"error.provider": "The fork provider cannot inherit context with tools disabled. The side question was not started.",
|
|
64
|
+
"error.stopped": "The side question plugin has stopped.",
|
|
65
|
+
"error.duplicate": "This side question was already submitted or closed.",
|
|
66
|
+
"error.cancelled": "Side question cancelled.",
|
|
67
|
+
"error.timeout": "The side question timed out and was cancelled.",
|
|
68
|
+
"error.incomplete": "The side question did not complete ({reason}).{detail}",
|
|
69
|
+
"error.noText": "The model returned no text.",
|
|
70
|
+
"error.cleanup": "Cleanup failed. Close again to retry. {detail}"
|
|
71
|
+
};
|
|
72
|
+
function translate(locale = "zh") {
|
|
73
|
+
const dictionary = locale === "zh" ? zh : en;
|
|
74
|
+
return (key, params) => dictionary[key].replace(/\{(\w+)\}/g, (match, name2) => String(params?.[name2] ?? match));
|
|
75
|
+
}
|
|
76
|
+
function normalizeLocale(value) {
|
|
77
|
+
return value === void 0 || typeof value === "string" && /^zh(?:-|$)/i.test(value) ? "zh" : "en";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/shared.ts
|
|
81
|
+
var RUN_COMMAND = "btw-run";
|
|
82
|
+
var CLOSE_COMMAND = "btw-close";
|
|
83
|
+
var MAX_QUESTION_LENGTH = 8e3;
|
|
84
|
+
function parseRequest(raw) {
|
|
85
|
+
if (raw.length > 6e4) throw new Error(translate()("error.requestLength"));
|
|
86
|
+
let value;
|
|
87
|
+
try {
|
|
88
|
+
value = JSON.parse(raw);
|
|
89
|
+
} catch {
|
|
90
|
+
throw new Error(translate()("error.request"));
|
|
91
|
+
}
|
|
92
|
+
if (typeof value !== "object" || value === null) throw new Error(translate()("error.request"));
|
|
93
|
+
const request = value;
|
|
94
|
+
const locale = normalizeLocale(request.locale);
|
|
95
|
+
const t = translate(locale);
|
|
96
|
+
if (typeof request.id !== "string" || !/^[a-zA-Z0-9-]{8,80}$/.test(request.id)) throw new Error(t("error.id"));
|
|
97
|
+
if (typeof request.question !== "string" || !request.question.trim()) throw new Error(t("error.empty"));
|
|
98
|
+
if (request.question.length > MAX_QUESTION_LENGTH) throw new Error(t("error.length"));
|
|
99
|
+
return { id: request.id, question: request.question.trim(), locale };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/server/jobs.ts
|
|
103
|
+
var failure = (error) => ({ kind: "error", text: error instanceof Error ? error.message : String(error) });
|
|
104
|
+
var SideJobs = class {
|
|
105
|
+
constructor(start, timeoutMs = 9e4) {
|
|
106
|
+
this.start = start;
|
|
107
|
+
this.timeoutMs = timeoutMs;
|
|
108
|
+
}
|
|
109
|
+
jobs = /* @__PURE__ */ new Map();
|
|
110
|
+
retired = /* @__PURE__ */ new Map();
|
|
111
|
+
disposed = false;
|
|
112
|
+
get size() {
|
|
113
|
+
return this.jobs.size;
|
|
114
|
+
}
|
|
115
|
+
ask(scope, id, question, signal, context, locale = "zh") {
|
|
116
|
+
const key = JSON.stringify([scope, id]);
|
|
117
|
+
const t = translate(locale);
|
|
118
|
+
this.prune();
|
|
119
|
+
if (this.disposed) return Promise.resolve(failure(t("error.stopped")));
|
|
120
|
+
if (this.jobs.has(key) || this.retired.has(key)) return Promise.resolve(failure(t("error.duplicate")));
|
|
121
|
+
if (this.jobs.size >= 8) return Promise.resolve(failure(t("error.capacity")));
|
|
122
|
+
const job = { controller: new AbortController(), finished: Promise.resolve({ kind: "success", text: "" }), t };
|
|
123
|
+
this.jobs.set(key, job);
|
|
124
|
+
const cancel = () => job.controller.abort(new Error(t("error.cancelled")));
|
|
125
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
126
|
+
if (signal?.aborted) cancel();
|
|
127
|
+
const timer = setTimeout(() => job.controller.abort(new Error(t("error.timeout"))), this.timeoutMs);
|
|
128
|
+
timer.unref?.();
|
|
129
|
+
job.finished = this.execute(job, { scope, id, question, signal: job.controller.signal, toolFilter: { allow: [] }, context, locale }).finally(() => {
|
|
130
|
+
clearTimeout(timer);
|
|
131
|
+
signal?.removeEventListener("abort", cancel);
|
|
132
|
+
if (!job.run) {
|
|
133
|
+
this.jobs.delete(key);
|
|
134
|
+
this.retire(key);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return job.finished;
|
|
138
|
+
}
|
|
139
|
+
/** 关闭可先于启动到达;短期墓碑阻止网络乱序重新启动请求。 */
|
|
140
|
+
async close(scope, id) {
|
|
141
|
+
const key = JSON.stringify([scope, id]);
|
|
142
|
+
this.retire(key);
|
|
143
|
+
const job = this.jobs.get(key);
|
|
144
|
+
if (!job) return { kind: "success", text: "" };
|
|
145
|
+
job.controller.abort(new Error(job.t("error.cancelled")));
|
|
146
|
+
await job.finished;
|
|
147
|
+
try {
|
|
148
|
+
await this.cleanup(job);
|
|
149
|
+
this.jobs.delete(key);
|
|
150
|
+
return { kind: "success", text: "" };
|
|
151
|
+
} catch (error) {
|
|
152
|
+
return failure(error);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async dispose() {
|
|
156
|
+
this.disposed = true;
|
|
157
|
+
const outcomes = await Promise.all([...this.jobs.keys()].map((key) => {
|
|
158
|
+
const [scope, id] = JSON.parse(key);
|
|
159
|
+
return this.close(scope, id);
|
|
160
|
+
}));
|
|
161
|
+
const errors = outcomes.filter((outcome) => outcome.kind === "error");
|
|
162
|
+
if (errors.length) throw new Error(errors.map((error) => error.text).join("\n"));
|
|
163
|
+
}
|
|
164
|
+
async execute(job, request) {
|
|
165
|
+
let result;
|
|
166
|
+
let off = () => {
|
|
167
|
+
};
|
|
168
|
+
try {
|
|
169
|
+
request.signal.throwIfAborted();
|
|
170
|
+
job.run = await this.start(request);
|
|
171
|
+
const interrupted = new Promise((_, reject) => {
|
|
172
|
+
const abort = () => reject(request.signal.reason);
|
|
173
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
174
|
+
off = () => request.signal.removeEventListener("abort", abort);
|
|
175
|
+
if (request.signal.aborted) abort();
|
|
176
|
+
});
|
|
177
|
+
const response = await Promise.race([job.run.result, interrupted]);
|
|
178
|
+
request.signal.throwIfAborted();
|
|
179
|
+
const text = response.output.filter((block) => block.type === "text").map((block) => block.text ?? "").join("").trim();
|
|
180
|
+
if (response.stopReason !== "completed") throw new Error(job.t("error.incomplete", { reason: response.stopReason, detail: text ? `
|
|
181
|
+
|
|
182
|
+
${text}` : "" }));
|
|
183
|
+
if (!text) throw new Error(job.t("error.noText"));
|
|
184
|
+
result = { kind: "success", text };
|
|
185
|
+
} catch (error) {
|
|
186
|
+
result = failure(error);
|
|
187
|
+
} finally {
|
|
188
|
+
off();
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
await this.cleanup(job);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return failure(job.t("error.cleanup", { detail: error instanceof Error ? error.message : String(error) }));
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
async cleanup(job) {
|
|
198
|
+
if (!job.run) return;
|
|
199
|
+
if (!job.cleanup) {
|
|
200
|
+
job.cleanup = Promise.resolve().then(() => job.run?.dispose()).then(() => {
|
|
201
|
+
job.run = void 0;
|
|
202
|
+
}).catch((error) => {
|
|
203
|
+
job.cleanup = void 0;
|
|
204
|
+
throw error;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
await job.cleanup;
|
|
208
|
+
}
|
|
209
|
+
retire(key) {
|
|
210
|
+
this.retired.set(key, Date.now());
|
|
211
|
+
this.prune();
|
|
212
|
+
}
|
|
213
|
+
prune() {
|
|
214
|
+
for (const [key, time] of this.retired) if (Date.now() - time > 3e5 || this.retired.size > 1e3) this.retired.delete(key);
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// src/server/tool-guard.ts
|
|
219
|
+
function createAnswerOnlyGuard(labels) {
|
|
220
|
+
const owned = /* @__PURE__ */ new WeakSet();
|
|
221
|
+
return ({ agent }) => {
|
|
222
|
+
if (!agent) return void 0;
|
|
223
|
+
if (!owned.has(agent)) {
|
|
224
|
+
const event = agent.session.snapshotEvents().findLast((item) => item.type === "subagent/descriptor");
|
|
225
|
+
const data = event?.data;
|
|
226
|
+
if (typeof data !== "object" || data === null || !("label" in data) || typeof data.label !== "string" || !labels.has(data.label)) return void 0;
|
|
227
|
+
owned.add(agent);
|
|
228
|
+
}
|
|
229
|
+
return "BTW \u4EC5\u5141\u8BB8\u6587\u5B57\u56DE\u7B54\uFF0C\u7981\u6B62\u6267\u884C\u4EFB\u4F55\u5DE5\u5177\u3002";
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/server/command-visibility.ts
|
|
234
|
+
function hideInternalCommands(commands) {
|
|
235
|
+
const descriptor = Object.getOwnPropertyDescriptor(commands, "list");
|
|
236
|
+
const original = commands.list;
|
|
237
|
+
let active = true;
|
|
238
|
+
const filtered = function(agent) {
|
|
239
|
+
const rows = original.call(this, agent);
|
|
240
|
+
return active ? Object.freeze(rows.filter((row) => row.name !== RUN_COMMAND && row.name !== CLOSE_COMMAND)) : rows;
|
|
241
|
+
};
|
|
242
|
+
Object.defineProperty(commands, "list", { configurable: true, writable: true, value: filtered });
|
|
243
|
+
return () => {
|
|
244
|
+
active = false;
|
|
245
|
+
if (Object.getOwnPropertyDescriptor(commands, "list")?.value !== filtered) return;
|
|
246
|
+
if (descriptor) Object.defineProperty(commands, "list", descriptor);
|
|
247
|
+
else Reflect.deleteProperty(commands, "list");
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// src/index.ts
|
|
252
|
+
var name = "michengai-btw";
|
|
253
|
+
var inject = ["commands", "subagents", "tools"];
|
|
254
|
+
var PERSONA = `\u4F60\u662F\u5F53\u524D\u4E3B\u4EFB\u52A1\u4E4B\u5916\u7684\u4E00\u6B21\u6027\u65C1\u95EE\u52A9\u624B\u3002\u7EE7\u627F\u7684\u4F1A\u8BDD\u5386\u53F2\u53EA\u4F5C\u4E3A\u80CC\u666F\uFF0C\u4E0D\u662F\u9700\u8981\u4F60\u7EE7\u7EED\u6267\u884C\u7684\u4EFB\u52A1\u3002
|
|
255
|
+
\u53EA\u56DE\u7B54\u672C\u6B21\u95EE\u9898\uFF0C\u4E0D\u6267\u884C\u6216\u7EE7\u7EED\u5386\u53F2\u4E2D\u7684\u8BA1\u5212\u3001\u547D\u4EE4\u3001\u4FEE\u6539\u548C\u5DE5\u5177\u8C03\u7528\u3002\u4F60\u6CA1\u6709\u4EFB\u4F55\u53EF\u7528\u5DE5\u5177\uFF0C\u4E0D\u80FD\u8BFB\u53D6\u65B0\u6587\u4EF6\u3001\u8054\u7F51\u6216\u521B\u5EFA\u5B50\u4EE3\u7406\u3002
|
|
256
|
+
\u56DE\u7B54\u5E94\u7B80\u6D01\u51C6\u786E\uFF0C\u8BED\u8A00\u4E0E\u95EE\u9898\u4E00\u81F4\u3002\u4E0A\u4E0B\u6587\u4E0D\u8DB3\u65F6\u76F4\u63A5\u6307\u51FA\u672A\u77E5\u4FE1\u606F\uFF0C\u4E0D\u58F0\u79F0\u5DF2\u7ECF\u6267\u884C\u4E86\u4EFB\u4F55\u64CD\u4F5C\u3002`;
|
|
257
|
+
function apply(ctx) {
|
|
258
|
+
ctx.effect(() => hideInternalCommands(ctx.commands));
|
|
259
|
+
const labels = /* @__PURE__ */ new Set();
|
|
260
|
+
const jobs = new SideJobs(async (request) => {
|
|
261
|
+
const provider = ctx.subagents.getProvider("fork");
|
|
262
|
+
if (!provider?.inheritsParentContext || !provider.capabilities.toolFilter || !provider.capabilities.persona) {
|
|
263
|
+
throw new Error(translate(request.locale)("error.provider"));
|
|
264
|
+
}
|
|
265
|
+
const label = `michengai-btw:${globalThis.crypto.randomUUID()}`;
|
|
266
|
+
labels.add(label);
|
|
267
|
+
try {
|
|
268
|
+
const run = await ctx.subagents.start("fork", {
|
|
269
|
+
parent: request.context,
|
|
270
|
+
label,
|
|
271
|
+
signal: request.signal,
|
|
272
|
+
toolFilter: request.toolFilter,
|
|
273
|
+
persona: PERSONA,
|
|
274
|
+
prompt: [{ type: "text", text: `\u4EE5\u4E0B\u662F\u552F\u4E00\u9700\u8981\u56DE\u7B54\u7684\u65B0\u95EE\u9898\uFF1B\u5148\u524D\u5185\u5BB9\u4EC5\u4F9B\u53C2\u8003\u3002
|
|
275
|
+
|
|
276
|
+
${request.question}` }]
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
result: run.result,
|
|
280
|
+
dispose: async () => {
|
|
281
|
+
await run.dispose();
|
|
282
|
+
labels.delete(label);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
} catch (error) {
|
|
286
|
+
labels.delete(label);
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
ctx.effect(() => ctx.commands.register({
|
|
291
|
+
name: RUN_COMMAND,
|
|
292
|
+
description: "BTW \u6C14\u6CE1\u5185\u90E8\u8BF7\u6C42",
|
|
293
|
+
recordInput: false,
|
|
294
|
+
handler: (invocation) => {
|
|
295
|
+
try {
|
|
296
|
+
const request = parseRequest(invocation.rawInput);
|
|
297
|
+
return jobs.ask(invocation.agent.session.header.id, request.id, request.question, invocation.signal, invocation.agent, request.locale);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
return { kind: "error", text: error instanceof Error ? error.message : translate()("error.request") };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}));
|
|
303
|
+
ctx.effect(() => ctx.commands.register({
|
|
304
|
+
name: CLOSE_COMMAND,
|
|
305
|
+
description: "\u5173\u95ED\u5E76\u6E05\u7406\u6307\u5B9A BTW \u8BF7\u6C42",
|
|
306
|
+
recordInput: false,
|
|
307
|
+
handler: (invocation) => {
|
|
308
|
+
const id = invocation.rawInput.trim();
|
|
309
|
+
if (!/^[a-zA-Z0-9-]{8,80}$/.test(id)) return { kind: "error", text: translate()("error.id") };
|
|
310
|
+
return jobs.close(invocation.agent.session.header.id, id);
|
|
311
|
+
}
|
|
312
|
+
}));
|
|
313
|
+
ctx.effect(function* () {
|
|
314
|
+
yield ctx.tools.guard(createAnswerOnlyGuard(labels));
|
|
315
|
+
yield () => jobs.dispose();
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
export {
|
|
319
|
+
apply,
|
|
320
|
+
inject,
|
|
321
|
+
name
|
|
322
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Bubble, BubbleStore } from './bubbles';
|
|
3
|
+
import type { BtwTranslate } from '../locales';
|
|
4
|
+
export declare function BubbleView({ item, close, t }: {
|
|
5
|
+
item: Bubble;
|
|
6
|
+
close: () => void;
|
|
7
|
+
t: BtwTranslate;
|
|
8
|
+
}): React.JSX.Element;
|
|
9
|
+
export declare function BubbleDock({ store, sessionId, t }: {
|
|
10
|
+
store: BubbleStore;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
t: BtwTranslate;
|
|
13
|
+
}): React.JSX.Element | null;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type SideResult } from '../shared';
|
|
2
|
+
import { type BtwTranslate } from '../locales';
|
|
3
|
+
export interface Bubble {
|
|
4
|
+
id: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
question: string;
|
|
7
|
+
answer: string;
|
|
8
|
+
phase: 'answering' | 'done' | 'error' | 'closing';
|
|
9
|
+
error?: string;
|
|
10
|
+
closeFailed?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface SideTransport {
|
|
13
|
+
run(session: string, id: string, question: string, signal: AbortSignal): Promise<SideResult>;
|
|
14
|
+
close(session: string, id: string): Promise<SideResult>;
|
|
15
|
+
}
|
|
16
|
+
/** 仅内存 UI 状态;关闭请求与答案返回按请求身份分别收敛。 */
|
|
17
|
+
export declare class BubbleStore {
|
|
18
|
+
private readonly transport;
|
|
19
|
+
private readonly t;
|
|
20
|
+
private snapshot;
|
|
21
|
+
private readonly listeners;
|
|
22
|
+
private readonly active;
|
|
23
|
+
private readonly closing;
|
|
24
|
+
constructor(transport: SideTransport, t?: BtwTranslate);
|
|
25
|
+
getSnapshot: () => readonly Bubble[];
|
|
26
|
+
subscribe: (listener: () => void) => (() => void);
|
|
27
|
+
ask(sessionId: string, question: string): string;
|
|
28
|
+
close(id: string): Promise<void>;
|
|
29
|
+
dispose(): Promise<void>;
|
|
30
|
+
private update;
|
|
31
|
+
private publish;
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CSS = "\n.btw-dock{display:flex;flex:none;flex-direction:column;gap:8px;width:calc(100% - var(--dsh-composer-side-clearance,0px) - var(--dsh-composer-side-clearance,0px));max-width:var(--dsh-composer-card-max-width,100%);margin:0 auto;max-height:440px;overflow:auto;padding:4px 0 10px;box-sizing:border-box;letter-spacing:0}\n.btw-bubble{background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;min-width:0;flex-shrink:0;font-size:13px;line-height:1.6;font-family:var(--dsw-font-family,inherit);letter-spacing:0}\n.btw-header{display:flex;align-items:center;gap:8px;padding:8px 10px 8px 14px;min-width:0;min-height:44px}\n.btw-symbol{display:flex;align-items:center;color:var(--dsw-alias-label-secondary);height:28px;flex-shrink:0}\n.btw-question{flex:1;min-width:0;overflow-wrap:anywhere;font-size:13px;font-weight:500;line-height:22px;max-height:66px;overflow:auto}\n.btw-actions{display:flex;gap:4px;flex-shrink:0}\n.btw-actions button{display:grid;place-items:center;width:28px;height:28px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;flex:none;transition:background-color 120ms ease}\n.btw-actions button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}\n.btw-actions button:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,var(--dsw-alias-label-primary));outline-offset:-2px}\n.btw-actions button:disabled{cursor:wait;opacity:.5}\n.btw-status{display:flex;align-items:center;gap:8px;padding:0 14px 12px;color:var(--dsw-alias-label-secondary);font-size:13px}\n.btw-spinner{animation:btw-spin 1.2s linear infinite}@keyframes btw-spin{to{transform:rotate(360deg)}}\n.btw-answer{padding:0 14px 14px;max-height:300px;overflow:auto;overflow-wrap:anywhere;font-size:13px;line-height:1.7}\n.btw-answer>*:first-child{margin-top:0}.btw-answer>*:last-child{margin-bottom:0}\n.btw-answer p{margin:8px 0}.btw-answer h1,.btw-answer h2,.btw-answer h3{font-size:15px;line-height:1.5;margin:12px 0 6px;font-weight:600}\n.btw-answer pre{max-width:100%;overflow:auto;background:var(--dsw-alias-markdown-code-block);padding:10px;border-radius:8px;white-space:pre;font-size:12px}\n.btw-answer code{font-family:Consolas,monospace;font-size:12px}.btw-answer :not(pre)>code{background:var(--dsw-alias-markdown-code-block);border-radius:4px;padding:1px 3px}.btw-answer a{color:var(--dsw-alias-label-primary);text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}\n.btw-answer table{display:block;max-width:100%;overflow:auto;border-collapse:collapse}.btw-answer th,.btw-answer td{border:1px solid var(--dsw-alias-border-l2);padding:5px 9px}\n.btw-answer ul,.btw-answer ol{padding-left:22px}.btw-answer blockquote{margin:8px 0;padding-left:10px;border-left:2px solid var(--dsw-alias-border-l4);color:var(--dsw-alias-label-secondary)}\n.btw-error{margin:0;padding:0 14px 12px;font-size:13px;color:var(--dsw-alias-state-error-primary);overflow-wrap:anywhere}\n@media(max-width:480px){.btw-dock{max-height:330px}.btw-header{padding:6px 8px 6px 12px;gap:6px}.btw-answer{padding:0 12px 12px;max-height:230px}.btw-status,.btw-error{padding-left:12px}.btw-actions{gap:0}.btw-actions button{width:36px;height:36px}}\n@media(prefers-reduced-motion:reduce){.btw-spinner{animation:none}.btw-actions button{transition:none}}\n";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Translate } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
export declare const NS = "michengai.btw";
|
|
3
|
+
export declare const zh: {
|
|
4
|
+
'locale.id': string;
|
|
5
|
+
'bubble.label': string;
|
|
6
|
+
'bubble.image': string;
|
|
7
|
+
'action.copy': string;
|
|
8
|
+
'action.copied': string;
|
|
9
|
+
'action.expand': string;
|
|
10
|
+
'action.collapse': string;
|
|
11
|
+
'action.close': string;
|
|
12
|
+
'action.retryClose': string;
|
|
13
|
+
'status.answering': string;
|
|
14
|
+
'status.closing': string;
|
|
15
|
+
'command.hint': string;
|
|
16
|
+
'menu.group': string;
|
|
17
|
+
'command.description': string;
|
|
18
|
+
'error.copy': string;
|
|
19
|
+
'error.images': string;
|
|
20
|
+
'error.removeImages': string;
|
|
21
|
+
'error.backend': string;
|
|
22
|
+
'error.empty': string;
|
|
23
|
+
'error.length': string;
|
|
24
|
+
'error.capacity': string;
|
|
25
|
+
'error.close': string;
|
|
26
|
+
'error.requestLength': string;
|
|
27
|
+
'error.request': string;
|
|
28
|
+
'error.id': string;
|
|
29
|
+
'error.provider': string;
|
|
30
|
+
'error.stopped': string;
|
|
31
|
+
'error.duplicate': string;
|
|
32
|
+
'error.cancelled': string;
|
|
33
|
+
'error.timeout': string;
|
|
34
|
+
'error.incomplete': string;
|
|
35
|
+
'error.noText': string;
|
|
36
|
+
'error.cleanup': string;
|
|
37
|
+
};
|
|
38
|
+
export type MessageKey = keyof typeof zh;
|
|
39
|
+
export type BtwTranslate = Translate<MessageKey>;
|
|
40
|
+
export type BtwLocale = 'zh' | 'en';
|
|
41
|
+
export declare const en: Record<MessageKey, string>;
|
|
42
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
43
|
+
interface LocaleNamespaceMap {
|
|
44
|
+
'michengai.btw': MessageKey;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** 服务端按请求语言生成提示;客户端使用宿主 locale.bind 和 slot 的 t。 */
|
|
48
|
+
export declare function translate(locale?: BtwLocale): BtwTranslate;
|
|
49
|
+
export declare function normalizeLocale(value: unknown): BtwLocale;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SideResult } from '../shared';
|
|
2
|
+
import { type BtwLocale } from '../locales';
|
|
3
|
+
export interface ChildResult {
|
|
4
|
+
stopReason: string;
|
|
5
|
+
output: readonly {
|
|
6
|
+
type: string;
|
|
7
|
+
text?: string;
|
|
8
|
+
}[];
|
|
9
|
+
}
|
|
10
|
+
export interface ChildRun {
|
|
11
|
+
result: Promise<ChildResult>;
|
|
12
|
+
dispose(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface ChildRequest {
|
|
15
|
+
scope: string;
|
|
16
|
+
id: string;
|
|
17
|
+
question: string;
|
|
18
|
+
signal: AbortSignal;
|
|
19
|
+
toolFilter: {
|
|
20
|
+
allow: readonly string[];
|
|
21
|
+
};
|
|
22
|
+
context?: unknown;
|
|
23
|
+
locale: BtwLocale;
|
|
24
|
+
}
|
|
25
|
+
export type StartChild = (request: ChildRequest) => Promise<ChildRun>;
|
|
26
|
+
/** 管理一次性任务;父会话和请求标识共同构成所有权边界。 */
|
|
27
|
+
export declare class SideJobs {
|
|
28
|
+
private readonly start;
|
|
29
|
+
private readonly timeoutMs;
|
|
30
|
+
private readonly jobs;
|
|
31
|
+
private readonly retired;
|
|
32
|
+
private disposed;
|
|
33
|
+
constructor(start: StartChild, timeoutMs?: number);
|
|
34
|
+
get size(): number;
|
|
35
|
+
ask(scope: string, id: string, question: string, signal?: AbortSignal, context?: unknown, locale?: BtwLocale): Promise<SideResult>;
|
|
36
|
+
/** 关闭可先于启动到达;短期墓碑阻止网络乱序重新启动请求。 */
|
|
37
|
+
close(scope: string, id: string): Promise<SideResult>;
|
|
38
|
+
dispose(): Promise<void>;
|
|
39
|
+
private execute;
|
|
40
|
+
private cleanup;
|
|
41
|
+
private retire;
|
|
42
|
+
private prune;
|
|
43
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface AgentIdentity {
|
|
2
|
+
session: {
|
|
3
|
+
snapshotEvents(): readonly {
|
|
4
|
+
type: string;
|
|
5
|
+
data: unknown;
|
|
6
|
+
}[];
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** 空白名单过滤继承工具;本保护额外拒绝 PTC 及子作用域自注册工具。 */
|
|
10
|
+
export declare function createAnswerOnlyGuard(labels: ReadonlySet<string>): (execution: {
|
|
11
|
+
agent?: AgentIdentity;
|
|
12
|
+
}) => string | undefined;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type BtwLocale } from './locales';
|
|
2
|
+
export declare const RUN_COMMAND = "btw-run";
|
|
3
|
+
export declare const CLOSE_COMMAND = "btw-close";
|
|
4
|
+
export declare const MAX_QUESTION_LENGTH = 8000;
|
|
5
|
+
export interface SideRequest {
|
|
6
|
+
id: string;
|
|
7
|
+
question: string;
|
|
8
|
+
locale: BtwLocale;
|
|
9
|
+
}
|
|
10
|
+
export interface SideResult {
|
|
11
|
+
kind: 'success' | 'error';
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
/** 内部命令使用结构化请求,身份始终由宿主会话提供。 */
|
|
15
|
+
export declare function parseRequest(raw: string): SideRequest;
|
package/package.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@michengai/dsh-btw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DSH 一次性只读旁问气泡",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/MichengAI/dsh-btw.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/MichengAI/dsh-btw#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/MichengAI/dsh-btw/issues"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public",
|
|
15
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "lib/index.js",
|
|
19
|
+
"types": "lib/types/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./lib/types/index.d.ts",
|
|
23
|
+
"default": "./lib/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./client": "./lib/client.js",
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"lib",
|
|
30
|
+
"assets",
|
|
31
|
+
"cordis.patch.yml",
|
|
32
|
+
"docs",
|
|
33
|
+
"README.md",
|
|
34
|
+
"README.zh-CN.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"CHANGELOG.zh-CN.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"dsh": {
|
|
40
|
+
"bundle": {
|
|
41
|
+
"patch": "./cordis.patch.yml"
|
|
42
|
+
},
|
|
43
|
+
"client": {
|
|
44
|
+
"platform": "web",
|
|
45
|
+
"inject": [
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-input-trigger",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-chat",
|
|
49
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
50
|
+
"@deepseek-ai/dsh-client-locale"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "node scripts/build.mjs",
|
|
56
|
+
"typecheck": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:release": "node --test tests/release.test.mjs",
|
|
59
|
+
"check": "npm run typecheck && npm test && npm run test:release && npm run build",
|
|
60
|
+
"preview": "vite preview --host 127.0.0.1",
|
|
61
|
+
"dev": "vite --host 127.0.0.1",
|
|
62
|
+
"test:browser": "playwright test",
|
|
63
|
+
"prepack": "npm run build"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=22"
|
|
67
|
+
},
|
|
68
|
+
"license": "Apache-2.0",
|
|
69
|
+
"peerDependencies": {
|
|
70
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
71
|
+
"@deepseek-ai/dsh-commands": "0.1.2-rc.1",
|
|
72
|
+
"@deepseek-ai/dsh-subagent": "0.1.2-rc.1",
|
|
73
|
+
"@deepseek-ai/dsh-tools": "0.1.2-rc.1",
|
|
74
|
+
"react": "^18.3.1"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
78
|
+
"@deepseek-ai/dsh-agent": "0.1.2-rc.1",
|
|
79
|
+
"@deepseek-ai/dsh-api-gateway": "0.1.2-rc.1",
|
|
80
|
+
"@deepseek-ai/dsh-api-session-controller": "0.1.2-rc.1",
|
|
81
|
+
"@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
|
|
82
|
+
"@deepseek-ai/dsh-client-locale": "0.1.2-rc.1",
|
|
83
|
+
"@deepseek-ai/dsh-client-ui-chat": "0.1.2-rc.1",
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.1.2-rc.1",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "0.1.2-rc.1",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-renderer": "0.1.2-rc.1",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.2-rc.1",
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-theme": "0.1.2-rc.1",
|
|
89
|
+
"@deepseek-ai/dsh-commands": "0.1.2-rc.1",
|
|
90
|
+
"@deepseek-ai/dsh-scope": "0.1.2-rc.1",
|
|
91
|
+
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
|
|
92
|
+
"@deepseek-ai/dsh-subagent": "0.1.2-rc.1",
|
|
93
|
+
"@deepseek-ai/dsh-tools": "0.1.2-rc.1",
|
|
94
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.2-rc.1",
|
|
95
|
+
"@deepseek-ai/dsh-typert-registry": "0.1.2-rc.1",
|
|
96
|
+
"@playwright/test": "^1.58.0",
|
|
97
|
+
"@types/node": "^22.0.0",
|
|
98
|
+
"@types/react": "^18.3.1",
|
|
99
|
+
"@types/react-dom": "^18.3.1",
|
|
100
|
+
"esbuild": "^0.25.0",
|
|
101
|
+
"lucide-react": "^0.468.0",
|
|
102
|
+
"react": "^18.3.1",
|
|
103
|
+
"react-dom": "^18.3.1",
|
|
104
|
+
"react-markdown": "^10.1.0",
|
|
105
|
+
"remark-gfm": "^4.0.1",
|
|
106
|
+
"typescript": "~5.7.2",
|
|
107
|
+
"vite": "^6.4.0",
|
|
108
|
+
"vitest": "^3.2.0"
|
|
109
|
+
}
|
|
110
|
+
}
|