@y9931666/dsh-plugin-crossfire 0.1.0 → 0.1.1

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/lib/client.js CHANGED
@@ -76,7 +76,7 @@ async function fetchReviews(session, signal) {
76
76
  try {
77
77
  const query = session === "" ? "" : `?session=${encodeURIComponent(session)}`;
78
78
  const response = await fetch(`${STATE_ROUTE}${query}`, {
79
- cache: "no-store",
79
+ cache: "no-cache",
80
80
  ...signal === void 0 ? {} : { signal }
81
81
  });
82
82
  const body = await response.json();
@@ -222,9 +222,180 @@ function routeText(provider, model) {
222
222
  return [provider, model].filter((part) => part !== void 0 && part.trim() !== "").join(" / ");
223
223
  }
224
224
 
225
+ //#endregion
226
+ //#region src/client/defectTitle.ts
227
+ /**
228
+ * 观点标题:一条缺陷卡片的"抬头"。
229
+ *
230
+ * 红队的报告信封要求每条发言以 `观点 N(指向 X,维度Y):正文` 开头,而这个前缀
231
+ * 恰恰就是最有信息量的一行(编号 + 定位 + 维度)。用户反馈:"观点虽然说我可以理解,
232
+ * 把完整内容给写出来了,但我觉得还是要在最开始那里立一个标题,这样目的明确"。
233
+ *
234
+ * ## 为什么单独一个模块
235
+ *
236
+ * 标题按钮是**折叠态唯一可见的内容**,所以这里的两条边界(空括号信封、内嵌换行)
237
+ * 一旦退化,肉眼看到的是"整张卡片塌成一团",而不是一条错误文案。它是纯字符串函数,
238
+ * 因此抽出来直接单测 —— 不需要 jsdom / react 测试环境(本仓库只有 `node:test`)。
239
+ *
240
+ * 兜底顺序:① 用信封前缀(`观点 N(指向 X,维度Y)`)→ ② 未按格式提交时取首句
241
+ * (按换行、再按中英文句末标点切,限长)。**绝不**返回空串 —— 标题位空了比不显示更糟。
242
+ */
243
+ /** 标题硬上限(字符)。超过即截断并加省略号。 */
244
+ const TITLE_MAX = 80;
245
+ /**
246
+ * 信封前缀:`观点 N(…)` / `观点N(…)`。
247
+ *
248
+ * 括号内必须**至少有一个既非空白、也非右括号的字符**:`观点 1()`、`观点 1( )`、
249
+ * `观点 1(\u3000)` 是"格式没填",不是"合法信封"。旧写法 `[^))]*` 会把它们当成命中
250
+ * 直接返回,于是标题退化成无信息串、还绕过了首句兜底。
251
+ *
252
+ * 首个字符的字符类是 `[^))\s]` 而不是 `\S`:`\S` 能匹配**右括号本身**,于是
253
+ * `观点 1()真正的首句。` 会靠回溯让 `\S` 吃掉 `)`、再由 `[^))]*[))]` 一路吃到
254
+ * 下一个 `)`(跨过右括号),把一条没按格式提交的发言重新判成合法信封。
255
+ * 两侧的 `\s*` 只作用于**括号内首尾的空白**,正文与冒号之间仍允许空格(`观点 1(指向 y):`)。
256
+ */
257
+ const ENVELOPE = /^(观点\s*\d+\s*[((]\s*[^))\s]\s*[^))]*[))])\s*[::]?/;
258
+ /**
259
+ * 从一条缺陷正文里导出标题。
260
+ *
261
+ * @param content 缺陷原文(可以是任意多行文本)。
262
+ * @returns 非空标题:信封前缀,或首句/首行,最长 {@link TITLE_MAX} 字符。
263
+ */
264
+ function defectTitle(content) {
265
+ const text = content.trim();
266
+ const envelope = ENVELOPE.exec(text);
267
+ if (envelope !== null && envelope[1] !== void 0) return take(envelope[1].trim(), TITLE_MAX);
268
+ const firstLine = text.split(/\r?\n/)[0] ?? text;
269
+ const title = (firstLine.split(/(?<=[。..;;!!??])/)[0] ?? firstLine).trim();
270
+ if (title === "") return take(text, TITLE_MAX);
271
+ return take(title, TITLE_MAX);
272
+ }
273
+ /**
274
+ * 截断到 `max` 个字符;被截断时补省略号。
275
+ *
276
+ * 省略号只在**真的截掉了内容**时加:`text` 恰好 80 字时不该显示一个假的"还有更多"。
277
+ */
278
+ function take(text, max) {
279
+ return text.length > max ? `${text.slice(0, max)}…` : text;
280
+ }
281
+
282
+ //#endregion
283
+ //#region src/client/session-cache.ts
284
+ /**
285
+ * 会话身份的**浏览器侧缓存**(设置页保存失败的真实修复)。
286
+ *
287
+ * ## 它修的是什么
288
+ *
289
+ * `crossfire/prefs.set` 在宿主侧要求"必须声称一个身份"(`judgeActor(undefined, session)`)。
290
+ * 而宿主的 slot 契约决定了设置页**结构上拿不到** `sessionId` —— 运行时 slot 目录里:
291
+ *
292
+ * conversation.view → standardProps 含 `sessionId: SessionId`
293
+ * tool.call.toolview → 含
294
+ * settings.section → **不含**(只有 useResource / useWorkspaces / usePanelInfo /
295
+ * useSessions / useSessionStatus / useSessionRetainInfo)
296
+ *
297
+ * 于是卡片(conversation.view / toolcall)保存偏好成功,而设置页的每一次保存都被
298
+ * 拒成"保存失败"—— Bug 的真实形态:**角色预设 / 阵容预设 / 强度 / 预算全部存不进去**。
299
+ *
300
+ * ## 为什么用一个模块级缓存而不是给设置页造假身份
301
+ *
302
+ * 设置页确实是全局的(偏好也是全局的),但它不能在**不知道自己是谁**的情况下写入。
303
+ * 而会话标签页(`conversation.view`)与轨迹卡片(`tool.call.toolview`)都拿到了宿主
304
+ * 注入的真实 `sessionId` —— 用户打开过任意一个,就等于把身份告诉了插件。这里把它记下来,
305
+ * 供设置页在提交时声明。
306
+ *
307
+ * ## 诚实的边界
308
+ *
309
+ * 冷启动后**直接**打开设置页、且本次会话从未渲染过上述任一入口时,缓存是空的,
310
+ * 保存仍会被宿主拒绝(这是刻意的:插件不编造身份)。绕开方式很简单——先看一眼
311
+ * 「针锋」标签页或对话卡片,再回来保存。
312
+ */
313
+ /** 最近一次从宿主注入里观察到的会话 id(空串 = 还不知道)。 */
314
+ let lastKnownSessionId = "";
315
+ /**
316
+ * 记下宿主注入的会话 id。
317
+ *
318
+ * 只接受非空字符串;重复值不产生副作用。返回是否**发生了变化**,便于调用方避免
319
+ * 无意义的重渲染。
320
+ */
321
+ function rememberSessionId(value) {
322
+ if (typeof value !== "string") return false;
323
+ const trimmed = value.trim();
324
+ if (trimmed === "" || trimmed === lastKnownSessionId) return false;
325
+ lastKnownSessionId = trimmed;
326
+ return true;
327
+ }
328
+ /** 当前已知的会话 id(空串 = 尚未观察到,调用方不得据此编造一个)。 */
329
+ function knownSessionId() {
330
+ return lastKnownSessionId;
331
+ }
332
+ /**
333
+ * 把一个 payload 补上会话身份(已有 `session` 时不覆盖)。
334
+ *
335
+ * 这是设置页真正用得上的那个函数:它只在**确实知道**身份时才附加字段,否则原样返回,
336
+ * 让宿主按它自己的规则拒绝 —— 而不是伪造一个 `session: ''` 去骗过存在性检查。
337
+ */
338
+ function withSessionIdentity(payload) {
339
+ if ("session" in payload) return payload;
340
+ const known = knownSessionId();
341
+ if (known === "") return payload;
342
+ return {
343
+ ...payload,
344
+ session: known
345
+ };
346
+ }
347
+
348
+ //#endregion
349
+ //#region src/client/promptView.ts
350
+ /**
351
+ * 点击"查看原提示词":无条目 → 写入 `loading` 并要求发请求;有条目 → 删除该条目
352
+ * (收起),`start` 为 false。
353
+ *
354
+ * 三态一律同样对待:`ready` / `error` / 正在 `loading` 的条目,再点都是收起。
355
+ * 失败后保留 `error` 条目正是为此 —— 用户再点即收起,而不是无限重拉。
356
+ */
357
+ function togglePromptView(table, key) {
358
+ if (table[key] !== void 0) {
359
+ const next = { ...table };
360
+ delete next[key];
361
+ return {
362
+ table: next,
363
+ start: false
364
+ };
365
+ }
366
+ return {
367
+ table: {
368
+ ...table,
369
+ [key]: { status: "loading" }
370
+ },
371
+ start: true
372
+ };
373
+ }
374
+ /**
375
+ * 把一次 RPC 结果写回展开表。
376
+ *
377
+ * **只有该键仍处于 `loading` 时才写回**。用户已收起(键不存在)、或该键已经处于
378
+ * 终态时,返回原表**引用不变** —— 这样 `setState` 不会触发一次无意义的重渲染。
379
+ */
380
+ function applyPromptResult(table, key, result) {
381
+ const current = table[key];
382
+ if (current === void 0 || current.status !== "loading") return table;
383
+ const next = result.ok ? {
384
+ status: "ready",
385
+ persona: result.persona
386
+ } : {
387
+ status: "error",
388
+ message: result.message
389
+ };
390
+ return {
391
+ ...table,
392
+ [key]: next
393
+ };
394
+ }
395
+
225
396
  //#endregion
