@klarkxy/dsh-safe-auto 0.4.0 → 0.5.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/README.md +24 -7
- package/README.zh-CN.md +22 -5
- package/dsh.plugin.json +2 -2
- package/lib/client.js +413 -30
- package/package.json +5 -5
- package/src/audit.js +707 -0
- package/src/index.js +228 -15
- package/src/reviewer.js +16 -8
- package/src/ui-host.js +6 -0
package/lib/client.js
CHANGED
|
@@ -294,7 +294,14 @@ function useModelCatalog(load, options = {}) {
|
|
|
294
294
|
refresh
|
|
295
295
|
};
|
|
296
296
|
}
|
|
297
|
-
/**
|
|
297
|
+
/**
|
|
298
|
+
* Also accepts a mapped state from a caller that owns broader async orchestration.
|
|
299
|
+
*
|
|
300
|
+
* The dsh-ui-* classes carry the styling and resolve through the consuming
|
|
301
|
+
* plugin's own officialUiCss scope (every consumer mounts its root class, so
|
|
302
|
+
* e.g. `.dsh-model-hub .dsh-ui-loading` paints this); dsh-model-catalog-status
|
|
303
|
+
* stays as a structural hook for tests and layout, never a colour source.
|
|
304
|
+
*/
|
|
298
305
|
function ModelCatalogStatus({ state, locale }) {
|
|
299
306
|
const zh = locale?.toLowerCase().startsWith("zh") === true;
|
|
300
307
|
const copy = zh ? {
|
|
@@ -310,15 +317,17 @@ function ModelCatalogStatus({ state, locale }) {
|
|
|
310
317
|
};
|
|
311
318
|
if (!state.loading && !state.error && state.failures.length === 0) return null;
|
|
312
319
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
313
|
-
className: "dsh-model-catalog-status",
|
|
320
|
+
className: "dsh-model-catalog-status dsh-ui-stack",
|
|
314
321
|
lang: zh ? "zh" : "en",
|
|
315
322
|
children: [
|
|
316
323
|
state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
317
324
|
role: "status",
|
|
325
|
+
className: "dsh-ui-loading",
|
|
318
326
|
children: copy.loading
|
|
319
327
|
}) : null,
|
|
320
328
|
state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
321
329
|
role: "alert",
|
|
330
|
+
className: "dsh-ui-error dsh-ui-wrap",
|
|
322
331
|
children: [
|
|
323
332
|
copy.error,
|
|
324
333
|
": ",
|
|
@@ -327,20 +336,27 @@ function ModelCatalogStatus({ state, locale }) {
|
|
|
327
336
|
}) : null,
|
|
328
337
|
state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
329
338
|
role: "status",
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
failure.
|
|
334
|
-
|
|
339
|
+
className: "dsh-ui-meta",
|
|
340
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
341
|
+
className: "dsh-ui-list",
|
|
342
|
+
children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
343
|
+
failure.name || failure.id,
|
|
344
|
+
": ",
|
|
345
|
+
failure.message
|
|
346
|
+
] }, `${failure.id}:${index}`))
|
|
347
|
+
})]
|
|
335
348
|
}) : null,
|
|
336
|
-
state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
state.
|
|
342
|
-
|
|
343
|
-
|
|
349
|
+
state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
350
|
+
className: "dsh-ui-actions",
|
|
351
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
352
|
+
type: "button",
|
|
353
|
+
size: "sm",
|
|
354
|
+
disabled: state.loading,
|
|
355
|
+
onClick: () => {
|
|
356
|
+
state.refresh();
|
|
357
|
+
},
|
|
358
|
+
children: copy.retry
|
|
359
|
+
})
|
|
344
360
|
}) : null
|
|
345
361
|
]
|
|
346
362
|
});
|
|
@@ -358,7 +374,8 @@ function ModelCatalogStatus({ state, locale }) {
|
|
|
358
374
|
* Sticky group headings, subsequence ranking, highlight walking and focus
|
|
359
375
|
* hand-back all match the host. SYNC POINT: when the host's ModelSelect or
|
|
360
376
|
* ModelSelect.module.css changes, this copy must be re-transcribed; styles
|
|
361
|
-
* below may only reference
|
|
377
|
+
* below may only reference allowlisted theme tokens (the --dsw-* set plus the
|
|
378
|
+
* host's --ds-* motion tokens), never literal colors.
|
|
362
379
|
*
|
|
363
380
|
* It statically imports the primitives, so only browser clients should load
|
|
364
381
|
* this entry; host code and node tests use the core entry (`./index.ts`).
|
|
@@ -866,7 +883,8 @@ function ModelMenu(props) {
|
|
|
866
883
|
* sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
|
|
867
884
|
* elevation at portal z-index 1100. The trigger mirrors the composer's 28px
|
|
868
885
|
* model pill, chevron rotating open. MenuSurface paints fill and backdrop.
|
|
869
|
-
* Only --dsw
|
|
886
|
+
* Only allowlisted --dsw- and --ds- theme tokens may appear here — a literal
|
|
887
|
+
* color is a bug.
|
|
870
888
|
*/
|
|
871
889
|
const modelMenuCss = `
|
|
872
890
|
.dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
|
|
@@ -889,12 +907,14 @@ const modelMenuCss = `
|
|
|
889
907
|
display: flex;
|
|
890
908
|
}
|
|
891
909
|
.dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
892
|
-
.dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0
|
|
910
|
+
.dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
|
|
893
911
|
.dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
|
|
894
912
|
.dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
|
|
895
913
|
.dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
|
|
896
|
-
.dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform
|
|
914
|
+
.dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
|
|
897
915
|
.dsh-model-menu-chevronOpen { transform: rotate(180deg); }
|
|
916
|
+
/* Portal overlay: the host publishes no z-index token, so the card keeps the
|
|
917
|
+
* native menu layer's 1100 verbatim. */
|
|
898
918
|
.dsh-model-menu-menu {
|
|
899
919
|
z-index: 1100;
|
|
900
920
|
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
@@ -920,6 +940,8 @@ const modelMenuCss = `
|
|
|
920
940
|
.dsh-model-menu-searchWithQuery { padding-right: 34px; }
|
|
921
941
|
.dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
|
|
922
942
|
.dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
|
|
943
|
+
/* The clear button is a circle by geometry (border-radius: 50%), a shape the
|
|
944
|
+
* radius token scale does not describe. */
|
|
923
945
|
.dsh-model-menu-searchClear {
|
|
924
946
|
width: 24px;
|
|
925
947
|
height: 24px;
|
|
@@ -1167,6 +1189,17 @@ ${scope} {
|
|
|
1167
1189
|
/* --- Layout scaffolding. --- */
|
|
1168
1190
|
& .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
|
|
1169
1191
|
& .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
|
|
1192
|
+
/* Plugin detail pages share one reading column. These are layout recipes;
|
|
1193
|
+
* controls keep their own primitive styles. Form spacing follows the host's
|
|
1194
|
+
* settings-form/fields.module.css and SettingsForm.module.css. */
|
|
1195
|
+
& .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
|
|
1196
|
+
& .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
|
|
1197
|
+
&.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
|
|
1198
|
+
& .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
|
|
1199
|
+
& .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
|
|
1200
|
+
@media (max-width: 560px) {
|
|
1201
|
+
& .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
|
|
1202
|
+
}
|
|
1170
1203
|
& .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
1171
1204
|
& .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
1172
1205
|
& .dsh-ui-spacer { flex: 1; min-width: 0; }
|
|
@@ -1194,6 +1227,9 @@ ${scope} {
|
|
|
1194
1227
|
* official primitive, so this only owns the vertical rhythm. --- */
|
|
1195
1228
|
& .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
1196
1229
|
& .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
|
|
1230
|
+
/* Grid fields share a row, so the stacked-field divider must not win later in
|
|
1231
|
+
* the cascade. Reset it here, after the field recipe. */
|
|
1232
|
+
& .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
|
|
1197
1233
|
& .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
|
|
1198
1234
|
& .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
1199
1235
|
& .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
|
|
@@ -1384,7 +1420,7 @@ ${scope} {
|
|
|
1384
1420
|
& *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
|
|
1385
1421
|
}
|
|
1386
1422
|
}
|
|
1387
|
-
`.replace(/^& (\.dsh-ui-[a-z0-9-]+)(
|
|
1423
|
+
`.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
|
|
1388
1424
|
}
|
|
1389
1425
|
//#endregion
|
|
1390
1426
|
//#region ../dsh-plugin-kit/lib/client-utils.js
|
|
@@ -1870,9 +1906,6 @@ const css = `${officialUiCss("dsh-safe-auto")}${modelMenuCss}
|
|
|
1870
1906
|
* legend keeps the UA inline padding from indenting the group title. */
|
|
1871
1907
|
.dsh-safe-auto legend { padding: 0; }
|
|
1872
1908
|
`;
|
|
1873
|
-
function Style() {
|
|
1874
|
-
return h("style", null, css);
|
|
1875
|
-
}
|
|
1876
1909
|
function ErrorText({ error, t }) {
|
|
1877
1910
|
return error ? h("p", {
|
|
1878
1911
|
role: "alert",
|
|
@@ -1965,7 +1998,7 @@ function PermissionMenu({ sessionId, locked, api, locale, initialSnapshot = null
|
|
|
1965
1998
|
return h("span", {
|
|
1966
1999
|
className: "dsh-safe-auto dsh-safe-auto-permission",
|
|
1967
2000
|
"aria-busy": busy
|
|
1968
|
-
}, h(
|
|
2001
|
+
}, h(_deepseek_ai_dsh_client_ui_primitives.Menu, {
|
|
1969
2002
|
open,
|
|
1970
2003
|
side: "top",
|
|
1971
2004
|
align: "start",
|
|
@@ -2004,7 +2037,7 @@ function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
|
|
|
2004
2037
|
const retained = effort && h("span", null, " ", t("savedEffortRetained", effort));
|
|
2005
2038
|
return h("fieldset", {
|
|
2006
2039
|
disabled,
|
|
2007
|
-
className: "dsh-ui-card"
|
|
2040
|
+
className: "dsh-safe-auto dsh-ui-card"
|
|
2008
2041
|
}, h("legend", { className: "dsh-ui-heading" }, t("reviewer")), h("div", { className: "dsh-ui-field" }, h("span", { className: "dsh-ui-label" }, t("model")), h(ModelMenu, {
|
|
2009
2042
|
open,
|
|
2010
2043
|
onOpenChange: setOpen,
|
|
@@ -2047,6 +2080,339 @@ function ReviewerFields({ values, catalog, disabled, onChange, locale }) {
|
|
|
2047
2080
|
clearSearchLabel: t("clearSearch")
|
|
2048
2081
|
})), state.follow && h("p", { className: "dsh-ui-help" }, t("followHelp"), retained), !state.follow && !state.known && h("p", { className: "dsh-ui-help" }, t("modelAbsent")), !state.follow && unknownEffort && h("p", { className: "dsh-ui-help" }, t("effortAbsent")), !state.follow && h("p", { className: "dsh-ui-help" }, t("effortClears")));
|
|
2049
2082
|
}
|
|
2083
|
+
const HISTORY_TEXT = {
|
|
2084
|
+
zh: {
|
|
2085
|
+
title: "审核历史",
|
|
2086
|
+
help: "保存启用此功能后的审核。内容已尽力脱敏;记录持续保留,可导出供审计。",
|
|
2087
|
+
search: "搜索内容",
|
|
2088
|
+
session: "会话 ID",
|
|
2089
|
+
tool: "工具",
|
|
2090
|
+
outcome: "审核结果",
|
|
2091
|
+
risk: "风险",
|
|
2092
|
+
from: "开始时间",
|
|
2093
|
+
to: "结束时间",
|
|
2094
|
+
all: "全部",
|
|
2095
|
+
apply: "查询",
|
|
2096
|
+
refresh: "刷新",
|
|
2097
|
+
next: "下一页",
|
|
2098
|
+
back: "上一页",
|
|
2099
|
+
detail: "查看详情",
|
|
2100
|
+
empty: "没有匹配的审核记录",
|
|
2101
|
+
loading: "正在查询…",
|
|
2102
|
+
exportOne: "导出此记录",
|
|
2103
|
+
exportAll: "导出筛选结果",
|
|
2104
|
+
cancel: "取消导出",
|
|
2105
|
+
exporting: (count) => `正在导出 ${count} 条…`,
|
|
2106
|
+
count: (count) => `共 ${count} 条`,
|
|
2107
|
+
missing: "审核记录不存在",
|
|
2108
|
+
execution: "工具结果",
|
|
2109
|
+
approval: "实际审批",
|
|
2110
|
+
approvalUnknown: "未观察到审批结果",
|
|
2111
|
+
unavailable: "未记录",
|
|
2112
|
+
outcomes: {
|
|
2113
|
+
pending: "审核中",
|
|
2114
|
+
interrupted: "审核中断",
|
|
2115
|
+
"allowed-once": "审核通过",
|
|
2116
|
+
rejected: "拒绝",
|
|
2117
|
+
cancelled: "取消",
|
|
2118
|
+
unavailable: "审批不可用"
|
|
2119
|
+
},
|
|
2120
|
+
risks: {
|
|
2121
|
+
low: "低",
|
|
2122
|
+
medium: "中",
|
|
2123
|
+
high: "高",
|
|
2124
|
+
critical: "严重"
|
|
2125
|
+
},
|
|
2126
|
+
states: {
|
|
2127
|
+
"not-observed": "未观察到执行结果",
|
|
2128
|
+
succeeded: "工具返回成功",
|
|
2129
|
+
failed: "工具返回错误"
|
|
2130
|
+
}
|
|
2131
|
+
},
|
|
2132
|
+
en: {
|
|
2133
|
+
title: "Review history",
|
|
2134
|
+
help: "Reviews are retained from the time this feature is enabled. Content is redacted on a best-effort basis and can be exported for audit.",
|
|
2135
|
+
search: "Search contents",
|
|
2136
|
+
session: "Session ID",
|
|
2137
|
+
tool: "Tool",
|
|
2138
|
+
outcome: "Outcome",
|
|
2139
|
+
risk: "Risk",
|
|
2140
|
+
from: "From",
|
|
2141
|
+
to: "To",
|
|
2142
|
+
all: "All",
|
|
2143
|
+
apply: "Search",
|
|
2144
|
+
refresh: "Refresh",
|
|
2145
|
+
next: "Next",
|
|
2146
|
+
back: "Previous",
|
|
2147
|
+
detail: "View details",
|
|
2148
|
+
empty: "No matching reviews",
|
|
2149
|
+
loading: "Loading reviews…",
|
|
2150
|
+
exportOne: "Export this record",
|
|
2151
|
+
exportAll: "Export filtered results",
|
|
2152
|
+
cancel: "Cancel export",
|
|
2153
|
+
exporting: (count) => `Exporting ${count} records…`,
|
|
2154
|
+
count: (count) => `${count} records`,
|
|
2155
|
+
missing: "Review record not found",
|
|
2156
|
+
execution: "Tool result",
|
|
2157
|
+
approval: "Actual approval",
|
|
2158
|
+
approvalUnknown: "No approval result observed",
|
|
2159
|
+
unavailable: "Not recorded",
|
|
2160
|
+
outcomes: {
|
|
2161
|
+
pending: "Pending",
|
|
2162
|
+
interrupted: "Interrupted",
|
|
2163
|
+
"allowed-once": "Review passed",
|
|
2164
|
+
rejected: "Rejected",
|
|
2165
|
+
cancelled: "Cancelled",
|
|
2166
|
+
unavailable: "Approval unavailable"
|
|
2167
|
+
},
|
|
2168
|
+
risks: {
|
|
2169
|
+
low: "Low",
|
|
2170
|
+
medium: "Medium",
|
|
2171
|
+
high: "High",
|
|
2172
|
+
critical: "Critical"
|
|
2173
|
+
},
|
|
2174
|
+
states: {
|
|
2175
|
+
"not-observed": "No execution result observed",
|
|
2176
|
+
succeeded: "Tool returned success",
|
|
2177
|
+
failed: "Tool returned an error"
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
function historyFilters(values) {
|
|
2182
|
+
return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== "").map(([key, value]) => [key, key === "from" || key === "to" ? new Date(value).toISOString() : value]));
|
|
2183
|
+
}
|
|
2184
|
+
function downloadHistory(records, filename = "safe-auto-history.json") {
|
|
2185
|
+
const url = URL.createObjectURL(new Blob([JSON.stringify({
|
|
2186
|
+
format: "safe-auto-audit-v1",
|
|
2187
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2188
|
+
count: records.length,
|
|
2189
|
+
records
|
|
2190
|
+
}, null, 2)], { type: "application/json" }));
|
|
2191
|
+
const link = document.createElement("a");
|
|
2192
|
+
link.href = url;
|
|
2193
|
+
link.download = filename;
|
|
2194
|
+
link.click();
|
|
2195
|
+
setTimeout(() => URL.revokeObjectURL(url), 6e4);
|
|
2196
|
+
}
|
|
2197
|
+
function HistoryPanel({ api, locale, initialPage = null, download = downloadHistory }) {
|
|
2198
|
+
const language = (0, react.useSyncExternalStore)((listener) => locale?.subscribe?.(listener) ?? (() => {}), () => localeLanguage(locale), () => localeLanguage(locale));
|
|
2199
|
+
const t = HISTORY_TEXT[language];
|
|
2200
|
+
const [draft, setDraft] = (0, react.useState)({
|
|
2201
|
+
query: "",
|
|
2202
|
+
sessionId: "",
|
|
2203
|
+
tool: "",
|
|
2204
|
+
outcome: "",
|
|
2205
|
+
risk: "",
|
|
2206
|
+
from: "",
|
|
2207
|
+
to: ""
|
|
2208
|
+
});
|
|
2209
|
+
const [filter, setFilter] = (0, react.useState)({});
|
|
2210
|
+
const [page, setPage] = (0, react.useState)(initialPage);
|
|
2211
|
+
const [cursors, setCursors] = (0, react.useState)([void 0]);
|
|
2212
|
+
const [index, setIndex] = (0, react.useState)(0);
|
|
2213
|
+
const [error, setError] = (0, react.useState)("");
|
|
2214
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
2215
|
+
const [detail, setDetail] = (0, react.useState)(null);
|
|
2216
|
+
const [exporting, setExporting] = (0, react.useState)(null);
|
|
2217
|
+
const generation = (0, react.useRef)(0), detailsGeneration = (0, react.useRef)(0), exportGeneration = (0, react.useRef)(0);
|
|
2218
|
+
const alive = (0, react.useRef)(true), filterRef = (0, react.useRef)(filter);
|
|
2219
|
+
filterRef.current = filter;
|
|
2220
|
+
const load = (0, react.useCallback)(async (filters, cursor) => {
|
|
2221
|
+
const ticket = ++generation.current;
|
|
2222
|
+
++detailsGeneration.current;
|
|
2223
|
+
++exportGeneration.current;
|
|
2224
|
+
setBusy(true);
|
|
2225
|
+
setError("");
|
|
2226
|
+
setDetail(null);
|
|
2227
|
+
setExporting(null);
|
|
2228
|
+
try {
|
|
2229
|
+
const result = await api.historyList({
|
|
2230
|
+
...filters,
|
|
2231
|
+
limit: 25,
|
|
2232
|
+
...cursor ? { cursor } : {}
|
|
2233
|
+
});
|
|
2234
|
+
if (alive.current && ticket === generation.current) setPage(result);
|
|
2235
|
+
} catch (e) {
|
|
2236
|
+
if (alive.current && ticket === generation.current) {
|
|
2237
|
+
setError(errorMessage(e));
|
|
2238
|
+
setPage(null);
|
|
2239
|
+
}
|
|
2240
|
+
} finally {
|
|
2241
|
+
if (alive.current && ticket === generation.current) setBusy(false);
|
|
2242
|
+
}
|
|
2243
|
+
}, [api]);
|
|
2244
|
+
(0, react.useEffect)(() => {
|
|
2245
|
+
alive.current = true;
|
|
2246
|
+
load({}, void 0);
|
|
2247
|
+
const stop = api.subscribeGeneration?.(() => {
|
|
2248
|
+
setCursors([void 0]);
|
|
2249
|
+
setIndex(0);
|
|
2250
|
+
load(filterRef.current, void 0);
|
|
2251
|
+
});
|
|
2252
|
+
return () => {
|
|
2253
|
+
alive.current = false;
|
|
2254
|
+
generation.current++;
|
|
2255
|
+
detailsGeneration.current++;
|
|
2256
|
+
exportGeneration.current++;
|
|
2257
|
+
stop?.();
|
|
2258
|
+
};
|
|
2259
|
+
}, [load, api]);
|
|
2260
|
+
const search = (e) => {
|
|
2261
|
+
e?.preventDefault();
|
|
2262
|
+
try {
|
|
2263
|
+
const next = historyFilters(draft);
|
|
2264
|
+
setFilter(next);
|
|
2265
|
+
setCursors([void 0]);
|
|
2266
|
+
setIndex(0);
|
|
2267
|
+
load(next);
|
|
2268
|
+
} catch (e) {
|
|
2269
|
+
setError(errorMessage(e));
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
const showDetail = async (id) => {
|
|
2273
|
+
const ticket = ++detailsGeneration.current;
|
|
2274
|
+
setError("");
|
|
2275
|
+
setDetail(null);
|
|
2276
|
+
try {
|
|
2277
|
+
const row = await api.historyGet(id);
|
|
2278
|
+
if (!row) throw new Error(t.missing);
|
|
2279
|
+
if (alive.current && ticket === detailsGeneration.current) setDetail(row);
|
|
2280
|
+
} catch (e) {
|
|
2281
|
+
if (alive.current && ticket === detailsGeneration.current) setError(errorMessage(e));
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
const exportAll = async () => {
|
|
2285
|
+
const ticket = ++exportGeneration.current;
|
|
2286
|
+
const records = [];
|
|
2287
|
+
let cursor;
|
|
2288
|
+
setExporting(0);
|
|
2289
|
+
setError("");
|
|
2290
|
+
try {
|
|
2291
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2292
|
+
const filters = {
|
|
2293
|
+
...filter,
|
|
2294
|
+
to: filter.to && Date.parse(filter.to) < Date.parse(now) ? filter.to : now
|
|
2295
|
+
};
|
|
2296
|
+
do {
|
|
2297
|
+
const batch = await api.historyList({
|
|
2298
|
+
...filters,
|
|
2299
|
+
limit: 100,
|
|
2300
|
+
...cursor ? { cursor } : {}
|
|
2301
|
+
});
|
|
2302
|
+
if (!alive.current || ticket !== exportGeneration.current) return;
|
|
2303
|
+
for (const item of batch.items) {
|
|
2304
|
+
const row = await api.historyGet(item.id);
|
|
2305
|
+
if (!alive.current || ticket !== exportGeneration.current) return;
|
|
2306
|
+
if (!row) throw new Error(t.missing);
|
|
2307
|
+
records.push(row);
|
|
2308
|
+
setExporting(records.length);
|
|
2309
|
+
}
|
|
2310
|
+
if (batch.nextCursor && batch.nextCursor === cursor) throw new Error("Invalid history cursor");
|
|
2311
|
+
cursor = batch.nextCursor;
|
|
2312
|
+
} while (cursor);
|
|
2313
|
+
download(records);
|
|
2314
|
+
} catch (e) {
|
|
2315
|
+
if (alive.current && ticket === exportGeneration.current) setError(errorMessage(e));
|
|
2316
|
+
} finally {
|
|
2317
|
+
if (alive.current && ticket === exportGeneration.current) setExporting(null);
|
|
2318
|
+
}
|
|
2319
|
+
};
|
|
2320
|
+
const field = (key, label, options) => h("label", {
|
|
2321
|
+
key,
|
|
2322
|
+
className: "dsh-ui-field"
|
|
2323
|
+
}, h("span", { className: "dsh-ui-label" }, label), options ? h("select", {
|
|
2324
|
+
className: "dsh-ui-select",
|
|
2325
|
+
value: draft[key],
|
|
2326
|
+
onChange: (e) => setDraft({
|
|
2327
|
+
...draft,
|
|
2328
|
+
[key]: e.target.value
|
|
2329
|
+
})
|
|
2330
|
+
}, h("option", { value: "" }, t.all), ...Object.entries(options).map(([value, label]) => h("option", {
|
|
2331
|
+
key: value,
|
|
2332
|
+
value
|
|
2333
|
+
}, label))) : h(_deepseek_ai_dsh_client_ui_primitives.Input, {
|
|
2334
|
+
type: key === "from" || key === "to" ? "datetime-local" : "text",
|
|
2335
|
+
maxLength: 256,
|
|
2336
|
+
value: draft[key],
|
|
2337
|
+
onChange: (e) => setDraft({
|
|
2338
|
+
...draft,
|
|
2339
|
+
[key]: e.target.value
|
|
2340
|
+
})
|
|
2341
|
+
}));
|
|
2342
|
+
return h("section", {
|
|
2343
|
+
className: "dsh-ui-stack",
|
|
2344
|
+
"aria-label": t.title,
|
|
2345
|
+
"aria-busy": busy
|
|
2346
|
+
}, h("p", { className: "dsh-ui-help" }, t.help), h("form", {
|
|
2347
|
+
className: "dsh-ui-stack",
|
|
2348
|
+
onSubmit: search
|
|
2349
|
+
}, h("div", { className: "dsh-ui-fields-grid" }, field("query", t.search), field("sessionId", t.session), field("tool", t.tool), field("outcome", t.outcome, t.outcomes), field("risk", t.risk, t.risks), field("from", t.from), field("to", t.to)), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2350
|
+
type: "submit",
|
|
2351
|
+
variant: "primary"
|
|
2352
|
+
}, t.apply), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2353
|
+
type: "button",
|
|
2354
|
+
variant: "ghost",
|
|
2355
|
+
onClick: () => {
|
|
2356
|
+
setCursors([void 0]);
|
|
2357
|
+
setIndex(0);
|
|
2358
|
+
load(filter);
|
|
2359
|
+
}
|
|
2360
|
+
}, t.refresh), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2361
|
+
type: "button",
|
|
2362
|
+
variant: "ghost",
|
|
2363
|
+
disabled: busy || exporting !== null || !page?.total,
|
|
2364
|
+
onClick: exportAll
|
|
2365
|
+
}, t.exportAll))), error && h("p", {
|
|
2366
|
+
role: "alert",
|
|
2367
|
+
className: "dsh-ui-error"
|
|
2368
|
+
}, error), exporting !== null && h("div", {
|
|
2369
|
+
className: "dsh-ui-actions",
|
|
2370
|
+
role: "status"
|
|
2371
|
+
}, t.exporting(exporting), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2372
|
+
variant: "ghost",
|
|
2373
|
+
onClick: () => {
|
|
2374
|
+
exportGeneration.current++;
|
|
2375
|
+
setExporting(null);
|
|
2376
|
+
}
|
|
2377
|
+
}, t.cancel)), busy ? h("p", { role: "status" }, t.loading) : page && h(react.default.Fragment, null, h("p", { className: "dsh-ui-meta" }, t.count(page.total)), !page.items.length && h("p", { className: "dsh-ui-empty" }, t.empty), ...page.items.map((row) => h("article", {
|
|
2378
|
+
key: row.id,
|
|
2379
|
+
className: "dsh-ui-card dsh-ui-stack"
|
|
2380
|
+
}, h("div", { className: "dsh-ui-row-wrap" }, h("strong", null, row.tool), h("span", null, t.outcomes[row.reviewOutcome || row.outcome] || row.reviewOutcome || row.outcome), h("span", null, t.risks[row.risk] || row.risk || "")), h("p", { className: "dsh-ui-meta dsh-ui-wrap" }, new Date(row.startedAt).toLocaleString(language), " · ", row.sessionId, " · ", row.reviewer ? `${row.reviewer.provider}/${row.reviewer.model}` : t.unavailable), row.rationale && h("p", { className: "dsh-ui-wrap" }, row.rationale), h("p", { className: "dsh-ui-help" }, t.approval, ": ", !row.approvalState || row.approvalState === "not-observed" ? t.approvalUnknown : row.approvalState === "allowed-once" ? language === "zh" ? "单次放行" : "Allowed once" : t.outcomes[row.approvalState]), h("p", { className: "dsh-ui-help" }, t.execution, ": ", t.states[row.executionState || "not-observed"]), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2381
|
+
variant: "ghost",
|
|
2382
|
+
onClick: () => showDetail(row.id)
|
|
2383
|
+
}, t.detail))), h("div", { className: "dsh-ui-actions" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2384
|
+
variant: "ghost",
|
|
2385
|
+
disabled: index === 0,
|
|
2386
|
+
onClick: () => {
|
|
2387
|
+
const next = index - 1;
|
|
2388
|
+
setIndex(next);
|
|
2389
|
+
load(filter, cursors[next]);
|
|
2390
|
+
}
|
|
2391
|
+
}, t.back), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2392
|
+
variant: "ghost",
|
|
2393
|
+
disabled: !page.nextCursor,
|
|
2394
|
+
onClick: () => {
|
|
2395
|
+
const next = index + 1;
|
|
2396
|
+
setCursors([...cursors.slice(0, next), page.nextCursor]);
|
|
2397
|
+
setIndex(next);
|
|
2398
|
+
load(filter, page.nextCursor);
|
|
2399
|
+
}
|
|
2400
|
+
}, t.next))), detail && h("section", {
|
|
2401
|
+
className: "dsh-ui-stack",
|
|
2402
|
+
"aria-label": t.detail
|
|
2403
|
+
}, h("h3", null, detail.id), h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2404
|
+
variant: "ghost",
|
|
2405
|
+
onClick: () => download([detail], `safe-auto-${detail.id}.json`)
|
|
2406
|
+
}, t.exportOne), h("pre", {
|
|
2407
|
+
className: "dsh-ui-wrap",
|
|
2408
|
+
style: {
|
|
2409
|
+
whiteSpace: "pre-wrap",
|
|
2410
|
+
overflowWrap: "anywhere",
|
|
2411
|
+
maxHeight: "60vh",
|
|
2412
|
+
overflow: "auto"
|
|
2413
|
+
}
|
|
2414
|
+
}, JSON.stringify(detail, null, 2))));
|
|
2415
|
+
}
|
|
2050
2416
|
function SettingsPanel({ view, api, locale, initialSettings = null, initialCatalog = null, initialTab = "reviewer" }) {
|
|
2051
2417
|
const t = useText(locale);
|
|
2052
2418
|
const [settings, setSettings] = (0, react.useState)(initialSettings);
|
|
@@ -2197,21 +2563,22 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2197
2563
|
tabIndex: 0,
|
|
2198
2564
|
className
|
|
2199
2565
|
}, children);
|
|
2200
|
-
if (view === "summary") return h("div", { className: "dsh-safe-auto" }, h(
|
|
2566
|
+
if (view === "summary") return h("div", { className: "dsh-safe-auto" }, h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("summary")), h(ErrorText, {
|
|
2201
2567
|
error,
|
|
2202
2568
|
t
|
|
2203
2569
|
}));
|
|
2204
2570
|
return h("section", {
|
|
2205
|
-
className: "dsh-safe-auto dsh-ui-
|
|
2571
|
+
className: "dsh-safe-auto dsh-ui-page",
|
|
2206
2572
|
"aria-label": t("panelLabel"),
|
|
2207
2573
|
"aria-busy": busy
|
|
2208
|
-
}, h(
|
|
2574
|
+
}, h("p", { className: "dsh-ui-compact dsh-ui-wrap" }, t("intro")), !values && h("p", { className: "dsh-ui-loading" }, t("loading")), values && h(react.default.Fragment, null, h(_deepseek_ai_dsh_client_ui_primitives.SegmentedTabs, {
|
|
2209
2575
|
value: activeTab,
|
|
2210
2576
|
onChange: setTab,
|
|
2211
2577
|
label: t("tabsLabel"),
|
|
2212
2578
|
items: [
|
|
2213
2579
|
tabItem("reviewer", t("tabReviewer")),
|
|
2214
2580
|
tabItem("details", t("details")),
|
|
2581
|
+
tabItem("history", HISTORY_TEXT[localeLanguage(locale)].title),
|
|
2215
2582
|
...settings?.policyLimits ? [tabItem("limits", t("tabLimits"))] : []
|
|
2216
2583
|
]
|
|
2217
2584
|
}), tabPanel("reviewer", "dsh-ui-stack", [
|
|
@@ -2243,13 +2610,16 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2243
2610
|
id: "sa-prompt-hint",
|
|
2244
2611
|
className: "dsh-ui-help"
|
|
2245
2612
|
}, t("promptHint", (values.reviewerPrompt || "").length))
|
|
2246
|
-
]), tabPanel("details", "dsh-ui-stack", h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null, t("reviewInput")), h("p", null, t("breakerDetail")))),
|
|
2613
|
+
]), tabPanel("details", "dsh-ui-stack", h("div", { className: "dsh-ui-help" }, h("p", null, t("safetyDetail")), h("p", null, t("reviewInput")), h("p", null, t("breakerDetail")))), tabPanel("history", "dsh-ui-stack", activeTab === "history" ? h(HistoryPanel, {
|
|
2614
|
+
api,
|
|
2615
|
+
locale
|
|
2616
|
+
}) : null), settings?.policyLimits && tabPanel("limits", "dsh-ui-stack", [h("p", {
|
|
2247
2617
|
key: "line",
|
|
2248
2618
|
className: "dsh-ui-meta dsh-ui-wrap"
|
|
2249
2619
|
}, t("limitsLine", settings.policyLimits)), h("p", {
|
|
2250
2620
|
key: "help",
|
|
2251
2621
|
className: "dsh-ui-help"
|
|
2252
|
-
}, t("limitsHelp"))]), h("div", { className: "dsh-ui-
|
|
2622
|
+
}, t("limitsHelp"))]), activeTab === "reviewer" && h("div", { className: "dsh-ui-form-footer" }, h(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
2253
2623
|
variant: "primary",
|
|
2254
2624
|
disabled: busy || externalChange || !dirty.current || (values.reviewerPrompt || "").length > 4096,
|
|
2255
2625
|
onClick: save
|
|
@@ -2275,6 +2645,14 @@ function SettingsPanel({ view, api, locale, initialSettings = null, initialCatal
|
|
|
2275
2645
|
}, t("saved")));
|
|
2276
2646
|
}
|
|
2277
2647
|
function apply(ctx) {
|
|
2648
|
+
ctx.effect(() => {
|
|
2649
|
+
if (typeof document === "undefined") return void 0;
|
|
2650
|
+
const style = document.createElement("style");
|
|
2651
|
+
style.setAttribute("data-plugin", BUNDLE);
|
|
2652
|
+
style.textContent = css;
|
|
2653
|
+
document.head.appendChild(style);
|
|
2654
|
+
return () => style.remove();
|
|
2655
|
+
});
|
|
2278
2656
|
mountModelSettingsChanges(ctx, BUNDLE);
|
|
2279
2657
|
const settingsSubscriber = createModelSettingsSubscriber(ctx, BUNDLE);
|
|
2280
2658
|
const call = (endpoint, payload) => unwrapRpc((...args) => ctx.connection.rpc.call(...args), endpoint, payload);
|
|
@@ -2284,6 +2662,8 @@ function apply(ctx) {
|
|
|
2284
2662
|
getSettings: () => call("settings.get", {}),
|
|
2285
2663
|
saveSettings: (payload) => call("settings.save", payload),
|
|
2286
2664
|
modelCatalog: () => readModelCatalog(ctx),
|
|
2665
|
+
historyList: (payload) => call("history.list", payload),
|
|
2666
|
+
historyGet: (id) => call("history.get", { id }),
|
|
2287
2667
|
getSession: (sessionId) => call("session.get", { sessionId }),
|
|
2288
2668
|
selectSession: (payload) => call("session.select", payload)
|
|
2289
2669
|
});
|
|
@@ -2307,12 +2687,15 @@ function apply(ctx) {
|
|
|
2307
2687
|
//#endregion
|
|
2308
2688
|
exports.BUNDLE = BUNDLE;
|
|
2309
2689
|
exports.CHANNEL = CHANNEL;
|
|
2690
|
+
exports.HistoryPanel = HistoryPanel;
|
|
2310
2691
|
exports.PermissionMenu = PermissionMenu;
|
|
2311
2692
|
exports.ReviewerFields = ReviewerFields;
|
|
2312
2693
|
exports.SettingsPanel = SettingsPanel;
|
|
2313
2694
|
exports.apply = apply;
|
|
2314
2695
|
exports.createRequestScope = createRequestScope;
|
|
2696
|
+
exports.downloadHistory = downloadHistory;
|
|
2315
2697
|
exports.errorText = errorText;
|
|
2698
|
+
exports.historyFilters = historyFilters;
|
|
2316
2699
|
exports.inject = inject;
|
|
2317
2700
|
exports.localeLanguage = localeLanguage;
|
|
2318
2701
|
exports.name = name;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klarkxy/dsh-safe-auto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Safe Auto: a reviewer model answers native approval requests, Codex auto-review style; only a pass approves that one call.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"LICENSE"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "node --check src/config.js && node --check src/dangerous.js && node --check src/transcript.js && node --check src/model-route.js && node --check src/reviewer.js && node --check src/index.js && node --check src/control.js && node --check src/ui-host.js && tsdown && node ../../scripts/editor-plugins/wrap-client.mjs @klarkxy/dsh-safe-auto",
|
|
26
|
+
"build": "node --check src/config.js && node --check src/dangerous.js && node --check src/transcript.js && node --check src/model-route.js && node --check src/reviewer.js && node --check src/audit.js && node --check src/index.js && node --check src/control.js && node --check src/ui-host.js && tsdown && node ../../scripts/editor-plugins/wrap-client.mjs @klarkxy/dsh-safe-auto",
|
|
27
27
|
"test": "node --test test/*.test.js"
|
|
28
28
|
},
|
|
29
29
|
"engines": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@klarkxy/dsh-plugin-kit": "0.
|
|
61
|
+
"@klarkxy/dsh-plugin-kit": "0.5.0",
|
|
62
62
|
"zod": "^4.1.5"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"@deepseek-ai/cordis": "4.0.5",
|
|
82
82
|
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.1",
|
|
83
83
|
"@deepseek-ai/dsh-storage-domain": "0.2.1-rc.1",
|
|
84
|
-
"@klarkxy/dsh-model-route": "0.2.
|
|
84
|
+
"@klarkxy/dsh-model-route": "0.2.6",
|
|
85
85
|
"jsdom": "29.1.1",
|
|
86
86
|
"@deepseek-ai/dsh-working-directory": "0.2.1-rc.1"
|
|
87
87
|
},
|
|
@@ -90,6 +90,6 @@
|
|
|
90
90
|
"registry": "https://registry.npmjs.org/"
|
|
91
91
|
},
|
|
92
92
|
"dshRelease": {
|
|
93
|
-
"contentHash": "
|
|
93
|
+
"contentHash": "f1c3c6a5282fcee7e6ae41538bd2c87f4eaa362ec2165412a4b5292d46f430ff"
|
|
94
94
|
}
|
|
95
95
|
}
|