@y9931666/dsh-plugin-roundtable 1.0.0-rc.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 +328 -0
- package/cordis.patch.yml +22 -0
- package/lib/client.js +4276 -0
- package/lib/edge-helper-iqXOG5o_.js +12 -0
- package/lib/index.js +4535 -0
- package/lib/state-BzBQig2q.js +479 -0
- package/lib/state-pJShWoUr.js +3 -0
- package/lib/types/aggregator.d.ts +15 -0
- package/lib/types/budget.d.ts +46 -0
- package/lib/types/charter.d.ts +13 -0
- package/lib/types/client/RoundTableSettings.d.ts +14 -0
- package/lib/types/client/RoundTableView.d.ts +27 -0
- package/lib/types/client/brand-logos.generated.d.ts +5 -0
- package/lib/types/client/index.d.ts +23 -0
- package/lib/types/client/locales.d.ts +14 -0
- package/lib/types/client/slot-boundary.d.ts +49 -0
- package/lib/types/client/tool-views-model.d.ts +65 -0
- package/lib/types/client/tool-views.d.ts +31 -0
- package/lib/types/client/wire.d.ts +238 -0
- package/lib/types/edge-helper.d.ts +7 -0
- package/lib/types/harness-compat.d.ts +204 -0
- package/lib/types/index.d.ts +53 -0
- package/lib/types/kb-digest.d.ts +47 -0
- package/lib/types/members.d.ts +83 -0
- package/lib/types/node-events.d.ts +90 -0
- package/lib/types/plan.d.ts +86 -0
- package/lib/types/preferences.d.ts +143 -0
- package/lib/types/prompt.d.ts +52 -0
- package/lib/types/proxy-thinking.d.ts +12 -0
- package/lib/types/review-split.d.ts +61 -0
- package/lib/types/rpc.d.ts +89 -0
- package/lib/types/skills.d.ts +61 -0
- package/lib/types/snapshot.d.ts +96 -0
- package/lib/types/state.d.ts +142 -0
- package/lib/types/token-usage.d.ts +71 -0
- package/lib/types/tools.d.ts +59 -0
- package/lib/types/types.d.ts +330 -0
- package/lib/types/version.d.ts +21 -0
- package/lib/types/web-guard.d.ts +37 -0
- package/lib/types/workspace-candidates.d.ts +17 -0
- package/lib/workspace-candidates-B7isleLt.js +3 -0
- package/lib/workspace-candidates-DNmGPpSc.js +20 -0
- package/package.json +161 -0
- package/release-notes/v0.2.0.md +39 -0
- package/release-notes/v0.2.1.md +39 -0
- package/release-notes/v0.2.2.md +54 -0
- package/release-notes/v0.2.21.md +42 -0
- package/release-notes/v0.2.31.md +70 -0
- package/release-notes/v0.2.32.md +52 -0
- package/release-notes/v0.2.35.md +116 -0
- package/release-notes/v0.2.36.md +84 -0
- package/release-notes/v1.0.0-rc.1.md +165 -0
- package/release-notes/v1.0.0-rc.2.md +180 -0
|
@@ -0,0 +1,479 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { appendFile, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
//#region src/state.ts
|
|
6
|
+
/** Stable directory id from a display name (keeps CJK, lowercases latin). */
|
|
7
|
+
function sanitizeKey(value) {
|
|
8
|
+
const cleaned = value.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-").replace(/^-+|-+$/g, "");
|
|
9
|
+
return cleaned === "" ? "meeting" : cleaned;
|
|
10
|
+
}
|
|
11
|
+
/** Process-local mutex chain keyed by an arbitrary lock key. */
|
|
12
|
+
const locks = /* @__PURE__ */ new Map();
|
|
13
|
+
/** Serialize async operations sharing one key inside this process. */
|
|
14
|
+
async function withMeetingLock(key, operation) {
|
|
15
|
+
const current = (locks.get(key) ?? Promise.resolve()).then(operation, operation);
|
|
16
|
+
locks.set(key, current.then(() => void 0, () => void 0));
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
/** Absolute state root under a workspace. */
|
|
20
|
+
function stateRootOf(workspace, stateDir) {
|
|
21
|
+
return join(workspace, stateDir);
|
|
22
|
+
}
|
|
23
|
+
/** Absolute directory of one meeting. */
|
|
24
|
+
function meetingDirOf(stateRoot, meetingId) {
|
|
25
|
+
return join(stateRoot, meetingId);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Drop every persisted record belonging to one meeting directory.
|
|
29
|
+
*
|
|
30
|
+
* `roundtable_create` reuses the id of a finished meeting (the id IS the
|
|
31
|
+
* sanitized name), and while `meeting.json` gets rewritten, `transcript.jsonl`
|
|
32
|
+
* is append-only and `review.json` / `user-actions.jsonl` / `export.md` /
|
|
33
|
+
* `kb-digest.json` simply stay behind. Without this reset a brand-new meeting
|
|
34
|
+
* inherits the previous one's utterances and review verbatim: the user is shown
|
|
35
|
+
* a review window for a plan that belongs to an entirely different meeting and
|
|
36
|
+
* is asked to endorse or reject flaws of a plan that is not the one on the
|
|
37
|
+
* table (P3).
|
|
38
|
+
*
|
|
39
|
+
* Idempotent — a missing directory is not an error.
|
|
40
|
+
*/
|
|
41
|
+
async function resetMeetingDirectory(stateRoot, meetingId) {
|
|
42
|
+
await rm(meetingDirOf(stateRoot, meetingId), {
|
|
43
|
+
recursive: true,
|
|
44
|
+
force: true
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Cached host atomic writer: `null` = not resolved yet, `undefined` = unavailable. */
|
|
48
|
+
let hostAtomicWriter = null;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the host's atomic writer **without making it a load gate**.
|
|
51
|
+
*
|
|
52
|
+
* A static import would mean a composition that lacks
|
|
53
|
+
* `@deepseek-ai/dsh-atomic-write` fails to load this whole module — the same
|
|
54
|
+
* "optional capability became a hard gate" failure v0.2.36 fixed for cordis
|
|
55
|
+
* `inject`. So it is resolved once, lazily, and absence falls back to the local
|
|
56
|
+
* implementation below.
|
|
57
|
+
*/
|
|
58
|
+
async function resolveHostWriter() {
|
|
59
|
+
if (hostAtomicWriter !== null) return hostAtomicWriter;
|
|
60
|
+
try {
|
|
61
|
+
const write = (await import("@deepseek-ai/dsh-atomic-write")).writeFileAtomic;
|
|
62
|
+
hostAtomicWriter = typeof write === "function" ? (file, text) => write(file, text, { mode: 420 }) : void 0;
|
|
63
|
+
} catch {
|
|
64
|
+
hostAtomicWriter = void 0;
|
|
65
|
+
}
|
|
66
|
+
return hostAtomicWriter;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Atomically publish a UTF-8 text file.
|
|
70
|
+
*
|
|
71
|
+
* Prefers the host's `@deepseek-ai/dsh-atomic-write`: same-directory
|
|
72
|
+
* random-suffix temp + rename, plus the bounded retry for transient Windows
|
|
73
|
+
* `EACCES`/`EBUSY`/`EPERM` rename failures that our hand-rolled version lacked —
|
|
74
|
+
* it moved the temp aside as `.stale-*` and threw, so **that write was simply
|
|
75
|
+
* lost**. The host version removes its temp and rethrows instead.
|
|
76
|
+
*
|
|
77
|
+
* When the host does not provide that package, the local fallback keeps every
|
|
78
|
+
* state write working (same tmp + rename, minus the Windows retry) rather than
|
|
79
|
+
* failing the whole plugin.
|
|
80
|
+
*/
|
|
81
|
+
async function writeTextAtomic(file, text) {
|
|
82
|
+
const hostWrite = await resolveHostWriter();
|
|
83
|
+
if (hostWrite !== void 0) {
|
|
84
|
+
await hostWrite(file, text);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const tmp = join(dirname(file), `.${randomUUID()}.tmp`);
|
|
88
|
+
await writeFile(tmp, text, "utf8");
|
|
89
|
+
await rename(tmp, file);
|
|
90
|
+
}
|
|
91
|
+
/** Atomically publish a JSON file (same-directory tmp + rename). */
|
|
92
|
+
async function writeJsonAtomic(file, value) {
|
|
93
|
+
await writeTextAtomic(file, JSON.stringify(value, null, 2));
|
|
94
|
+
}
|
|
95
|
+
/** Lock key serializing append (UI) against clear (captain) for one meeting's
|
|
96
|
+
* `user-actions.jsonl`. Without it a clear could rename over a line the UI
|
|
97
|
+
* was midway through appending, orphaning a user action (A2). */
|
|
98
|
+
function userActionsLockKey(stateRoot, meetingId) {
|
|
99
|
+
return `user-actions:${stateRoot}:${meetingId}`;
|
|
100
|
+
}
|
|
101
|
+
/** Read one meeting record; undefined when absent.
|
|
102
|
+
*
|
|
103
|
+
* 与 {@link readReview} 对齐:读出来的记录一律先过 {@link normalizeMeeting}
|
|
104
|
+
* 升级到当前 schema 版本。v0.2.36 之前这里是全插件唯一的「裸
|
|
105
|
+
* `JSON.parse(...) as Meeting`」——形状一变老会议就静默读坏,而且没有任何
|
|
106
|
+
* 版本号可供分支。 */
|
|
107
|
+
async function readMeeting(stateRoot, meetingId) {
|
|
108
|
+
try {
|
|
109
|
+
const raw = await readFile(join(stateRoot, meetingId, "meeting.json"), "utf8");
|
|
110
|
+
return normalizeMeeting(JSON.parse(raw));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error.code === "ENOENT") return void 0;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Persist one meeting record (atomic). 写入前同样过一遍 normalize:这样
|
|
117
|
+
* 「读时升级 + 下次写入顺带落盘」形成一个闭环,老文件不会永远停在旧版本。 */
|
|
118
|
+
async function writeMeeting(stateRoot, meeting) {
|
|
119
|
+
const dir = meetingDirOf(stateRoot, meeting.id);
|
|
120
|
+
await mkdir(dir, { recursive: true });
|
|
121
|
+
const normalized = normalizeMeeting(meeting);
|
|
122
|
+
normalized.updatedAt = Date.now();
|
|
123
|
+
await writeJsonAtomic(join(dir, "meeting.json"), normalized);
|
|
124
|
+
}
|
|
125
|
+
/** 当前 `meeting.json` 的形状版本。加字段/改语义时递增,并在
|
|
126
|
+
* {@link MEETING_MIGRATIONS} 补一条从「前一版」升上来的函数。 */
|
|
127
|
+
const CURRENT_MEETING_SCHEMA_VERSION = 1;
|
|
128
|
+
/**
|
|
129
|
+
* 相邻版本迁移表:键 = 源版本,值 = 把该版本升到「源版本 + 1」的函数。
|
|
130
|
+
*
|
|
131
|
+
* 当前 1 就是最新版,所以表为空——**这不是占位符**:把机制先建起来,是为了
|
|
132
|
+
* 让下一次改形状时只需要「加一条函数 + 递增常量」,而不是像 review.json 那样
|
|
133
|
+
* 等到不得不迁移时才回头补一套。
|
|
134
|
+
*/
|
|
135
|
+
const MEETING_MIGRATIONS = {};
|
|
136
|
+
/** 每个会话只播报一次迁移告警,避免 1 秒轮询的快照把日志刷爆。 */
|
|
137
|
+
const notifiedMeetingMigrations = /* @__PURE__ */ new Set();
|
|
138
|
+
/** 把「字段缺失/类型不符」的最小集合补成合法缺省值(幂等)。
|
|
139
|
+
*
|
|
140
|
+
* 只处理**不改语义**的兼容补齐,绝不猜测用户的意图,也绝不让缺失字段变成
|
|
141
|
+
* `undefined` 混进后续计算(那正是「静默读坏」的样子)。 */
|
|
142
|
+
function withMeetingDefaults(meeting) {
|
|
143
|
+
if (!Array.isArray(meeting.nodes)) meeting.nodes = [];
|
|
144
|
+
if (!Array.isArray(meeting.edges)) meeting.edges = [];
|
|
145
|
+
if (!Array.isArray(meeting.decisions)) meeting.decisions = [];
|
|
146
|
+
if (typeof meeting.round !== "number" || !Number.isFinite(meeting.round)) meeting.round = 0;
|
|
147
|
+
if (typeof meeting.charter !== "string") meeting.charter = "";
|
|
148
|
+
const budget = meeting.budget;
|
|
149
|
+
meeting.budget = {
|
|
150
|
+
maxRounds: typeof budget?.maxRounds === "number" ? budget.maxRounds : 10,
|
|
151
|
+
maxTokens: typeof budget?.maxTokens === "number" ? budget.maxTokens : 2e5,
|
|
152
|
+
usedRounds: typeof budget?.usedRounds === "number" ? budget.usedRounds : 0,
|
|
153
|
+
usedTokens: typeof budget?.usedTokens === "number" ? budget.usedTokens : 0,
|
|
154
|
+
...typeof budget?.usedTokensReal === "number" ? { usedTokensReal: budget.usedTokensReal } : {}
|
|
155
|
+
};
|
|
156
|
+
return meeting;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Idempotently upgrade one meeting record to
|
|
160
|
+
* {@link CURRENT_MEETING_SCHEMA_VERSION}(**内存内**,不写回;下次
|
|
161
|
+
* {@link writeMeeting} 落盘时顺带升级)。
|
|
162
|
+
*
|
|
163
|
+
* 缺失 `schemaVersion` 视为 1(v0.2.36 及之前的文件都是这个形状),然后
|
|
164
|
+
* 依次跑迁移表。三条防御:
|
|
165
|
+
* - 迁移函数抛错 → 保留原记录并告警,**绝不让读会议失败**(读不出来比读得
|
|
166
|
+
* 不完美严重得多,那会让整场会议从界面上消失);
|
|
167
|
+
* - 版本高于当前(用户从新版本回退到老版本)→ 原样返回并告警,不做降级
|
|
168
|
+
* 猜测;
|
|
169
|
+
* - 版本非法(非整数 / < 1)→ 归一到 1 再走迁移。
|
|
170
|
+
*/
|
|
171
|
+
function normalizeMeeting(meeting) {
|
|
172
|
+
const declared = meeting.schemaVersion;
|
|
173
|
+
const from = typeof declared === "number" && Number.isInteger(declared) && declared >= 1 ? declared : 1;
|
|
174
|
+
if (declared !== from) meeting.schemaVersion = from;
|
|
175
|
+
if (from > CURRENT_MEETING_SCHEMA_VERSION) {
|
|
176
|
+
const key = `future:${meeting.id}:${from}`;
|
|
177
|
+
if (!notifiedMeetingMigrations.has(key)) {
|
|
178
|
+
notifiedMeetingMigrations.add(key);
|
|
179
|
+
console.warn(`[roundtable] meeting "${meeting.id}" was written by a newer plugin (schemaVersion ${from} > ${CURRENT_MEETING_SCHEMA_VERSION}); reading it as-is`);
|
|
180
|
+
}
|
|
181
|
+
return withMeetingDefaults(meeting);
|
|
182
|
+
}
|
|
183
|
+
let current = meeting;
|
|
184
|
+
let migrated = false;
|
|
185
|
+
for (let version = from; version < CURRENT_MEETING_SCHEMA_VERSION; version += 1) {
|
|
186
|
+
const step = MEETING_MIGRATIONS[version];
|
|
187
|
+
if (step === void 0) continue;
|
|
188
|
+
try {
|
|
189
|
+
current = step(current);
|
|
190
|
+
migrated = true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
const key = `failed:${meeting.id}:${version}`;
|
|
193
|
+
if (!notifiedMeetingMigrations.has(key)) {
|
|
194
|
+
notifiedMeetingMigrations.add(key);
|
|
195
|
+
console.warn(`[roundtable] meeting "${meeting.id}" schema migration ${version}→${version + 1} failed:`, error instanceof Error ? error.message : String(error));
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
current = withMeetingDefaults(current);
|
|
201
|
+
if (migrated || from < CURRENT_MEETING_SCHEMA_VERSION || declared !== from) current.schemaVersion = CURRENT_MEETING_SCHEMA_VERSION;
|
|
202
|
+
return current;
|
|
203
|
+
}
|
|
204
|
+
/** Lock key serializing transcript appends for one meeting (A3).
|
|
205
|
+
*
|
|
206
|
+
* `O_APPEND` already makes a single short line atomic, but this closes the
|
|
207
|
+
* remaining interleaving window (long lines, lifecycle-event races) so two
|
|
208
|
+
* utterances can never end up written into each other. */
|
|
209
|
+
function transcriptLockKey(stateRoot, meetingId) {
|
|
210
|
+
return `transcript:${stateRoot}:${meetingId}`;
|
|
211
|
+
}
|
|
212
|
+
/** Append one utterance to the meeting transcript (JSONL). */
|
|
213
|
+
async function appendUtterance(stateRoot, meetingId, utterance) {
|
|
214
|
+
await withMeetingLock(transcriptLockKey(stateRoot, meetingId), async () => {
|
|
215
|
+
const dir = meetingDirOf(stateRoot, meetingId);
|
|
216
|
+
await mkdir(dir, { recursive: true });
|
|
217
|
+
await appendFile(join(dir, "transcript.jsonl"), JSON.stringify(utterance) + "\n", "utf8");
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/** Read the full transcript, skipping torn tails and malformed lines. */
|
|
221
|
+
async function readTranscript(stateRoot, meetingId) {
|
|
222
|
+
try {
|
|
223
|
+
const raw = await readFile(join(stateRoot, meetingId, "transcript.jsonl"), "utf8");
|
|
224
|
+
const out = [];
|
|
225
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
226
|
+
if (line.trim() === "") continue;
|
|
227
|
+
try {
|
|
228
|
+
out.push(JSON.parse(line));
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error.code === "ENOENT") return [];
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/** List every meeting id under a state root (missing root = empty). */
|
|
238
|
+
async function listMeetings(stateRoot) {
|
|
239
|
+
try {
|
|
240
|
+
return (await readdir(stateRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error.code === "ENOENT") return [];
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Append one user action to the meeting's `user-actions.jsonl` (JSONL).
|
|
247
|
+
* Serialized against {@link clearUserActions} so an append can never race a
|
|
248
|
+
* clear (A2). A single short line stays atomic under `O_APPEND`. */
|
|
249
|
+
async function appendUserAction(stateRoot, meetingId, action) {
|
|
250
|
+
await withMeetingLock(userActionsLockKey(stateRoot, meetingId), async () => {
|
|
251
|
+
const dir = meetingDirOf(stateRoot, meetingId);
|
|
252
|
+
await mkdir(dir, { recursive: true });
|
|
253
|
+
await appendFile(join(dir, "user-actions.jsonl"), JSON.stringify(action) + "\n", "utf8");
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
/** Read every pending user action, counting rather than hiding bad lines. */
|
|
257
|
+
async function readUserActionsReport(stateRoot, meetingId) {
|
|
258
|
+
try {
|
|
259
|
+
const raw = await readFile(join(stateRoot, meetingId, "user-actions.jsonl"), "utf8");
|
|
260
|
+
const actions = [];
|
|
261
|
+
let malformed = 0;
|
|
262
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
263
|
+
if (line.trim() === "") continue;
|
|
264
|
+
try {
|
|
265
|
+
actions.push(JSON.parse(line));
|
|
266
|
+
} catch {
|
|
267
|
+
malformed += 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
actions,
|
|
272
|
+
malformed
|
|
273
|
+
};
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (error.code === "ENOENT") return {
|
|
276
|
+
actions: [],
|
|
277
|
+
malformed: 0
|
|
278
|
+
};
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/** Read every pending user action, skipping torn tails and malformed lines. */
|
|
283
|
+
async function readUserActions(stateRoot, meetingId) {
|
|
284
|
+
return (await readUserActionsReport(stateRoot, meetingId)).actions;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Clear every pending user action, atomically and under the append lock.
|
|
288
|
+
*
|
|
289
|
+
* Returns both how many actions were cleared and how many unparseable lines
|
|
290
|
+
* were dropped, so the captain can tell the user the truth about a loss
|
|
291
|
+
* instead of reporting a clean sweep that never happened (A2).
|
|
292
|
+
*/
|
|
293
|
+
async function clearUserActions(stateRoot, meetingId) {
|
|
294
|
+
return withMeetingLock(userActionsLockKey(stateRoot, meetingId), async () => {
|
|
295
|
+
const dir = meetingDirOf(stateRoot, meetingId);
|
|
296
|
+
await mkdir(dir, { recursive: true });
|
|
297
|
+
const { actions, malformed } = await readUserActionsReport(stateRoot, meetingId);
|
|
298
|
+
await writeTextAtomic(join(dir, "user-actions.jsonl"), "");
|
|
299
|
+
return {
|
|
300
|
+
cleared: actions.length,
|
|
301
|
+
malformed
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/** Read the 针锋相对 review record; undefined when absent. Migrates old
|
|
306
|
+
* schemaVersion-1 records (endorsed boolean, whole-utterance viewpoints) to
|
|
307
|
+
* the current shape in memory — idempotent, no write-back (next writeReview
|
|
308
|
+
* persists the upgraded shape). */
|
|
309
|
+
async function readReview(stateRoot, meetingId) {
|
|
310
|
+
try {
|
|
311
|
+
const raw = await readFile(join(meetingDirOf(stateRoot, meetingId), "review.json"), "utf8");
|
|
312
|
+
return normalizeReview(JSON.parse(raw));
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (error.code === "ENOENT") return void 0;
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/** Idempotently upgrade a review record to schemaVersion 2 (in-memory only).
|
|
319
|
+
* v1: viewpoint = {id(utterance id), nodeKey, content, endorsed, ts} →
|
|
320
|
+
* v2: {id: `${utteranceId}#${seq}`, utteranceId, nodeKey, content,
|
|
321
|
+
* status, quote?, dimension, ts, seq}. v2 records created before the
|
|
322
|
+
* C3 复审轮次 fields existed are given reviewPass=1 / maxReviewPass=3. */
|
|
323
|
+
function normalizeReview(review) {
|
|
324
|
+
if (((review.schemaVersion ?? 1) === 2 ? review : void 0) !== void 0) {
|
|
325
|
+
if (typeof review.reviewPass !== "number" || review.reviewPass < 1) review.reviewPass = 1;
|
|
326
|
+
if (typeof review.maxReviewPass !== "number" || review.maxReviewPass < 1) review.maxReviewPass = 3;
|
|
327
|
+
if (!Array.isArray(review.history)) review.history = [];
|
|
328
|
+
return review;
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
meetingId: review.meetingId,
|
|
332
|
+
question: review.question,
|
|
333
|
+
plan: review.plan,
|
|
334
|
+
status: review.status,
|
|
335
|
+
reviewPass: 1,
|
|
336
|
+
maxReviewPass: 3,
|
|
337
|
+
history: [],
|
|
338
|
+
schemaVersion: 2,
|
|
339
|
+
viewpoints: review.viewpoints.map((viewpoint) => {
|
|
340
|
+
const legacy = viewpoint;
|
|
341
|
+
const utteranceId = typeof legacy.id === "string" ? legacy.id : "";
|
|
342
|
+
const content = typeof legacy.content === "string" ? legacy.content : "";
|
|
343
|
+
return {
|
|
344
|
+
id: `${utteranceId}#0`,
|
|
345
|
+
utteranceId,
|
|
346
|
+
nodeKey: typeof legacy.nodeKey === "string" ? legacy.nodeKey : "",
|
|
347
|
+
content,
|
|
348
|
+
status: legacy.endorsed === true ? "endorsed" : "pending",
|
|
349
|
+
dimension: "其他",
|
|
350
|
+
ts: typeof legacy.ts === "number" ? legacy.ts : Date.now(),
|
|
351
|
+
seq: 0
|
|
352
|
+
};
|
|
353
|
+
}),
|
|
354
|
+
startedAt: review.startedAt,
|
|
355
|
+
updatedAt: review.updatedAt
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/** Persist the review record (atomic). */
|
|
359
|
+
async function writeReview(stateRoot, meetingId, review) {
|
|
360
|
+
const dir = meetingDirOf(stateRoot, meetingId);
|
|
361
|
+
await mkdir(dir, { recursive: true });
|
|
362
|
+
review.updatedAt = Date.now();
|
|
363
|
+
await writeJsonAtomic(join(dir, "review.json"), review);
|
|
364
|
+
}
|
|
365
|
+
/** Set one viewpoint's three-state status (pending/endorsed/rejected);
|
|
366
|
+
* when rejecting, the caller must supply a non-empty reason (C2).
|
|
367
|
+
* Returns true when the value changed. */
|
|
368
|
+
async function setViewpointStatus(stateRoot, meetingId, viewpointId, status, rejectReason) {
|
|
369
|
+
const review = await readReview(stateRoot, meetingId);
|
|
370
|
+
if (review === void 0) return false;
|
|
371
|
+
const viewpoint = review.viewpoints.find((candidate) => candidate.id === viewpointId);
|
|
372
|
+
if (viewpoint === void 0 || viewpoint.status === status) return false;
|
|
373
|
+
if (status === "rejected") {
|
|
374
|
+
const reason = rejectReason?.trim() ?? "";
|
|
375
|
+
if (reason === "") throw new Error("rejecting a viewpoint requires a non-empty reject_reason (驳回必填理由)");
|
|
376
|
+
viewpoint.rejectReason = reason.slice(0, 500);
|
|
377
|
+
}
|
|
378
|
+
viewpoint.status = status;
|
|
379
|
+
await writeReview(stateRoot, meetingId, review);
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
/** Structural guard: only well-formed entries survive a hand-edited cache file. */
|
|
383
|
+
function isKbDigestEntry(value) {
|
|
384
|
+
if (value === null || typeof value !== "object") return false;
|
|
385
|
+
const entry = value;
|
|
386
|
+
return typeof entry.path === "string" && entry.path !== "" && typeof entry.size === "number" && typeof entry.mtimeMs === "number" && typeof entry.digest === "string" && typeof entry.ts === "number";
|
|
387
|
+
}
|
|
388
|
+
/** Read the knowledge-base digest cache.
|
|
389
|
+
*
|
|
390
|
+
* A missing OR corrupt file reads as empty: a bad cache must never block a
|
|
391
|
+
* meeting — the worst case is that the captain re-reads a file, which is
|
|
392
|
+
* exactly the behaviour that existed before the cache. */
|
|
393
|
+
async function readKbDigest(stateRoot, meetingId) {
|
|
394
|
+
const empty = {
|
|
395
|
+
meetingId,
|
|
396
|
+
entries: [],
|
|
397
|
+
updatedAt: 0
|
|
398
|
+
};
|
|
399
|
+
let raw;
|
|
400
|
+
try {
|
|
401
|
+
raw = await readFile(join(meetingDirOf(stateRoot, meetingId), "kb-digest.json"), "utf8");
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error.code === "ENOENT") return empty;
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const parsed = JSON.parse(raw);
|
|
408
|
+
return {
|
|
409
|
+
meetingId,
|
|
410
|
+
entries: Array.isArray(parsed.entries) ? parsed.entries.filter(isKbDigestEntry) : [],
|
|
411
|
+
updatedAt: typeof parsed.updatedAt === "number" ? parsed.updatedAt : 0
|
|
412
|
+
};
|
|
413
|
+
} catch {
|
|
414
|
+
return empty;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
/** Persist the knowledge-base digest cache (atomic). */
|
|
418
|
+
async function writeKbDigest(stateRoot, meetingId, entries) {
|
|
419
|
+
const dir = meetingDirOf(stateRoot, meetingId);
|
|
420
|
+
await mkdir(dir, { recursive: true });
|
|
421
|
+
const file = {
|
|
422
|
+
meetingId,
|
|
423
|
+
entries: [...entries],
|
|
424
|
+
updatedAt: Date.now()
|
|
425
|
+
};
|
|
426
|
+
await writeJsonAtomic(join(dir, "kb-digest.json"), file);
|
|
427
|
+
}
|
|
428
|
+
/** Lock key serializing every `feedback.jsonl` writer of one workspace (P11).
|
|
429
|
+
*
|
|
430
|
+
* The file is workspace-scoped, so a meeting-scoped lock is not enough: one
|
|
431
|
+
* meeting's "clear then append" used to rewrite the whole file while another
|
|
432
|
+
* meeting was appending, silently dropping that entry. */
|
|
433
|
+
function feedbackLockKey(stateRoot) {
|
|
434
|
+
return `feedback:${stateRoot}`;
|
|
435
|
+
}
|
|
436
|
+
/** Append one feedback entry to `<stateRoot>/feedback.jsonl`. */
|
|
437
|
+
async function appendFeedback(stateRoot, entry) {
|
|
438
|
+
return withMeetingLock(feedbackLockKey(stateRoot), async () => {
|
|
439
|
+
await mkdir(stateRoot, { recursive: true });
|
|
440
|
+
await appendFile(join(stateRoot, "feedback.jsonl"), JSON.stringify(entry) + "\n", "utf8");
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/** Read every feedback entry (newest last), skipping torn tails. */
|
|
444
|
+
async function readFeedback(stateRoot) {
|
|
445
|
+
try {
|
|
446
|
+
const raw = await readFile(join(stateRoot, "feedback.jsonl"), "utf8");
|
|
447
|
+
const out = [];
|
|
448
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
449
|
+
if (line.trim() === "") continue;
|
|
450
|
+
try {
|
|
451
|
+
out.push(JSON.parse(line));
|
|
452
|
+
} catch {}
|
|
453
|
+
}
|
|
454
|
+
return out;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (error.code === "ENOENT") return [];
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/** Remove every feedback entry for one meeting id (dedupe); returns count removed. */
|
|
461
|
+
async function clearFeedbackForMeeting(stateRoot, meetingId) {
|
|
462
|
+
return withMeetingLock(feedbackLockKey(stateRoot), async () => {
|
|
463
|
+
const before = await readFeedback(stateRoot);
|
|
464
|
+
const remaining = before.filter((entry) => entry.meetingId !== meetingId);
|
|
465
|
+
await writeTextAtomic(join(stateRoot, "feedback.jsonl"), remaining.map((entry) => JSON.stringify(entry)).join("\n") + (remaining.length > 0 ? "\n" : ""));
|
|
466
|
+
return before.length - remaining.length;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
/** Remove every feedback entry; returns how many were dropped. */
|
|
470
|
+
async function clearFeedback(stateRoot) {
|
|
471
|
+
return withMeetingLock(feedbackLockKey(stateRoot), async () => {
|
|
472
|
+
const before = await readFeedback(stateRoot);
|
|
473
|
+
await writeTextAtomic(join(stateRoot, "feedback.jsonl"), "");
|
|
474
|
+
return before.length;
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
export { withMeetingLock as C, writeTextAtomic as D, writeReview as E, stateRootOf as S, writeMeeting as T, readUserActions as _, clearFeedback as a, sanitizeKey as b, listMeetings as c, normalizeReview as d, readFeedback as f, readTranscript as g, readReview as h, appendUtterance as i, meetingDirOf as l, readMeeting as m, appendFeedback as n, clearFeedbackForMeeting as o, readKbDigest as p, appendUserAction as r, clearUserActions as s, CURRENT_MEETING_SCHEMA_VERSION as t, normalizeMeeting as u, readUserActionsReport as v, writeKbDigest as w, setViewpointStatus as x, resetMeetingDirectory as y };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { C as withMeetingLock, D as writeTextAtomic, E as writeReview, S as stateRootOf, T as writeMeeting, _ as readUserActions, a as clearFeedback, b as sanitizeKey, c as listMeetings, d as normalizeReview, f as readFeedback, g as readTranscript, h as readReview, i as appendUtterance, l as meetingDirOf, m as readMeeting, n as appendFeedback, o as clearFeedbackForMeeting, p as readKbDigest, r as appendUserAction, s as clearUserActions, t as CURRENT_MEETING_SCHEMA_VERSION, u as normalizeMeeting, v as readUserActionsReport, w as writeKbDigest, x as setViewpointStatus, y as resetMeetingDirectory } from "./state-BzBQig2q.js";
|
|
2
|
+
|
|
3
|
+
export { CURRENT_MEETING_SCHEMA_VERSION, appendFeedback, appendUserAction, appendUtterance, clearFeedback, clearFeedbackForMeeting, clearUserActions, listMeetings, meetingDirOf, normalizeMeeting, normalizeReview, readFeedback, readKbDigest, readMeeting, readReview, readTranscript, readUserActions, readUserActionsReport, resetMeetingDirectory, sanitizeKey, setViewpointStatus, stateRootOf, withMeetingLock, writeKbDigest, writeMeeting, writeReview, writeTextAtomic };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The aggregation gateway (V1): a deterministic structured merge of the
|
|
3
|
+
* transcript — no extra model. It groups utterances by speaker, keeps the
|
|
4
|
+
* most recent lines per speaker, and emits a compact canonical view for the
|
|
5
|
+
* captain. A model-side summary can later be layered on top by the captain.
|
|
6
|
+
* @module dsh-plugin-roundtable/aggregator
|
|
7
|
+
*/
|
|
8
|
+
import type { MeetingUtterance } from './types.ts';
|
|
9
|
+
/** Pull the "core output" / stance out of a formatted contribution, else compact the whole text.
|
|
10
|
+
*
|
|
11
|
+
* 导出给 `roundtable_status` 复用:同一条 400 字符的配额,取 `[核心产出]`
|
|
12
|
+
* 比取发言开头(`[当前状态]`)信息量大得多 —— 不是配额不够,是取错了地方。 */
|
|
13
|
+
export declare function extractCore(text: string): string;
|
|
14
|
+
/** Merge utterances into a compact, chronological exchange (who → whom: core point). */
|
|
15
|
+
export declare function aggregateUtterances(utterances: readonly MeetingUtterance[], maxLength?: number): string;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meeting budget: round/token caps and the mute (闭麦) gate.
|
|
3
|
+
* @module dsh-plugin-roundtable/budget
|
|
4
|
+
*/
|
|
5
|
+
import type { Meeting } from './types.ts';
|
|
6
|
+
/** Which budget axis is exceeded, if any. */
|
|
7
|
+
export declare function budgetExceeded(meeting: Meeting): 'rounds' | 'tokens' | undefined;
|
|
8
|
+
/** Error when the meeting has ended. */
|
|
9
|
+
export declare class MeetingEndedError extends Error {
|
|
10
|
+
constructor(meetingId: string);
|
|
11
|
+
}
|
|
12
|
+
/** Error when the meeting is muted (闭麦) on a budget axis. */
|
|
13
|
+
export declare class MeetingMutedError extends Error {
|
|
14
|
+
readonly axis: 'rounds' | 'tokens';
|
|
15
|
+
constructor(meetingId: string, axis: 'rounds' | 'tokens');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Gate a write against the meeting's status and budget WITHOUT mutating it.
|
|
19
|
+
*
|
|
20
|
+
* The previous `ensureActive` both flipped `status` to `'muted'` and then threw
|
|
21
|
+
* on the same call. Because the throw happened before the caller could persist
|
|
22
|
+
* anything, `muted` never reached disk, and the very tools meant to resolve the
|
|
23
|
+
* situation (`roundtable_set_budget`, `roundtable_close`, the two exporters)
|
|
24
|
+
* were rejected by that same gate — a meeting that had spent its budget stayed
|
|
25
|
+
* `active` forever while refusing every action.
|
|
26
|
+
*
|
|
27
|
+
* @param meeting - meeting to test.
|
|
28
|
+
* @param options.allowMuted - resolve/escalate paths (top up the budget, close,
|
|
29
|
+
* export, clear the user-action queue) stay reachable while muted; everything
|
|
30
|
+
* that produces new work still throws.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertUsable(meeting: Meeting, options?: {
|
|
33
|
+
allowMuted?: boolean;
|
|
34
|
+
}): void;
|
|
35
|
+
/**
|
|
36
|
+
* Persist the fact that the meeting is out of budget.
|
|
37
|
+
*
|
|
38
|
+
* @param meeting - meeting mutated in place.
|
|
39
|
+
* @returns true when the status just became `'muted'`, in which case the caller
|
|
40
|
+
* owns writing the meeting back so the mute is actually observable.
|
|
41
|
+
*/
|
|
42
|
+
export declare function markMuted(meeting: Meeting): boolean;
|
|
43
|
+
/** Estimate the token cost of a text (coarse: ~0.6 tokens per CJK char, ~0.3 per latin). */
|
|
44
|
+
export declare function estimateTokens(text: string): number;
|
|
45
|
+
/** Advance the meeting round and return the new round number. */
|
|
46
|
+
export declare function beginRound(meeting: Meeting): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The《全局协作总纲》generated at meeting start and injected into every
|
|
3
|
+
* expert node's persona. Four fixed sections per the V1 spec.
|
|
4
|
+
*
|
|
5
|
+
* 体积纪律:这份文本进入**每一位专家**的 persona,并跟着他的每一次请求
|
|
6
|
+
* 付费。所以这里只写"所有专家共同的协议与红线";"你这个节点具体怎么做"
|
|
7
|
+
* 写在 {@link ./prompt.ts} 的 `nodePersona()`,同一条约束绝不两处各写一遍。
|
|
8
|
+
* 体积极限由 `test/prompt-budget.test.mjs` 断言。
|
|
9
|
+
* @module dsh-plugin-roundtable/charter
|
|
10
|
+
*/
|
|
11
|
+
import type { Meeting } from './types.ts';
|
|
12
|
+
/** Build the charter text for one meeting's current roster and edges. */
|
|
13
|
+
export declare function buildCharter(meeting: Meeting): string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoundTable preference page (settings.section): default collaboration mode
|
|
3
|
+
* and budget defaults. Switching to egalitarian mode surfaces the safety
|
|
4
|
+
* limits (max rounds / max tokens) with a warning-style hint.
|
|
5
|
+
* @module dsh-plugin-roundtable/client/RoundTableSettings
|
|
6
|
+
*/
|
|
7
|
+
import type { RpcCaller } from './wire.ts';
|
|
8
|
+
export interface RoundTableSettingsInjected {
|
|
9
|
+
rpc: RpcCaller;
|
|
10
|
+
t: (key: string) => string;
|
|
11
|
+
}
|
|
12
|
+
export interface RoundTableSettingsProps extends RoundTableSettingsInjected {
|
|
13
|
+
}
|
|
14
|
+
export declare function RoundTableSettings(props: RoundTableSettingsProps): JSX.Element;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RoundTable topology tab: the "圆桌会议" conversation view.
|
|
3
|
+
*
|
|
4
|
+
* Minimal two-column layout:
|
|
5
|
+
* - main: meeting header (title/badges/budget) → topology canvas (captain
|
|
6
|
+
* anchor, ring of expert nodes with brand avatars, directed edges) →
|
|
7
|
+
* collapsible aggregation-gateway digest.
|
|
8
|
+
* - sidebar: expert status list, role breakdown, knowledge-base skeleton,
|
|
9
|
+
* recent-utterance activity log, and file outputs (none yet).
|
|
10
|
+
*
|
|
11
|
+
* The tab lists ALL meetings in the workspace (no session filter), so past
|
|
12
|
+
* meetings stay visible across session switches, plugin updates and restarts;
|
|
13
|
+
* a switcher dropdown is shown when more than one meeting exists.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-plugin-roundtable/client/RoundTableView
|
|
16
|
+
*/
|
|
17
|
+
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
|
|
18
|
+
import type { RpcCaller } from './wire.ts';
|
|
19
|
+
export interface RoundTableViewInjected {
|
|
20
|
+
rpc: RpcCaller;
|
|
21
|
+
/** Locale-bound translator for the roundtable namespace. */
|
|
22
|
+
t: (key: string) => string;
|
|
23
|
+
}
|
|
24
|
+
export interface RoundTableViewProps extends RoundTableViewInjected {
|
|
25
|
+
sessionId: SessionId;
|
|
26
|
+
}
|
|
27
|
+
export declare function RoundTableView(props: RoundTableViewProps): JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-plugin-roundtable — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Two registrations:
|
|
5
|
+
* 1. `conversation.view` slot, id `roundtable` — the "圆桌会议" topology tab
|
|
6
|
+
* (left captain anchor, ring of expert nodes, directed edges with arrows,
|
|
7
|
+
* breathing-light activity, right-click edge menu, drag-to-connect).
|
|
8
|
+
* 2. `settings.section` slot, id `roundtable` — the preference page
|
|
9
|
+
* (default collaboration mode + budget defaults).
|
|
10
|
+
*
|
|
11
|
+
* @module @y9931666/dsh-plugin-roundtable/client
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
14
|
+
/**
|
|
15
|
+
* Required services: the two slots registries plus locale. `connection` is NOT
|
|
16
|
+
* listed — cordis `inject` is load-gating (a name here keeps the whole client
|
|
17
|
+
* plugin PENDING until that service exists), so listing the OPTIONAL fallback
|
|
18
|
+
* transport meant a composition without it silently registered neither the
|
|
19
|
+
* 圆桌会议 tab nor the settings page. It is read live via `ctx.get('connection')`
|
|
20
|
+
* instead, which is what the RPC fallback below actually needs.
|
|
21
|
+
*/
|
|
22
|
+
export declare const inject: string[];
|
|
23
|
+
export declare function apply(ctx: Context): void;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locale dictionaries for the RoundTable browser UI.
|
|
3
|
+
* @module dsh-plugin-roundtable/client/locales
|
|
4
|
+
*/
|
|
5
|
+
export declare const NS = "roundtable";
|
|
6
|
+
export type RoundTableKey = 'tab' | 'empty' | 'emptyHint' | 'modeOrchestrated' | 'modeEgalitarian' | 'modeRedteam' | 'round' | 'roundsBudget' | 'tokensBudget' | 'pendingDecision' | 'pendingDecisionOptions' | 'gatewayDigest' | 'noDigest' | 'edgeSetForward' | 'edgeSetBidirectional' | 'edgeRemove' | 'activityRunning' | 'activityIdle' | 'activityReady' | 'activityRemoved' | 'activityUnspawned' | 'activityMissing' | 'activityLifecycleHint' | 'legendTitle' | 'legendOrchestrated' | 'legendEgalitarian' | 'legendRedteam' | 'legendSynthetic' | 'legendPorts' | 'legendHide' | 'settingsLegend' | 'settingsLegendHint' | 'settingsSquadTitle' | 'settingsSquadHint' | 'settingsSquadAdd' | 'settingsSquadEmpty' | 'settingsSquadName' | 'settingsSquadNamePlaceholder' | 'settingsSquadMembers' | 'settingsSquadMemberKey' | 'settingsSquadMemberKeyPlaceholder' | 'settingsSquadAddMember' | 'settingsSquadRemoveMember' | 'settingsSquadSave' | 'settingsSquadCancel' | 'settingsSquadInvalid' | 'settingsSquadLimit' | 'settingsSquadUpdated' | 'settingsSquadEdit' | 'settingsSquadDelete' | 'settingsSquadMemberCount' | 'manageSquadLabel' | 'manageSquadPlaceholder' | 'manageSquadEmpty' | 'manageSquadAdded' | 'manageSquadSkipped' | 'settingsNav' | 'settingsDefaultMode' | 'settingsDefaultModeHint' | 'settingsEgalitarianWarning' | 'settingsMaxRounds' | 'settingsMaxTokens' | 'settingsSave' | 'settingsSaved' | 'settingsLoadFailed' | 'settingsSaveFailed' | 'fetchFailed' | 'renderFailed' | 'renderFailedHint' | 'meetingSelect' | 'agents' | 'tasks' | 'kb' | 'activity' | 'files' | 'kbEmpty' | 'filesEmpty' | 'noActivity' | 'editAgents' | 'editKb' | 'kbTitle' | 'kbPathLabel' | 'kbPathPlaceholder' | 'kbPathRequired' | 'kbSave' | 'kbSaved' | 'kbSaveFailed' | 'kbFiles' | 'kbLoadHint' | 'kbContentChanged' | 'kbBrowseHint' | 'kbNotSet' | 'meetingDelete' | 'meetingDeleteConfirm' | 'settingsShowAll' | 'settingsShowAllHint' | 'settingsShowAllOn' | 'settingsShowAllOff' | 'settingsLimitsTitle' | 'settingsSkillTitle' | 'settingsSkillDelivery' | 'settingsSkillDeliveryRelay' | 'settingsSkillDeliveryDirect' | 'settingsSkillDeliveryRelayHint' | 'settingsSkillDeliveryDirectHint' | 'skillsTitle' | 'skillsEmpty' | 'skillsDeliveryRelay' | 'skillsDeliveryDirect' | 'settingsPanelsTitle' | 'settingsPanelsHint' | 'panelOn' | 'panelOff' | 'settingsExpertMaxTokens' | 'settingsExpertMaxTokensHint' | 'settingsExpertMaxOpinions' | 'settingsExpertMaxOpinionsHint' | 'feedbackTitle' | 'feedbackEnabled' | 'feedbackEnabledHint' | 'feedbackPrivacyNote' | 'feedbackListEmpty' | 'feedbackListTitle' | 'feedbackClear' | 'feedbackCleared' | 'feedbackLoadFailed' | 'feedbackEntryMeta' | 'feedbackAskTitle' | 'feedbackAskGood' | 'feedbackAskMeh' | 'feedbackAskBad' | 'feedbackAskNotePlaceholder' | 'feedbackAskSkip' | 'feedbackAskSubmitted' | 'feedbackAskFailed' | 'pendingBadge' | 'manageTitle' | 'managePendingNote' | 'manageExisting' | 'manageRemove' | 'manageRemoveConfirm' | 'manageRemovedSoon' | 'manageQueueFailed' | 'managePendingRemove' | 'managePendingAdd' | 'managePendingAddRole' | 'manageAddTitle' | 'manageName' | 'manageNamePlaceholder' | 'manageNameRequired' | 'manageNameTaken' | 'manageRole' | 'manageRolePlaceholder' | 'manageModel' | 'manageModelInherit' | 'manageAddBtn' | 'manageAddedSoon' | 'manageEffectiveHint' | 'manageClose' | 'manageNoExperts' | 'reviewTitle' | 'reviewQuestion' | 'reviewPlan' | 'reviewViewpoints' | 'reviewEmpty' | 'reviewSupport' | 'reviewSupported' | 'reviewSupportedToast' | 'reviewEndorsed' | 'reviewEndorseFailed' | 'reviewHint' | 'reviewClose' | 'reviewBadge' | 'reviewOpen' | 'reviewPanelTitle' | 'reviewPanelEmpty' | 'reviewStatusReady' | 'reviewStatusReviewing' | 'reviewStatusDone' | 'reviewEndorsedCount' | 'reviewPending' | 'reviewRejected' | 'reviewReject' | 'reviewRejectedDone' | 'reviewRejectedToast' | 'reviewPendingToast' | 'reviewRejectReasonPrompt' | 'reviewRejectReasonPlaceholder' | 'reviewRejectReasonRequired' | 'reviewRejectConfirm' | 'reviewRejectCancel' | 'reviewEvidenceRepro' | 'reviewEvidenceArgument' | 'reviewPassBadge' | 'reviewImpactTitle' | 'reviewImpactThresholdHint' | 'reviewImpactOk' | 'reviewExportHint' | 'settingsPresetsTitle' | 'settingsPresetsHint' | 'settingsPresetsEmpty' | 'settingsPresetName' | 'settingsPresetNamePlaceholder' | 'settingsPresetRole' | 'settingsPresetRolePlaceholder' | 'settingsPresetModel' | 'settingsPresetAdd' | 'settingsPresetEdit' | 'settingsPresetSave' | 'settingsPresetCancel' | 'settingsPresetDelete' | 'settingsPresetInvalid' | 'settingsPresetLimit' | 'settingsPresetUpdated' | 'managePresetLabel' | 'managePresetPlaceholder' | 'managePresetEmpty';
|
|
7
|
+
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
8
|
+
interface LocaleNamespaceMap {
|
|
9
|
+
/** The roundtable topology tab + settings page copy. */
|
|
10
|
+
roundtable: RoundTableKey;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export declare const zh: Record<RoundTableKey, string>;
|
|
14
|
+
export declare const en: Record<RoundTableKey, string>;
|