226
397
  //#region \0dsh-css:F:\AI\DSH\1changyong\dsh-plugin-crossfire\src\client\ReviewCard.module.css.mjs
227
- const css$1 = "/*\n * Crossfire Chat card styles.\n *\n * 硬约束:**每个颜色都来自主题令牌**(`Theme.dsefkao_listTokens` 的 `--dsw-alias-*` 一族),\n * 所以浅色与深色主题都无需自己写 override。圆桌的设置页 CSS 用了硬编码\n * `#ffffff` / `rgba(0,0,0,.8)`,在深色主题下是坏的 —— 这里不重复那个错误。\n */\n\n.dsefkao_root {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.dsefkao_card {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dsefkao_card + .dsefkao_card {\n margin-top: 10px;\n padding-top: 10px;\n border-top: 1px dashed var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n}\n\n/* 折叠的评审:一行摘要,点一下切换到它。卡片会列出本会话的全部评审,全展开\n 就是一堵墙(验收期间真实攒到 5 场、12 条缺陷)。 */\n.dsefkao_collapsed {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n margin-top: 6px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font: inherit;\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n\n.dsefkao_collapsed:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n.dsefkao_header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_title {\n font-weight: 700;\n font-size: 13px;\n letter-spacing: 0.02em;\n}\n\n.dsefkao_subjectTitle {\n font-weight: 600;\n}\n\n.dsefkao_badge {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_badge_draft {\n border-color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.5));\n}\n\n.dsefkao_badge_attacking {\n border-color: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_badge_judging {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-brand-primary, inherit);\n}\n\n.dsefkao_badge_settled {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n}\n\n/* ---- 状态区:当前处于哪一态 + 下一步该做什么(唯一主行动) ----------------\n *\n * 这是卡片上**唯一**的强对比区域,其余段落一律保持低对比(标题灰、正文常规)。\n * 主行动是这一区里唯一的主色实心按钮,所以\"现在该干什么\"不需要读完全卡去找。\n */\n.dsefkao_statusStrip {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* 左侧信息块:态名 + 轮次 + 一句话说明;吃掉剩余宽度,主行动因此被推到右侧。 */\n.dsefkao_statusStripInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1 1 240px;\n min-width: 0;\n}\n\n.dsefkao_statusStripHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 态名用反色实心药丸:它与主行动按钮同色,一眼就能配对(\"现在在这里 → 下一步在那里\")。 */\n.dsefkao_statusState {\n font-size: 12px;\n font-weight: 600;\n padding: 2px 10px;\n border-radius: 999px;\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_statusHint {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 右侧行动块:一个主按钮 + 一行\"下一步\"小标题。 */\n.dsefkao_statusStripAction {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n margin-left: auto;\n}\n\n.dsefkao_statusNextLabel {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 主行动:唯一的主色实心按钮。字号与内边距都比次要按钮大一档,视觉权重随之提高。 */\n.dsefkao_cta {\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n padding: 7px 18px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_cta:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_statusError {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n word-break: break-word;\n}\n\n.dsefkao_statusOk {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n word-break: break-word;\n}\n\n.dsefkao_section {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ---- 品牌块(provider 缩写 + 品牌色) ----\n *\n * 底色是**第三方商标色**(深色主题下也一样),文字固定白色 —— 这是本插件里唯一\n * 不使用主题令牌的颜色。面积被刻意压到 18px 见方:它是身份标记,不是信息主体,\n * 主题(浅色/深色)不影响其可读性。\n */\n.dsefkao_brandBlock {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 18px;\n height: 18px;\n border-radius: 5px;\n font-size: 9px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n.dsefkao_sectionTitle {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_subject,\n.dsefkao_stance {\n white-space: pre-wrap;\n word-break: break-word;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_stance {\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n/* 立场编辑框:左侧那根品牌色竖线是刻意的 —— 它是\"锋\"的视觉标记,提醒用户\n 这段文字会被红队逐条对攻。 */\n.dsefkao_stanceInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_stanceInput:disabled {\n opacity: 0.7;\n}\n\n.dsefkao_chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n align-items: center;\n}\n\n.dsefkao_chip {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_chipWarn {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_chipStrike {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.6));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n.dsefkao_expertRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 2px 0;\n}\n\n.dsefkao_expertName {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_impactBar {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n font-size: 12px;\n}\n\n.dsefkao_countEndorsed {\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n font-weight: 600;\n}\n\n.dsefkao_countRejected {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n font-weight: 600;\n}\n\n.dsefkao_countPending {\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_countStrike {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.dsefkao_defect {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_defectHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 11px;\n}\n\n.dsefkao_quote {\n font-size: 12px;\n padding-left: 8px;\n border-left: 2px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_defectContent {\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.dsefkao_evidence {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.dsefkao_evidenceLabel {\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 复审核对区:用虚线框与\"待裁决\"的实线框区分开 —— 它问的是另一个问题\n (修没修),而不是\"这条缺陷成不成立\"。 */\n.dsefkao_recheckBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px dashed var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n}\n\n.dsefkao_recheckHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 11px;\n}\n\n.dsefkao_rejectReason {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_btnPrimary,\n.dsefkao_btnPrimaryOn,\n.dsefkao_btnDanger,\n.dsefkao_btnDangerOn,\n.dsefkao_btnGhost {\n font: inherit;\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_btnPrimaryOn {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnDangerOn {\n border-color: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnGhost {\n border-color: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_btnPrimary:disabled,\n.dsefkao_btnPrimaryOn:disabled,\n.dsefkao_btnDanger:disabled,\n.dsefkao_btnDangerOn:disabled,\n.dsefkao_btnGhost:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_rejectBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.4));\n}\n\n.dsefkao_rejectPrompt {\n font-size: 12px;\n font-weight: 600;\n}\n\n.dsefkao_rejectInput {\n font: inherit;\n font-size: 12px;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_link {\n align-self: flex-start;\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n color: var(--dsw-alias-brand-primary, #4a6cf7);\n cursor: pointer;\n}\n\n.dsefkao_footer {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.25));\n}\n\n.dsefkao_footerNote {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_muted {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_warn {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_notice {\n font-size: 12px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.5));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n.dsefkao_empty {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ==========================================================================\n * 用户反馈驱动的视觉改造(分区 / 三色表态 / 专家身份 / 可关闭说明 / 折叠按钮)\n *\n * 核心目标:**次要信息让位,主要信息抓得住**。手段只有三种 —— 分隔线、颜色语义、\n * 按钮形状。刻意不引入背景色块堆叠:那只会让\"一堵墙\"变成\"一堵彩色的墙\"。\n * 颜色仍然全部来自主题令牌(品牌色块是唯一例外,见 brand.dsefkao_ts)。\n * ========================================================================== */\n\n/* ---- 分区:带竖线的小标题 + 硬分隔线 ---- */\n\n.dsefkao_sectionDivider {\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.28));\n padding-top: 10px;\n margin-top: 2px;\n}\n\n.dsefkao_sectionHeadRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 分区标题:左侧品牌色竖线 + 加粗,成为扫读时的视觉锚点。 */\n.dsefkao_sectionTitleStrong {\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.04em;\n padding-left: 8px;\n border-left: 3px solid var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_sectionCount {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 展开 / 折叠:做成按钮,不要看起来像正文链接 ---- */\n\n.dsefkao_foldButton {\n font: inherit;\n font-size: 11px;\n padding: 2px 10px;\n border-radius: 999px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_foldButton:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 可关闭的解释性说明 ---- */\n\n.dsefkao_explain {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainText {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.dsefkao_explainClose {\n flex: none;\n font: inherit;\n font-size: 11px;\n line-height: 1;\n padding: 3px 7px;\n border-radius: 6px;\n cursor: pointer;\n border: 1px solid transparent;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainClose:hover {\n border-color: var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 专家:头像 + 名称 + 维度 + 状态 ---- */\n\n.dsefkao_expertCard {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_expertHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 专家名用正文色与加粗:它是\"这条观点是谁提的\"的答案,必须比元数据更显眼。 */\n.dsefkao_expertNameStrong {\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 缺陷子卡抬头里的作者标识(头像 + 名称)。 */\n.dsefkao_defectAuthor {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 三态表态:绿 / 红 / 灰 -----------------------------------------------\n *\n * 用户要求:三个都做成按钮形状并各对应颜色;点中之后整块填充该颜色、文字变白。\n * 未点中 = 该颜色的描边 + 同色文字(颜色语义仍在,但不抢主行动的视觉权重)。\n */\n.dsefkao_verdictButton {\n font: inherit;\n font-size: 12px;\n font-weight: 600;\n padding: 4px 14px;\n border-radius: 8px;\n cursor: pointer;\n background: transparent;\n}\n\n.dsefkao_verdictEndorse {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n}\n\n.dsefkao_verdictEndorseOn {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n background: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictReject {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.dsefkao_verdictRejectOn {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n background: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictPending {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.5));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_verdictPendingOn {\n border: 1px solid var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n background: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictButton:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n/* ---- 立场全貌(可滚动) ---- */\n\n.dsefkao_stanceFull {\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 320px;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 8px;\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* ---- 角色定位编辑器 ---- */\n\n.dsefkao_roleEditor {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.dsefkao_roleInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n";
398
+ const css$1 = "/*\n * Crossfire Chat card styles.\n *\n * 硬约束:**每个颜色都来自主题令牌**(`Theme.dsefkao_listTokens` 的 `--dsw-alias-*` 一族),\n * 所以浅色与深色主题都无需自己写 override。圆桌的设置页 CSS 用了硬编码\n * `#ffffff` / `rgba(0,0,0,.8)`,在深色主题下是坏的 —— 这里不重复那个错误。\n */\n\n.dsefkao_root {\n display: flex;\n flex-direction: column;\n gap: 10px;\n padding: 12px 14px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.dsefkao_card {\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.dsefkao_card + .dsefkao_card {\n margin-top: 10px;\n padding-top: 10px;\n border-top: 1px dashed var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n}\n\n/* 折叠的评审:一行摘要,点一下切换到它。卡片会列出本会话的全部评审,全展开\n 就是一堵墙(验收期间真实攒到 5 场、12 条缺陷)。 */\n.dsefkao_collapsed {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n margin-top: 6px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n font: inherit;\n font-size: 12px;\n text-align: left;\n cursor: pointer;\n}\n\n.dsefkao_collapsed:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n.dsefkao_header {\n display: flex;\n align-items: baseline;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_title {\n font-weight: 700;\n font-size: 13px;\n letter-spacing: 0.02em;\n}\n\n.dsefkao_subjectTitle {\n font-weight: 600;\n}\n\n.dsefkao_badge {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_badge_draft {\n border-color: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.5));\n}\n\n.dsefkao_badge_attacking {\n border-color: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_badge_judging {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-brand-primary, inherit);\n}\n\n.dsefkao_badge_settled {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n}\n\n/* ---- 状态区:当前处于哪一态 + 下一步该做什么(唯一主行动) ----------------\n *\n * 这是卡片上**唯一**的强对比区域,其余段落一律保持低对比(标题灰、正文常规)。\n * 主行动是这一区里唯一的主色实心按钮,所以\"现在该干什么\"不需要读完全卡去找。\n */\n.dsefkao_statusStrip {\n display: flex;\n align-items: flex-start;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* 左侧信息块:态名 + 轮次 + 一句话说明;吃掉剩余宽度,主行动因此被推到右侧。 */\n.dsefkao_statusStripInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex: 1 1 240px;\n min-width: 0;\n}\n\n.dsefkao_statusStripHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 态名用反色实心药丸:它与主行动按钮同色,一眼就能配对(\"现在在这里 → 下一步在那里\")。\n *\n * 底色**按状态取色**,与抬头 badge 同源(draft=idle 灰 / attacking=warn 黄 /\n * judging=brand 蓝 / settled=success 绿)—— 此前这里恒为 brand 蓝,于是同一句\n * 「攻击中」在抬头是黄、在状态区是蓝,药丸对四态零区分,\"与主行动同色即可配对\"\n * 自然不成立。\n *\n * 缺省值就是 judging(品牌色,与主行动按钮同色);其余三态由下面的修饰类覆盖背景,\n * 文字色四态共用一个反色。四个状态名都来自调用点同一张\n * `styles[`statusState_${review.dsefkao_status}`]` 查表,颜色一律走 `--dsw-alias-*` 令牌。 */\n.dsefkao_statusState {\n font-size: 12px;\n font-weight: 600;\n padding: 2px 10px;\n border-radius: 999px;\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n/* draft:与 .dsefkao_badge_draft 同源(idle 灰)。 */\n.dsefkao_statusState_draft {\n background: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n}\n\n/* attacking:与 .dsefkao_badge_attacking 同源(warn 黄)。 */\n.dsefkao_statusState_attacking {\n background: var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.9));\n}\n\n/* settled:与 .dsefkao_badge_settled 同源(success 绿)。 */\n.dsefkao_statusState_settled {\n background: var(--dsw-alias-state-success-primary, rgba(46, 125, 50, 0.9));\n}\n\n.dsefkao_statusHint {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 右侧行动块:一个主按钮 + 一行\"下一步\"小标题。 */\n.dsefkao_statusStripAction {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n gap: 4px;\n margin-left: auto;\n}\n\n.dsefkao_statusNextLabel {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* 主行动:唯一的主色实心按钮。字号与内边距都比次要按钮大一档,视觉权重随之提高。 */\n.dsefkao_cta {\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n padding: 7px 18px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_cta:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_statusError {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-error-primary, #d9534f);\n word-break: break-word;\n}\n\n.dsefkao_statusOk {\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n word-break: break-word;\n}\n\n.dsefkao_section {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ---- 品牌块(provider 缩写 + 品牌色) ----\n *\n * 底色是**第三方商标色**(深色主题下也一样),文字固定白色 —— 这是本插件里唯一\n * 不使用主题令牌的颜色。面积被刻意压到 18px 见方:它是身份标记,不是信息主体,\n * 主题(浅色/深色)不影响其可读性。\n */\n.dsefkao_brandBlock {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: 18px;\n height: 18px;\n border-radius: 5px;\n font-size: 9px;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: #ffffff;\n line-height: 1;\n user-select: none;\n}\n\n.dsefkao_sectionTitle {\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.04em;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_subject,\n.dsefkao_stance {\n white-space: pre-wrap;\n word-break: break-word;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_stance {\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n/* 立场编辑框:左侧那根品牌色竖线是刻意的 —— 它是\"锋\"的视觉标记,提醒用户\n 这段文字会被红队逐条对攻。 */\n.dsefkao_stanceInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_stanceInput:disabled {\n opacity: 0.7;\n}\n\n.dsefkao_chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n align-items: center;\n}\n\n.dsefkao_chip {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_chipWarn {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-warn-primary, rgba(200, 150, 0, 0.6));\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_chipStrike {\n font-size: 11px;\n padding: 1px 8px;\n border-radius: 999px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.6));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n.dsefkao_expertRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n padding: 2px 0;\n}\n\n.dsefkao_expertName {\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_impactBar {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n font-size: 12px;\n}\n\n.dsefkao_countEndorsed {\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n font-weight: 600;\n}\n\n.dsefkao_countRejected {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n font-weight: 600;\n}\n\n.dsefkao_countPending {\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_countStrike {\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.dsefkao_defect {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_defectHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 11px;\n}\n\n.dsefkao_quote {\n font-size: 12px;\n padding-left: 8px;\n border-left: 2px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_defectContent {\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n/* ---- 观点标题 + 折叠 ----\n *\n * 用户诉求:①\"每条观点都要在开头立一个标题,这样目的明确\";②\"每条观点都可以折叠,\n * 折叠后只显示标题和对应专家\"。\n *\n * 标题做成**按钮**而不是纯文字:整行可点即展开/收起,省掉一个额外的折叠控件;视觉上\n * 它是这一条的抬头(比正文重一档),折叠态下也就是唯一可见的内容。\n */\n.dsefkao_defectTitle {\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n line-height: 1.5;\n text-align: left;\n width: 100%;\n padding: 2px 0;\n border: none;\n background: transparent;\n color: var(--dsw-alias-label-primary, inherit);\n cursor: pointer;\n /* 折叠态下标题是这一条**唯一可见**的内容,所以它必须永远是单行:标题由\n `defectTitle()` 生成、硬上限 80 字符,13px 下 80 个汉字约 1040px,远超卡片列宽,\n 不约束就会折成 2–4 行,折叠体随之长回大块。 */\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n /* 长英文 token(文件名 / URL)没有可断点:nowrap 下它只会被省略号吃掉,但补上\n `anywhere` 后即使将来放宽 nowrap 也不会横向溢出。 */\n overflow-wrap: anywhere;\n}\n\n.dsefkao_defectTitle:hover {\n color: var(--dsw-alias-brand-primary, #4a6cf7);\n}\n\n.dsefkao_evidence {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.dsefkao_evidenceLabel {\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 复审核对区:用虚线框与\"待裁决\"的实线框区分开 —— 它问的是另一个问题\n (修没修),而不是\"这条缺陷成不成立\"。 */\n.dsefkao_recheckBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px dashed var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n}\n\n.dsefkao_recheckHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n font-size: 11px;\n}\n\n.dsefkao_rejectReason {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_actions {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n.dsefkao_btnPrimary,\n.dsefkao_btnPrimaryOn,\n.dsefkao_btnDanger,\n.dsefkao_btnDangerOn,\n.dsefkao_btnGhost {\n font: inherit;\n font-size: 12px;\n padding: 4px 12px;\n border-radius: 8px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-layer-1, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_btnPrimaryOn {\n border-color: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnDangerOn {\n border-color: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, inherit);\n font-weight: 600;\n}\n\n.dsefkao_btnGhost {\n border-color: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_btnPrimary:disabled,\n.dsefkao_btnPrimaryOn:disabled,\n.dsefkao_btnDanger:disabled,\n.dsefkao_btnDangerOn:disabled,\n.dsefkao_btnGhost:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n.dsefkao_rejectBox {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.4));\n}\n\n.dsefkao_rejectPrompt {\n font-size: 12px;\n font-weight: 600;\n}\n\n.dsefkao_rejectInput {\n font: inherit;\n font-size: 12px;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n.dsefkao_link {\n align-self: flex-start;\n border: none;\n background: none;\n padding: 0;\n font: inherit;\n font-size: 12px;\n color: var(--dsw-alias-brand-primary, #4a6cf7);\n cursor: pointer;\n}\n\n.dsefkao_footer {\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n padding-top: 4px;\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.25));\n}\n\n.dsefkao_footerNote {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_muted {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_warn {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary, inherit);\n}\n\n.dsefkao_notice {\n font-size: 12px;\n padding: 6px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-state-error-primary, rgba(217, 83, 79, 0.5));\n color: var(--dsw-alias-state-error-primary, inherit);\n}\n\n.dsefkao_empty {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n/* ==========================================================================\n * 用户反馈驱动的视觉改造(分区 / 三色表态 / 专家身份 / 可关闭说明 / 折叠按钮)\n *\n * 核心目标:**次要信息让位,主要信息抓得住**。手段只有三种 —— 分隔线、颜色语义、\n * 按钮形状。刻意不引入背景色块堆叠:那只会让\"一堵墙\"变成\"一堵彩色的墙\"。\n * 颜色仍然全部来自主题令牌(品牌色块是唯一例外,见 brand.dsefkao_ts)。\n * ========================================================================== */\n\n/* ---- 分区:带竖线的小标题 + 硬分隔线 ---- */\n\n.dsefkao_sectionDivider {\n border-top: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.28));\n padding-top: 10px;\n margin-top: 2px;\n}\n\n.dsefkao_sectionHeadRow {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 分区标题:左侧品牌色竖线 + 加粗,成为扫读时的视觉锚点。 */\n.dsefkao_sectionTitleStrong {\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 0.04em;\n padding-left: 8px;\n border-left: 3px solid var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n.dsefkao_sectionCount {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 展开 / 折叠:做成按钮,不要看起来像正文链接 ---- */\n\n.dsefkao_foldButton {\n font: inherit;\n font-size: 11px;\n padding: 2px 10px;\n border-radius: 999px;\n cursor: pointer;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n background: var(--dsw-alias-bg-layer-2, transparent);\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_foldButton:hover {\n border-color: var(--dsw-alias-brand-primary, #4a6cf7);\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 可关闭的解释性说明 ---- */\n\n.dsefkao_explain {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainText {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.dsefkao_explainClose {\n flex: none;\n font: inherit;\n font-size: 11px;\n line-height: 1;\n padding: 3px 7px;\n border-radius: 6px;\n cursor: pointer;\n border: 1px solid transparent;\n background: transparent;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_explainClose:hover {\n border-color: var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.45));\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* ---- 专家:头像 + 名称 + 维度 + 状态 ---- */\n\n.dsefkao_expertCard {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px 10px;\n border-radius: 8px;\n border: 1px solid var(--dsw-alias-border-l1, rgba(128, 128, 128, 0.3));\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n.dsefkao_expertHead {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n}\n\n/* 专家名用正文色与加粗:它是\"这条观点是谁提的\"的答案,必须比元数据更显眼。 */\n.dsefkao_expertNameStrong {\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary, inherit);\n}\n\n/* 缺陷子卡抬头里的作者标识(头像 + 名称)。 */\n.dsefkao_defectAuthor {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n/* ---- 三态表态:绿 / 红 / 灰 -----------------------------------------------\n *\n * 用户要求:三个都做成按钮形状并各对应颜色;点中之后整块填充该颜色、文字变白。\n * 未点中 = 该颜色的描边 + 同色文字(颜色语义仍在,但不抢主行动的视觉权重)。\n */\n.dsefkao_verdictButton {\n font: inherit;\n font-size: 12px;\n font-weight: 600;\n padding: 4px 14px;\n border-radius: 8px;\n cursor: pointer;\n background: transparent;\n}\n\n.dsefkao_verdictEndorse {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-state-success-primary, #2e7d32);\n}\n\n.dsefkao_verdictEndorseOn {\n border: 1px solid var(--dsw-alias-state-success-primary, #2e7d32);\n background: var(--dsw-alias-state-success-primary, #2e7d32);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictReject {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-state-error-primary, #d9534f);\n}\n\n.dsefkao_verdictRejectOn {\n border: 1px solid var(--dsw-alias-state-error-primary, #d9534f);\n background: var(--dsw-alias-state-error-primary, #d9534f);\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictPending {\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.5));\n color: var(--dsw-alias-label-secondary, inherit);\n}\n\n.dsefkao_verdictPendingOn {\n border: 1px solid var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n background: var(--dsw-alias-state-idle-primary, rgba(128, 128, 128, 0.85));\n color: var(--dsw-alias-bg-base, #ffffff);\n}\n\n.dsefkao_verdictButton:disabled {\n cursor: progress;\n opacity: 0.6;\n}\n\n/* ---- 立场全貌(可滚动) ---- */\n\n.dsefkao_stanceFull {\n white-space: pre-wrap;\n word-break: break-word;\n max-height: 320px;\n overflow: auto;\n padding: 8px 10px;\n border-radius: 8px;\n border-left: 2px solid var(--dsw-alias-brand-primary, #4a6cf7);\n background: var(--dsw-alias-bg-layer-2, transparent);\n}\n\n/* ---- 角色定位编辑器 ---- */\n\n.dsefkao_roleEditor {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.dsefkao_roleInput {\n font: inherit;\n font-size: 12px;\n line-height: 1.6;\n padding: 6px 8px;\n border-radius: 6px;\n border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.4));\n background: var(--dsw-alias-bg-base, transparent);\n color: var(--dsw-alias-label-primary, inherit);\n resize: vertical;\n}\n\n";
228
399
  const classMap$1 = {
229
400
  "listTokens": "dsefkao_listTokens",
230
401
  "root": "dsefkao_root",
@@ -241,7 +412,11 @@ const classMap$1 = {
241
412
  "statusStrip": "dsefkao_statusStrip",
242
413
  "statusStripInfo": "dsefkao_statusStripInfo",
243
414
  "statusStripHead": "dsefkao_statusStripHead",
415
+ "status": "dsefkao_status",
244
416
  "statusState": "dsefkao_statusState",
417
+ "statusState_draft": "dsefkao_statusState_draft",
418
+ "statusState_attacking": "dsefkao_statusState_attacking",
419
+ "statusState_settled": "dsefkao_statusState_settled",
245
420
  "statusHint": "dsefkao_statusHint",
246
421
  "statusStripAction": "dsefkao_statusStripAction",
247
422
  "statusNextLabel": "dsefkao_statusNextLabel",
@@ -269,6 +444,7 @@ const classMap$1 = {
269
444
  "defectHead": "dsefkao_defectHead",
270
445
  "quote": "dsefkao_quote",
271
446
  "defectContent": "dsefkao_defectContent",
447
+ "defectTitle": "dsefkao_defectTitle",
272
448
  "evidence": "dsefkao_evidence",
273
449
  "evidenceLabel": "dsefkao_evidenceLabel",
274
450
  "recheckBox": "dsefkao_recheckBox",
@@ -344,6 +520,7 @@ function CrossfireCard(props) {
344
520
  * (见 rpc.ts 的 judgeActor)。
345
521
  */
346
522
  const sessionId = typeof props.sessionId === "string" ? props.sessionId : "";
523
+ rememberSessionId(sessionId);
347
524
  const [reviews, setReviews] = (0, react.useState)([]);
348
525
  const [note, setNote] = (0, react.useState)(void 0);
349
526
  const [notice, setNotice] = (0, react.useState)(void 0);
@@ -361,6 +538,8 @@ function CrossfireCard(props) {
361
538
  const [stanceExpanded, setStanceExpanded] = (0, react.useState)({});
362
539
  const [recheckApproval, setRecheckApproval] = (0, react.useState)(null);
363
540
  const [dismissedHints, setDismissedHints] = (0, react.useState)({});
541
+ const [expandedDefects, setExpandedDefects] = (0, react.useState)({});
542
+ const [promptViews, setPromptViews] = (0, react.useState)({});
364
543
  const [dismissBusy, setDismissBusy] = (0, react.useState)(false);
365
544
  const alive = (0, react.useRef)(true);
366
545
  /**
@@ -590,7 +769,7 @@ function CrossfireCard(props) {
590
769
  })
591
770
  }));
592
771
  };
593
- /** 导出为 Markdown 并触发下载(文件同时已落盘到评审目录)。 */
772
+ /** 导出为 Markdown 并触发下载(盘上同时落 `export.md` 与带 BOM 的 `export.txt`)。 */
594
773
  const exportReview = async (review) => {
595
774
  setBusy(true);
596
775
  const result = await rpc("crossfire/review.export", {
@@ -604,13 +783,13 @@ function CrossfireCard(props) {
604
783
  reportNotice(`${t("exportFailed")}:${result.error.message}`);
605
784
  return;
606
785
  }
607
- reportOk(`${t("exportDone")}:${result.value.path}`);
786
+ reportOk(result.value.textPath === void 0 || result.value.textPath === "" ? `${t("exportDone")}:${result.value.path}` : `${t("exportDone")}:${result.value.path} + ${result.value.textPath}`);
608
787
  try {
609
- const blob = new Blob([result.value.markdown], { type: "text/markdown;charset=utf-8" });
788
+ const blob = new Blob([result.value.markdown], { type: "text/plain;charset=utf-8" });
610
789
  const url = URL.createObjectURL(blob);
611
790
  const anchor = document.createElement("a");
612
791
  anchor.href = url;
613
- anchor.download = `${review.reviewId}.md`;
792
+ anchor.download = `${review.reviewId}.txt`;
614
793
  anchor.click();
615
794
  URL.revokeObjectURL(url);
616
795
  } catch {}
@@ -682,6 +861,29 @@ function CrossfireCard(props) {
682
861
  if (expert === void 0 || expert.provider === void 0) return fallback;
683
862
  return routeText(expert.provider, expert.model);
684
863
  }
864
+ /**
865
+ * 缺陷排序:**打穿立场的优先**,其次按维度分组,组内保持原始顺序。
866
+ *
867
+ * 这是 §5.3 方向 4 的落地:"按维度分组、按是否打穿立场排序,把最需要裁决的排在最前"。
868
+ * 用 `map + index` 稳定排序,保证同一组内的相对次序与收集时一致(否则每轮表态后
869
+ * 卡片会重排,用户会找不到刚看过的那条)。
870
+ */
871
+ function orderDefects(defects) {
872
+ return defects.map((defect, index) => ({
873
+ defect,
874
+ index
875
+ })).sort((left, right) => {
876
+ const strikeDelta = Number(right.defect.strikesStance === true) - Number(left.defect.strikesStance === true);
877
+ if (strikeDelta !== 0) return strikeDelta;
878
+ const dimensionDelta = left.defect.dimension.localeCompare(right.defect.dimension, "zh");
879
+ if (dimensionDelta !== 0) return dimensionDelta;
880
+ return left.index - right.index;
881
+ }).map((entry) => entry.defect);
882
+ }
883
+ /** 折叠状态的键:`${reviewId}::${defectId}`。 */
884
+ function defectFoldKey(reviewId, defectId) {
885
+ return `${reviewId}::${defectId}`;
886
+ }
685
887
  if (reviews.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
686
888
  className: ReviewCard_module_css_default.root,
687
889
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -777,7 +979,7 @@ function CrossfireCard(props) {
777
979
  className: ReviewCard_module_css_default.statusStripHead,
778
980
  children: [
779
981
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
780
- className: ReviewCard_module_css_default.statusState,
982
+ className: `${ReviewCard_module_css_default.statusState} ${ReviewCard_module_css_default[`statusState_${review.status}`] ?? ""}`,
781
983
  children: t(STATUS_LABEL_KEY[review.status])
782
984
  }),
783
985
  review.status === "judging" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1079,6 +1281,37 @@ function CrossfireCard(props) {
1079
1281
  setRoleEditing(roleKey);
1080
1282
  },
1081
1283
  children: t("expertRoleEdit")
1284
+ }),
1285
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1286
+ type: "button",
1287
+ className: ReviewCard_module_css_default.foldButton,
1288
+ onClick: () => {
1289
+ const viewKey = `${review.reviewId}::${expert.key}`;
1290
+ let start = false;
1291
+ setPromptViews((previous) => {
1292
+ const toggled = togglePromptView(previous, viewKey);
1293
+ start = toggled.start;
1294
+ return toggled.table;
1295
+ });
1296
+ if (!start) return;
1297
+ (async () => {
1298
+ const result = await rpc("crossfire/expert.prompt", {
1299
+ reviewId: review.reviewId,
1300
+ stateRoot: review.stateRoot,
1301
+ session: sessionId,
1302
+ expertKey: expert.key
1303
+ });
1304
+ if (!alive.current) return;
1305
+ setPromptViews((previous) => applyPromptResult(previous, viewKey, result.ok ? {
1306
+ ok: true,
1307
+ persona: result.value.persona
1308
+ } : {
1309
+ ok: false,
1310
+ message: result.error.message
1311
+ }));
1312
+ })();
1313
+ },
1314
+ children: t("expertPromptView")
1082
1315
  })
1083
1316
  ]
1084
1317
  }),
@@ -1086,6 +1319,28 @@ function CrossfireCard(props) {
1086
1319
  className: ReviewCard_module_css_default.muted,
1087
1320
  children: `${t("expertRoleLabel")}:${expert.role}`
1088
1321
  }),
1322
+ (() => {
1323
+ const view = promptViews[`${review.reviewId}::${expert.key}`];
1324
+ if (view === void 0) return null;
1325
+ if (view.status === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1326
+ className: ReviewCard_module_css_default.statusError,
1327
+ children: view.message
1328
+ });
1329
+ if (view.status === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1330
+ className: ReviewCard_module_css_default.muted,
1331
+ children: t("loading")
1332
+ });
1333
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1334
+ className: ReviewCard_module_css_default.section,
1335
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1336
+ className: ReviewCard_module_css_default.sectionTitle,
1337
+ children: t("expertPromptTitle")
1338
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1339
+ className: ReviewCard_module_css_default.stanceFull,
1340
+ children: view.persona
1341
+ })]
1342
+ });
1343
+ })(),
1089
1344
  roleEditing === roleKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1090
1345
  className: ReviewCard_module_css_default.roleEditor,
1091
1346
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
@@ -1183,210 +1438,240 @@ function CrossfireCard(props) {
1183
1438
  review.defects.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1184
1439
  className: ReviewCard_module_css_default.muted,
1185
1440
  children: t("defectEmpty")
1186
- }) : review.defects.map((defect) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1187
- className: ReviewCard_module_css_default.defect,
1188
- children: [
1189
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1190
- className: ReviewCard_module_css_default.defectHead,
1191
- children: [
1192
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1193
- className: ReviewCard_module_css_default.chip,
1194
- children: defect.dimension
1441
+ }) : orderDefects(review.defects).map((defect) => {
1442
+ const foldKey = defectFoldKey(review.reviewId, defect.id);
1443
+ const isOpen = expandedDefects[foldKey] === true;
1444
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1445
+ className: ReviewCard_module_css_default.defect,
1446
+ children: [
1447
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1448
+ className: ReviewCard_module_css_default.defectHead,
1449
+ children: [
1450
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1451
+ className: ReviewCard_module_css_default.chip,
1452
+ children: defect.dimension
1453
+ }),
1454
+ defect.dimensionFallback === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1455
+ className: ReviewCard_module_css_default.chipWarn,
1456
+ children: t("dimensionFallback")
1457
+ }) : null,
1458
+ defect.strikesStance === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1459
+ className: ReviewCard_module_css_default.chipStrike,
1460
+ children: t("strikesStance")
1461
+ }) : null,
1462
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: ReviewCard_module_css_default.spacer }),
1463
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1464
+ className: ReviewCard_module_css_default.defectAuthor,
1465
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1466
+ className: ReviewCard_module_css_default.brandBlock,
1467
+ style: { background: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).color },
1468
+ title: routeOf(authorOf(defect.expertKey, review.experts), t("modelInherit")),
1469
+ children: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).abbr
1470
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1471
+ className: ReviewCard_module_css_default.expertNameStrong,
1472
+ children: defect.expertKey
1473
+ })]
1474
+ }),
1475
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1476
+ type: "button",
1477
+ className: ReviewCard_module_css_default.foldButton,
1478
+ "aria-expanded": isOpen,
1479
+ onClick: () => setExpandedDefects((previous) => {
1480
+ const next = { ...previous };
1481
+ if (next[foldKey] === true) delete next[foldKey];
1482
+ else next[foldKey] = true;
1483
+ return next;
1484
+ }),
1485
+ children: isOpen ? t("collapseSection") : t("expandSection")
1486
+ })
1487
+ ]
1488
+ }),
1489
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1490
+ type: "button",
1491
+ className: ReviewCard_module_css_default.defectTitle,
1492
+ "aria-expanded": isOpen,
1493
+ onClick: () => setExpandedDefects((previous) => {
1494
+ const next = { ...previous };
1495
+ if (next[foldKey] === true) delete next[foldKey];
1496
+ else next[foldKey] = true;
1497
+ return next;
1195
1498
  }),
1196
- defect.dimensionFallback === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1197
- className: ReviewCard_module_css_default.chipWarn,
1198
- children: t("dimensionFallback")
1199
- }) : null,
1200
- defect.strikesStance === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1201
- className: ReviewCard_module_css_default.chipStrike,
1202
- children: t("strikesStance")
1203
- }) : null,
1204
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: ReviewCard_module_css_default.spacer }),
1205
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1206
- className: ReviewCard_module_css_default.defectAuthor,
1207
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1208
- className: ReviewCard_module_css_default.brandBlock,
1209
- style: { background: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).color },
1210
- title: routeOf(authorOf(defect.expertKey, review.experts), t("modelInherit")),
1211
- children: providerBrand(authorOf(defect.expertKey, review.experts)?.provider).abbr
1212
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1213
- className: ReviewCard_module_css_default.expertNameStrong,
1214
- children: defect.expertKey
1215
- })]
1216
- })
1217
- ]
1218
- }),
1219
- defect.quote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1220
- className: ReviewCard_module_css_default.quote,
1221
- children: [
1222
- "“",
1223
- defect.quote,
1224
- "”"
1225
- ]
1226
- }),
1227
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1228
- className: ReviewCard_module_css_default.defectContent,
1229
- children: defect.content
1230
- }),
1231
- defect.evidence === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1232
- className: ReviewCard_module_css_default.evidence,
1233
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1234
- className: ReviewCard_module_css_default.evidenceLabel,
1235
- children: defect.evidence.kind === "repro" ? `[${t("evidenceRepro")}]` : `[${t("evidenceArgument")}]`
1236
- }), defect.evidence.text]
1237
- }),
1238
- defect.recheck === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1239
- className: ReviewCard_module_css_default.recheckBox,
1240
- children: [
1241
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1242
- className: ReviewCard_module_css_default.recheckHead,
1499
+ children: defectTitle(defect.content)
1500
+ }),
1501
+ isOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1502
+ defect.quote === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1503
+ className: ReviewCard_module_css_default.quote,
1504
+ children: [
1505
+ "“",
1506
+ defect.quote,
1507
+ "”"
1508
+ ]
1509
+ }),
1510
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1511
+ className: ReviewCard_module_css_default.defectContent,
1512
+ children: defect.content
1513
+ }),
1514
+ defect.evidence === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1515
+ className: ReviewCard_module_css_default.evidence,
1243
1516
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1244
1517
  className: ReviewCard_module_css_default.evidenceLabel,
1245
- children: t("recheckLabel")
1246
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1247
- className: ReviewCard_module_css_default.muted,
1248
- children: `#${String(defect.recheck.pass)}`
1249
- })]
1518
+ children: defect.evidence.kind === "repro" ? `[${t("evidenceRepro")}]` : `[${t("evidenceArgument")}]`
1519
+ }), defect.evidence.text]
1250
1520
  }),
1251
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1252
- className: ReviewCard_module_css_default.actions,
1253
- children: [RECHECK_VERDICTS.map((verdict) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1521
+ defect.recheck === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1522
+ className: ReviewCard_module_css_default.recheckBox,
1523
+ children: [
1524
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1525
+ className: ReviewCard_module_css_default.recheckHead,
1526
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1527
+ className: ReviewCard_module_css_default.evidenceLabel,
1528
+ children: t("recheckLabel")
1529
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1530
+ className: ReviewCard_module_css_default.muted,
1531
+ children: `#${String(defect.recheck.pass)}`
1532
+ })]
1533
+ }),
1534
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1535
+ className: ReviewCard_module_css_default.actions,
1536
+ children: [RECHECK_VERDICTS.map((verdict) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1537
+ type: "button",
1538
+ disabled: busy,
1539
+ className: defect.recheck?.verdict === verdict ? ReviewCard_module_css_default.btnPrimaryOn : ReviewCard_module_css_default.btnGhost,
1540
+ onClick: () => {
1541
+ setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? "");
1542
+ },
1543
+ children: t(VERDICT_KEY[verdict] ?? "verdictUnfixed")
1544
+ }, verdict)), defect.recheck?.verdict === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1545
+ className: ReviewCard_module_css_default.muted,
1546
+ children: t("verdictUndecided")
1547
+ }) : null]
1548
+ }),
1549
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1550
+ className: ReviewCard_module_css_default.actions,
1551
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1552
+ className: ReviewCard_module_css_default.rejectInput,
1553
+ disabled: busy,
1554
+ value: recheckNotes[defect.id] ?? defect.recheck?.note ?? "",
1555
+ placeholder: t("recheckNote"),
1556
+ onChange: (event) => {
1557
+ const value = event.target.value;
1558
+ setRecheckNotes((previous) => ({
1559
+ ...previous,
1560
+ [defect.id]: value
1561
+ }));
1562
+ }
1563
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1564
+ type: "button",
1565
+ disabled: busy || defect.recheck?.verdict === void 0 || (recheckNotes[defect.id] ?? defect.recheck?.note ?? "") === (defect.recheck?.note ?? ""),
1566
+ title: defect.recheck?.verdict === void 0 ? t("recheckNoteNeedsVerdict") : void 0,
1567
+ className: ReviewCard_module_css_default.btnGhost,
1568
+ onClick: () => {
1569
+ const verdict = defect.recheck?.verdict;
1570
+ if (verdict === void 0) return;
1571
+ setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? defect.recheck?.note ?? "");
1572
+ },
1573
+ children: t("presetSave")
1574
+ })]
1575
+ }),
1576
+ defect.recheck.note === void 0 || defect.recheck.note === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1577
+ className: ReviewCard_module_css_default.muted,
1578
+ children: defect.recheck.note
1579
+ })
1580
+ ]
1581
+ })
1582
+ ] }) : null,
1583
+ defect.status === "rejected" && defect.rejectReason !== void 0 && defect.rejectReason !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1584
+ className: ReviewCard_module_css_default.rejectReason,
1585
+ children: [
1586
+ t("btnReject"),
1587
+ ":",
1588
+ defect.rejectReason
1589
+ ]
1590
+ }) : null,
1591
+ review.status === "settled" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1592
+ className: ReviewCard_module_css_default.actions,
1593
+ children: [
1594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1254
1595
  type: "button",
1255
1596
  disabled: busy,
1256
- className: defect.recheck?.verdict === verdict ? ReviewCard_module_css_default.btnPrimaryOn : ReviewCard_module_css_default.btnGhost,
1257
- onClick: () => {
1258
- setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? "");
1259
- },
1260
- children: t(VERDICT_KEY[verdict] ?? "verdictUnfixed")
1261
- }, verdict)), defect.recheck?.verdict === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1262
- className: ReviewCard_module_css_default.muted,
1263
- children: t("verdictUndecided")
1264
- }) : null]
1265
- }),
1266
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1267
- className: ReviewCard_module_css_default.actions,
1268
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1269
- className: ReviewCard_module_css_default.rejectInput,
1270
- disabled: busy,
1271
- value: recheckNotes[defect.id] ?? defect.recheck?.note ?? "",
1272
- placeholder: t("recheckNote"),
1273
- onChange: (event) => {
1274
- const value = event.target.value;
1275
- setRecheckNotes((previous) => ({
1276
- ...previous,
1277
- [defect.id]: value
1278
- }));
1279
- }
1280
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1281
- type: "button",
1282
- disabled: busy || defect.recheck?.verdict === void 0 || (recheckNotes[defect.id] ?? defect.recheck?.note ?? "") === (defect.recheck?.note ?? ""),
1283
- title: defect.recheck?.verdict === void 0 ? t("recheckNoteNeedsVerdict") : void 0,
1284
- className: ReviewCard_module_css_default.btnGhost,
1597
+ className: defect.status === "endorsed" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorseOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorse}`,
1285
1598
  onClick: () => {
1286
- const verdict = defect.recheck?.verdict;
1287
- if (verdict === void 0) return;
1288
- setVerdict(review, defect, verdict, recheckNotes[defect.id] ?? defect.recheck?.note ?? "");
1599
+ setStatus(review, defect, defect.status === "endorsed" ? "pending" : "endorsed");
1289
1600
  },
1290
- children: t("presetSave")
1291
- })]
1292
- }),
1293
- defect.recheck.note === void 0 || defect.recheck.note === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1294
- className: ReviewCard_module_css_default.muted,
1295
- children: defect.recheck.note
1296
- })
1297
- ]
1298
- }),
1299
- defect.status === "rejected" && defect.rejectReason !== void 0 && defect.rejectReason !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1300
- className: ReviewCard_module_css_default.rejectReason,
1301
- children: [
1302
- t("btnReject"),
1303
- ":",
1304
- defect.rejectReason
1305
- ]
1306
- }) : null,
1307
- review.status === "settled" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1308
- className: ReviewCard_module_css_default.actions,
1309
- children: [
1310
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1311
- type: "button",
1312
- disabled: busy,
1313
- className: defect.status === "endorsed" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorseOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictEndorse}`,
1314
- onClick: () => {
1315
- setStatus(review, defect, defect.status === "endorsed" ? "pending" : "endorsed");
1316
- },
1317
- children: t("btnEndorse")
1318
- }),
1319
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1320
- type: "button",
1321
- disabled: busy,
1322
- className: defect.status === "rejected" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictRejectOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictReject}`,
1323
- onClick: () => {
1324
- if (defect.status === "rejected") {
1325
- setStatus(review, defect, "pending");
1326
- return;
1327
- }
1328
- setRejectingId(defect.id);
1329
- setRejectDraft("");
1330
- },
1331
- children: t("btnReject")
1332
- }),
1333
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1334
- type: "button",
1335
- disabled: busy,
1336
- className: defect.status === "pending" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPendingOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPending}`,
1337
- onClick: () => {
1338
- setStatus(review, defect, "pending");
1339
- },
1340
- children: t("btnPending")
1341
- })
1342
- ]
1343
- }),
1344
- rejectingId === defect.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1345
- className: ReviewCard_module_css_default.rejectBox,
1346
- children: [
1347
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1348
- className: ReviewCard_module_css_default.rejectPrompt,
1349
- children: t("rejectPrompt")
1350
- }),
1351
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1352
- className: ReviewCard_module_css_default.rejectInput,
1353
- value: rejectDraft,
1354
- autoFocus: true,
1355
- rows: 2,
1356
- placeholder: t("rejectPlaceholder"),
1357
- onChange: (event) => setRejectDraft(event.target.value)
1358
- }),
1359
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1360
- className: ReviewCard_module_css_default.actions,
1361
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1601
+ children: t("btnEndorse")
1602
+ }),
1603
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1362
1604
  type: "button",
1363
1605
  disabled: busy,
1364
- className: ReviewCard_module_css_default.btnDangerOn,
1606
+ className: defect.status === "rejected" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictRejectOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictReject}`,
1365
1607
  onClick: () => {
1366
- const reason = rejectDraft.trim();
1367
- if (reason === "") {
1368
- reportNotice(t("rejectPrompt"));
1608
+ if (defect.status === "rejected") {
1609
+ setStatus(review, defect, "pending");
1369
1610
  return;
1370
1611
  }
1371
- setStatus(review, defect, "rejected", reason);
1372
- setRejectingId(null);
1612
+ setRejectingId(defect.id);
1373
1613
  setRejectDraft("");
1374
1614
  },
1375
- children: t("rejectConfirm")
1376
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1615
+ children: t("btnReject")
1616
+ }),
1617
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1377
1618
  type: "button",
1378
- className: ReviewCard_module_css_default.btnGhost,
1619
+ disabled: busy,
1620
+ className: defect.status === "pending" ? `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPendingOn}` : `${ReviewCard_module_css_default.verdictButton} ${ReviewCard_module_css_default.verdictPending}`,
1379
1621
  onClick: () => {
1380
- setRejectingId(null);
1381
- setRejectDraft("");
1622
+ setStatus(review, defect, "pending");
1382
1623
  },
1383
- children: t("rejectCancel")
1384
- })]
1385
- })
1386
- ]
1387
- }) : null
1388
- ]
1389
- }, defect.id)),
1624
+ children: t("btnPending")
1625
+ })
1626
+ ]
1627
+ }),
1628
+ rejectingId === defect.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1629
+ className: ReviewCard_module_css_default.rejectBox,
1630
+ children: [
1631
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1632
+ className: ReviewCard_module_css_default.rejectPrompt,
1633
+ children: t("rejectPrompt")
1634
+ }),
1635
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1636
+ className: ReviewCard_module_css_default.rejectInput,
1637
+ value: rejectDraft,
1638
+ autoFocus: true,
1639
+ rows: 2,
1640
+ placeholder: t("rejectPlaceholder"),
1641
+ onChange: (event) => setRejectDraft(event.target.value)
1642
+ }),
1643
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1644
+ className: ReviewCard_module_css_default.actions,
1645
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1646
+ type: "button",
1647
+ disabled: busy,
1648
+ className: ReviewCard_module_css_default.btnDangerOn,
1649
+ onClick: () => {
1650
+ const reason = rejectDraft.trim();
1651
+ if (reason === "") {
1652
+ reportNotice(t("rejectPrompt"));
1653
+ return;
1654
+ }
1655
+ setStatus(review, defect, "rejected", reason);
1656
+ setRejectingId(null);
1657
+ setRejectDraft("");
1658
+ },
1659
+ children: t("rejectConfirm")
1660
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1661
+ type: "button",
1662
+ className: ReviewCard_module_css_default.btnGhost,
1663
+ onClick: () => {
1664
+ setRejectingId(null);
1665
+ setRejectDraft("");
1666
+ },
1667
+ children: t("rejectCancel")
1668
+ })]
1669
+ })
1670
+ ]
1671
+ }) : null
1672
+ ]
1673
+ }, defect.id);
1674
+ }),
1390
1675
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1391
1676
  className: ReviewCard_module_css_default.footer,
1392
1677
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -2738,6 +3023,8 @@ const zh = {
2738
3023
  expertRoleEdit: "改角色定位",
2739
3024
  expertRoleSaved: "角色定位已保存",
2740
3025
  expertRoleFromRoster: "(来自阵容预设)",
3026
+ expertPromptView: "查看原提示词",
3027
+ expertPromptTitle: "这位专家的原提示词(只读)",
2741
3028
  stanceSectionTitle: "我的立场与理由",
2742
3029
  stanceExpand: "展开看全文",
2743
3030
  stanceCollapse: "收起立场",
@@ -2913,6 +3200,8 @@ const en = {
2913
3200
  expertRoleEdit: "Edit role",
2914
3201
  expertRoleSaved: "Role saved",
2915
3202
  expertRoleFromRoster: "(from a squad preset)",
3203
+ expertPromptView: "View original prompt",
3204
+ expertPromptTitle: "This expert's original prompt (read-only)",
2916
3205
  stanceSectionTitle: "Your position and reasons",
2917
3206
  stanceExpand: "Expand full text",
2918
3207
  stanceCollapse: "Collapse stance",
@@ -2994,8 +3283,11 @@ function apply(ctx) {
2994
3283
  return direct;
2995
3284
  }
2996
3285
  };
3286
+ const settingsRpc = async (endpoint, payload) => {
3287
+ return await rpc(endpoint, payload !== null && typeof payload === "object" && !Array.isArray(payload) ? withSessionIdentity(payload) : payload);
3288
+ };
2997
3289
  const settingsInjected = () => ({
2998
- rpc,
3290
+ rpc: settingsRpc,
2999
3291
  t: ctx.locale.bind(NS)
3000
3292
  });
3001
3293
  slots.inject("settings.section", () => slots.register({